2ae3710a82
小程序 pages/performance/index 请求的 /yxe-user-task/userRank 与
/yxe-task/data 同时返回 {"code":1,"error":"java.lang.NullPointerException"},
页面完成进度与人员排行榜全部空白。
根因:yxe_task.reward 允许为 NULL,历史数据中存在 NULL 记录(本机库
id=663)。两个接口在累加奖金时写作 BigDecimal.ZERO.add(task.getReward()),
BigDecimal.add(null) 直接抛 NPE。userRank 不传 name 时会遍历全部用户,
任何一人名下有该脏任务,整个排行榜即整体失败。
改动分读、写两侧:
读侧(不再因脏数据报错):
- YxeTaskController#data、#userRank 对 reward 做 null 兜底(nullToZero)
- 顺带把 userTask.getStatus().equals(2) / getLoginUser().getCanAuth().equals(1)
改为 Integer.valueOf(x).equals(...),避免字段为 NULL 时拆箱再抛 NPE
(sys_user.can_auth 列本身可空,属同类隐患)
- userRank 过滤 task_id 为 NULL 的用户任务,并去重后批量查任务
写侧(不再产生新的 NULL):
- save / saveBatch 在新增时把缺失的 reward 兜底为 0
- update / updateBatch **刻意不补**,并在代码注释中写明原因:
二者走 MyBatis-Plus 默认 NOT_NULL 部分更新语义,字段为 null 表示
“本次不修改该字段”。管理端普通任务的 reward 由产品奖励自动汇总,
产品行可能为空导致该字段整个缺失;若在此兜底为 0,一次普通编辑
就会把任务原有奖金静默清零,直接影响用户实得收益。
该风险已有专门护栏测试 updateMustNotDefaultRewardToZero 覆盖
(注入错误修法后该测试转红:expected:<null> but was:<0>)。
回归测试 TaskRewardNullSafetyTest 共 8 例,覆盖读侧两接口的 NULL 容忍、
task_id 为 NULL 不阻断排行榜、以及新增侧补默认值、更新侧必须保持 null。
已验证:回退主修后 3 例转红(save/saveBatch/data),update 护栏保持绿色。
业务影响评估(补 0 安全):
- 库中已有 67 条 reward=0 的任务,其中 6 条已完整走通接单→提交→审核→
income 落库 0.00,说明“奖金为 0”本就是系统接受的正常语义
- 审核结算处 income = reward == null ? ZERO : reward,服务端本就把 NULL 当 0,
补 0 不改变任何用户实得金额
- 小程序端 12 个源文件逐一排查,无任何依赖 reward 为 null 的分支判断
SQL: fix_task_reward_null_npe_2026-10-11.sql(幂等,本次未执行)
50 lines
2.8 KiB
SQL
50 lines
2.8 KiB
SQL
-- 用途:把 yxe_task.reward 为 NULL 的历史脏数据规范化为 0,消除业绩统计接口的空指针隐患
|
||
-- 日期:2026-10-11
|
||
-- 背景:小程序 pages/performance/index 页面请求的 /yxe-user-task/userRank 与 /yxe-task/data
|
||
-- 同时返回 {"code":1,"message":"操作失败","error":"java.lang.NullPointerException"},
|
||
-- 页面「我的完成进度/完成率」与人员排行榜全部空白。
|
||
-- 根因:yxe_task.reward 为 NULL 的任务被计入完成数后执行
|
||
-- BigDecimal.ZERO.add(task.getReward())
|
||
-- → BigDecimal.add(null) 抛 NPE,整个接口失败。
|
||
-- 本库命中数据为 yxe_task.id=663(title='测试手动',status=2 已完成,
|
||
-- 其 yxe_user_task.id=630 / user_id=875 的 status=2)。注意任务状态为 2 的只有这 1 条,
|
||
-- 但它足以让「全量排行榜」接口整体失败——userRank 不传 name 时遍历全部用户。
|
||
-- 影响:1 行
|
||
-- 性质:幂等,可重复执行;只处理 status 已完成但仍为 NULL 的奖励金额
|
||
-- 配套代码:
|
||
-- 1) 读侧兜底:YxeTaskController#data、YxeUserTaskController#userRank
|
||
-- 对 reward 做 nullToZero,接口不再报错;
|
||
-- 2) 写侧堵源:YxeTaskController#save / #saveBatch 在新增时把缺失的 reward 兜底为 0,
|
||
-- 不再产生新的 NULL 脏数据。
|
||
-- 注意 #update / #updateBatch **刻意不补默认值**:二者是部分更新语义
|
||
-- (MyBatis-Plus 默认 NOT_NULL 策略),null 表示“本次不修改该字段”,
|
||
-- 若在那里兜底会把任务原有奖金静默清零,直接影响用户实得收益。
|
||
-- 3) 回归测试:src/test/java/com/gxwebsoft/yxe/controller/TaskRewardNullSafetyTest.java
|
||
-- 说明:本脚本属于「历史数据规范化」。即使不执行,接口也已不再报错(读侧兜底),
|
||
-- 但金额会按 0 计;执行后数据本身也归于一致。
|
||
|
||
-- 执行前核对:先看清 NULL 分布在哪些任务、是否已有人完成任务
|
||
SELECT t.id AS task_id,
|
||
t.title,
|
||
t.status AS task_status,
|
||
t.reward,
|
||
COUNT(ut.id) AS user_task_cnt,
|
||
SUM(ut.status = 2) AS completed_cnt
|
||
FROM yxe_task t
|
||
LEFT JOIN yxe_user_task ut ON ut.task_id = t.id
|
||
WHERE t.reward IS NULL
|
||
GROUP BY t.id, t.title, t.status, t.reward
|
||
ORDER BY t.id;
|
||
|
||
-- 变更:仅规范化「已完成/进行中」且 reward 为 NULL 的任务,避免影响尚未开始的任务
|
||
UPDATE yxe_task
|
||
SET reward = 0.00
|
||
WHERE reward IS NULL
|
||
AND status IN (1, 2);
|
||
|
||
-- 执行后核对(remaining_null 应为 0)
|
||
SELECT SUM(reward IS NULL) AS remaining_null,
|
||
SUM(reward IS NULL AND status IN (1, 2)) AS remaining_null_active,
|
||
COUNT(*) AS total
|
||
FROM yxe_task;
|