❓布景

前阵子从同事手里接过一个项目。随后项目经理反馈一个BUG,说是一个导入功用有问题。

咱们假设这个功用需要导入一个excel文件,里面保存了业务主体。包括的内容大约便是保存一个使命信息报名了这个使命的人员信息。通过导入保存这个使命和报名使命的人员,使命自身有一个分数,完成使命就可以获得这个分数,可是导入的时分是可以手动调整某一个报名人员的分数的。

BUG的表现便是导入完应该展示导入人员的自定义分数,偏偏导入完了一切报名人员的分数都是使命自身的分数。比如使命名叫“坚持七点起床打卡一周”完成这个使命获得2分,张三导入时教师觉得他表现好,给他填了个9分,可是导入之后张三和其他人一样都是2分,张三郁闷不已=-=。

其实如果这个问题是单纯的没有把报名人员的分数正确赋值也简单了,去看看字段调用啥的应该就可以了。嘿嘿,等我去测验环境实测今后发现,当项目重启后,第一次导入时,一切正常,张三获得了他的定制分数9分,再导入一次,嘿,成2分了,随后进行了几回测验状况也都差不多,基本上都是第一次成功,之后就不行了(其实有些时分是前两次成功,有些时分是1、3次成功,厉害吧、enmmm后来问了下,测验环境后端是两个节点,也便是差不多每个节点的第一次恳求都是正常的)。

这段布景好像写的有点长了,反正大约是这么个状况,由于我也头一次看这个项目的代码,业务也不熟,就只能从接口一点一点捋了


@Slf4j
@Service
public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> implements ProjectService {
    @Override
    @Transactional
    public Map<Integer,String> projectImport(String filePath) {
        //接口从这儿开端!!!
        //首要使命信息和报名的人员信息是存在了两个sheet中,所以他先对使命信息进行了处理
        ImportParams params = new ImportParams();
        params.setTitleRows(0);
        params.setHeadRows(1);
        params.setSheetNum(1);
        List<ProjectImportParam> list = null;
        try {
            list = ExcelImportUtil.importExcel(new File(filePath), ProjectImportParam.class, params);
        } catch (Exception e) {
            log.error(">>> 导入数据反常:{}", e.getMessage());
        }
        //1.校验使命信息的相关字段
        ......
        //2.处理一些关联表数据,进行使命主体的信息保存--把使命数据存库里了
        this.save(project);
        //3.然后准备去处理报名的人员数据
        signService.projectSignImport(projectNoMap,filePath);
        for (Project project : finishProjectList) {
        //4.这儿是去把报名人员获得的分数由使命自带的分数更新为自定义的--问题触发点在3和4
            this.awardToStudent(project.getProjectNo(),null);
        }
        return projectNoMap;
        //到这儿导入就完事了
    }
}

上边这一段是直接从项目里摘出来的,省略了些其他的,下边让咱们来看下signService.projectSignImport(projectNoMap,filePath);

@Slf4j
@Service
public class SignServiceImpl extends ServiceImpl<SignMapper, Sign> implements SignService {
    @Override
    public void projectSignImport(Map<Integer, String> projectNoMap, String filePath) {
        //这个办法都是去查验报名人员的信息还有查验下这些人是不是存在使命抵触啥的
        //相关的是下边这个办法
        this.userImport(projectNo2StudentParamList.get(projectNo), project, phone, isNotComplete,null);
    }
    /*
     * 导入报名记载
     * */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void userImport(List<StudentParam> studentList, Project project, String importUserPhone, boolean needSendMessage,Integer overProject) {
        //在这个办法里才是去组装报名人员的目标信息,为了显得字数多,粘贴点
        // 1.添加子项目报名记载,后续子活动主动新增
        List<SignDetail> list = new ArrayList<>(projectDetailIds.size());
        for (Long projectDetailId : projectDetailIds) {
            SignDetail signDetail = new SignDetail();
            ......
            list.add(signDetail);
        }
        try {
            signDetailService.saveBatch(list);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new GpowerException("批量导入数据反常,请检查文件导入信息!");
        }
        ......
        // 2.同步项目报名
        Sign sign = new Sign();
        ......
        //要害点是这个实体目标Sign
        this.save(sign);
        ......
    }
}

所以最终是this.userImport()这个办法中对报名信息Sign类进行了保存,嗯Sign类是重点,然后让咱们回到上边第四点要履行的办法

this.awardToStudent(project.getProjectNo(),null);

在这个办法中也是去计算报名人员的一些数据,他是分了两次给人员分数赋值,先把使命自身的值给更新了上去,然后再去更新报名人员自定义的分数,由于分数的更新需要有些联动操作(其实也是没啥脑子去捋这业务为什么要分好几回去处理分数=-=),让咱们接着看


private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 100, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(20));
@Transactional(rollbackFor = Exception.class)
@Override
public void awardToStudent(String projectNo, Long awardStudentId) {
    //进行了一通操作,各种map,list的最终只把使命自带的分数更新给了报名人员=-=
    //啰嗦了半响重点来了,你看下边写了这个办法履行时刻长了,为了页面快速响应所以用了一个异步的方式去修正,并且还是用的线程池
    // 导入项目及其报名人的时分有一个时长修正,这个知道时刻或许比较长,所以开一个异步吧--这个是前人加的
    threadPool.execute(() -> {
        if (projectDetailId1 == null) {
            log.error("导入项目及其报名人的项目无子项目id:" + projectDetailId1);
            return;
        }
        LambdaQueryWrapper<Sign> signLambdaQueryWrapper = new LambdaQueryWrapper<>();
        signLambdaQueryWrapper.eq(Sign::getProjectNo, projectNo);
        signLambdaQueryWrapper.isNotNull(Sign::getDuration);
        signLambdaQueryWrapper.ne(Sign::getSignType, SignTypeEnum.USER_DELETE.getCode());
        if (ObjectUtil.isNotNull(awardStudentId)) {
            signLambdaQueryWrapper.eq(Sign::getStudentId, awardStudentId);
        }
        List<Sign> signList = signService.list(signLambdaQueryWrapper);
        if (CollUtil.isNotEmpty(signList)) {
            signList.forEach(sign -> {
            //1.这儿这儿这儿,下边这个办法是去更新正确的分数,便是把自定义分数更新给报名人,张三的9分就在这儿被更新(当然必定也包括其他操作)具体的咱不看了
                laborHoursService.changeStudentHours(projectDetailId1.toString(), sign.getStudentId().toString(), sign.getDuration(), "导入项目及其报名人员时修正");
            });
        }
    });
}

通过上边的代码咱们知道Sign类是一个要害点,在awardToStudent()办法异步处理的时分,他先去查了使命的报名人员信息调集signList,当signList不等于空的时分进行分数的更新。

最开端吧我必定没一行一行读代码,我就大约看了看字段赋值了,Sign目标也都新增了,咋就一会好使一会不好使了=-=。我基本没置疑过signList是空的,我觉得它不或许是空,由于上边的代码有写保存,都履行到这了,指定是都保存了,到底是为啥导致下边laborHoursService.changeStudentHours()办法没正确履行,或者说没履行呢,啊是有时履行有时不履行,各位看到这的好心人请踊跃发言吧

+1+1+1

每天一个BUG--记一个业务注解和异步线程导致数据反常的故事