6 Commits

Author SHA1 Message Date
jsowell
de312c934b 更新待办中心任务执行状态 2026-07-16 17:08:15 +08:00
jsowell
cbc2e3c721 接入司机开票申请待办流程 2026-07-16 17:06:49 +08:00
jsowell
974e68cf54 新增待办中心菜单和基础权限脚本 2026-07-16 16:50:18 +08:00
jsowell
f84ec43ceb 新增当前用户待办接口和权限测试 2026-07-16 16:20:34 +08:00
jsowell
181d4a5b40 新增待办任务通用领域服务 2026-07-16 16:11:38 +08:00
jsowell
e65017ab22 新增待办中心任务拆分和建表脚本 2026-07-16 15:56:58 +08:00
23 changed files with 1846 additions and 13 deletions

View File

@@ -0,0 +1,190 @@
# 待办事项中心开发任务跟踪
## 任务概述
- 需求来源:`docs/PRD-待办事项功能.md`
- 开发目标:先交付面向当前登录用户的通用待办闭环,再接入首期开票业务,最后补充管理和扩展能力。
- 后端仓库:当前根仓库,分支 `feature-待办中心`
- 前端仓库:`jsowell-charge-ui/` 独立 Git 仓库,当前分支 `dev`
- 状态定义:`todo` 未开始、`in_progress` 进行中、`blocked` 阻塞、`done` 已完成
## 优先级说明
- P0首期上线必需缺失时无法形成可用闭环。
- P1首期开票业务落地和运营管理必需可在通用能力稳定后接入。
- P2安全、运维和体验增强不阻塞首期核心流程。
- P3后续业务扩展和实时化能力。
## 关键设计决策
### 幂等键生命周期
PRD 同时要求“幂等键唯一”和“任务完成后允许同一业务重新产生任务”。MySQL 普通唯一索引无法同时满足这两个要求,因此采用双字段方案:
- `idempotent_key`:保存业务方传入的原始幂等键,历史记录不修改。
- `active_idempotent_key`:仅非终态任务保存原始幂等键,并建立唯一索引。
- 任务进入 `COMPLETED``CANCELLED``EXPIRED` 后,将 `active_idempotent_key` 置空。
该方案利用 MySQL 唯一索引允许多个 `NULL` 的特性,保证并发创建时只有一条有效待办,同时允许终态后再次创建。
### 数据权限边界
- 用户端接口不接收接收人 ID统一从当前登录态获取用户 ID。
- 用户端详情、已读、完成和取消更新均在 SQL 条件中携带 `assignee_user_id`,避免先查后改产生越权窗口。
- 管理端能力使用独立路径和权限标识,不复用用户端接口绕过接收人过滤。
### 前后端仓库边界
`jsowell-charge-ui/` 是独立 Git 仓库。后端和前端分别提交中文 commit不将前端目录作为未跟踪目录加入后端仓库。
## 执行批次
| 批次 | 范围 | 优先级 | 状态 | 验收结果 | Commit |
| --- | --- | --- | --- | --- | --- |
| B01 | 任务拆分、数据表和索引 | P0 | done | SQL 结构和索引评审通过 | `e65017ab2` |
| B02 | 实体、命令模型、枚举、Mapper、Service | P0 | done | 模块编译和 Mapper XML 校验通过 | `181d4a5b4` |
| B03 | 当前用户查询、已读、完成、取消接口及测试 | P0 | done | 全量依赖编译、9 个专项测试通过 | `f84ec43ce` |
| B04 | 前端 API、待办中心页面和路由跳转 | P0 | done | ESLint 和生产构建通过 | 前端 `ff6b890` |
| B05 | 首页待办卡片、数量展示和轮询刷新 | P0 | done | ESLint 和生产构建通过 | 前端 `0627061` |
| B06 | 菜单权限 SQL、联调和首期回归 | P0 | done | 菜单 SQL、构建和专项回归通过待测试库执行 SQL | `974e68cf5` |
| B07 | 平台采购开票待办接入 | P1 | blocked | 仓库内未找到平台采购开票模型、审核状态或接口入口 | - |
| B08 | 司机开票申请待办接入 | P1 | done | 全依赖编译、12 个专项测试通过 | `cbc2e3c72` |
| B09 | 管理端创建、分配、取消和隐藏能力 | P1 | todo | - | - |
| B10 | 归档清理、过期任务和操作审计增强 | P2 | todo | - | - |
| B11 | 路由白名单、性能压测和安全专项验证 | P2 | todo | - | - |
| B12 | 汇付、提现、清分、订单异常和实时推送扩展 | P3 | todo | - | - |
## 任务清单
### TODO-001 建表和索引
- 优先级P0
- 批次B01
- 状态done
- 目标:创建 `sys_todo_task` 表,覆盖任务状态、阅读状态、业务路由、接收人、幂等和审计字段。
- 验收标准:
- 有效任务幂等键具备数据库唯一约束。
- 当前用户首页和列表查询具备组合索引。
- 业务单据、运营商范围和历史清理具备查询索引。
- SQL 可重复交付,不包含真实环境配置或数据。
### TODO-002 通用领域模型和服务
- 优先级P0
- 批次B02
- 状态done
- 目标新增实体、创建命令、状态常量、Mapper 和统一 `TodoTaskService`
- 验收标准:
- 创建接口校验必要字段并设置默认状态、优先级和阅读状态。
- 并发重复创建返回已有有效任务,不产生重复记录。
- 完成、取消和过期操作释放有效幂等键并记录操作信息。
- 业务模块仅依赖 Service不直接依赖 Mapper。
### TODO-003 当前用户后端接口
- 优先级P0
- 批次B03
- 状态done
- 目标:实现未完成数量、首页列表、分页列表、详情、已读、全部已读、完成和取消接口。
- 验收标准:
- 所有用户端查询和更新强制使用当前登录用户 ID。
- 首页最多返回 10 条,默认 3 条,按优先级和创建时间倒序。
- 已完成、已取消和已过期任务不计入首页数量。
- 状态流转非法时返回明确业务错误。
- 核心 Service 和 Controller 权限边界有自动化测试。
### TODO-004 待办中心前端页面
- 优先级P0
- 批次B04
- 状态done
- 目标:增加 API 封装、待办中心列表、筛选、分页、批量已读和安全跳转。
- 验收标准:
- 支持任务状态、阅读状态、任务类型和关键字筛选。
- 未读任务有清晰样式,点击后标记已读。
- 跳转只使用后端返回的已配置路由名称,并附带 `businessType``businessId``todoId`
- 已完成记录默认保留展示,不执行物理删除。
### TODO-005 首页待办卡片
- 优先级P0
- 批次B05
- 状态done
- 目标:在运营商工作台首页展示未完成数量和最近 3 条任务。
- 验收标准:
- 数量超过 99 显示 `99+`
- 无任务时显示空状态。
- 页面打开立即加载,并按 60 秒轮询刷新。
- 完成任务或返回首页后主动刷新。
### TODO-006 菜单权限和联调
- 优先级P0
- 批次B06
- 状态done
- 当前进展:
- 菜单、查询、已读、完成和取消权限 SQL 已完成。
- 列表、查询和已读权限默认授予正常角色;完成和取消权限保留为按业务角色授权。
- 后端 9 个专项测试、前端专项 ESLint 和生产构建已通过。
- 待在测试库执行 `docs/sql/sys_todo_menu.sql` 后进行登录态 HTTP 联调。
- 目标:补充菜单与权限 SQL完成前后端构建和核心流程联调。
- 验收标准:
- 普通用户只看到本人待办。
- 无权限用户不能访问管理接口或完成无权业务。
-`sys_notice` 功能和菜单不受影响。
- 后端编译、相关测试、前端 lint/build 通过或记录环境阻塞原因。
### TODO-007 平台采购开票接入
- 优先级P1
- 批次B07
- 状态blocked
- 阻塞原因:当前仓库检索不到平台采购开票对应的数据库模型、审核拒绝/补充材料状态和业务接口,无法安全判断创建与完成时机。
- 目标:在平台采购开票审核拒绝或要求补充材料时创建待办,重新提交或平台关闭时完成。
- 验收标准:同一开票事件对同一接收人只产生一条有效待办,跳转参数和完成条件正确。
### TODO-008 司机开票申请接入
- 优先级P1
- 批次B08
- 状态in_progress
- 目标:收到司机开票申请时创建待办,审核通过、驳回或关闭时完成。
- 验收标准:创建和完成与开票业务事务一致,异常回滚时不产生脏任务状态。
### TODO-009 管理端异常任务处理
- 优先级P1
- 批次B09
- 状态todo
- 目标:实现管理员列表、人工创建、分配、取消和逻辑隐藏。
- 验收标准:接口具备独立权限标识和操作日志,普通用户不能访问。
### TODO-010 运维与安全增强
- 优先级P2
- 批次B10-B11
- 状态todo
- 目标补充过期、180 天归档清理、操作审计、路由白名单和性能验证。
- 验收标准:首页查询目标响应时间小于 500ms路由不能被请求参数任意覆盖历史任务按策略保留。
### TODO-011 后续业务扩展
- 优先级P3
- 批次B12
- 状态todo
- 目标:接入汇付开户、结算账户、提现、清分和订单异常,并评估 WebSocket/SSE。
- 验收标准:新增业务只通过统一待办 Service 接入,不修改通用数据权限和状态模型。
## 提交记录
每完成一个批次,在此追加:完成时间、验证命令、结果和 commit hash。
- 2026-07-16B01完成优先级拆分、任务跟踪文档和 `sys_todo_task` 建表脚本;通过 SQL 人工结构评审与 `git diff --check`commit `e65017ab2`
- 2026-07-16B01 环境进展:用户已在测试数据库创建 `sys_todo_task` 表,可供后续接口联调。
- 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 `f84ec43ce`
- 2026-07-16B04完成前端 API、待办中心筛选分页、批量已读和安全路由跳转新增文件专项 ESLint 通过,`NODE_OPTIONS=--openssl-legacy-provider npm run build:prd` 构建通过;前端 commit `ff6b890`
- 2026-07-16B05完成首页右侧待办卡片、`99+` 数量、最近 3 条、空状态、错误重试和 60 秒轮询;专项 ESLint 与生产构建通过;前端 commit `0627061`
- 2026-07-16B06完成幂等菜单/权限 SQL列表、查询和已读权限默认授予正常角色完成和取消权限需按业务角色显式授权后端 9 个专项测试再次通过,前端构建沿用 B05 验证结果commit `974e68cf5`
- 2026-07-16B07 分析:当前仓库未发现平台采购开票业务落点,需补充对应模块/表/接口信息后继续。
- 2026-07-16B08司机开票记录创建后按运营商部门树为正常后台用户幂等创建待办开票状态更新为已开票时自动完成删除/关闭记录时自动取消;业务与待办状态处于同一事务;全依赖编译和 12 个专项测试通过commit `cbc2e3c72`

142
docs/sql/sys_todo_menu.sql Normal file
View File

@@ -0,0 +1,142 @@
-- 待办中心菜单与基础权限
-- 执行效果:
-- 1. 新增可访问 /system/todo 的顶级菜单。
-- 2. 为所有正常角色授予列表、详情和已读权限;接口始终只返回当前登录用户数据。
-- 3. 完成和取消权限默认不授予普通角色,需按业务职责显式分配。
SET @todoMenuId := (
SELECT menu_id
FROM sys_menu
WHERE path = 'system/todo'
AND menu_type = 'C'
LIMIT 1
);
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, remark
)
SELECT
'待办中心', 0, 8, 'system/todo', 'system/todo/index', 1, 0,
'C', '0', '0', 'system:todo:list', 'list', 'system', sysdate(), '当前登录用户待办中心'
FROM DUAL
WHERE @todoMenuId IS NULL;
SET @todoMenuId := COALESCE(@todoMenuId, LAST_INSERT_ID());
SET @todoQueryMenuId := (
SELECT menu_id FROM sys_menu
WHERE parent_id = @todoMenuId AND perms = 'system:todo:query'
LIMIT 1
);
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, remark
)
SELECT
'待办查询', @todoMenuId, 1, '#', '', 1, 0,
'F', '0', '0', 'system:todo:query', '#', 'system', sysdate(), ''
FROM DUAL
WHERE @todoQueryMenuId IS NULL;
SET @todoQueryMenuId := COALESCE(@todoQueryMenuId, LAST_INSERT_ID());
SET @todoReadMenuId := (
SELECT menu_id FROM sys_menu
WHERE parent_id = @todoMenuId AND perms = 'system:todo:read'
LIMIT 1
);
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, remark
)
SELECT
'待办已读', @todoMenuId, 2, '#', '', 1, 0,
'F', '0', '0', 'system:todo:read', '#', 'system', sysdate(), ''
FROM DUAL
WHERE @todoReadMenuId IS NULL;
SET @todoReadMenuId := COALESCE(@todoReadMenuId, LAST_INSERT_ID());
SET @todoCompleteMenuId := (
SELECT menu_id FROM sys_menu
WHERE parent_id = @todoMenuId AND perms = 'system:todo:complete'
LIMIT 1
);
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, remark
)
SELECT
'待办完成', @todoMenuId, 3, '#', '', 1, 0,
'F', '0', '0', 'system:todo:complete', '#', 'system', sysdate(), '仅分配给具备对应业务处理权限的角色'
FROM DUAL
WHERE @todoCompleteMenuId IS NULL;
SET @todoCompleteMenuId := COALESCE(@todoCompleteMenuId, LAST_INSERT_ID());
SET @todoCancelMenuId := (
SELECT menu_id FROM sys_menu
WHERE parent_id = @todoMenuId AND perms = 'system:todo:cancel'
LIMIT 1
);
INSERT INTO sys_menu (
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, remark
)
SELECT
'待办取消', @todoMenuId, 4, '#', '', 1, 0,
'F', '0', '0', 'system:todo:cancel', '#', 'system', sysdate(), '仅分配给具备关闭任务权限的角色'
FROM DUAL
WHERE @todoCancelMenuId IS NULL;
SET @todoCancelMenuId := COALESCE(@todoCancelMenuId, LAST_INSERT_ID());
-- 所有正常角色均可查看、查询并标记本人的待办。
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT role_id, @todoMenuId
FROM sys_role role_info
WHERE role_info.status = '0'
AND role_info.del_flag = '0'
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu relation
WHERE relation.role_id = role_info.role_id
AND relation.menu_id = @todoMenuId
);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT role_id, @todoQueryMenuId
FROM sys_role role_info
WHERE role_info.status = '0'
AND role_info.del_flag = '0'
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu relation
WHERE relation.role_id = role_info.role_id
AND relation.menu_id = @todoQueryMenuId
);
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT role_id, @todoReadMenuId
FROM sys_role role_info
WHERE role_info.status = '0'
AND role_info.del_flag = '0'
AND NOT EXISTS (
SELECT 1 FROM sys_role_menu relation
WHERE relation.role_id = role_info.role_id
AND relation.menu_id = @todoReadMenuId
);
-- 完成/取消权限按业务角色手工授权示例:
-- INSERT INTO sys_role_menu (role_id, menu_id) VALUES (<业务处理角色ID>, @todoCompleteMenuId);
-- INSERT INTO sys_role_menu (role_id, menu_id) VALUES (<任务关闭角色ID>, @todoCancelMenuId);
SELECT
@todoMenuId AS todo_menu_id,
@todoQueryMenuId AS todo_query_menu_id,
@todoReadMenuId AS todo_read_menu_id,
@todoCompleteMenuId AS todo_complete_menu_id,
@todoCancelMenuId AS todo_cancel_menu_id;

View File

@@ -0,0 +1,39 @@
-- 待办事项中心任务表
-- 说明active_idempotent_key 仅在非终态任务中保留,终态时置空,
-- 用于同时满足“有效任务唯一”和“终态后允许重新创建”的要求。
CREATE TABLE `sys_todo_task` (
`todo_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '待办任务主键',
`task_type` varchar(64) NOT NULL COMMENT '待办类型编码',
`title` varchar(200) NOT NULL COMMENT '待办标题',
`summary` varchar(500) DEFAULT NULL COMMENT '待办摘要',
`content` text COMMENT '待办详细内容(纯文本)',
`business_type` varchar(64) NOT NULL COMMENT '业务类型编码',
`business_id` varchar(64) NOT NULL COMMENT '业务主键或单号',
`route_name` varchar(100) NOT NULL COMMENT '前端白名单路由名称',
`route_params` varchar(1000) DEFAULT NULL COMMENT '前端路由参数 JSON',
`assignee_user_id` bigint(20) NOT NULL COMMENT '接收用户 ID',
`assignee_merchant_id` varchar(64) DEFAULT NULL COMMENT '接收运营商 ID',
`priority` tinyint(3) NOT NULL DEFAULT '1' COMMENT '优先级1普通 2重要 3紧急',
`task_status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态PENDING PROCESSING COMPLETED CANCELLED EXPIRED',
`read_status` char(1) NOT NULL DEFAULT '0' COMMENT '阅读状态0未读 1已读',
`read_time` datetime DEFAULT NULL COMMENT '阅读时间',
`due_time` datetime DEFAULT NULL COMMENT '截止时间',
`completed_time` datetime DEFAULT NULL COMMENT '完成时间',
`completed_by` bigint(20) DEFAULT NULL COMMENT '完成人用户 ID',
`cancel_reason` varchar(500) DEFAULT NULL COMMENT '取消原因',
`idempotent_key` varchar(200) NOT NULL COMMENT '业务原始幂等键',
`active_idempotent_key` varchar(200) DEFAULT NULL COMMENT '有效任务幂等键,终态置空',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除0正常 2删除',
PRIMARY KEY (`todo_id`) USING BTREE,
UNIQUE KEY `uk_todo_active_idempotent` (`active_idempotent_key`) USING BTREE,
KEY `idx_todo_user_status` (`assignee_user_id`, `task_status`, `read_status`, `priority`, `create_time`) USING BTREE,
KEY `idx_todo_merchant_status` (`assignee_merchant_id`, `task_status`, `create_time`) USING BTREE,
KEY `idx_todo_business` (`business_type`, `business_id`) USING BTREE,
KEY `idx_todo_history_cleanup` (`del_flag`, `task_status`, `completed_time`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户待办任务表';

View File

@@ -90,7 +90,7 @@ public class OrderInvoiceRecordController extends BaseController {
@Log(title = "申请开票", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody OrderInvoiceRecord orderInvoiceRecord) {
return toAjax(orderInvoiceRecordService.updateOrderInvoiceRecord(orderInvoiceRecord));
return toAjax(orderInvoiceRecordService.updateOrderInvoiceRecord(orderInvoiceRecord, getUserId(), getUsername()));
}
/**
@@ -100,6 +100,6 @@ public class OrderInvoiceRecordController extends BaseController {
@Log(title = "申请开票", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Integer[] ids) {
return toAjax(orderInvoiceRecordService.deleteOrderInvoiceRecordByIds(ids));
return toAjax(orderInvoiceRecordService.deleteOrderInvoiceRecordByIds(ids, getUsername()));
}
}

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,117 @@
package com.jsowell.pile.service;
import com.jsowell.pile.domain.OrderInvoiceRecord;
import com.jsowell.pile.domain.PileMerchantInfo;
import com.jsowell.pile.mapper.OrderInvoiceRecordMapper;
import com.jsowell.pile.service.impl.OrderInvoiceRecordServiceImpl;
import com.jsowell.system.constant.TodoTaskConstants;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
import com.jsowell.system.service.TodoTaskAssigneeService;
import com.jsowell.system.service.TodoTaskService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class OrderInvoiceRecordTodoTest {
@Test
void insert_shouldCreateTodoForEachMerchantUser() {
OrderInvoiceRecordMapper mapper = mock(OrderInvoiceRecordMapper.class);
PileMerchantInfoService merchantService = mock(PileMerchantInfoService.class);
TodoTaskAssigneeService assigneeService = mock(TodoTaskAssigneeService.class);
TodoTaskService todoTaskService = mock(TodoTaskService.class);
OrderInvoiceRecordServiceImpl service = newService(mapper, merchantService, assigneeService, todoTaskService);
OrderInvoiceRecord invoiceRecord = new OrderInvoiceRecord();
invoiceRecord.setMerchantId("100");
doAnswer(invocation -> {
OrderInvoiceRecord record = invocation.getArgument(0);
record.setId(55);
return 1;
}).when(mapper).insertOrderInvoiceRecord(invoiceRecord);
PileMerchantInfo merchantInfo = new PileMerchantInfo();
merchantInfo.setDeptId("200");
when(merchantService.selectPileMerchantInfoById(100L)).thenReturn(merchantInfo);
when(assigneeService.findActiveUserIdsByDeptTree(200L)).thenReturn(Arrays.asList(8L, 9L));
when(todoTaskService.createTask(any(TodoTaskCreateCommand.class))).thenReturn(new SysTodoTask());
service.insertOrderInvoiceRecord(invoiceRecord);
ArgumentCaptor<TodoTaskCreateCommand> captor = ArgumentCaptor.forClass(TodoTaskCreateCommand.class);
verify(todoTaskService, times(2)).createTask(captor.capture());
List<TodoTaskCreateCommand> commands = captor.getAllValues();
assertEquals(Arrays.asList(8L, 9L), Arrays.asList(
commands.get(0).getAssigneeUserId(), commands.get(1).getAssigneeUserId()));
assertEquals("ORDER_INVOICE_RECORD", commands.get(0).getBusinessType());
assertEquals("55", commands.get(0).getBusinessId());
assertEquals("invoiceDetail", commands.get(0).getRouteName());
assertEquals(TodoTaskConstants.PRIORITY_IMPORTANT, commands.get(0).getPriority());
assertEquals("INVOICE_REVIEW:ORDER_INVOICE_RECORD:55:8", commands.get(0).getIdempotentKey());
}
@Test
void updateCompleted_shouldCompleteBusinessTodos() {
OrderInvoiceRecordMapper mapper = mock(OrderInvoiceRecordMapper.class);
TodoTaskService todoTaskService = mock(TodoTaskService.class);
OrderInvoiceRecordServiceImpl service = newService(mapper, mock(PileMerchantInfoService.class),
mock(TodoTaskAssigneeService.class), todoTaskService);
OrderInvoiceRecord invoiceRecord = new OrderInvoiceRecord();
invoiceRecord.setId(55);
invoiceRecord.setStatus("1");
when(mapper.updateOrderInvoiceRecord(invoiceRecord)).thenReturn(1);
service.updateOrderInvoiceRecord(invoiceRecord, 1L, "admin");
verify(todoTaskService).completeByBusiness("ORDER_INVOICE_RECORD", "55", null, 1L, "admin");
}
@Test
void delete_shouldCancelBusinessTodos() {
OrderInvoiceRecordMapper mapper = mock(OrderInvoiceRecordMapper.class);
TodoTaskService todoTaskService = mock(TodoTaskService.class);
OrderInvoiceRecordServiceImpl service = newService(mapper, mock(PileMerchantInfoService.class),
mock(TodoTaskAssigneeService.class), todoTaskService);
Integer[] ids = { 55, 56 };
when(mapper.deleteOrderInvoiceRecordByIds(ids)).thenReturn(2);
service.deleteOrderInvoiceRecordByIds(ids, "admin");
verify(todoTaskService).cancelByBusiness("ORDER_INVOICE_RECORD", "55", null,
"admin", "开票申请已关闭");
verify(todoTaskService).cancelByBusiness("ORDER_INVOICE_RECORD", "56", null,
"admin", "开票申请已关闭");
}
private static OrderInvoiceRecordServiceImpl newService(OrderInvoiceRecordMapper mapper,
PileMerchantInfoService merchantService, TodoTaskAssigneeService assigneeService,
TodoTaskService todoTaskService) {
OrderInvoiceRecordServiceImpl service = new OrderInvoiceRecordServiceImpl();
setField(service, "orderInvoiceRecordMapper", mapper);
setField(service, "pileMerchantInfoService", merchantService);
setField(service, "todoTaskAssigneeService", assigneeService);
setField(service, "todoTaskService", todoTaskService);
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,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

@@ -61,7 +61,7 @@ public interface OrderInvoiceRecordService {
* @param orderInvoiceRecord 申请开票
* @return 结果
*/
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord);
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord, Long operatorId, String operatorName);
/**
* 批量删除申请开票
@@ -69,7 +69,7 @@ public interface OrderInvoiceRecordService {
* @param ids 需要删除的申请开票主键集合
* @return 结果
*/
public int deleteOrderInvoiceRecordByIds(Integer[] ids);
public int deleteOrderInvoiceRecordByIds(Integer[] ids, String operatorName);
/**
* 删除申请开票信息
@@ -77,5 +77,5 @@ public interface OrderInvoiceRecordService {
* @param id 申请开票主键
* @return 结果
*/
public int deleteOrderInvoiceRecordById(Integer id);
public int deleteOrderInvoiceRecordById(Integer id, String operatorName);
}

View File

@@ -2,21 +2,31 @@ package com.jsowell.pile.service.impl;
import com.google.common.collect.Lists;
import com.jsowell.common.core.domain.vo.AuthorizedDeptVO;
import com.jsowell.common.util.StringUtils;
import com.jsowell.pile.domain.OrderInvoiceRecord;
import com.jsowell.pile.domain.PileMerchantInfo;
import com.jsowell.pile.dto.GetInvoiceInfoDTO;
import com.jsowell.pile.dto.QueryInvoiceRecordDTO;
import com.jsowell.pile.mapper.OrderInvoiceRecordMapper;
import com.jsowell.pile.service.MemberInvoiceTitleService;
import com.jsowell.pile.service.OrderInvoiceRecordService;
import com.jsowell.pile.service.OrderBasicInfoService;
import com.jsowell.pile.service.PileMerchantInfoService;
import com.jsowell.pile.util.UserUtils;
import com.jsowell.pile.vo.base.OrderAmountDetailVO;
import com.jsowell.pile.vo.uniapp.customer.InvoiceTitleVO;
import com.jsowell.pile.vo.web.InvoiceRecordVO;
import com.jsowell.pile.vo.web.OrderInvoiceRecordVO;
import com.jsowell.system.constant.TodoTaskConstants;
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
import com.jsowell.system.service.TodoTaskAssigneeService;
import com.jsowell.system.service.TodoTaskService;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -30,6 +40,11 @@ import java.util.List;
*/
@Service
public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService {
private static final Logger log = LoggerFactory.getLogger(OrderInvoiceRecordServiceImpl.class);
private static final String TODO_TASK_TYPE = "INVOICE_REVIEW";
private static final String TODO_BUSINESS_TYPE = "ORDER_INVOICE_RECORD";
private static final String TODO_ROUTE_NAME = "invoiceDetail";
@Autowired
private OrderInvoiceRecordMapper orderInvoiceRecordMapper;
@@ -39,6 +54,15 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private PileMerchantInfoService pileMerchantInfoService;
@Autowired
private TodoTaskAssigneeService todoTaskAssigneeService;
@Autowired
private TodoTaskService todoTaskService;
/**
* 查询申请开票
*
@@ -132,8 +156,13 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord) {
return orderInvoiceRecordMapper.insertOrderInvoiceRecord(orderInvoiceRecord);
int rows = orderInvoiceRecordMapper.insertOrderInvoiceRecord(orderInvoiceRecord);
if (rows > 0) {
createInvoiceReviewTodos(orderInvoiceRecord);
}
return rows;
}
/**
@@ -143,8 +172,14 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
* @return 结果
*/
@Override
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord) {
return orderInvoiceRecordMapper.updateOrderInvoiceRecord(orderInvoiceRecord);
@Transactional(rollbackFor = Exception.class)
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord, Long operatorId, String operatorName) {
int rows = orderInvoiceRecordMapper.updateOrderInvoiceRecord(orderInvoiceRecord);
if (rows > 0 && "1".equals(orderInvoiceRecord.getStatus())) {
todoTaskService.completeByBusiness(TODO_BUSINESS_TYPE, String.valueOf(orderInvoiceRecord.getId()),
null, operatorId, operatorName);
}
return rows;
}
/**
@@ -154,8 +189,18 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
* @return 结果
*/
@Override
public int deleteOrderInvoiceRecordByIds(Integer[] ids) {
return orderInvoiceRecordMapper.deleteOrderInvoiceRecordByIds(ids);
@Transactional(rollbackFor = Exception.class)
public int deleteOrderInvoiceRecordByIds(Integer[] ids, String operatorName) {
int rows = orderInvoiceRecordMapper.deleteOrderInvoiceRecordByIds(ids);
if (rows > 0 && ids != null) {
for (Integer id : ids) {
if (id != null) {
todoTaskService.cancelByBusiness(TODO_BUSINESS_TYPE, String.valueOf(id), null,
operatorName, "开票申请已关闭");
}
}
}
return rows;
}
/**
@@ -165,7 +210,71 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
* @return 结果
*/
@Override
public int deleteOrderInvoiceRecordById(Integer id) {
return orderInvoiceRecordMapper.deleteOrderInvoiceRecordById(id);
@Transactional(rollbackFor = Exception.class)
public int deleteOrderInvoiceRecordById(Integer id, String operatorName) {
int rows = orderInvoiceRecordMapper.deleteOrderInvoiceRecordById(id);
if (rows > 0) {
todoTaskService.cancelByBusiness(TODO_BUSINESS_TYPE, String.valueOf(id), null,
operatorName, "开票申请已关闭");
}
return rows;
}
private void createInvoiceReviewTodos(OrderInvoiceRecord invoiceRecord) {
if (invoiceRecord.getId() == null || StringUtils.isBlank(invoiceRecord.getMerchantId())) {
log.warn("开票申请缺少主键或运营商无法创建待办invoiceId={}, merchantId={}",
invoiceRecord.getId(), invoiceRecord.getMerchantId());
return;
}
PileMerchantInfo merchantInfo;
try {
merchantInfo = pileMerchantInfoService.selectPileMerchantInfoById(Long.parseLong(invoiceRecord.getMerchantId()));
} catch (NumberFormatException exception) {
log.warn("开票申请运营商 ID 非法无法创建待办invoiceId={}, merchantId={}",
invoiceRecord.getId(), invoiceRecord.getMerchantId());
return;
}
if (merchantInfo == null || StringUtils.isBlank(merchantInfo.getDeptId())) {
log.warn("开票申请运营商未配置部门无法创建待办invoiceId={}, merchantId={}",
invoiceRecord.getId(), invoiceRecord.getMerchantId());
return;
}
Long merchantDeptId;
try {
merchantDeptId = Long.parseLong(merchantInfo.getDeptId());
} catch (NumberFormatException exception) {
log.warn("开票申请运营商部门 ID 非法无法创建待办invoiceId={}, deptId={}",
invoiceRecord.getId(), merchantInfo.getDeptId());
return;
}
List<Long> assigneeUserIds = todoTaskAssigneeService.findActiveUserIdsByDeptTree(merchantDeptId);
if (CollectionUtils.isEmpty(assigneeUserIds)) {
log.warn("开票申请运营商部门下没有正常用户无法创建待办invoiceId={}, deptId={}",
invoiceRecord.getId(), merchantInfo.getDeptId());
return;
}
for (Long assigneeUserId : assigneeUserIds) {
String idempotentKey = TODO_TASK_TYPE + ":" + TODO_BUSINESS_TYPE + ":"
+ invoiceRecord.getId() + ":" + assigneeUserId;
todoTaskService.createTask(TodoTaskCreateCommand.builder()
.taskType(TODO_TASK_TYPE)
.title("司机开票申请")
.summary("有新的司机开票申请需要处理,申请单号:" + invoiceRecord.getId())
.businessType(TODO_BUSINESS_TYPE)
.businessId(String.valueOf(invoiceRecord.getId()))
.routeName(TODO_ROUTE_NAME)
.routeParams("{\"params\":{\"id\":\"" + invoiceRecord.getId() + "\"}}")
.assigneeUserId(assigneeUserId)
.assigneeMerchantId(invoiceRecord.getMerchantId())
.priority(TodoTaskConstants.PRIORITY_IMPORTANT)
.idempotentKey(idempotentKey)
.createBy("invoice-system")
.build());
}
}
}

View File

@@ -0,0 +1,44 @@
package com.jsowell.system.constant;
/**
* 待办任务常量。
*
* @author jsowell
*/
public final class TodoTaskConstants {
private TodoTaskConstants() {
}
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_PROCESSING = "PROCESSING";
public static final String STATUS_COMPLETED = "COMPLETED";
public static final String STATUS_CANCELLED = "CANCELLED";
public static final String STATUS_EXPIRED = "EXPIRED";
public static final String STATUS_ACTIVE = "ACTIVE";
public static final String READ_UNREAD = "0";
public static final String READ_READ = "1";
public static final int PRIORITY_NORMAL = 1;
public static final int PRIORITY_IMPORTANT = 2;
public static final int PRIORITY_URGENT = 3;
public static final int DEFAULT_HOME_LIMIT = 3;
public static final int MAX_HOME_LIMIT = 10;
public static boolean isActiveStatus(String status) {
return STATUS_PENDING.equals(status) || STATUS_PROCESSING.equals(status);
}
public static boolean isTerminalStatus(String status) {
return STATUS_COMPLETED.equals(status)
|| STATUS_CANCELLED.equals(status)
|| STATUS_EXPIRED.equals(status);
}
public static boolean isSupportedStatus(String status) {
return STATUS_ACTIVE.equals(status)
|| isActiveStatus(status)
|| isTerminalStatus(status);
}
}

View File

@@ -0,0 +1,47 @@
package com.jsowell.system.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.jsowell.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* 用户待办任务表 sys_todo_task。
*
* @author jsowell
*/
@Data
public class SysTodoTask extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long todoId;
private String taskType;
private String title;
private String summary;
private String content;
private String businessType;
private String businessId;
private String routeName;
private String routeParams;
private Long assigneeUserId;
private String assigneeMerchantId;
private Integer priority;
private String taskStatus;
private String readStatus;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date readTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date dueTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date completedTime;
private Long completedBy;
private String cancelReason;
private String idempotentKey;
private String activeIdempotentKey;
private String delFlag;
}

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;
}

View File

@@ -0,0 +1,38 @@
package com.jsowell.system.domain.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* 创建待办任务命令。
*
* @author jsowell
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TodoTaskCreateCommand implements Serializable {
private static final long serialVersionUID = 1L;
private String taskType;
private String title;
private String summary;
private String content;
private String businessType;
private String businessId;
private String routeName;
private String routeParams;
private Long assigneeUserId;
private String assigneeMerchantId;
private Integer priority;
private Date dueTime;
private String idempotentKey;
private String createBy;
private String remark;
}

View File

@@ -0,0 +1,23 @@
package com.jsowell.system.domain.dto;
import lombok.Data;
import java.io.Serializable;
/**
* 当前用户待办查询条件。
*
* @author jsowell
*/
@Data
public class TodoTaskQuery implements Serializable {
private static final long serialVersionUID = 1L;
/** 由服务端覆盖,不信任请求参数。 */
private Long assigneeUserId;
private String taskStatus;
private String readStatus;
private String taskType;
private String keyword;
private Boolean hideCompleted;
}

View File

@@ -0,0 +1,65 @@
package com.jsowell.system.mapper;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.domain.dto.TodoTaskQuery;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 待办任务数据层。
*
* @author jsowell
*/
public interface SysTodoTaskMapper {
SysTodoTask selectTodoById(@Param("todoId") Long todoId);
SysTodoTask selectTodoByIdForAssignee(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId);
SysTodoTask selectActiveByIdempotentKey(@Param("activeIdempotentKey") String activeIdempotentKey);
List<SysTodoTask> selectTodoList(TodoTaskQuery query);
List<SysTodoTask> selectHomeList(@Param("assigneeUserId") Long assigneeUserId,
@Param("limit") int limit);
int countActive(@Param("assigneeUserId") Long assigneeUserId);
int countUnread(@Param("assigneeUserId") Long assigneeUserId);
int insertTodoTask(SysTodoTask todoTask);
int markTodoRead(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("updateBy") String updateBy);
int markAllRead(@Param("assigneeUserId") Long assigneeUserId,
@Param("updateBy") String updateBy);
int completeTodoForAssignee(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("completedBy") Long completedBy,
@Param("updateBy") String updateBy);
int completeTodoByBusiness(@Param("businessType") String businessType,
@Param("businessId") String businessId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("completedBy") Long completedBy,
@Param("updateBy") String updateBy);
int cancelTodoForAssignee(@Param("todoId") Long todoId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("reason") String reason,
@Param("updateBy") String updateBy);
int cancelTodo(@Param("todoId") Long todoId,
@Param("reason") String reason,
@Param("updateBy") String updateBy);
int cancelTodoByBusiness(@Param("businessType") String businessType,
@Param("businessId") String businessId,
@Param("assigneeUserId") Long assigneeUserId,
@Param("reason") String reason,
@Param("updateBy") String updateBy);
}

View File

@@ -19,6 +19,14 @@ public interface SysUserMapper {
*/
List<SysUser> selectUserList(SysUser sysUser);
/**
* 查询部门及下级部门的正常用户 ID。
*
* @param deptId 部门 ID
* @return 用户 ID 集合
*/
List<Long> selectActiveUserIdsByDeptTree(@Param("deptId") Long deptId);
/**
* 根据条件分页查询已配用户角色列表
*

View File

@@ -0,0 +1,12 @@
package com.jsowell.system.service;
import java.util.List;
/**
* 待办接收人解析服务。
*
* @author jsowell
*/
public interface TodoTaskAssigneeService {
List<Long> findActiveUserIdsByDeptTree(Long deptId);
}

View File

@@ -0,0 +1,42 @@
package com.jsowell.system.service;
import com.jsowell.system.domain.SysTodoTask;
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
import com.jsowell.system.domain.dto.TodoTaskQuery;
import java.util.List;
/**
* 统一待办任务服务。
*
* @author jsowell
*/
public interface TodoTaskService {
SysTodoTask createTask(TodoTaskCreateCommand command);
SysTodoTask getForAssignee(Long todoId, Long assigneeUserId);
List<SysTodoTask> listForAssignee(TodoTaskQuery query, Long assigneeUserId);
List<SysTodoTask> homeList(Long assigneeUserId, Integer limit);
int countActive(Long assigneeUserId);
int countUnread(Long assigneeUserId);
boolean markRead(Long todoId, Long assigneeUserId, String operatorName);
int markAllRead(Long assigneeUserId, String operatorName);
boolean complete(Long todoId, Long assigneeUserId, String operatorName);
int completeByBusiness(String businessType, String businessId, Long assigneeUserId,
Long operatorId, String operatorName);
boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason);
boolean cancel(Long todoId, Long operatorId, String operatorName, String reason);
int cancelByBusiness(String businessType, String businessId, Long assigneeUserId,
String operatorName, String reason);
}

View File

@@ -0,0 +1,28 @@
package com.jsowell.system.service.impl;
import com.jsowell.common.exception.ServiceException;
import com.jsowell.system.mapper.SysUserMapper;
import com.jsowell.system.service.TodoTaskAssigneeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 待办接收人解析服务实现。
*
* @author jsowell
*/
@Service
public class TodoTaskAssigneeServiceImpl implements TodoTaskAssigneeService {
@Autowired
private SysUserMapper userMapper;
@Override
public List<Long> findActiveUserIdsByDeptTree(Long deptId) {
if (deptId == null || deptId <= 0) {
throw new ServiceException("待办接收部门不能为空");
}
return userMapper.selectActiveUserIdsByDeptTree(deptId);
}
}

View File

@@ -0,0 +1,300 @@
package com.jsowell.system.service.impl;
import com.jsowell.common.exception.ServiceException;
import com.jsowell.common.util.StringUtils;
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.TodoTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 统一待办任务服务实现。
*
* @author jsowell
*/
@Service
public class TodoTaskServiceImpl implements TodoTaskService {
private static final String SYSTEM_OPERATOR = "system";
@Autowired
private SysTodoTaskMapper todoTaskMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public SysTodoTask createTask(TodoTaskCreateCommand command) {
if (command == null) {
throw new ServiceException("待办创建参数不能为空");
}
SysTodoTask todoTask = buildTask(command);
SysTodoTask existingTask = todoTaskMapper.selectActiveByIdempotentKey(todoTask.getActiveIdempotentKey());
if (existingTask != null) {
return existingTask;
}
try {
todoTaskMapper.insertTodoTask(todoTask);
return todoTask;
} catch (DuplicateKeyException exception) {
existingTask = todoTaskMapper.selectActiveByIdempotentKey(todoTask.getActiveIdempotentKey());
if (existingTask != null) {
return existingTask;
}
throw exception;
}
}
@Override
public SysTodoTask getForAssignee(Long todoId, Long assigneeUserId) {
validateIdentity(todoId, assigneeUserId);
SysTodoTask todoTask = todoTaskMapper.selectTodoByIdForAssignee(todoId, assigneeUserId);
if (todoTask == null) {
throw new ServiceException("待办任务不存在或无权访问");
}
return todoTask;
}
@Override
public List<SysTodoTask> listForAssignee(TodoTaskQuery query, Long assigneeUserId) {
validateUserId(assigneeUserId);
TodoTaskQuery safeQuery = query == null ? new TodoTaskQuery() : query;
validateQuery(safeQuery);
safeQuery.setAssigneeUserId(assigneeUserId);
return todoTaskMapper.selectTodoList(safeQuery);
}
@Override
public List<SysTodoTask> homeList(Long assigneeUserId, Integer limit) {
validateUserId(assigneeUserId);
int safeLimit = limit == null ? TodoTaskConstants.DEFAULT_HOME_LIMIT : limit;
if (safeLimit < 1) {
safeLimit = TodoTaskConstants.DEFAULT_HOME_LIMIT;
}
if (safeLimit > TodoTaskConstants.MAX_HOME_LIMIT) {
safeLimit = TodoTaskConstants.MAX_HOME_LIMIT;
}
return todoTaskMapper.selectHomeList(assigneeUserId, safeLimit);
}
@Override
public int countActive(Long assigneeUserId) {
validateUserId(assigneeUserId);
return todoTaskMapper.countActive(assigneeUserId);
}
@Override
public int countUnread(Long assigneeUserId) {
validateUserId(assigneeUserId);
return todoTaskMapper.countUnread(assigneeUserId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean markRead(Long todoId, Long assigneeUserId, String operatorName) {
validateIdentity(todoId, assigneeUserId);
int rows = todoTaskMapper.markTodoRead(todoId, assigneeUserId, normalizeOperator(operatorName, assigneeUserId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
return TodoTaskConstants.READ_READ.equals(todoTask.getReadStatus());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int markAllRead(Long assigneeUserId, String operatorName) {
validateUserId(assigneeUserId);
return todoTaskMapper.markAllRead(assigneeUserId, normalizeOperator(operatorName, assigneeUserId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean complete(Long todoId, Long assigneeUserId, String operatorName) {
validateIdentity(todoId, assigneeUserId);
int rows = todoTaskMapper.completeTodoForAssignee(todoId, assigneeUserId, assigneeUserId,
normalizeOperator(operatorName, assigneeUserId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
if (TodoTaskConstants.STATUS_COMPLETED.equals(todoTask.getTaskStatus())) {
return true;
}
throw new ServiceException("当前待办状态不允许完成");
}
@Override
@Transactional(rollbackFor = Exception.class)
public int completeByBusiness(String businessType, String businessId, Long assigneeUserId,
Long operatorId, String operatorName) {
String safeBusinessType = normalizeRequired(businessType, "业务类型", 64);
String safeBusinessId = normalizeRequired(businessId, "业务主键", 64);
if (assigneeUserId != null) {
validateUserId(assigneeUserId);
}
if (operatorId == null || operatorId <= 0) {
throw new ServiceException("待办完成人不能为空");
}
return todoTaskMapper.completeTodoByBusiness(safeBusinessType, safeBusinessId, assigneeUserId,
operatorId, normalizeOperator(operatorName, operatorId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason) {
validateIdentity(todoId, assigneeUserId);
String safeReason = normalizeRequired(reason, "取消原因", 500);
int rows = todoTaskMapper.cancelTodoForAssignee(todoId, assigneeUserId, safeReason,
normalizeOperator(operatorName, assigneeUserId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
if (TodoTaskConstants.STATUS_CANCELLED.equals(todoTask.getTaskStatus())) {
return true;
}
throw new ServiceException("当前待办状态不允许取消");
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancel(Long todoId, Long operatorId, String operatorName, String reason) {
if (todoId == null || todoId <= 0) {
throw new ServiceException("待办任务 ID 不能为空");
}
if (operatorId == null || operatorId <= 0) {
throw new ServiceException("取消操作人不能为空");
}
String safeReason = normalizeRequired(reason, "取消原因", 500);
int rows = todoTaskMapper.cancelTodo(todoId, safeReason, normalizeOperator(operatorName, operatorId));
if (rows > 0) {
return true;
}
SysTodoTask todoTask = todoTaskMapper.selectTodoById(todoId);
if (todoTask == null) {
throw new ServiceException("待办任务不存在");
}
if (TodoTaskConstants.STATUS_CANCELLED.equals(todoTask.getTaskStatus())) {
return true;
}
throw new ServiceException("当前待办状态不允许取消");
}
@Override
@Transactional(rollbackFor = Exception.class)
public int cancelByBusiness(String businessType, String businessId, Long assigneeUserId,
String operatorName, String reason) {
String safeBusinessType = normalizeRequired(businessType, "业务类型", 64);
String safeBusinessId = normalizeRequired(businessId, "业务主键", 64);
if (assigneeUserId != null) {
validateUserId(assigneeUserId);
}
String safeReason = normalizeRequired(reason, "取消原因", 500);
return todoTaskMapper.cancelTodoByBusiness(safeBusinessType, safeBusinessId, assigneeUserId,
safeReason, normalizeOperator(operatorName, null));
}
private SysTodoTask buildTask(TodoTaskCreateCommand command) {
if (command.getAssigneeUserId() == null || command.getAssigneeUserId() <= 0) {
throw new ServiceException("待办接收用户不能为空");
}
Integer priority = command.getPriority() == null
? TodoTaskConstants.PRIORITY_NORMAL : command.getPriority();
if (priority < TodoTaskConstants.PRIORITY_NORMAL || priority > TodoTaskConstants.PRIORITY_URGENT) {
throw new ServiceException("待办优先级只能为 1、2 或 3");
}
SysTodoTask todoTask = new SysTodoTask();
todoTask.setTaskType(normalizeRequired(command.getTaskType(), "待办类型", 64));
todoTask.setTitle(normalizeRequired(command.getTitle(), "待办标题", 200));
todoTask.setSummary(normalizeOptional(command.getSummary(), "待办摘要", 500));
todoTask.setContent(normalizeOptional(command.getContent(), "待办内容", 20000));
todoTask.setBusinessType(normalizeRequired(command.getBusinessType(), "业务类型", 64));
todoTask.setBusinessId(normalizeRequired(command.getBusinessId(), "业务主键", 64));
todoTask.setRouteName(normalizeRequired(command.getRouteName(), "业务路由", 100));
todoTask.setRouteParams(normalizeOptional(command.getRouteParams(), "路由参数", 1000));
todoTask.setAssigneeUserId(command.getAssigneeUserId());
todoTask.setAssigneeMerchantId(normalizeOptional(command.getAssigneeMerchantId(), "运营商 ID", 64));
todoTask.setPriority(priority);
todoTask.setTaskStatus(TodoTaskConstants.STATUS_PENDING);
todoTask.setReadStatus(TodoTaskConstants.READ_UNREAD);
todoTask.setDueTime(command.getDueTime());
todoTask.setIdempotentKey(normalizeRequired(command.getIdempotentKey(), "幂等键", 200));
todoTask.setActiveIdempotentKey(todoTask.getIdempotentKey());
todoTask.setCreateBy(normalizeOperator(command.getCreateBy(), null));
todoTask.setRemark(normalizeOptional(command.getRemark(), "备注", 500));
todoTask.setDelFlag("0");
return todoTask;
}
private void validateQuery(TodoTaskQuery query) {
if (StringUtils.isNotEmpty(query.getTaskStatus())
&& !TodoTaskConstants.isSupportedStatus(query.getTaskStatus())) {
throw new ServiceException("不支持的待办状态筛选条件");
}
if (StringUtils.isNotEmpty(query.getReadStatus())
&& !TodoTaskConstants.READ_UNREAD.equals(query.getReadStatus())
&& !TodoTaskConstants.READ_READ.equals(query.getReadStatus())) {
throw new ServiceException("不支持的阅读状态筛选条件");
}
query.setTaskType(normalizeOptional(query.getTaskType(), "待办类型", 64));
query.setKeyword(normalizeOptional(query.getKeyword(), "关键字", 100));
}
private void validateIdentity(Long todoId, Long assigneeUserId) {
if (todoId == null || todoId <= 0) {
throw new ServiceException("待办任务 ID 不能为空");
}
validateUserId(assigneeUserId);
}
private void validateUserId(Long userId) {
if (userId == null || userId <= 0) {
throw new ServiceException("待办接收用户不能为空");
}
}
private String normalizeRequired(String value, String fieldName, int maxLength) {
String safeValue = StringUtils.trim(value);
if (StringUtils.isEmpty(safeValue)) {
throw new ServiceException(fieldName + "不能为空");
}
if (safeValue.length() > maxLength) {
throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符");
}
return safeValue;
}
private String normalizeOptional(String value, String fieldName, int maxLength) {
if (StringUtils.isEmpty(value)) {
return null;
}
String safeValue = value.trim();
if (safeValue.length() > maxLength) {
throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符");
}
return safeValue;
}
private String normalizeOperator(String operatorName, Long operatorId) {
String safeOperator = StringUtils.isNotEmpty(operatorName)
? operatorName.trim()
: (operatorId == null ? SYSTEM_OPERATOR : String.valueOf(operatorId));
if (safeOperator.length() > 64) {
throw new ServiceException("操作人名称不能超过64个字符");
}
return safeOperator;
}
}

View File

@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jsowell.system.mapper.SysTodoTaskMapper">
<resultMap type="SysTodoTask" id="SysTodoTaskResult">
<id property="todoId" column="todo_id" />
<result property="taskType" column="task_type" />
<result property="title" column="title" />
<result property="summary" column="summary" />
<result property="content" column="content" />
<result property="businessType" column="business_type" />
<result property="businessId" column="business_id" />
<result property="routeName" column="route_name" />
<result property="routeParams" column="route_params" />
<result property="assigneeUserId" column="assignee_user_id" />
<result property="assigneeMerchantId" column="assignee_merchant_id" />
<result property="priority" column="priority" />
<result property="taskStatus" column="task_status" />
<result property="readStatus" column="read_status" />
<result property="readTime" column="read_time" />
<result property="dueTime" column="due_time" />
<result property="completedTime" column="completed_time" />
<result property="completedBy" column="completed_by" />
<result property="cancelReason" column="cancel_reason" />
<result property="idempotentKey" column="idempotent_key" />
<result property="activeIdempotentKey" column="active_idempotent_key" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="delFlag" column="del_flag" />
</resultMap>
<sql id="selectTodoVo">
select todo_id, task_type, title, summary, cast(content as char) as content,
business_type, business_id, route_name, route_params,
assignee_user_id, assignee_merchant_id, priority, task_status,
read_status, read_time, due_time, completed_time, completed_by,
cancel_reason, idempotent_key, active_idempotent_key,
create_by, create_time, update_by, update_time, remark, del_flag
from sys_todo_task
</sql>
<select id="selectTodoById" parameterType="Long" resultMap="SysTodoTaskResult">
<include refid="selectTodoVo" />
where todo_id = #{todoId}
and del_flag = '0'
</select>
<select id="selectTodoByIdForAssignee" resultMap="SysTodoTaskResult">
<include refid="selectTodoVo" />
where todo_id = #{todoId}
and assignee_user_id = #{assigneeUserId}
and del_flag = '0'
</select>
<select id="selectActiveByIdempotentKey" parameterType="String" resultMap="SysTodoTaskResult">
<include refid="selectTodoVo" />
where active_idempotent_key = #{activeIdempotentKey}
and del_flag = '0'
limit 1
</select>
<select id="selectTodoList" parameterType="com.jsowell.system.domain.dto.TodoTaskQuery"
resultMap="SysTodoTaskResult">
<include refid="selectTodoVo" />
<where>
del_flag = '0'
and assignee_user_id = #{assigneeUserId}
<if test="taskStatus != null and taskStatus == 'ACTIVE'">
and task_status in ('PENDING', 'PROCESSING')
</if>
<if test="taskStatus != null and taskStatus != '' and taskStatus != 'ACTIVE'">
and task_status = #{taskStatus}
</if>
<if test="readStatus != null and readStatus != ''">
and read_status = #{readStatus}
</if>
<if test="taskType != null and taskType != ''">
and task_type = #{taskType}
</if>
<if test="keyword != null and keyword != ''">
and (title like concat('%', #{keyword}, '%')
or business_id like concat('%', #{keyword}, '%'))
</if>
<if test="hideCompleted != null and hideCompleted">
and task_status != 'COMPLETED'
</if>
</where>
order by priority desc, create_time desc, todo_id desc
</select>
<select id="selectHomeList" resultMap="SysTodoTaskResult">
<include refid="selectTodoVo" />
where assignee_user_id = #{assigneeUserId}
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
order by priority desc, create_time desc, todo_id desc
limit #{limit}
</select>
<select id="countActive" parameterType="Long" resultType="int">
select count(1)
from sys_todo_task
where assignee_user_id = #{assigneeUserId}
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
</select>
<select id="countUnread" parameterType="Long" resultType="int">
select count(1)
from sys_todo_task
where assignee_user_id = #{assigneeUserId}
and task_status in ('PENDING', 'PROCESSING')
and read_status = '0'
and del_flag = '0'
</select>
<insert id="insertTodoTask" parameterType="SysTodoTask" useGeneratedKeys="true" keyProperty="todoId">
insert into sys_todo_task (
task_type, title, summary, content, business_type, business_id,
route_name, route_params, assignee_user_id, assignee_merchant_id,
priority, task_status, read_status, due_time, idempotent_key,
active_idempotent_key, create_by, create_time, remark, del_flag
) values (
#{taskType}, #{title}, #{summary}, #{content}, #{businessType}, #{businessId},
#{routeName}, #{routeParams}, #{assigneeUserId}, #{assigneeMerchantId},
#{priority}, #{taskStatus}, #{readStatus}, #{dueTime}, #{idempotentKey},
#{activeIdempotentKey}, #{createBy}, sysdate(), #{remark}, #{delFlag}
)
</insert>
<update id="markTodoRead">
update sys_todo_task
set read_status = '1',
read_time = sysdate(),
update_by = #{updateBy},
update_time = sysdate()
where todo_id = #{todoId}
and assignee_user_id = #{assigneeUserId}
and read_status = '0'
and del_flag = '0'
</update>
<update id="markAllRead">
update sys_todo_task
set read_status = '1',
read_time = sysdate(),
update_by = #{updateBy},
update_time = sysdate()
where assignee_user_id = #{assigneeUserId}
and read_status = '0'
and del_flag = '0'
</update>
<update id="completeTodoForAssignee">
update sys_todo_task
set task_status = 'COMPLETED',
completed_time = sysdate(),
completed_by = #{completedBy},
active_idempotent_key = null,
update_by = #{updateBy},
update_time = sysdate()
where todo_id = #{todoId}
and assignee_user_id = #{assigneeUserId}
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
</update>
<update id="completeTodoByBusiness">
update sys_todo_task
set task_status = 'COMPLETED',
completed_time = sysdate(),
completed_by = #{completedBy},
active_idempotent_key = null,
update_by = #{updateBy},
update_time = sysdate()
where business_type = #{businessType}
and business_id = #{businessId}
<if test="assigneeUserId != null">
and assignee_user_id = #{assigneeUserId}
</if>
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
</update>
<update id="cancelTodoForAssignee">
update sys_todo_task
set task_status = 'CANCELLED',
cancel_reason = #{reason},
active_idempotent_key = null,
update_by = #{updateBy},
update_time = sysdate()
where todo_id = #{todoId}
and assignee_user_id = #{assigneeUserId}
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
</update>
<update id="cancelTodo">
update sys_todo_task
set task_status = 'CANCELLED',
cancel_reason = #{reason},
active_idempotent_key = null,
update_by = #{updateBy},
update_time = sysdate()
where todo_id = #{todoId}
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
</update>
<update id="cancelTodoByBusiness">
update sys_todo_task
set task_status = 'CANCELLED',
cancel_reason = #{reason},
active_idempotent_key = null,
update_by = #{updateBy},
update_time = sysdate()
where business_type = #{businessType}
and business_id = #{businessId}
<if test="assigneeUserId != null">
and assignee_user_id = #{assigneeUserId}
</if>
and task_status in ('PENDING', 'PROCESSING')
and del_flag = '0'
</update>
</mapper>

View File

@@ -91,6 +91,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
${params.dataScope}
</select>
<select id="selectActiveUserIdsByDeptTree" resultType="Long">
select distinct u.user_id
from sys_user u
where u.status = '0'
and u.del_flag = '0'
and (
u.dept_id = #{deptId}
or u.dept_id in (
select d.dept_id
from sys_dept d
where find_in_set(#{deptId}, d.ancestors)
and d.status = '0'
)
)
order by u.user_id
</select>
<select id="selectAllocatedList" parameterType="com.jsowell.common.core.domain.entity.SysUser" resultMap="SysUserResult">
select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phone, u.status, u.create_time
from sys_user u