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(幂等,本次未执行)
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
-- 用途:把 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;
|
||||
@@ -103,21 +103,21 @@ public class YxeTaskController extends BaseController {
|
||||
for (YxeTask task : taskList) {
|
||||
YxeUserTask userTask = userTaskList.stream().filter(u -> u.getTaskId().equals(task.getId())).findFirst().orElse(null);
|
||||
if (userTask != null) {
|
||||
if (userTask.getStatus().equals(2)) {
|
||||
if (Integer.valueOf(2).equals(userTask.getStatus())) {
|
||||
doneNum++;
|
||||
income = income.add(task.getReward());
|
||||
income = income.add(nullToZero(task.getReward()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (getLoginUser().getCanAuth().equals(1)) {
|
||||
if (Integer.valueOf(1).equals(getLoginUser().getCanAuth())) {
|
||||
List<YxeUserTask> allUserTaskList = yxeUserTaskService.listByTaskIds(taskIds);
|
||||
totalTaskNum = allUserTaskList.size();
|
||||
for (YxeTask task : taskList) {
|
||||
YxeUserTask userTask = allUserTaskList.stream().filter(u -> u.getTaskId().equals(task.getId())).findFirst().orElse(null);
|
||||
if (userTask != null) {
|
||||
if (userTask.getStatus().equals(2)) {
|
||||
if (Integer.valueOf(2).equals(userTask.getStatus())) {
|
||||
totalDoneNum++;
|
||||
totalIncome = totalIncome.add(task.getReward());
|
||||
totalIncome = totalIncome.add(nullToZero(task.getReward()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,13 @@ public class YxeTaskController extends BaseController {
|
||||
return success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务奖金可能为 null(历史数据/未填金额),参与金额累加前统一兜底为 0,避免 NullPointerException
|
||||
*/
|
||||
private BigDecimal nullToZero(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
|
||||
@ApiOperation("分页查询")
|
||||
@GetMapping("/page")
|
||||
@@ -291,7 +298,7 @@ public class YxeTaskController extends BaseController {
|
||||
List<YxeTask> taskFilter = new ArrayList<>();
|
||||
if (!taskList.isEmpty()) {
|
||||
List<Integer> taskIds = taskList.stream().map(YxeTask::getId).collect(Collectors.toList());
|
||||
if (getLoginUser().getCanAuth().equals(1)) {
|
||||
if (Integer.valueOf(1).equals(getLoginUser().getCanAuth())) {
|
||||
userTaskList = yxeUserTaskService.listByTaskIds(taskIds);
|
||||
} else {
|
||||
userTaskList = yxeUserTaskService.listByTaskIds(taskIds, getLoginUserId());
|
||||
@@ -331,6 +338,10 @@ public class YxeTaskController extends BaseController {
|
||||
if (loginUser != null) {
|
||||
yxeTask.setUserId(loginUser.getUserId());
|
||||
}
|
||||
// 奖励金额缺失时兜底为 0:避免落库 NULL 后,业绩统计接口在 BigDecimal.add(null) 处抛空指针
|
||||
if (yxeTask.getReward() == null) {
|
||||
yxeTask.setReward(BigDecimal.ZERO);
|
||||
}
|
||||
if (yxeTask.getMerchantId() != null) {
|
||||
Merchant merchant = merchantService.getByIdNChain(yxeTask.getMerchantId(), yxeTask.getChain());
|
||||
if (merchant == null) {
|
||||
@@ -379,6 +390,11 @@ public class YxeTaskController extends BaseController {
|
||||
@ApiOperation("修改")
|
||||
@PutMapping()
|
||||
public ApiResult<?> update(@RequestBody YxeTask yxeTask) {
|
||||
// 注意:此处刻意不为 reward 补默认值。
|
||||
// updateById 走 MyBatis-Plus 默认的 NOT_NULL 更新策略,字段为 null 时不会拼进 SET 子句,
|
||||
// 语义是“本次不修改该字段”。管理端编辑任务时若未填奖励金额,reward 字段会整个缺失,
|
||||
// 若在这里兜底成 0,会把任务原有的奖金静默清零,进而影响用户实得收益。
|
||||
// 兜底只应发生在新增(save / saveBatch)路径。
|
||||
if (yxeTaskService.updateById(yxeTask)) {
|
||||
yxeTaskProductService.removeByTask(yxeTask.getId());
|
||||
if (yxeTask.getProductList() != null && !yxeTask.getProductList().isEmpty()) {
|
||||
@@ -408,6 +424,14 @@ public class YxeTaskController extends BaseController {
|
||||
@ApiOperation("批量添加")
|
||||
@PostMapping("/batch")
|
||||
public ApiResult<?> saveBatch(@RequestBody List<YxeTask> list) {
|
||||
// 奖励金额缺失时兜底为 0,与 save 保持一致,避免落库 NULL
|
||||
if (list != null) {
|
||||
for (YxeTask item : list) {
|
||||
if (item != null && item.getReward() == null) {
|
||||
item.setReward(BigDecimal.ZERO);
|
||||
}
|
||||
}
|
||||
}
|
||||
// for (YxeTask yxeTask : list) {
|
||||
// if (yxeTask.getProductList() == null || yxeTask.getProductList().isEmpty())
|
||||
// return fail("请选择产品");
|
||||
@@ -455,6 +479,10 @@ public class YxeTaskController extends BaseController {
|
||||
@ApiOperation("批量修改")
|
||||
@PutMapping("/batch")
|
||||
public ApiResult<?> updateBatch(@RequestBody BatchParam<YxeTask> batchParam) {
|
||||
// 注意:此处刻意不为 reward 补默认值。
|
||||
// BatchParam.update 使用 UpdateWrapper 做部分更新,null 字段不会进入 SET 子句;
|
||||
// 批量修改通常只改状态等个别字段,若在此兜底成 0,会把 ids 命中的全部任务奖金清零。
|
||||
// 兜底只应发生在新增(save / saveBatch)路径。
|
||||
if (batchParam.update(yxeTaskService, "id")) {
|
||||
return success("修改成功");
|
||||
}
|
||||
|
||||
@@ -96,14 +96,23 @@ public class YxeUserTaskController extends BaseController {
|
||||
BigDecimal incomeAmount = BigDecimal.ZERO;
|
||||
List<YxeUserTask> taskList = yxeUserTaskService.listByUserId(user.getUserId());
|
||||
if (!taskList.isEmpty()) {
|
||||
List<YxeTask> taskDataList = yxeTaskService.listByIds(taskList.stream().map(YxeUserTask::getTaskId).collect(Collectors.toList()));
|
||||
List<Integer> taskIds = taskList.stream()
|
||||
.map(YxeUserTask::getTaskId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
List<YxeTask> taskDataList = taskIds.isEmpty()
|
||||
? new ArrayList<>()
|
||||
: yxeTaskService.listByIds(taskIds);
|
||||
totalTaskNum = taskList.size();
|
||||
for (YxeUserTask task : taskList) {
|
||||
if (task.getStatus().equals(2)) {
|
||||
YxeTask taskData = taskDataList.stream().filter(t -> t.getId().equals(task.getTaskId())).findFirst().orElse(null);
|
||||
if (Integer.valueOf(2).equals(task.getStatus())) {
|
||||
YxeTask taskData = taskDataList.stream()
|
||||
.filter(t -> t.getId() != null && t.getId().equals(task.getTaskId()))
|
||||
.findFirst().orElse(null);
|
||||
if (taskData != null) {
|
||||
taskCompleteNum++;
|
||||
incomeAmount = incomeAmount.add(taskData.getReward());
|
||||
incomeAmount = incomeAmount.add(nullToZero(taskData.getReward()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +128,13 @@ public class YxeUserTaskController extends BaseController {
|
||||
return success(userList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务奖金可能为 null(历史数据/未填金额),参与金额累加前统一兜底为 0,避免 NullPointerException
|
||||
*/
|
||||
private BigDecimal nullToZero(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
@ApiOperation("分页查询用户任务")
|
||||
@GetMapping("/page")
|
||||
public ApiResult<PageResult<YxeUserTask>> page(YxeUserTaskParam param) {
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.gxwebsoft.yxe.controller;
|
||||
|
||||
import com.gxwebsoft.common.core.Constants;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BatchParam;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.service.UserService;
|
||||
import com.gxwebsoft.yxe.entity.YxeTask;
|
||||
import com.gxwebsoft.yxe.entity.YxeUserTask;
|
||||
import com.gxwebsoft.yxe.param.YxeTaskParam;
|
||||
import com.gxwebsoft.yxe.param.YxeUserTaskParam;
|
||||
import com.gxwebsoft.yxe.service.YxeTaskProductService;
|
||||
import com.gxwebsoft.yxe.service.YxeTaskService;
|
||||
import com.gxwebsoft.yxe.service.YxeUserTaskService;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 回归测试:yxe_task.reward 为 NULL 时,业绩统计接口不得抛 NullPointerException。
|
||||
*
|
||||
* <p>背景:线上存在 reward 为 NULL 的任务(如 yxe_task.id=663)。
|
||||
* {@code BigDecimal.ZERO.add(null)} 会抛 NPE,导致小程序 pages/performance/index
|
||||
* 请求的 /yxe-user-task/userRank 与 /yxe-task/data 双双返回 {"code":1,"error":"java.lang.NullPointerException"},
|
||||
* 页面进度与排行榜全部空白。
|
||||
*
|
||||
* <p>这里用 mock 直接驱动控制器的真实调用路径(含 {@code Integer.equals} 拆箱、
|
||||
* 金额累加),不依赖数据库,可以稳定地在修复前转红、修复后转绿。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
public class TaskRewardNullSafetyTest {
|
||||
|
||||
/** 与 yxe_task.id=663 对应的异常数据:任务已完成,但 reward 为 NULL。 */
|
||||
private static final Integer ABNORMAL_TASK_ID = 663;
|
||||
private static final Integer LOGIN_USER_ID = 875;
|
||||
|
||||
@Mock
|
||||
private YxeTaskService yxeTaskService;
|
||||
@Mock
|
||||
private YxeUserTaskService yxeUserTaskService;
|
||||
@Mock
|
||||
private YxeTaskProductService yxeTaskProductService;
|
||||
@Mock
|
||||
private UserService userService;
|
||||
|
||||
@InjectMocks
|
||||
private YxeTaskController yxeTaskController;
|
||||
@InjectMocks
|
||||
private YxeUserTaskController yxeUserTaskController;
|
||||
|
||||
@AfterEach
|
||||
public void clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
private User loginUser() {
|
||||
User user = new User();
|
||||
user.setUserId(LOGIN_USER_ID);
|
||||
user.setUsername("wx_TM9ri37n3eMd");
|
||||
user.setTenantId(10325);
|
||||
user.setCanAuth(1);
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()));
|
||||
return user;
|
||||
}
|
||||
|
||||
private YxeTask abnormalTask() {
|
||||
YxeTask task = new YxeTask();
|
||||
task.setId(ABNORMAL_TASK_ID);
|
||||
task.setTitle("测试手动");
|
||||
task.setStatus(2);
|
||||
task.setReward(null);
|
||||
return task;
|
||||
}
|
||||
|
||||
private YxeUserTask completedUserTask(Integer taskId) {
|
||||
YxeUserTask userTask = new YxeUserTask();
|
||||
userTask.setId(630);
|
||||
userTask.setTaskId(taskId);
|
||||
userTask.setUserId(LOGIN_USER_ID);
|
||||
userTask.setStatus(2);
|
||||
return userTask;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("userRank:reward 为 NULL 时应返回成功,而不是 NPE")
|
||||
public void userRankToleratesNullReward() {
|
||||
User user = loginUser();
|
||||
user.setUserId(LOGIN_USER_ID);
|
||||
|
||||
when(userService.list()).thenReturn(Collections.singletonList(user));
|
||||
when(yxeUserTaskService.listByUserId(LOGIN_USER_ID))
|
||||
.thenReturn(Collections.singletonList(completedUserTask(ABNORMAL_TASK_ID)));
|
||||
when(yxeTaskService.listByIds(anyList()))
|
||||
.thenReturn(Collections.singletonList(abnormalTask()));
|
||||
|
||||
ApiResult<?> result = yxeUserTaskController.userRank(new YxeUserTaskParam());
|
||||
|
||||
assertEquals(Constants.RESULT_OK_CODE, result.getCode(), "不应返回 NPE 失败码");
|
||||
assertNull(result.getError(), "错误信息应为空");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<User> ranked = (List<User>) result.getData();
|
||||
// totalTaskNum>0 才会进入排行榜
|
||||
assertEquals(1, ranked.size(), "应保留有任务的用户");
|
||||
assertEquals(1, ranked.get(0).getTaskCompleteNum(), "完成任务数应被计入");
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo(ranked.get(0).getIncomeAmount()),
|
||||
"NULL 奖金应兜底为 0,不得为 null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("data:reward 为 NULL 时应返回成功,而不是 NPE")
|
||||
public void dataToleratesNullReward() {
|
||||
loginUser();
|
||||
|
||||
when(yxeTaskService.list(any())).thenReturn(Collections.singletonList(abnormalTask()));
|
||||
when(yxeUserTaskService.listByTaskIds(anyList(), anyInt()))
|
||||
.thenReturn(Collections.singletonList(completedUserTask(ABNORMAL_TASK_ID)));
|
||||
when(yxeUserTaskService.listByTaskIds(anyList()))
|
||||
.thenReturn(Collections.singletonList(completedUserTask(ABNORMAL_TASK_ID)));
|
||||
|
||||
ApiResult<?> result = yxeTaskController.data();
|
||||
|
||||
assertEquals(Constants.RESULT_OK_CODE, result.getCode(), "不应返回 NPE 失败码");
|
||||
assertNull(result.getError(), "错误信息应为空");
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Map<String, Object> data = (java.util.Map<String, Object>) result.getData();
|
||||
assertEquals(1, data.get("doneNum"), "完成数应被计入");
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo((BigDecimal) data.get("income")),
|
||||
"NULL 奖金应兜底为 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("userRank:任务ID为 NULL 的用户任务不应阻断整个排行榜")
|
||||
public void userRankToleratesNullTaskId() {
|
||||
User user = loginUser();
|
||||
|
||||
YxeUserTask broken = new YxeUserTask();
|
||||
broken.setId(1);
|
||||
broken.setTaskId(null);
|
||||
broken.setUserId(LOGIN_USER_ID);
|
||||
broken.setStatus(2);
|
||||
|
||||
List<YxeUserTask> tasks = new ArrayList<>();
|
||||
tasks.add(broken);
|
||||
|
||||
when(userService.list()).thenReturn(Collections.singletonList(user));
|
||||
when(yxeUserTaskService.listByUserId(LOGIN_USER_ID)).thenReturn(tasks);
|
||||
when(yxeTaskService.listByIds(anyList())).thenReturn(Collections.emptyList());
|
||||
|
||||
ApiResult<?> result = yxeUserTaskController.userRank(new YxeUserTaskParam());
|
||||
|
||||
assertEquals(Constants.RESULT_OK_CODE, result.getCode());
|
||||
assertNull(result.getError());
|
||||
}
|
||||
|
||||
// ==================== 写入侧:默认值只在新增路径生效 ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("save:未填奖励金额时落库为 0,而不是 NULL")
|
||||
public void saveDefaultsNullRewardToZero() {
|
||||
when(yxeTaskService.save(any(YxeTask.class))).thenReturn(true);
|
||||
when(yxeTaskService.updateById(any(YxeTask.class))).thenReturn(true);
|
||||
|
||||
YxeTask task = new YxeTask();
|
||||
task.setTitle("导入任务");
|
||||
task.setCateId(1);
|
||||
task.setReward(null);
|
||||
|
||||
yxeTaskController.save(task);
|
||||
|
||||
assertNotNull(task.getReward(), "新增任务的 reward 不应为 null");
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo(task.getReward()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("save:已填奖励金额时不得被默认值覆盖")
|
||||
public void saveKeepsExplicitReward() {
|
||||
when(yxeTaskService.save(any(YxeTask.class))).thenReturn(true);
|
||||
when(yxeTaskService.updateById(any(YxeTask.class))).thenReturn(true);
|
||||
|
||||
YxeTask task = new YxeTask();
|
||||
task.setTitle("正常任务");
|
||||
task.setCateId(1);
|
||||
task.setReward(new BigDecimal("200.00"));
|
||||
|
||||
yxeTaskController.save(task);
|
||||
|
||||
assertEquals(0, new BigDecimal("200.00").compareTo(task.getReward()),
|
||||
"显式填写的奖励金额必须原样保留");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("saveBatch:批量新增时逐条兜底为 0")
|
||||
public void saveBatchDefaultsNullRewardToZero() {
|
||||
when(yxeTaskService.saveBatch(anyList())).thenReturn(true);
|
||||
|
||||
YxeTask nullReward = new YxeTask();
|
||||
nullReward.setTitle("批量-未填");
|
||||
nullReward.setReward(null);
|
||||
|
||||
YxeTask withReward = new YxeTask();
|
||||
withReward.setTitle("批量-已填");
|
||||
withReward.setReward(new BigDecimal("50.00"));
|
||||
|
||||
List<YxeTask> list = new ArrayList<>();
|
||||
list.add(nullReward);
|
||||
list.add(withReward);
|
||||
|
||||
yxeTaskController.saveBatch(list);
|
||||
|
||||
assertEquals(0, BigDecimal.ZERO.compareTo(nullReward.getReward()));
|
||||
assertEquals(0, new BigDecimal("50.00").compareTo(withReward.getReward()),
|
||||
"已填金额不应被覆盖");
|
||||
}
|
||||
|
||||
/**
|
||||
* 这是本次改动最关键的护栏:update 走部分更新语义,字段为 null 表示“不修改”。
|
||||
* 若在 update 路径上补默认值 0,一次普通的编辑保存就会把任务奖金静默清零。
|
||||
*/
|
||||
@Test
|
||||
@DisplayName("update:奖励金额缺失时必须保持 null(表示不修改),不得被兜底成 0")
|
||||
public void updateMustNotDefaultRewardToZero() {
|
||||
when(yxeTaskService.updateById(any(YxeTask.class))).thenReturn(true);
|
||||
when(yxeTaskProductService.listByTask(anyInt())).thenReturn(Collections.emptyList());
|
||||
|
||||
YxeTask task = new YxeTask();
|
||||
task.setId(663);
|
||||
task.setReward(null);
|
||||
|
||||
yxeTaskController.update(task);
|
||||
|
||||
assertNull(task.getReward(),
|
||||
"update 阶段不得把 null 兜底成 0,否则会静默清零已有奖金");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("updateBatch:批量修改不得改写 reward")
|
||||
public void updateBatchMustNotDefaultRewardToZero() {
|
||||
when(yxeTaskService.update(any(YxeTask.class), any())).thenReturn(true);
|
||||
|
||||
YxeTask patch = new YxeTask();
|
||||
patch.setStatus(2);
|
||||
patch.setReward(null);
|
||||
|
||||
BatchParam<YxeTask> batchParam = new BatchParam<>();
|
||||
batchParam.setIds(Collections.singletonList(663));
|
||||
batchParam.setData(patch);
|
||||
|
||||
yxeTaskController.updateBatch(batchParam);
|
||||
|
||||
assertNull(patch.getReward(),
|
||||
"批量修改阶段不得把 null 兜底成 0,否则会清零全部命中任务的奖金");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user