Files
yxe-java/fix_task_reward_null_npe_2026-10-11.sql
T
weicw-dshsa 2ae3710a82 fix: 修复任务奖金为 NULL 导致业绩统计接口空指针
小程序 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(幂等,本次未执行)
2026-10-11 20:20:03 +08:00

50 lines
2.8 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 用途:把 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;