非正规集成+压力测试

非正规集成测试和性能测试 6月份公司最重要的续费活动就要开始了,期间功能方面的改动不是特别大。但需要做性能测试和全部数据方面的验证。 基于以上(全部数据的验证、性能测试)目的,于是需要对接口进行性能测试。 1、工具调研 在工具调研方面,市面上常见的性能测试工具有以下。 JMeter Apache Bench TestNG LoadRunner 基于熟悉上手和对公司业务数据的了解程度,最后选择了TestNG。原因有: 编码方便,灵活 数据驱动方式还不错 灵活的设置线程数 基于TestNG可以很灵活的设计一样宽表来保存结果集。 最后设计的表结构如下 create table renewalintegrationtesting.t_2023_renewal_list ( id bigint auto_increment primary key, student_num varchar(128) null, student_id varchar(36) null, phone varchar(255) null, customer_id varchar(500) null, app_token varchar(255) null, authorization varchar(255) null, step1_url varchar(500) null comment '步骤一的接口地址', step1_request text null, step1_response text null, step1_success tinyint default 0 not null comment '步骤一执行结果0-不成功1-成功', step2_url varchar(500) null comment '步骤二的接口地址', step2_request text null, step2_response text null, step2_success tinyint default 0 not null comment '步骤二执行结果0-不成功1-成功', step3_url varchar(500) null comment '步骤3的接口地址', step3_request text null, step3_response text null, step3_success tinyint default 0 not null comment '步骤3执行结果0-不成功1-成功', step4_url varchar(500) null comment '步骤4的接口地址', step4_request text null, step4_response text null, step4_success tinyint default 0 not null comment '步骤4执行结果0-不成功1-成功', step5_url varchar(500) null comment '步骤5的接口地址', step5_request text null, step5_response text null, step5_success tinyint default 0 not null comment '步骤5执行结果0-不成功1-成功', step6_url varchar(500) null comment '步骤6的接口地址', step6_request text null, step6_response text null, step6_success tinyint default 0 not null comment '步骤6执行结果0-不成功1-成功', step7_url varchar(500) null comment '步骤7的接口地址', step7_request text null, step7_response text null, step7_success tinyint default 0 not null comment '步骤7执行结果0-不成功1-成功', step8_url varchar(500) null comment '步骤8的接口地址', step8_request text null, step8_response text null, step8_success tinyint default 0 not null comment '步骤8执行结果0-不成功1-成功', step9_url varchar(500) null comment '步骤9的接口地址', step9_request text null, step9_response text null, step9_success tinyint default 0 not null comment '步骤9执行结果0-不成功1-成功', step10_url varchar(500) null comment '步骤10的接口地址', step10_request text null, step10_response text null, step10_success tinyint default 0 not null comment '步骤10执行结果0-不成功1-成功' ); 2、测试用例的编写 示例测试用例代码如查看订单列表

apache shenyu重构之AbstractShenyuPlugin

重构AbstractShenyuPlugin.execute()方法 原来的执行流程 initMatchCacheConfig(); String pluginName = named(); PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName); if (Objects.nonNull(pluginData) && pluginData.getEnabled()) { final String path = exchange.getRequest().getURI().getPath(); SelectorData selectorData = obtainSelectorDataCacheIfEnabled(exchange); if (Objects.isNull(selectorData)) { List<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName); if (CollectionUtils.isEmpty(selectors)) { return handleSelectorIfNull(pluginName, exchange, chain); } Pair<Boolean, SelectorData> matchSelectorData = matchSelector(exchange, selectors); selectorData = matchSelectorData.getRight(); if (Objects.isNull(selectorData)) { if (matchCacheConfig.getSelectorEnabled() && matchSelectorData.getLeft()) { selectorData = new SelectorData(); selectorData.setPluginName(named()); cacheSelectorData(path, selectorData); } return handleSelectorIfNull(pluginName, exchange, chain); } else { if (matchCacheConfig.getSelectorEnabled() && matchSelectorData.getLeft()) { cacheSelectorData(path, selectorData); } } } else { if (StringUtils.isBlank(selectorData.getId())) { return handleSelectorIfNull(pluginName, exchange, chain); } } selectorLog(selectorData, pluginName); if (Objects.nonNull(selectorData.getContinued()) && !selectorData.getContinued()) { // if continued, not match rules return doExecute(exchange, chain, selectorData, defaultRuleData(selectorData)); } // handle rule RuleData ruleData = obtainRuleDataCacheIfEnabled(exchange); if (Objects.isNull(ruleData)) { // execute no cache List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId()); if (CollectionUtils.isEmpty(rules)) { return handleRuleIfNull(pluginName, exchange, chain); } if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) { //get last RuleData rule = rules.get(rules.size() - 1); ruleLog(rule, pluginName); return doExecute(exchange, chain, selectorData, rule); } else { Pair<Boolean, RuleData> matchRuleData = matchRule(exchange, rules); ruleData = matchRuleData.getRight(); if (Objects.isNull(ruleData)) { this.cacheRuleData(pluginName, path, matchRuleData); return handleRuleIfNull(pluginName, exchange, chain); } else { // if match success, cache rule. if (matchCacheConfig.getRuleEnabled() && matchRuleData.getLeft()) { cacheRuleData(path, ruleData); } } } } else { if (Objects.isNull(ruleData.getId())) { // handle rule not match return handleRuleIfNull(pluginName, exchange, chain); } } ruleLog(ruleData, pluginName); return doExecute(exchange, chain, selectorData, ruleData); } return chain.execute(exchange); 重构流程 获取变量数据 plugin selector rule 卫语句提前结束 processSelector 处理select为空的情况,卫语句提前执行 记录日志,不为空记录日志 已知selecor对象 缓存获取 不需要缓存对象 执行select选中逻辑 processRule 处理rule为空的情况,卫语句提前执行 记录日志,不为空记录日志 已知rule对象 缓存获取 不需要缓存对象 执行select选中逻辑 重构后的代码 initMatchCacheConfig(); final String pluginName = named(); final String path = exchange.getRequest().getURI().getPath(); PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName); List<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName); SelectorData selectorData = obtainSelectorDataCacheIfEnabled(path); // early exit if (Objects.isNull(pluginData) || !pluginData.getEnabled()) { return chain.execute(exchange); } // handle Selector if (Objects.nonNull(selectorData) && StringUtils.isBlank(selectorData.getId())) { return handleSelectorIfNull(pluginName, exchange, chain); } if (Objects.isNull(selectorData)) { if (CollectionUtils.isEmpty(selectors)) { return handleSelectorIfNull(pluginName, exchange, chain); } Pair<Boolean, SelectorData> matchSelectorData = matchSelector(exchange, selectors); selectorData = matchSelectorData.getRight(); if (Objects.isNull(selectorData)) { if (matchCacheConfig.getSelectorEnabled() && matchSelectorData.getLeft()) { selectorData = new SelectorData(); selectorData.setPluginName(pluginName); cacheSelectorData(path, selectorData); } return handleSelectorIfNull(pluginName, exchange, chain); } else { if (matchCacheConfig.getSelectorEnabled() && matchSelectorData.getLeft()) { cacheSelectorData(path, selectorData); } } } printLog(selectorData, pluginName); if (Objects.nonNull(selectorData.getContinued()) && !selectorData.getContinued()) { // if continued, not match rules return doExecute(exchange, chain, selectorData, defaultRuleData(selectorData)); } // handle Rule RuleData ruleData = obtainRuleDataCacheIfEnabled(path); List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId()); if (Objects.nonNull(ruleData) && Objects.isNull(ruleData.getId())) { return handleRuleIfNull(pluginName, exchange, chain); } if (CollectionUtils.isEmpty(rules)) { return handleRuleIfNull(pluginName, exchange, chain); } if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) { //get last RuleData rule = rules.get(rules.size() - 1); printLog(rule, pluginName); return doExecute(exchange, chain, selectorData, rule); } else { Pair<Boolean, RuleData> matchRuleData = matchRule(exchange, rules); ruleData = matchRuleData.getRight(); if (Objects.isNull(ruleData)) { this.cacheRuleData(pluginName, path, matchRuleData); return handleRuleIfNull(pluginName, exchange, chain); } else { // if match success, cache rule. if (matchCacheConfig.getRuleEnabled() && matchRuleData.getLeft()) { cacheRuleData(path, ruleData); } } } printLog(ruleData, pluginName); return doExecute(exchange, chain, selectorData, ruleData);

apache shenyu 介绍

shenyu是什么 是一个java实现的网关、高性能、跨语言的、响应式API网关。 架构图 模块说明 admin 管理后台和配置模块 bootstrap 程序的主入口 client 客户端模块,提供支持Spring MVC 、Dubbo 、Spring Cloud 快速访问 disruptor 基于disruptor的封装 register-center shenyu-client提供各种rpc接入注册中心的支持 common 框架的通用类 dist 构建项目 metrics prometheus(普罗米修斯)实现的 metrics plugin 插件集合 spin 定义的spi spring-boot-starter 支持 spring-boot-starter sync-data-center 提供 ZooKeeper,HTTP,WebSocket,Nacos 的方式同步数据 examples 示例模块 web 包括插件、请求路由和转发等核心处理包

redis使用分布式锁记

使用redis分布式锁记 背景介绍 在项目中使用封装的redis锁注解中,会出现异常信息。抛出拿分布式锁异常。 跟源码 其中lock对象为false的情况下会抛出Get redis lock failed的业务异常 @Around("redisLockPoint() && @annotation(redisLock)") public Object around(ProceedingJoinPoint pjp, RedisLock redisLock) throws Throwable { String key = redisLock.key(); if (StringUtils.isBlank(key)) { Object[] args = pjp.getArgs(); if (redisLock.bindType().equals(RedisLock.BindType.DEFAULT)) { key = StringUtils.join(args); } else if (redisLock.bindType().equals(RedisLock.BindType.ARGS_INDEX)) { key = getArgsKey(redisLock, args); } else if (redisLock.bindType().equals(RedisLock.BindType.OBJECT_PROPERTIES)) { key = getObjectPropertiesKey(redisLock, args); } } Assert.hasText(key, "key does not exist"); String prefix = redisLock.prefix()+"_"; boolean lock = distributedRedisLock.lock(prefix + key, redisLock.expire(), redisLock.retryTimes(), redisLock.retryInterval()); if (!lock) { log.warn("get lock failed : " + key); if (redisLock.errorStrategy().equals(RedisLock.ErrorStrategy.THROW_EXCEPTION)) { throw new RedisException("Get redis lock failed"); } return null; } log.info("get lock success : {}", key); try { return pjp.proceed(); } finally { boolean result = distributedRedisLock.unLock(prefix + key); log.info("release lock : {} {}", prefix + key, result ? " success" : " failed"); } } 加锁的方法,通过redisTemplate执行execute方法,然后其中的SetOption参数为SET_IF_ABSENT /** * 加锁 * @param key 锁key * @param expire 过期时间 * @param retryTimes 重试次数 * @param retryInterval 重试间隔 * @return true 加锁成功, false 加锁失败 */ public boolean lock(String key, long expire, int retryTimes, long retryInterval) { boolean result = setRedisLock(key, expire); /** * 如果获取锁失败,进行重试 */ while((!result) && retryTimes-- > 0){ try { log.info("lock failed, retrying..." + retryTimes); Thread.sleep(retryInterval); } catch (InterruptedException e) { return false; } result = setRedisLock(key, expire); } return result; } /** * 设置redis锁 * @param key 锁key * @param expire 过期时间 * @return true 设置成功,false 设置失败 */ private boolean setRedisLock(String key, long expire) { try { RedisCallback<Boolean> callback = (connection) -> { String uuid = UUID.randomUUID().toString(); lockKey.set(uuid); return connection.set(key.getBytes(), uuid.getBytes(), Expiration.milliseconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT); }; return redisTemplate.execute(callback); }catch (Exception e){ log.error("set redis error", e); } return false; } 其中connection.set方法的释意如下,参数为 key,value,过期时间,和option /** * Set {@code value} for {@code key} applying timeouts from {@code expiration} if set and inserting/updating values * depending on {@code option}. * * @param key must not be {@literal null}. * @param value must not be {@literal null}. * @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} to not set any ttl. * @param option must not be {@literal null}. Use {@link SetOption#upsert()} to add non existing. * @return {@literal null} when used in pipeline / transaction. * @since 1.7 * @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a> */ @Nullable Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option); 其中option的值如下所示例 当前程序使用的是SET_IF_ABSENT 等同于redis的SETNX命令 /** * {@code SET} command arguments for {@code NX}, {@code XX}. * * @author Christoph Strobl * @since 1.7 */ enum SetOption { /** * Do not set any additional command argument. * * @return */ UPSERT, /** * {@code NX} * * @return */ SET_IF_ABSENT, /** * {@code XX} * * @return */ SET_IF_PRESENT; /** * Do not set any additional command argument. * * @return */ public static SetOption upsert() { return UPSERT; } /** * {@code XX} * * @return */ public static SetOption ifPresent() { return SET_IF_PRESENT; } /** * {@code NX} * * @return */ public static SetOption ifAbsent() { return SET_IF_ABSENT; } } 大胆假设,小心求证 观点一: redis写会失败 利用多线程模拟写操作 本地模拟10万个线程同时写操作,并未复现 写操作不包含重复的key 业务代码中 观点二: 写重复key导致失败 利用多线程模拟写操作 短时间内重复key 业务代码中有满足的条件 创建订单:key为customerId,表中存在customerId重复 发放优惠券:key为activeId,活动为key,也存在重复 观点一代码,多次模拟,未出现拿锁异常的情况 @PostMapping("/testRedisLock") public Result testRedisLock(@RequestParam Integer param) { final boolean[] flag = {false}; for(int i = 0 ;i<param;i++){ //方式1:相当于继承了Thread类,作为子类重写run()实现 int finalI = i; new Thread() { @Override public void run() { try{ payV2Service.testRedisLock(finalI); }catch (Exception e){ e.printStackTrace(); flag[0] = true; throw new RuntimeException("失败:"); } }; }.start(); } return Result.succeed(flag[0],"成功"); } @Override @RedisLock(prefix = "CREATORDER:TEST") public Object testRedisLock(Integer param) { log.info("加锁成功:" + param); return true; } 观点二代码,次次都会复线出拿锁失败的情况 @PostMapping("/testRedisLock") public Result testRedisLock(@RequestParam Integer param) { final boolean[] flag = {false}; for(int i = 0 ;i<param;i++){ //方式1:相当于继承了Thread类,作为子类重写run()实现 int finalI = i; new Thread() { @Override public void run() { try{ payV2Service.testRedisLock(finalI); }catch (Exception e){ e.printStackTrace(); flag[0] = true; throw new RuntimeException("失败:"); } //System.out.println("匿名内部类创建线程方式1..."); }; }.start(); } for(int i = 0 ;i<param;i++){ //方式1:相当于继承了Thread类,作为子类重写run()实现 int finalI = i; new Thread() { @Override public void run() { try{ payV2Service.testRedisLock(finalI); }catch (Exception e){ e.printStackTrace(); flag[0] = true; throw new RuntimeException("失败:"); } //System.out.println("匿名内部类创建线程方式1..."); }; }.start(); } return Result.succeed(flag[0],"成功"); } @Override @RedisLock(prefix = "CREATORDER:TEST") public Object testRedisLock(Integer param) { log.info("加锁成功:" + param); return true; } 假设后,可近似求证的结果 基于以上两种假设,更可能出现的结论是写重复key导致失败,假设带着改结论去论证业务代码中由于数据问题,导致key重复的可能。于是针对加锁的key,去验证数据是否有相同。

bug 记录

bug清单表格 1、springboot上传文件,报流关闭错误 现象 : springboot上传文件,报流关闭错误 原因 : 项目中增加了全局的GlobalWebAppConfig,其中的配置导致流关闭失效 解决方式 : 移除@EnableWebMvc注解 过程总结 : 本地复现 定位问题点 (移除pom),注释依赖 确定问题点确实是新增配置 网上搜索解决方式 记录时间 : 2021-08-21 2、某些sql会在limit后追加一个limit,导致查询sql报错 现象 : 某些sql会在limit后追加一个limit,导致查询sql报错 原因 : PageHelper 非安全调用方式,项目中使用方式为PageHelper.startPage,此种方式背后实现原理为: ThreadLocal设置线程局部变量,tomcat线程次会缓存请求,可能导致threadlocal中的线程变量被其他线程访问到 解决方式 : springmvc层增加全局的拦截器, 然后手动移除一下pageHelper的变量 过程总结 : 定位问题 网上搜索确实发现limit可能会是pagehelper导致 再次定位threadlocal设置变量,设置tomcat工作线程设置为1个 本地手写分页复现场景,分页参数设置,未查询数据库,手动触发报错 增加全局分页拦截器解决 记录时间 : 2021-08-21 3、请求接口,返回90044错误 现象 : 请求接口,返回90044错误 原因 : 2.0网关配置中,只建立了bi的服务,在角色那一栏并未配置bi相关的服务 解决方式 : 2.0网关配置erp-api的相关权限 1、定位问题9004 2、查询9004为请求接口超出授权范围 3、访问灰度复现出改问题 过程总结 : 定位问题9004 查询9004为请求接口超出授权范围 访问灰度复现出改问题 找运维老师确认2.0网关的配置问题 确认配置问题、增加配置 记录时间 : 2021-08-23 4、小程序获取手机号授权,并未生成响应的学员记录 现象 : 小程序获取手机号授权,并未生成响应的学员记录 原因 : 营销服务调用crm服务,crm服务新增授权类型客户的时候没有写入电话字段 解决方式 : 入口出增加电话字段 过程总结 : httplog寻找入参数, ```sql select * from alc-http-log.http_log where system = 'marketing-center' and url = '/recruitMini/mobile' and request_time >= '20201-08-23 18:40:00' 入参拿着本地调试 定位问题为crm写入处 crm处写入值 记录时间 : 2021-08-25 5、crm服务导入数据、特定的获客渠道(异界交换)才能够导入 现象 : crm服务导入数据、特定的获客渠道(异界交换)才能够导入 原因 : crm调用基础字典服务获取获取渠道字典只能获取parentId不为空的获取渠道、 实际的数据有很多条 解决方式 : 获取字典处调整为获取所有的获客渠道, crm也调用获取所有的获客渠道 过程总结 : 下载导入模版 启动对应环境参数的项目、复现问题 定位问题为获取字典处,未获取到完全的获客渠道 记录时间 : 2021-08-25 6、支付成功后,在crm的h5页面填入分单的数据、结果分单后的比例并没有按对应比例保存 现象 : 支付成功后,在crm的h5页面填入分单的数据、结果分单后的比例并没有按对应比例保存 原因 : 历史设计如此、在填写分单业绩处逻辑为判断是否有无生成业绩, 若是业绩未生成,其对应填写的比例也不会保存 解决方式 : 无 过程总结 : 本地启动程序 复现该问题 读历史的代码,发现历史代码也是要生成业绩后才会记录分单的比例 记录时间 : 2021-08-25 7、有一个定时然后推送消息的功能,同时推送短信和微信模版消息。 现象 : 有一个定时然后推送消息的功能,同时推送短信和微信模版消息。 执行定时后,短信推送了,但是模版消息并未推送。 原因 : 服务调用链路为:教务服务 -》营销服务 -》推送消息服务 其中营销服务调用推送消息服务的请求地址被改成空了,然后调用服务出问题。 解决方式 : 还原为正确的调用地址 过程总结 : 定位问题 查看推送服务的日志 查看推送服务的http日志 查看营销调用推送服务的代码 把调用地址修改为正确的地址 记录时间 : 2021-08-25 8、订单支付成功,生成了3条相同的费用明细,同时生成了三个正式学员, 有预警邮件提醒 现象 : 订单支付成功,生成了3条相同的费用明细,同时生成了三个正式学员, 有预警邮件提醒 原因 : 支付回调后置处理策略未添加事物注解,导致部分流程成功,部分流程失败 解决方式 : 回调后置策略增加事物注解 过程总结 : 观察现象,确定费用明细生成,同时排查到正式学员生成多条,初步定位回调后置处理策略未在同一个事物里面 定位问题,确认代码中为增加事物注解 后置处理策略增加事物注解,解决改问题 扩展点 : 事物注解是如何生效的?同时默认回滚策略是RuntimeExcetipn类 记录时间 : 2021-08-25 9、在正式学员列表里面,搜索姓名姓名无法找到该学员,但是搜索学员昵称或者电话号码又可以找到该学员。 现象 : 在正式学员列表里面,搜索姓名姓名无法找到该学员,但是搜索学员昵称或者电话号码又可以找到该学员。 点击进入详细页里面,发现学员的姓名又是正常显示.包括学员费用明细里面也是一样的 初步现象就是意向学员的名字和正式学员的名字不一致 erp修改意向校区,发赠课券后所属校区为空 原因 : 正式学员表的名字和意向学员表的名字不一致,编辑crm信息处有bug, 导致信息未保持一致性 解决方式 : 修复crm编辑处的bug,使两处编辑保持一致 过程总结 : 观察现象,拿着贾舒涵的名称搜索确实会出现改问题 定位问题点,正式学员状态的变更入口,编辑客户信息导致 修复问题 记录时间 : 2021-08-30 10、通过转介绍渠道来的新的意向客户数据,其跟进人的扫码归属人字段未写入值 现象 : 通过转介绍渠道来的新的意向客户数据,其跟进人的扫码归属人字段未写入值 原因 : 营销服务调用crm服务编辑客户的跟进人信息时候,未传递跟进人参数 迁移代码导致代码丢失 解决方式 : 营销服务调crm服务时候,传跟进人参数 过程总结 : 复现(耗时长),不同分支的程序,已经接口参数,接口要做什么事情不清晰,很久之前的代码。 接口做的事情用注释标注清楚 httplog 辅助,抓入参数 读代码逻辑,标注代码做了那些事情 拿入参调试,找现场的快照情况 DEBUG断点调试 定位问题(耗时长),对比代码 增加跟进人传参数,解决问题 记录时间 : 2021-08-30 11、omo转介绍小程序,提交了预约相关信息,其生成了活动记录,但是并未生成客户记录 现象 : omo转介绍小程序,提交了预约相关信息,其生成了活动记录,但是并未生成客户记录 原因 : crm代码迁移,新增客户逻辑的代码丢失,导致不会生成意向客户记录 解决方式 : 对比以前的customer分支,找回丢失的代码逻辑 过程总结 : 复现(耗时长) 拿参数 读代码逻辑 断点调试 找到出问题的点 排查原因 定位问题,代码丢失,相应分支和逻辑比较 记录时间 : 2021-08-30 12、启动项目提示拿不到数据库的链接 现象 : 启动项目提示拿不到数据库的链接 原因 : apollo配置问题 1.配置的格式问题 2.有特殊符号,然后认不到 3.app.id和apollo配置的不一致 解决方式 : 调整apollo的配置为正确的项目 过程总结 : 观察现象 启动项目拿不到正确的数据库url 初步定位问题为apollo配置问题 肉眼对比配置 肉眼对比app.id 重启检测是否生效 记录时间 : 2021-09-01 13、暑期三节课发起支付后,订单支付状态未变更为支付中 现象 : 暑期三节课发起支付后,订单支付状态未变更为支付中 原因 : 1.支付中心拆分后,更改订单状态为支付中逻辑上提到业务处理层 2.营销三节课未添加更改支付中的逻辑 解决方式 : 添加更改订单状态为支付中的代码逻辑 过程总结 : 观察现象 定位问题 提交代码修复问题 扩展点 : 状态的判断处理都抽枚举类来统一处理 记录时间 : 2021-09-02 14、修复跟进人bug 现象 : 修复跟进人bug 原因 : 1.客户详细存在跟进人为空的数据 2.sql查询的逻辑为即查询跟进人的id,又查询冗余跟进人名称, 同时冗余跟进人名称会存在为空的数据 解决方式 : 若名称为空,跟进id查询一遍名称 过程总结 : 定位问题 本地复现 修改bug,未完全本地验证查询逻辑就让测试打包验证 优化,本地验证数据正确性能后 记录时间 : 2021-09-02 15、优惠券退款,点击驳回或者废除。若原状态为已经回收, 其状态还是变更成未使用,并没有退回为原本状态 现象 : 优惠券退款,点击驳回或者废除。若原状态为已经回收, 其状态还是变更成未使用,并没有退回为原本状态 原因 : 1.用户领取优惠券状态字段只有一个。若点击退款后,其状态变更为退款中, 并没有历史状态的记录 解决方式 : 领取记录表扩展增加字段“回收状态”,若已经回收,把回收状态设置为已经回收 然后,审核的时候, 1.联合判断一下回收状态,若此时已经回收,则回退成回收 2.若已经过期,过期标志 + 过期时间联合判断 3.其他置为未使用 过程总结 : 定位问题。状态值并没有快照记录 解决问题。增加退款状态字段 回收时写入已经退款,其他后置操作把回收状态还原 回收完成后,同时过期状态没有生效 找当时的参数记录。打日志重要性。 定位问题点,用http的参数请求接口发现但是并未走到对应的代码分支,导致设置过期状态未执行 逻辑分支判断用布尔值去判断 记录时间 : 2021-09-09 16、线上支付在使用支付宝付款时候提示“操作失败:网络或者服务器错误” 现象 : 线上支付在使用支付宝付款时候提示“操作失败:网络或者服务器错误” 原因 : 1.网络问题导致? 2.后台接口报错? 解决方式 : 过程总结 : 定位问题,排查httplog的请求接口情况 观察并未发送请求到后台 同时源头并未接入httplog服务 再仔细观察报错信息 正确流程: 1.观察报错信息为“操作失败:网络或者服务器错误” 2.什么情况下后段会报该异常 3.初步定位问题为网络情况 记录时间 : 2021-09-09