新增当前用户待办接口和权限测试

This commit is contained in:
jsowell
2026-07-16 16:20:34 +08:00
parent 181d4a5b40
commit f84ec43ceb
5 changed files with 386 additions and 4 deletions

View File

@@ -42,8 +42,8 @@ PRD 同时要求“幂等键唯一”和“任务完成后允许同一业务重
| 批次 | 范围 | 优先级 | 状态 | 验收结果 | Commit | | 批次 | 范围 | 优先级 | 状态 | 验收结果 | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| B01 | 任务拆分、数据表和索引 | P0 | done | SQL 结构和索引评审通过 | `e65017ab2` | | B01 | 任务拆分、数据表和索引 | P0 | done | SQL 结构和索引评审通过 | `e65017ab2` |
| B02 | 实体、命令模型、枚举、Mapper、Service | P0 | done | 模块编译和 Mapper XML 校验通过 | 本批提交 | | B02 | 实体、命令模型、枚举、Mapper、Service | P0 | done | 模块编译和 Mapper XML 校验通过 | `181d4a5b4` |
| B03 | 当前用户查询、已读、完成、取消接口及测试 | P0 | todo | - | - | | B03 | 当前用户查询、已读、完成、取消接口及测试 | P0 | done | 全量依赖编译、9 个专项测试通过 | 本批提交 |
| B04 | 前端 API、待办中心页面和路由跳转 | P0 | todo | - | - | | B04 | 前端 API、待办中心页面和路由跳转 | P0 | todo | - | - |
| B05 | 首页待办卡片、数量展示和轮询刷新 | P0 | todo | - | - | | B05 | 首页待办卡片、数量展示和轮询刷新 | P0 | todo | - | - |
| B06 | 菜单权限 SQL、联调和首期回归 | P0 | todo | - | - | | B06 | 菜单权限 SQL、联调和首期回归 | P0 | todo | - | - |
@@ -84,7 +84,7 @@ PRD 同时要求“幂等键唯一”和“任务完成后允许同一业务重
- 优先级P0 - 优先级P0
- 批次B03 - 批次B03
- 状态:todo - 状态done
- 目标:实现未完成数量、首页列表、分页列表、详情、已读、全部已读、完成和取消接口。 - 目标:实现未完成数量、首页列表、分页列表、详情、已读、全部已读、完成和取消接口。
- 验收标准: - 验收标准:
- 所有用户端查询和更新强制使用当前登录用户 ID。 - 所有用户端查询和更新强制使用当前登录用户 ID。
@@ -175,4 +175,5 @@ PRD 同时要求“幂等键唯一”和“任务完成后允许同一业务重
- 2026-07-16B01完成优先级拆分、任务跟踪文档和 `sys_todo_task` 建表脚本;通过 SQL 人工结构评审与 `git diff --check`commit `e65017ab2` - 2026-07-16B01完成优先级拆分、任务跟踪文档和 `sys_todo_task` 建表脚本;通过 SQL 人工结构评审与 `git diff --check`commit `e65017ab2`
- 2026-07-16B01 环境进展:用户已在测试数据库创建 `sys_todo_task` 表,可供后续接口联调。 - 2026-07-16B01 环境进展:用户已在测试数据库创建 `sys_todo_task` 表,可供后续接口联调。
- 2026-07-16B02完成领域模型、创建/查询 DTO、状态常量、Mapper 和统一 Service`mvn -pl jsowell-system -am -DskipTests compile` 与 Mapper XML 语法校验通过commit hash 在下一批记录中回填。 - 2026-07-16B02完成领域模型、创建/查询 DTO、状态常量、Mapper 和统一 Service`mvn -pl jsowell-system -am -DskipTests compile` 与 Mapper XML 语法校验通过commit `181d4a5b4`
- 2026-07-16B03完成当前用户数量、首页列表、分页列表、详情、已读、全部已读、完成和取消接口`mvn -pl jsowell-admin -am -DskipTests compile` 通过9 个 Service/Controller 专项测试通过commit hash 在下一批记录中回填。

View File

@@ -0,0 +1,118 @@
package com.jsowell.web.controller.system;
import com.jsowell.common.annotation.Log;
import com.jsowell.common.core.controller.BaseController;
import com.jsowell.common.core.domain.AjaxResult;
import com.jsowell.common.core.page.TableDataInfo;
import com.jsowell.common.enums.BusinessType;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.domain.dto.TodoTaskCancelRequest;
import com.jsowell.system.domain.dto.TodoTaskQuery;
import com.jsowell.system.service.TodoTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 当前登录用户待办任务接口。
*
* @author jsowell
*/
@RestController
@RequestMapping("/system/todo")
public class SysTodoTaskController extends BaseController {
@Autowired
private TodoTaskService todoTaskService;
/**
* 获取当前用户待办数量。
*/
@PreAuthorize("@ss.hasPermi('system:todo:list')")
@GetMapping("/unreadCount")
public AjaxResult unreadCount() {
Long userId = getUserId();
Map<String, Integer> counts = new HashMap<>(2);
counts.put("activeCount", todoTaskService.countActive(userId));
counts.put("unreadCount", todoTaskService.countUnread(userId));
return AjaxResult.success(counts);
}
/**
* 获取首页最近待办。
*/
@PreAuthorize("@ss.hasPermi('system:todo:list')")
@GetMapping("/homeList")
public AjaxResult homeList(@RequestParam(required = false) Integer limit) {
return AjaxResult.success(todoTaskService.homeList(getUserId(), limit));
}
/**
* 分页查询当前用户待办。
*/
@PreAuthorize("@ss.hasPermi('system:todo:list')")
@GetMapping("/list")
public TableDataInfo list(TodoTaskQuery query) {
startPage();
List<SysTodoTask> list = todoTaskService.listForAssignee(query, getUserId());
return getDataTable(list);
}
/**
* 查询当前用户待办详情。
*/
@PreAuthorize("@ss.hasPermi('system:todo:query')")
@GetMapping("/{todoId}")
public AjaxResult getInfo(@PathVariable Long todoId) {
return AjaxResult.success(todoTaskService.getForAssignee(todoId, getUserId()));
}
/**
* 标记当前用户的一条待办为已读。
*/
@PreAuthorize("@ss.hasPermi('system:todo:read')")
@PutMapping("/{todoId}/read")
public AjaxResult markRead(@PathVariable Long todoId) {
return toAjax(todoTaskService.markRead(todoId, getUserId(), getUsername()));
}
/**
* 标记当前用户全部待办为已读。
*/
@PreAuthorize("@ss.hasPermi('system:todo:read')")
@PutMapping("/readAll")
public AjaxResult markAllRead() {
return AjaxResult.success(todoTaskService.markAllRead(getUserId(), getUsername()));
}
/**
* 完成当前用户待办。
*/
@PreAuthorize("@ss.hasPermi('system:todo:complete')")
@Log(title = "待办任务", businessType = BusinessType.UPDATE)
@PutMapping("/{todoId}/complete")
public AjaxResult complete(@PathVariable Long todoId) {
return toAjax(todoTaskService.complete(todoId, getUserId(), getUsername()));
}
/**
* 取消当前用户待办。
*/
@PreAuthorize("@ss.hasPermi('system:todo:cancel')")
@Log(title = "待办任务", businessType = BusinessType.UPDATE)
@PutMapping("/{todoId}/cancel")
public AjaxResult cancel(@PathVariable Long todoId,
@Validated @RequestBody TodoTaskCancelRequest request) {
return toAjax(todoTaskService.cancelForAssignee(todoId, getUserId(), getUsername(), request.getReason()));
}
}

View File

@@ -0,0 +1,149 @@
package com.jsowell.system.service;
import com.jsowell.common.exception.ServiceException;
import com.jsowell.system.constant.TodoTaskConstants;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
import com.jsowell.system.domain.dto.TodoTaskQuery;
import com.jsowell.system.mapper.SysTodoTaskMapper;
import com.jsowell.system.service.impl.TodoTaskServiceImpl;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.dao.DuplicateKeyException;
import java.lang.reflect.Field;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class TodoTaskServiceImplTest {
@Test
void createTask_shouldSetDefaultsAndInsert() {
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
TodoTaskServiceImpl service = newService(mapper);
TodoTaskCreateCommand command = createCommand();
when(mapper.selectActiveByIdempotentKey("INVOICE:100:8")).thenReturn(null);
doAnswer(invocation -> {
SysTodoTask task = invocation.getArgument(0);
task.setTodoId(11L);
return 1;
}).when(mapper).insertTodoTask(any(SysTodoTask.class));
SysTodoTask result = service.createTask(command);
ArgumentCaptor<SysTodoTask> captor = ArgumentCaptor.forClass(SysTodoTask.class);
verify(mapper).insertTodoTask(captor.capture());
SysTodoTask inserted = captor.getValue();
assertEquals(11L, result.getTodoId());
assertEquals(TodoTaskConstants.PRIORITY_NORMAL, inserted.getPriority());
assertEquals(TodoTaskConstants.STATUS_PENDING, inserted.getTaskStatus());
assertEquals(TodoTaskConstants.READ_UNREAD, inserted.getReadStatus());
assertEquals(inserted.getIdempotentKey(), inserted.getActiveIdempotentKey());
}
@Test
void createTask_shouldReturnExistingTask() {
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
TodoTaskServiceImpl service = newService(mapper);
SysTodoTask existing = new SysTodoTask();
existing.setTodoId(12L);
when(mapper.selectActiveByIdempotentKey("INVOICE:100:8")).thenReturn(existing);
SysTodoTask result = service.createTask(createCommand());
assertSame(existing, result);
verify(mapper, never()).insertTodoTask(any(SysTodoTask.class));
}
@Test
void createTask_shouldRecoverFromConcurrentDuplicate() {
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
TodoTaskServiceImpl service = newService(mapper);
SysTodoTask existing = new SysTodoTask();
existing.setTodoId(13L);
when(mapper.selectActiveByIdempotentKey("INVOICE:100:8")).thenReturn(null, existing);
doThrow(new DuplicateKeyException("duplicate"))
.when(mapper).insertTodoTask(any(SysTodoTask.class));
SysTodoTask result = service.createTask(createCommand());
assertSame(existing, result);
}
@Test
void listForAssignee_shouldOverrideRequestedUser() {
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
TodoTaskServiceImpl service = newService(mapper);
TodoTaskQuery query = new TodoTaskQuery();
query.setAssigneeUserId(999L);
when(mapper.selectTodoList(query)).thenReturn(Collections.emptyList());
service.listForAssignee(query, 8L);
assertEquals(8L, query.getAssigneeUserId());
verify(mapper).selectTodoList(query);
}
@Test
void complete_shouldBeIdempotentWhenAlreadyCompleted() {
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
TodoTaskServiceImpl service = newService(mapper);
SysTodoTask completed = new SysTodoTask();
completed.setTaskStatus(TodoTaskConstants.STATUS_COMPLETED);
when(mapper.completeTodoForAssignee(20L, 8L, 8L, "tester")).thenReturn(0);
when(mapper.selectTodoByIdForAssignee(20L, 8L)).thenReturn(completed);
assertTrue(service.complete(20L, 8L, "tester"));
}
@Test
void complete_shouldRejectCancelledTask() {
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
TodoTaskServiceImpl service = newService(mapper);
SysTodoTask cancelled = new SysTodoTask();
cancelled.setTaskStatus(TodoTaskConstants.STATUS_CANCELLED);
when(mapper.completeTodoForAssignee(20L, 8L, 8L, "tester")).thenReturn(0);
when(mapper.selectTodoByIdForAssignee(20L, 8L)).thenReturn(cancelled);
assertThrows(ServiceException.class, () -> service.complete(20L, 8L, "tester"));
}
private static TodoTaskCreateCommand createCommand() {
return TodoTaskCreateCommand.builder()
.taskType("INVOICE_REVIEW")
.title("司机开票申请")
.businessType("INVOICE_APPLY")
.businessId("100")
.routeName("InvoiceApplyDetail")
.assigneeUserId(8L)
.idempotentKey("INVOICE:100:8")
.createBy("tester")
.build();
}
private static TodoTaskServiceImpl newService(SysTodoTaskMapper mapper) {
TodoTaskServiceImpl service = new TodoTaskServiceImpl();
setField(service, "todoTaskMapper", mapper);
return service;
}
private static void setField(Object target, String fieldName, Object value) {
try {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
}

View File

@@ -0,0 +1,93 @@
package com.jsowell.web.controller.system;
import com.jsowell.common.core.domain.AjaxResult;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.service.TodoTaskService;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class SysTodoTaskControllerTest {
@Test
@SuppressWarnings("unchecked")
void unreadCount_shouldUseCurrentLoginUser() {
TodoTaskService service = mock(TodoTaskService.class);
TestController controller = newController(service, 8L, "tester");
when(service.countActive(8L)).thenReturn(5);
when(service.countUnread(8L)).thenReturn(3);
AjaxResult result = controller.unreadCount();
Map<String, Integer> data = (Map<String, Integer>) result.get(AjaxResult.DATA_TAG);
assertEquals(5, data.get("activeCount"));
assertEquals(3, data.get("unreadCount"));
}
@Test
void getInfo_shouldNotAcceptAnotherAssignee() {
TodoTaskService service = mock(TodoTaskService.class);
TestController controller = newController(service, 8L, "tester");
SysTodoTask task = new SysTodoTask();
task.setTodoId(20L);
when(service.getForAssignee(20L, 8L)).thenReturn(task);
AjaxResult result = controller.getInfo(20L);
assertSame(task, result.get(AjaxResult.DATA_TAG));
verify(service).getForAssignee(20L, 8L);
}
@Test
void complete_shouldPassCurrentUserAndOperator() {
TodoTaskService service = mock(TodoTaskService.class);
TestController controller = newController(service, 8L, "tester");
when(service.complete(20L, 8L, "tester")).thenReturn(true);
controller.complete(20L);
verify(service).complete(20L, 8L, "tester");
}
private static TestController newController(TodoTaskService service, Long userId, String username) {
TestController controller = new TestController(userId, username);
setField(controller, "todoTaskService", service);
return controller;
}
private static void setField(Object target, String fieldName, Object value) {
try {
Field field = SysTodoTaskController.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
private static class TestController extends SysTodoTaskController {
private final Long userId;
private final String username;
private TestController(Long userId, String username) {
this.userId = userId;
this.username = username;
}
@Override
public Long getUserId() {
return userId;
}
@Override
public String getUsername() {
return username;
}
}
}

View File

@@ -0,0 +1,21 @@
package com.jsowell.system.domain.dto;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
/**
* 取消待办请求。
*
* @author jsowell
*/
@Data
public class TodoTaskCancelRequest implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "取消原因不能为空")
@Size(max = 500, message = "取消原因不能超过500个字符")
private String reason;
}