mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-07-22 23:22:32 +08:00
Compare commits
19 Commits
9cdd984bcd
...
feature-待办
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7199228f8c | ||
|
|
6749a35b68 | ||
|
|
1bffb95b18 | ||
|
|
fd678c9ab3 | ||
|
|
b6b052b9df | ||
|
|
09d98eb18d | ||
|
|
558949f4e1 | ||
|
|
27ed7e717b | ||
|
|
3e299f40f9 | ||
|
|
bc20330f1b | ||
|
|
5c7f4dccec | ||
|
|
c416bdacc9 | ||
|
|
4720194fe7 | ||
|
|
de312c934b | ||
|
|
cbc2e3c721 | ||
|
|
974e68cf54 | ||
|
|
f84ec43ceb | ||
|
|
181d4a5b40 | ||
|
|
e65017ab22 |
@@ -55,14 +55,18 @@
|
|||||||
|
|
||||||
## 5. 首期业务范围
|
## 5. 首期业务范围
|
||||||
|
|
||||||
### 5.1 开票待办
|
### 5.1 首期开票待办
|
||||||
|
|
||||||
首期优先实现截图中的开票场景:
|
首期建设以下两个真实业务场景:
|
||||||
|
|
||||||
| 待办类型 | 触发条件 | 处理页面 | 完成条件 |
|
| 待办类型 | 接收人 | 触发条件 | 处理页面 | 完成条件 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| 平台采购开票 | 平台审核拒绝或要求补充材料 | 发票详情/开票申请页 | 用户重新提交或平台关闭任务 |
|
| 充电订单开票申请 | 对应运营商管理员 | 用户完成充电后提交订单开票申请 | 发票申请处理页 | 运营商完成开票、驳回或关闭申请 |
|
||||||
| 司机开票申请 | 收到司机开票申请 | 发票申请处理页 | 审核通过、驳回或关闭 |
|
| 运营商提现向平台开票 | 对应运营商管理员 | 运营商提现成功,提现状态变为 `已提现` | 财务详情/提现开票处理页 | 运营商提交发票号码及凭证后自动完成,或平台管理员关闭任务 |
|
||||||
|
|
||||||
|
首期接收人按运营商管理员角色解析,不向该运营商部门下所有普通用户广播。当前系统创建运营商时使用 `role_id = 3` 创建运营商管理员,实施时需要在测试库确认该角色的 `role_key`,并将角色配置化。
|
||||||
|
|
||||||
|
提现向平台开票按“一笔成功提现对应一条待办”实现。首期新增提现开票信息记录,至少保存发票状态、发票号码、凭证地址、提交人和提交时间;后续如平台允许合并多笔提现开具一张发票,再扩展为批量关联模型。
|
||||||
|
|
||||||
### 5.2 预留业务类型
|
### 5.2 预留业务类型
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@
|
|||||||
| 清分异常 | 清分失败、分账配置错误或账单待处理 |
|
| 清分异常 | 清分失败、分账配置错误或账单待处理 |
|
||||||
| 订单异常 | 订单退款、结算或支付异常需要人工介入 |
|
| 订单异常 | 订单退款、结算或支付异常需要人工介入 |
|
||||||
|
|
||||||
首期只实现开票待办,其他类型只完成通用模型和扩展接口,不要求全部接入。
|
首期只实现上述两个开票待办,其他类型只完成通用模型和扩展接口,不要求全部接入。
|
||||||
|
|
||||||
## 6. 业务流程
|
## 6. 业务流程
|
||||||
|
|
||||||
@@ -268,7 +272,7 @@ TodoTaskService.countUnread(Long userId);
|
|||||||
```java
|
```java
|
||||||
todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
||||||
.taskType("INVOICE_REVIEW")
|
.taskType("INVOICE_REVIEW")
|
||||||
.title("司机开票申请")
|
.title("充电订单开票申请")
|
||||||
.summary("有新的开票申请需要处理")
|
.summary("有新的开票申请需要处理")
|
||||||
.businessType("INVOICE_APPLY")
|
.businessType("INVOICE_APPLY")
|
||||||
.businessId(invoiceId.toString())
|
.businessId(invoiceId.toString())
|
||||||
@@ -329,8 +333,8 @@ todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
|||||||
|
|
||||||
### 首期业务
|
### 首期业务
|
||||||
|
|
||||||
- 平台采购开票待办可以正常生成、展示、跳转和完成。
|
- 用户充电订单开票申请待办可以按运营商管理员生成、展示、跳转和完成。
|
||||||
- 司机开票申请待办可以正常生成、展示、跳转和完成。
|
- 运营商提现成功后可以生成“向平台开票”待办,并在提交发票信息后完成。
|
||||||
- 原有 `sys_notice` 公告功能不受影响。
|
- 原有 `sys_notice` 公告功能不受影响。
|
||||||
|
|
||||||
## 14. 实施拆分
|
## 14. 实施拆分
|
||||||
@@ -344,10 +348,11 @@ todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
|||||||
|
|
||||||
### 第二期:开票接入
|
### 第二期:开票接入
|
||||||
|
|
||||||
- 接入平台采购开票事件。
|
- 将充电订单开票待办接收人收敛为运营商管理员。
|
||||||
- 接入司机开票申请事件。
|
- 接入提现成功事件,创建运营商向平台开票待办。
|
||||||
- 配置业务路由和完成条件。
|
- 新增提现开票信息表、提交发票信息接口和处理页面。
|
||||||
- 增加接口和页面联调测试。
|
- 配置两个业务场景的路由和完成条件。
|
||||||
|
- 增加接口、事务和页面联调测试。
|
||||||
|
|
||||||
### 第三期:平台业务扩展
|
### 第三期:平台业务扩展
|
||||||
|
|
||||||
|
|||||||
253
docs/plan/2026-07-16-todo-center-task-tracker.md
Normal file
253
docs/plan/2026-07-16-todo-center-task-tracker.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# 待办事项中心开发任务跟踪
|
||||||
|
|
||||||
|
## 任务概述
|
||||||
|
|
||||||
|
- 需求来源:`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`,避免先查后改产生越权窗口。
|
||||||
|
- 管理端能力使用独立路径和权限标识,不复用用户端接口绕过接收人过滤。
|
||||||
|
|
||||||
|
### 首期两个开票场景
|
||||||
|
|
||||||
|
- 充电订单开票:沿用 `order_invoice_record` 创建事件,接收人收敛为对应运营商管理员;通过 `todo.operator-admin-role-id` 配置管理员角色 ID,默认沿用现有运营商创建流程的 `role_id = 3`。
|
||||||
|
- 提现向平台开票:监听 `clearing_withdraw_info.withdraw_status` 从处理中变为已提现(`1`)的成功事件;每笔成功提现创建一条 `MERCHANT_WITHDRAW_INVOICE` 待办。
|
||||||
|
- 提现开票完成不能只依赖手工点完成,新增开票信息记录,保存发票状态、发票号码、凭证地址、提交人和提交时间,提交成功后与待办在同一事务内完成。
|
||||||
|
|
||||||
|
### 前后端仓库边界
|
||||||
|
|
||||||
|
`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` |
|
||||||
|
| B05-F1 | 待办中心页面上下间距优化 | P0 | done | ESLint 和生产构建通过 | 前端 `380e2dc` |
|
||||||
|
| B06 | 菜单权限 SQL、联调和首期回归 | P0 | done | 菜单 SQL、构建和专项回归通过;待测试库执行 SQL | `974e68cf5` |
|
||||||
|
| B07 | 提现成功后向平台开票待办 | P1 | done | 提现回调、开票记录、待办、提交/关闭接口和财务页闭环完成;待测试库执行建表 SQL | 后端 `3e299f40f`;前端 `b16865a` |
|
||||||
|
| B08 | 充电订单开票申请待办接入 | P1 | done | 已接入业务事件并收敛为运营商管理员接收 | `cbc2e3c72`、`5c7f4dcce` |
|
||||||
|
| B08-F1 | 充电订单开票接收人和文案收敛 | P1 | done | 后端编译和 14 个专项测试通过;前端 ESLint、生产构建通过 | 后端 `5c7f4dcce`;前端 `967a9bf` |
|
||||||
|
| B09 | 管理端创建、分配、取消和隐藏能力 | P1 | done | 全量查询、人工创建、重新分配、取消和终态隐藏接口完成;独立权限默认授予系统管理员 | `558949f4e` |
|
||||||
|
| B10 | 归档清理、过期任务和操作审计增强 | P2 | done | 到期流转、至少 180 天终态归档和每日维护任务完成;待测试库配置 Quartz | `b6b052b9d` |
|
||||||
|
| B11 | 路由白名单、性能压测和安全专项验证 | P2 | done | 路由及参数白名单、首页排序索引和验证清单完成;待测试库执行真实数据基准 | `1bffb95b1` |
|
||||||
|
| B12 | 汇付、清分、订单异常和实时推送扩展 | P3 | todo | - | - |
|
||||||
|
|
||||||
|
## 首期交付结论
|
||||||
|
|
||||||
|
- P0、P1、P2 开发批次已完成;B12 属于 PRD 明确预留、首期不要求接入的后续业务扩展。
|
||||||
|
- 后端最终专项回归共 30 个测试通过;前端专项 ESLint 和生产构建通过。
|
||||||
|
- 测试环境部署时按顺序执行或复核:
|
||||||
|
1. `docs/sql/sys_todo_menu.sql`
|
||||||
|
2. `docs/sql/clearing_withdraw_invoice.sql`
|
||||||
|
3. `docs/sql/sys_todo_performance_index.sql`
|
||||||
|
4. `docs/sql/sys_todo_maintenance_job.sql`
|
||||||
|
- 登录态、真实提现回调和性能基准按 `docs/plan/2026-07-17-todo-center-security-performance-checklist.md` 执行并记录结果。
|
||||||
|
|
||||||
|
## 任务清单
|
||||||
|
|
||||||
|
### 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
|
||||||
|
- 状态:done
|
||||||
|
- 现有入口:`AdapayService.drawCash()` 创建提现记录,提现成功回调 `cashSucceeded()` 将状态更新为 `1`。
|
||||||
|
- 目标:提现成功后为对应运营商管理员创建“向平台开票”待办,并提供提交发票信息后自动完成的闭环。
|
||||||
|
- 任务拆分:
|
||||||
|
- 新增提现开票信息表及唯一提现单号约束。
|
||||||
|
- 在提现成功状态更新事务中创建待办,使用 `MERCHANT_WITHDRAW_INVOICE` 类型。
|
||||||
|
- 新增查询提现开票详情、提交发票信息、平台关闭任务接口。
|
||||||
|
- 财务详情页增加待办处理入口,跳转携带 `merchantId`、`withdrawCode`、`todoId`。
|
||||||
|
- 完成情况:
|
||||||
|
- 新增 `clearing_withdraw_invoice` 表脚本,以提现编号唯一约束保证一笔提现只有一条开票记录。
|
||||||
|
- 提现成功回调在同一事务中更新提现和清分账单状态、创建开票记录,并为运营商管理员幂等创建待办。
|
||||||
|
- 当前待办接收人可查询并提交发票号码及凭证;提交后同一提现的全部待办自动完成。
|
||||||
|
- 平台关闭接口复用 `system:todo:cancel` 权限,关闭开票记录并取消关联待办。
|
||||||
|
- 财务详情页从待办跳转后自动打开开票弹窗,支持上传 PDF 或图片凭证。
|
||||||
|
- 待在测试库执行 `docs/sql/clearing_withdraw_invoice.sql`,并使用真实提现成功回调完成登录态联调。
|
||||||
|
- 验收标准:同一提现成功事件对同一管理员只产生一条有效待办;提交发票号码和凭证后待办自动完成;提现失败不创建该待办。
|
||||||
|
|
||||||
|
### TODO-008 充电订单开票申请接入
|
||||||
|
|
||||||
|
- 优先级:P1
|
||||||
|
- 批次:B08
|
||||||
|
- 状态:done
|
||||||
|
- 目标:收到用户充电订单开票申请时创建待办,运营商完成开票、驳回或关闭时完成。
|
||||||
|
- 当前实现:已接入 `order_invoice_record` 创建、完成和关闭事件,并与业务事务绑定。
|
||||||
|
- 当前实现:接收人仅解析运营商部门树下拥有配置角色的正常管理员;默认管理员角色 ID 为 `3`,可通过 `todo.operator-admin-role-id` 覆盖。
|
||||||
|
- 验收标准:创建和完成与开票业务事务一致,异常回滚时不产生脏任务状态。
|
||||||
|
|
||||||
|
### TODO-008-F1 充电订单开票接收人和文案收敛
|
||||||
|
|
||||||
|
- 优先级:P1
|
||||||
|
- 批次:B08-F1
|
||||||
|
- 状态:done
|
||||||
|
- 目标:将现有 `INVOICE_REVIEW` 待办的标题、类型文案从“司机开票申请”统一为“充电订单开票申请”,接收人限制为运营商管理员。
|
||||||
|
- 完成情况:后端创建待办时只查询配置的运营商管理员角色;前端移除不存在的“平台采购开票”筛选和类型文案,统一显示“充电订单开票申请”。
|
||||||
|
- 验收标准:运营商普通财务或普通操作员不会收到该待办;管理员收到的待办可正常跳转、完成和关闭。
|
||||||
|
|
||||||
|
### TODO-009 管理端异常任务处理
|
||||||
|
|
||||||
|
- 优先级:P1
|
||||||
|
- 批次:B09
|
||||||
|
- 状态:done
|
||||||
|
- 目标:实现管理员列表、人工创建、分配、取消和逻辑隐藏。
|
||||||
|
- 完成情况:
|
||||||
|
- 新增 `/system/todo/admin` 管理接口,覆盖全量分页查询、人工创建、重新分配、管理员取消和终态逻辑隐藏。
|
||||||
|
- 人工创建和重新分配均校验目标用户为正常用户,并按业务与接收人重建有效幂等键。
|
||||||
|
- 待处理任务必须先取消才能隐藏,避免隐藏有效任务后幂等键长期占用。
|
||||||
|
- 新增五项 `system:todo:admin:*` 独立权限,菜单 SQL 默认只授予 `role_key = 'admin'` 的系统管理员。
|
||||||
|
- 待在测试库重新执行 `docs/sql/sys_todo_menu.sql`,完成管理接口登录态验证。
|
||||||
|
- 验收标准:接口具备独立权限标识和操作日志,普通用户不能访问。
|
||||||
|
|
||||||
|
### TODO-010 运维与安全增强
|
||||||
|
|
||||||
|
- 优先级:P2
|
||||||
|
- 批次:B10-B11
|
||||||
|
- 状态:done
|
||||||
|
- 目标:补充过期、180 天归档清理、操作审计、路由白名单和性能验证。
|
||||||
|
- 当前进展:
|
||||||
|
- B10 已完成:到期待办批量转为 `EXPIRED`,同步记录终态时间并释放有效幂等键。
|
||||||
|
- B10 已完成:终态任务按 `todo.retention-days` 逻辑归档,配置小于 180 天时强制按 180 天保留。
|
||||||
|
- B10 已完成:新增 `todoTaskMaintenanceTask.maintain()` 每日维护任务和幂等 Quartz 配置脚本。
|
||||||
|
- 待在测试库执行 `docs/sql/sys_todo_maintenance_job.sql` 并观察首轮任务日志。
|
||||||
|
- B11 已完成:待办创建仅允许 `todo.route-whitelist` 中的路由,路由参数限制为安全标量 JSON。
|
||||||
|
- B11 已完成:新增首页待办排序索引迁移脚本和安全/性能验证清单。
|
||||||
|
- 待按 `docs/plan/2026-07-17-todo-center-security-performance-checklist.md` 在测试库执行真实数据量 EXPLAIN、P95 和越权验证。
|
||||||
|
- 验收标准:首页查询目标响应时间小于 500ms,路由不能被请求参数任意覆盖,历史任务按策略保留。
|
||||||
|
|
||||||
|
### TODO-011 后续业务扩展
|
||||||
|
|
||||||
|
- 优先级:P3
|
||||||
|
- 批次:B12
|
||||||
|
- 状态:todo
|
||||||
|
- 目标:接入汇付开户、结算账户、清分和订单异常,并评估 WebSocket/SSE。
|
||||||
|
- 验收标准:新增业务只通过统一待办 Service 接入,不修改通用数据权限和状态模型。
|
||||||
|
|
||||||
|
## 提交记录
|
||||||
|
|
||||||
|
每完成一个批次,在此追加:完成时间、验证命令、结果和 commit hash。
|
||||||
|
|
||||||
|
- 2026-07-16,B01:完成优先级拆分、任务跟踪文档和 `sys_todo_task` 建表脚本;通过 SQL 人工结构评审与 `git diff --check`;commit `e65017ab2`。
|
||||||
|
- 2026-07-16,B01 环境进展:用户已在测试数据库创建 `sys_todo_task` 表,可供后续接口联调。
|
||||||
|
- 2026-07-16,B02:完成领域模型、创建/查询 DTO、状态常量、Mapper 和统一 Service;`mvn -pl jsowell-system -am -DskipTests compile` 与 Mapper XML 语法校验通过;commit `181d4a5b4`。
|
||||||
|
- 2026-07-16,B03:完成当前用户数量、首页列表、分页列表、详情、已读、全部已读、完成和取消接口;`mvn -pl jsowell-admin -am -DskipTests compile` 通过,9 个 Service/Controller 专项测试通过;commit `f84ec43ce`。
|
||||||
|
- 2026-07-16,B04:完成前端 API、待办中心筛选分页、批量已读和安全路由跳转;新增文件专项 ESLint 通过,`NODE_OPTIONS=--openssl-legacy-provider npm run build:prd` 构建通过;前端 commit `ff6b890`。
|
||||||
|
- 2026-07-16,B05:完成首页右侧待办卡片、`99+` 数量、最近 3 条、空状态、错误重试和 60 秒轮询;专项 ESLint 与生产构建通过;前端 commit `0627061`。
|
||||||
|
- 2026-07-16,B06:完成幂等菜单/权限 SQL;列表、查询和已读权限默认授予正常角色,完成和取消权限需按业务角色显式授权;后端 9 个专项测试再次通过,前端构建沿用 B05 验证结果;commit `974e68cf5`。
|
||||||
|
- 2026-07-16,B08:司机开票记录创建后,按运营商部门树为正常后台用户幂等创建待办;开票状态更新为已开票时自动完成,删除/关闭记录时自动取消;业务与待办状态处于同一事务;全依赖编译和 12 个专项测试通过;commit `cbc2e3c72`。
|
||||||
|
- 2026-07-17,B05-F1:局部覆盖全局卡片纵向内边距,并补充待办页面顶部和底部留白;专项 ESLint 与生产构建通过;前端 commit `380e2dc`。
|
||||||
|
- 2026-07-17,需求调整:移除不存在的平台采购开票场景,新增“提现成功后运营商向平台开票”场景,并将充电订单开票接收人明确为运营商管理员。
|
||||||
|
- 2026-07-17,B08-F1:充电订单开票待办仅分配给配置的运营商管理员角色,统一前后端文案并移除不存在的平台采购开票选项;后端全依赖编译和 14 个专项测试通过,前端专项 ESLint 与生产构建通过;后端 commit `5c7f4dcce`,前端 commit `967a9bf`。
|
||||||
|
- 2026-07-17,B07:新增提现开票信息表脚本、提现成功事务闭环、运营商管理员待办、发票详情/提交/关闭接口及财务详情页处理弹窗;后端 20 个相关测试通过,前端新增 API 和待办页面专项 ESLint、生产构建通过;后端 commit `3e299f40f`,前端 commit `b16865a`。
|
||||||
|
- 2026-07-17,B09:新增管理端全量查询、人工创建、重新分配、管理员取消和终态逻辑隐藏接口,补充独立权限和系统管理员默认授权;全依赖编译、6 个专项测试通过;commit `558949f4e`。
|
||||||
|
- 2026-07-17,B10:新增到期待办自动过期、终态任务至少保留 180 天后逻辑归档、每日 Quartz 维护任务及配置 SQL;全依赖编译、8 个相关测试通过;commit `b6b052b9d`。
|
||||||
|
- 2026-07-17,B11:新增可配置路由白名单和安全路由参数校验,补充首页排序索引迁移脚本及测试库安全/性能验证清单;全依赖编译、24 个相关测试通过,应用 YAML 解析通过;commit `1bffb95b1`。
|
||||||
|
- 2026-07-17,首期最终回归:待办通用服务、两个开票业务、管理端、维护任务和路由安全共 30 个专项测试通过;前端沿用 B07 的专项 ESLint 与生产构建结果;P3 预留业务保留在后续迭代。
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# 待办中心安全与性能验证清单
|
||||||
|
|
||||||
|
## 部署前 SQL
|
||||||
|
|
||||||
|
按顺序在测试库执行:
|
||||||
|
|
||||||
|
1. `docs/sql/sys_todo_task.sql`(仅新环境)或 `docs/sql/sys_todo_performance_index.sql`(已有表)。
|
||||||
|
2. `docs/sql/sys_todo_menu.sql`。
|
||||||
|
3. `docs/sql/clearing_withdraw_invoice.sql`。
|
||||||
|
4. `docs/sql/sys_todo_maintenance_job.sql`。
|
||||||
|
|
||||||
|
## 路由安全
|
||||||
|
|
||||||
|
- 默认仅允许 `invoiceDetail`、`financeDetail` 两个业务路由。
|
||||||
|
- 新业务路由上线前必须追加到 `todo.route-whitelist`,不允许直接信任请求体中的路由名。
|
||||||
|
- `route_params` 必须是 JSON 对象,只允许 `params`、`query` 两层结构,最终值只能是字符串、数字、布尔值或空值。
|
||||||
|
- 用户端详情、已读、完成和取消接口继续使用当前登录用户 ID 作为 SQL 条件。
|
||||||
|
- 管理端接口使用独立的 `system:todo:admin:*` 权限,普通角色不得授权。
|
||||||
|
|
||||||
|
## 索引与执行计划
|
||||||
|
|
||||||
|
使用具有代表性的数据量执行:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
EXPLAIN
|
||||||
|
SELECT todo_id, task_type, title, summary, business_type, business_id,
|
||||||
|
route_name, route_params, priority, task_status, read_status, create_time
|
||||||
|
FROM sys_todo_task
|
||||||
|
WHERE assignee_user_id = 100
|
||||||
|
AND task_status IN ('PENDING', 'PROCESSING')
|
||||||
|
AND del_flag = '0'
|
||||||
|
ORDER BY priority DESC, create_time DESC, todo_id DESC
|
||||||
|
LIMIT 3;
|
||||||
|
|
||||||
|
EXPLAIN
|
||||||
|
SELECT count(1)
|
||||||
|
FROM sys_todo_task
|
||||||
|
WHERE assignee_user_id = 100
|
||||||
|
AND task_status IN ('PENDING', 'PROCESSING')
|
||||||
|
AND read_status = '0'
|
||||||
|
AND del_flag = '0';
|
||||||
|
```
|
||||||
|
|
||||||
|
首页列表应命中 `idx_todo_user_active_sort`,未读数量应命中 `idx_todo_user_status`。如果优化器未选择目标索引,执行 `ANALYZE TABLE sys_todo_task` 后复测。
|
||||||
|
|
||||||
|
## HTTP 性能抽样
|
||||||
|
|
||||||
|
登录测试账号后分别对以下接口执行不少于 100 次请求,记录 P50、P95 和最大响应时间:
|
||||||
|
|
||||||
|
- `GET /system/todo/unreadCount`
|
||||||
|
- `GET /system/todo/homeList?limit=3`
|
||||||
|
- `GET /system/todo/list?pageNum=1&pageSize=10&taskStatus=ACTIVE`
|
||||||
|
|
||||||
|
验收目标:首页数量和最近待办接口 P95 小于 500ms;分页接口无全表扫描、无跨用户数据。
|
||||||
|
|
||||||
|
## 回归场景
|
||||||
|
|
||||||
|
- 修改 `userId`、`assigneeUserId` 等请求参数不能查看或更新其他用户待办。
|
||||||
|
- 非白名单路由创建待办时返回明确业务错误。
|
||||||
|
- 嵌套对象、数组或非法 JSON 的路由参数被拒绝。
|
||||||
|
- 普通用户访问 `/system/todo/admin/**` 返回无权限。
|
||||||
|
- 到期待办释放 `active_idempotent_key`,同业务后续可重新生成待办。
|
||||||
|
- 终态任务在 180 天内不归档,超过保留期后仅逻辑隐藏,不物理删除。
|
||||||
33
docs/sql/clearing_withdraw_invoice.sql
Normal file
33
docs/sql/clearing_withdraw_invoice.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- 运营商提现向平台开票信息表
|
||||||
|
-- 执行前请确认 sys_todo_task、待办菜单权限 SQL 已部署。
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `clearing_withdraw_invoice` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`withdraw_code` varchar(64) NOT NULL COMMENT '提现编号',
|
||||||
|
`merchant_id` varchar(64) NOT NULL COMMENT '运营商ID',
|
||||||
|
`invoice_status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待提交、SUBMITTED已提交、CLOSED已关闭',
|
||||||
|
`invoice_number` varchar(100) DEFAULT NULL COMMENT '发票号码',
|
||||||
|
`voucher_url` varchar(1000) DEFAULT NULL COMMENT '发票凭证地址',
|
||||||
|
`submitted_by` bigint DEFAULT NULL COMMENT '提交人用户ID',
|
||||||
|
`submitted_name` varchar(64) DEFAULT NULL COMMENT '提交人账号',
|
||||||
|
`submitted_time` datetime DEFAULT NULL COMMENT '提交时间',
|
||||||
|
`closed_by` bigint DEFAULT NULL COMMENT '关闭人用户ID',
|
||||||
|
`closed_name` varchar(64) DEFAULT NULL COMMENT '关闭人账号',
|
||||||
|
`closed_time` datetime DEFAULT NULL COMMENT '关闭时间',
|
||||||
|
`close_reason` varchar(500) DEFAULT NULL COMMENT '关闭原因',
|
||||||
|
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标志(0正常 1删除)',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_withdraw_invoice_code` (`withdraw_code`),
|
||||||
|
KEY `idx_withdraw_invoice_merchant_status` (`merchant_id`, `invoice_status`, `create_time`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运营商提现向平台开票信息表';
|
||||||
|
|
||||||
|
-- 完成权限分配给运营商管理员角色,取消权限只分配给平台任务关闭角色。
|
||||||
|
-- INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
-- SELECT <运营商管理员角色ID>, menu_id FROM sys_menu WHERE perms = 'system:todo:complete';
|
||||||
|
-- INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
-- SELECT <平台任务关闭角色ID>, menu_id FROM sys_menu WHERE perms = 'system:todo:cancel';
|
||||||
19
docs/sql/sys_todo_maintenance_job.sql
Normal file
19
docs/sql/sys_todo_maintenance_job.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- 待办任务过期与历史归档定时任务
|
||||||
|
-- 默认每天 02:30 执行;归档保留天数通过 todo.retention-days 配置,最少保留 180 天。
|
||||||
|
|
||||||
|
INSERT INTO sys_job (
|
||||||
|
job_name, job_group, invoke_target, cron_expression,
|
||||||
|
misfire_policy, concurrent, status, create_by, create_time, remark
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'待办过期与历史归档', 'SYSTEM', 'todoTaskMaintenanceTask.maintain()', '0 30 2 * * ?',
|
||||||
|
'3', '1', '0', 'system', sysdate(), '过期待处理任务并逻辑归档至少180天前的终态任务'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys_job
|
||||||
|
WHERE invoke_target = 'todoTaskMaintenanceTask.maintain()'
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT job_id, job_name, invoke_target, cron_expression, status
|
||||||
|
FROM sys_job
|
||||||
|
WHERE invoke_target = 'todoTaskMaintenanceTask.maintain()';
|
||||||
183
docs/sql/sys_todo_menu.sql
Normal file
183
docs/sql/sys_todo_menu.sql
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
-- 待办中心菜单与基础权限
|
||||||
|
-- 执行效果:
|
||||||
|
-- 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);
|
||||||
|
|
||||||
|
-- 管理端异常任务处理权限,仅默认授予 role_key = 'admin' 的系统管理员。
|
||||||
|
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 permission_info.menu_name, @todoMenuId, permission_info.order_num, '#', '', 1, 0,
|
||||||
|
'F', '0', '0', permission_info.perms, '#', 'system', sysdate(), permission_info.remark
|
||||||
|
FROM (
|
||||||
|
SELECT '待办管理查询' AS menu_name, 11 AS order_num, 'system:todo:admin:list' AS perms, '查看全量待办' AS remark
|
||||||
|
UNION ALL SELECT '待办人工创建', 12, 'system:todo:admin:create', '人工补发异常待办'
|
||||||
|
UNION ALL SELECT '待办重新分配', 13, 'system:todo:admin:assign', '重新分配有效待办'
|
||||||
|
UNION ALL SELECT '待办管理取消', 14, 'system:todo:admin:cancel', '管理员关闭异常待办'
|
||||||
|
UNION ALL SELECT '待办逻辑隐藏', 15, 'system:todo:admin:remove', '隐藏终态待办记录'
|
||||||
|
) permission_info
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys_menu existing_menu
|
||||||
|
WHERE existing_menu.parent_id = @todoMenuId
|
||||||
|
AND existing_menu.perms = permission_info.perms
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
SELECT role_info.role_id, menu_info.menu_id
|
||||||
|
FROM sys_role role_info
|
||||||
|
JOIN sys_menu menu_info
|
||||||
|
ON menu_info.parent_id = @todoMenuId
|
||||||
|
AND menu_info.perms IN (
|
||||||
|
'system:todo:admin:list',
|
||||||
|
'system:todo:admin:create',
|
||||||
|
'system:todo:admin:assign',
|
||||||
|
'system:todo:admin:cancel',
|
||||||
|
'system:todo:admin:remove'
|
||||||
|
)
|
||||||
|
WHERE role_info.role_key = 'admin'
|
||||||
|
AND 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 = menu_info.menu_id
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
20
docs/sql/sys_todo_performance_index.sql
Normal file
20
docs/sql/sys_todo_performance_index.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- 待办首页列表排序索引(用于已存在的 sys_todo_task 表)
|
||||||
|
-- MySQL 5.7 兼容的幂等写法。
|
||||||
|
|
||||||
|
SET @todoIndexExists := (
|
||||||
|
SELECT count(1)
|
||||||
|
FROM information_schema.statistics
|
||||||
|
WHERE table_schema = database()
|
||||||
|
AND table_name = 'sys_todo_task'
|
||||||
|
AND index_name = 'idx_todo_user_active_sort'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @todoIndexSql := IF(
|
||||||
|
@todoIndexExists = 0,
|
||||||
|
'ALTER TABLE sys_todo_task ADD INDEX idx_todo_user_active_sort (assignee_user_id, del_flag, task_status, priority, create_time, todo_id)',
|
||||||
|
'SELECT ''idx_todo_user_active_sort already exists'''
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE todoIndexStatement FROM @todoIndexSql;
|
||||||
|
EXECUTE todoIndexStatement;
|
||||||
|
DEALLOCATE PREPARE todoIndexStatement;
|
||||||
40
docs/sql/sys_todo_task.sql
Normal file
40
docs/sql/sys_todo_task.sql
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
-- 待办事项中心任务表
|
||||||
|
-- 说明: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_user_active_sort` (`assignee_user_id`, `del_flag`, `task_status`, `priority`, `create_time`, `todo_id`) 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='用户待办任务表';
|
||||||
@@ -124,6 +124,9 @@ public class OrderService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ClearingWithdrawInfoService clearingWithdrawInfoService;
|
private ClearingWithdrawInfoService clearingWithdrawInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MerchantWithdrawInvoiceService merchantWithdrawInvoiceService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MemberAdapayRecordService memberAdapayRecordService;
|
private MemberAdapayRecordService memberAdapayRecordService;
|
||||||
|
|
||||||
@@ -1464,22 +1467,7 @@ public class OrderService {
|
|||||||
log.info("取现成功 data:{}", JSON.toJSONString(data));
|
log.info("取现成功 data:{}", JSON.toJSONString(data));
|
||||||
JSONObject jsonObject = JSON.parseObject(data);
|
JSONObject jsonObject = JSON.parseObject(data);
|
||||||
String withdrawCode = jsonObject.getString("id");
|
String withdrawCode = jsonObject.getString("id");
|
||||||
// 通过取现id查询取现数据
|
merchantWithdrawInvoiceService.handleWithdrawSucceeded(withdrawCode);
|
||||||
ClearingWithdrawInfo clearingWithdrawInfo = clearingWithdrawInfoService.selectByWithdrawCode(withdrawCode);
|
|
||||||
if (clearingWithdrawInfo != null) {
|
|
||||||
clearingWithdrawInfo.setWithdrawStatus(Constants.ONE);
|
|
||||||
clearingWithdrawInfo.setUpdateTime(DateUtils.getNowDate());
|
|
||||||
clearingWithdrawInfoService.updateByPrimaryKeySelective(clearingWithdrawInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清分账单数据更新
|
|
||||||
List<ClearingBillInfo> clearingBillInfos = clearingBillInfoService.selectByWithdrawCode(withdrawCode);
|
|
||||||
if (CollectionUtils.isNotEmpty(clearingBillInfos)) {
|
|
||||||
for (ClearingBillInfo clearingBillInfo : clearingBillInfos) {
|
|
||||||
clearingBillInfo.setBillStatus("4");
|
|
||||||
}
|
|
||||||
clearingBillInfoService.updateBatchSelective(clearingBillInfos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.jsowell.web.controller.pile;
|
||||||
|
|
||||||
|
import com.jsowell.common.annotation.Log;
|
||||||
|
import com.jsowell.common.core.controller.BaseController;
|
||||||
|
import com.jsowell.common.core.domain.AjaxResult;
|
||||||
|
import com.jsowell.common.enums.BusinessType;
|
||||||
|
import com.jsowell.pile.dto.WithdrawInvoiceCloseRequest;
|
||||||
|
import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest;
|
||||||
|
import com.jsowell.pile.service.MerchantWithdrawInvoiceService;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营商提现向平台开票接口。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/pile/withdrawInvoice")
|
||||||
|
public class MerchantWithdrawInvoiceController extends BaseController {
|
||||||
|
@Autowired
|
||||||
|
private MerchantWithdrawInvoiceService merchantWithdrawInvoiceService;
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:query')")
|
||||||
|
@GetMapping("/{withdrawCode}")
|
||||||
|
public AjaxResult getInfo(@PathVariable String withdrawCode, @RequestParam Long todoId) {
|
||||||
|
return AjaxResult.success(merchantWithdrawInvoiceService.getForAssignee(
|
||||||
|
withdrawCode, todoId, getUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:complete')")
|
||||||
|
@Log(title = "运营商提现开票", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/{withdrawCode}/submit")
|
||||||
|
public AjaxResult submit(@PathVariable String withdrawCode,
|
||||||
|
@Validated @RequestBody WithdrawInvoiceSubmitRequest request) {
|
||||||
|
return toAjax(merchantWithdrawInvoiceService.submit(withdrawCode, request,
|
||||||
|
getUserId(), getUsername()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:cancel')")
|
||||||
|
@Log(title = "运营商提现开票", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/{withdrawCode}/close")
|
||||||
|
public AjaxResult close(@PathVariable String withdrawCode,
|
||||||
|
@Validated @RequestBody WithdrawInvoiceCloseRequest request) {
|
||||||
|
return toAjax(merchantWithdrawInvoiceService.close(withdrawCode, request.getReason(),
|
||||||
|
getUserId(), getUsername()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ public class OrderInvoiceRecordController extends BaseController {
|
|||||||
@Log(title = "申请开票", businessType = BusinessType.UPDATE)
|
@Log(title = "申请开票", businessType = BusinessType.UPDATE)
|
||||||
@PutMapping
|
@PutMapping
|
||||||
public AjaxResult edit(@RequestBody OrderInvoiceRecord orderInvoiceRecord) {
|
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)
|
@Log(title = "申请开票", businessType = BusinessType.DELETE)
|
||||||
@DeleteMapping("/{ids}")
|
@DeleteMapping("/{ids}")
|
||||||
public AjaxResult remove(@PathVariable Integer[] ids) {
|
public AjaxResult remove(@PathVariable Integer[] ids) {
|
||||||
return toAjax(orderInvoiceRecordService.deleteOrderInvoiceRecordByIds(ids));
|
return toAjax(orderInvoiceRecordService.deleteOrderInvoiceRecordByIds(ids, getUsername()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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.TodoTaskAdminCreateRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminQuery;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAssignRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskCancelRequest;
|
||||||
|
import com.jsowell.system.service.TodoTaskAdminService;
|
||||||
|
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.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
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.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办管理端接口。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/system/todo/admin")
|
||||||
|
public class SysTodoTaskAdminController extends BaseController {
|
||||||
|
@Autowired
|
||||||
|
private TodoTaskAdminService todoTaskAdminService;
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:admin:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(TodoTaskAdminQuery query) {
|
||||||
|
startPage();
|
||||||
|
List<SysTodoTask> list = todoTaskAdminService.list(query);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:admin:create')")
|
||||||
|
@Log(title = "待办管理", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping("/create")
|
||||||
|
public AjaxResult create(@Validated @RequestBody TodoTaskAdminCreateRequest request) {
|
||||||
|
return AjaxResult.success(todoTaskAdminService.create(request, getUsername()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:admin:assign')")
|
||||||
|
@Log(title = "待办管理", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/{todoId}/assign")
|
||||||
|
public AjaxResult assign(@PathVariable Long todoId,
|
||||||
|
@Validated @RequestBody TodoTaskAssignRequest request) {
|
||||||
|
return toAjax(todoTaskAdminService.assign(todoId, request, getUsername()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:admin:cancel')")
|
||||||
|
@Log(title = "待办管理", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/{todoId}/cancel")
|
||||||
|
public AjaxResult cancel(@PathVariable Long todoId,
|
||||||
|
@Validated @RequestBody TodoTaskCancelRequest request) {
|
||||||
|
return toAjax(todoTaskAdminService.cancel(todoId, getUserId(), getUsername(), request.getReason()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('system:todo:admin:remove')")
|
||||||
|
@Log(title = "待办管理", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{todoId}")
|
||||||
|
public AjaxResult hide(@PathVariable Long todoId) {
|
||||||
|
return toAjax(todoTaskAdminService.hide(todoId, getUsername()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -214,3 +214,9 @@ sms:
|
|||||||
enable: false
|
enable: false
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
# 待办中心
|
||||||
|
todo:
|
||||||
|
operator-admin-role-id: 3
|
||||||
|
retention-days: 180
|
||||||
|
route-whitelist: invoiceDetail,financeDetail
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package com.jsowell.pile.service;
|
||||||
|
|
||||||
|
import com.jsowell.common.constant.Constants;
|
||||||
|
import com.jsowell.pile.constant.MerchantWithdrawInvoiceConstants;
|
||||||
|
import com.jsowell.pile.domain.ClearingBillInfo;
|
||||||
|
import com.jsowell.pile.domain.ClearingWithdrawInfo;
|
||||||
|
import com.jsowell.pile.domain.ClearingWithdrawInvoice;
|
||||||
|
import com.jsowell.pile.domain.PileMerchantInfo;
|
||||||
|
import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest;
|
||||||
|
import com.jsowell.pile.mapper.ClearingWithdrawInfoMapper;
|
||||||
|
import com.jsowell.pile.mapper.ClearingWithdrawInvoiceMapper;
|
||||||
|
import com.jsowell.pile.service.impl.MerchantWithdrawInvoiceServiceImpl;
|
||||||
|
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.Collections;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class MerchantWithdrawInvoiceServiceImplTest {
|
||||||
|
@Test
|
||||||
|
void handleSucceeded_shouldUpdateWithdrawAndCreateAdminTodos() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
MerchantWithdrawInvoiceServiceImpl service = dependencies.createService();
|
||||||
|
ClearingWithdrawInfo withdrawInfo = withdrawInfo(Constants.ZERO);
|
||||||
|
when(dependencies.withdrawInfoMapper.selectByWithdrawCode("W100")).thenReturn(withdrawInfo);
|
||||||
|
when(dependencies.invoiceMapper.selectByWithdrawCode("W100")).thenReturn(null);
|
||||||
|
when(dependencies.invoiceMapper.insertSelective(any(ClearingWithdrawInvoice.class))).thenReturn(1);
|
||||||
|
ClearingBillInfo billInfo = new ClearingBillInfo();
|
||||||
|
billInfo.setId(5);
|
||||||
|
when(dependencies.clearingBillInfoService.selectByWithdrawCode("W100"))
|
||||||
|
.thenReturn(Collections.singletonList(billInfo));
|
||||||
|
PileMerchantInfo merchantInfo = new PileMerchantInfo();
|
||||||
|
merchantInfo.setDeptId("200");
|
||||||
|
when(dependencies.pileMerchantInfoService.selectPileMerchantInfoById(100L)).thenReturn(merchantInfo);
|
||||||
|
when(dependencies.assigneeService.findActiveOperatorAdminUserIdsByDeptTree(200L))
|
||||||
|
.thenReturn(Arrays.asList(8L, 9L));
|
||||||
|
|
||||||
|
service.handleWithdrawSucceeded("W100");
|
||||||
|
|
||||||
|
verify(dependencies.withdrawInfoMapper).updateByPrimaryKeySelective(withdrawInfo);
|
||||||
|
assertEquals(Constants.ONE, withdrawInfo.getWithdrawStatus());
|
||||||
|
assertEquals("4", billInfo.getBillStatus());
|
||||||
|
verify(dependencies.clearingBillInfoService).updateBatchSelective(Collections.singletonList(billInfo));
|
||||||
|
ArgumentCaptor<TodoTaskCreateCommand> captor = ArgumentCaptor.forClass(TodoTaskCreateCommand.class);
|
||||||
|
verify(dependencies.todoTaskService, times(2)).createTask(captor.capture());
|
||||||
|
assertEquals(Arrays.asList(8L, 9L), Arrays.asList(
|
||||||
|
captor.getAllValues().get(0).getAssigneeUserId(),
|
||||||
|
captor.getAllValues().get(1).getAssigneeUserId()));
|
||||||
|
assertEquals(MerchantWithdrawInvoiceConstants.TASK_TYPE, captor.getValue().getTaskType());
|
||||||
|
assertEquals(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, captor.getValue().getBusinessType());
|
||||||
|
assertEquals("financeDetail", captor.getValue().getRouteName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleSucceeded_shouldNotRecreateTodoAfterInvoiceSubmitted() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
MerchantWithdrawInvoiceServiceImpl service = dependencies.createService();
|
||||||
|
when(dependencies.withdrawInfoMapper.selectByWithdrawCode("W100"))
|
||||||
|
.thenReturn(withdrawInfo(Constants.ONE));
|
||||||
|
ClearingWithdrawInvoice invoice = invoice(MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED);
|
||||||
|
when(dependencies.invoiceMapper.selectByWithdrawCode("W100")).thenReturn(invoice);
|
||||||
|
when(dependencies.clearingBillInfoService.selectByWithdrawCode("W100"))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
service.handleWithdrawSucceeded("W100");
|
||||||
|
|
||||||
|
verify(dependencies.todoTaskService, never()).createTask(any(TodoTaskCreateCommand.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void submit_shouldCompleteAllBusinessTodos() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
MerchantWithdrawInvoiceServiceImpl service = dependencies.createService();
|
||||||
|
when(dependencies.todoTaskService.getForAssignee(20L, 8L)).thenReturn(todoTask());
|
||||||
|
when(dependencies.invoiceMapper.selectByWithdrawCode("W100"))
|
||||||
|
.thenReturn(invoice(MerchantWithdrawInvoiceConstants.STATUS_PENDING));
|
||||||
|
when(dependencies.invoiceMapper.markSubmitted(any(ClearingWithdrawInvoice.class))).thenReturn(1);
|
||||||
|
WithdrawInvoiceSubmitRequest request = new WithdrawInvoiceSubmitRequest();
|
||||||
|
request.setTodoId(20L);
|
||||||
|
request.setInvoiceNumber("INV-100");
|
||||||
|
request.setVoucherUrl("/profile/upload/invoice.pdf");
|
||||||
|
|
||||||
|
assertTrue(service.submit("W100", request, 8L, "operator"));
|
||||||
|
|
||||||
|
verify(dependencies.todoTaskService).completeByBusiness(
|
||||||
|
MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, "W100", null, 8L, "operator");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void close_shouldCancelAllBusinessTodos() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
MerchantWithdrawInvoiceServiceImpl service = dependencies.createService();
|
||||||
|
when(dependencies.invoiceMapper.selectByWithdrawCode("W100"))
|
||||||
|
.thenReturn(invoice(MerchantWithdrawInvoiceConstants.STATUS_PENDING));
|
||||||
|
when(dependencies.invoiceMapper.markClosed(any(ClearingWithdrawInvoice.class))).thenReturn(1);
|
||||||
|
|
||||||
|
assertTrue(service.close("W100", "无需开票", 1L, "admin"));
|
||||||
|
|
||||||
|
verify(dependencies.todoTaskService).cancelByBusiness(
|
||||||
|
MerchantWithdrawInvoiceConstants.BUSINESS_TYPE, "W100", null, "admin", "无需开票");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClearingWithdrawInfo withdrawInfo(String status) {
|
||||||
|
ClearingWithdrawInfo withdrawInfo = new ClearingWithdrawInfo();
|
||||||
|
withdrawInfo.setId(1);
|
||||||
|
withdrawInfo.setMerchantId("100");
|
||||||
|
withdrawInfo.setWithdrawCode("W100");
|
||||||
|
withdrawInfo.setWithdrawStatus(status);
|
||||||
|
return withdrawInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClearingWithdrawInvoice invoice(String status) {
|
||||||
|
ClearingWithdrawInvoice invoice = new ClearingWithdrawInvoice();
|
||||||
|
invoice.setId(10L);
|
||||||
|
invoice.setMerchantId("100");
|
||||||
|
invoice.setWithdrawCode("W100");
|
||||||
|
invoice.setInvoiceStatus(status);
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SysTodoTask todoTask() {
|
||||||
|
SysTodoTask task = new SysTodoTask();
|
||||||
|
task.setTodoId(20L);
|
||||||
|
task.setTaskType(MerchantWithdrawInvoiceConstants.TASK_TYPE);
|
||||||
|
task.setBusinessType(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE);
|
||||||
|
task.setBusinessId("W100");
|
||||||
|
task.setTaskStatus("PENDING");
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
Field field = MerchantWithdrawInvoiceServiceImpl.class.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Dependencies {
|
||||||
|
private final ClearingWithdrawInfoMapper withdrawInfoMapper = mock(ClearingWithdrawInfoMapper.class);
|
||||||
|
private final ClearingWithdrawInvoiceMapper invoiceMapper = mock(ClearingWithdrawInvoiceMapper.class);
|
||||||
|
private final ClearingBillInfoService clearingBillInfoService = mock(ClearingBillInfoService.class);
|
||||||
|
private final PileMerchantInfoService pileMerchantInfoService = mock(PileMerchantInfoService.class);
|
||||||
|
private final TodoTaskAssigneeService assigneeService = mock(TodoTaskAssigneeService.class);
|
||||||
|
private final TodoTaskService todoTaskService = mock(TodoTaskService.class);
|
||||||
|
|
||||||
|
private MerchantWithdrawInvoiceServiceImpl createService() {
|
||||||
|
MerchantWithdrawInvoiceServiceImpl service = new MerchantWithdrawInvoiceServiceImpl();
|
||||||
|
setField(service, "clearingWithdrawInfoMapper", withdrawInfoMapper);
|
||||||
|
setField(service, "clearingWithdrawInvoiceMapper", invoiceMapper);
|
||||||
|
setField(service, "clearingBillInfoService", clearingBillInfoService);
|
||||||
|
setField(service, "pileMerchantInfoService", pileMerchantInfoService);
|
||||||
|
setField(service, "todoTaskAssigneeService", assigneeService);
|
||||||
|
setField(service, "todoTaskService", todoTaskService);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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_shouldCreateTodoForEachMerchantAdmin() {
|
||||||
|
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.findActiveOperatorAdminUserIdsByDeptTree(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("充电订单开票申请", commands.get(0).getTitle());
|
||||||
|
assertEquals("有新的充电订单开票申请需要处理,申请单号:55", commands.get(0).getSummary());
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.jsowell.quartz.task;
|
||||||
|
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult;
|
||||||
|
import com.jsowell.system.service.TodoTaskMaintenanceService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class TodoTaskMaintenanceTaskTest {
|
||||||
|
@Test
|
||||||
|
void maintain_shouldDelegateToMaintenanceService() {
|
||||||
|
TodoTaskMaintenanceService service = mock(TodoTaskMaintenanceService.class);
|
||||||
|
TodoTaskMaintenanceTask task = new TodoTaskMaintenanceTask();
|
||||||
|
setField(task, "todoTaskMaintenanceService", service);
|
||||||
|
when(service.maintain()).thenReturn(new TodoTaskMaintenanceResult(1, 2, new Date()));
|
||||||
|
|
||||||
|
task.maintain();
|
||||||
|
|
||||||
|
verify(service).maintain();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
Field field = TodoTaskMaintenanceTask.class.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
package com.jsowell.system.service;
|
||||||
|
|
||||||
|
import com.jsowell.common.constant.Constants;
|
||||||
|
import com.jsowell.common.core.domain.entity.SysUser;
|
||||||
|
import com.jsowell.common.exception.ServiceException;
|
||||||
|
import com.jsowell.system.constant.TodoTaskConstants;
|
||||||
|
import com.jsowell.system.domain.SysTodoTask;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminQuery;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAssignRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
|
||||||
|
import com.jsowell.system.mapper.SysTodoTaskMapper;
|
||||||
|
import com.jsowell.system.mapper.SysUserMapper;
|
||||||
|
import com.jsowell.system.service.impl.TodoTaskAdminServiceImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
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.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class TodoTaskAdminServiceImplTest {
|
||||||
|
@Test
|
||||||
|
void list_shouldUseAdminQueryWithoutForcingAssignee() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
TodoTaskAdminServiceImpl service = dependencies.createService();
|
||||||
|
TodoTaskAdminQuery query = new TodoTaskAdminQuery();
|
||||||
|
query.setTaskStatus(TodoTaskConstants.STATUS_ACTIVE);
|
||||||
|
when(dependencies.todoTaskMapper.selectAdminTodoList(query))
|
||||||
|
.thenReturn(Collections.singletonList(new SysTodoTask()));
|
||||||
|
|
||||||
|
assertEquals(1, service.list(query).size());
|
||||||
|
verify(dependencies.todoTaskMapper).selectAdminTodoList(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void create_shouldValidateUserAndGenerateIdempotentKey() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
TodoTaskAdminServiceImpl service = dependencies.createService();
|
||||||
|
when(dependencies.userMapper.selectUserById(8L)).thenReturn(activeUser(8L));
|
||||||
|
when(dependencies.todoTaskService.createTask(any(TodoTaskCreateCommand.class)))
|
||||||
|
.thenReturn(new SysTodoTask());
|
||||||
|
TodoTaskAdminCreateRequest request = createRequest();
|
||||||
|
|
||||||
|
service.create(request, "admin");
|
||||||
|
|
||||||
|
ArgumentCaptor<TodoTaskCreateCommand> captor = ArgumentCaptor.forClass(TodoTaskCreateCommand.class);
|
||||||
|
verify(dependencies.todoTaskService).createTask(captor.capture());
|
||||||
|
assertEquals("MANUAL_TASK:MANUAL_BUSINESS:B100:8", captor.getValue().getIdempotentKey());
|
||||||
|
assertEquals("admin", captor.getValue().getCreateBy());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void assign_shouldUpdateActiveTaskAndResetAssignmentKey() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
TodoTaskAdminServiceImpl service = dependencies.createService();
|
||||||
|
when(dependencies.userMapper.selectUserById(9L)).thenReturn(activeUser(9L));
|
||||||
|
SysTodoTask task = task(TodoTaskConstants.STATUS_PENDING);
|
||||||
|
when(dependencies.todoTaskMapper.selectTodoById(20L)).thenReturn(task);
|
||||||
|
when(dependencies.todoTaskMapper.assignTodo(20L, 9L, "100",
|
||||||
|
"MANUAL_TASK:MANUAL_BUSINESS:B100:9", "admin")).thenReturn(1);
|
||||||
|
TodoTaskAssignRequest request = new TodoTaskAssignRequest();
|
||||||
|
request.setAssigneeUserId(9L);
|
||||||
|
request.setAssigneeMerchantId("100");
|
||||||
|
|
||||||
|
assertTrue(service.assign(20L, request, "admin"));
|
||||||
|
|
||||||
|
verify(dependencies.todoTaskMapper).assignTodo(20L, 9L, "100",
|
||||||
|
"MANUAL_TASK:MANUAL_BUSINESS:B100:9", "admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void hide_shouldOnlyAllowTerminalTask() {
|
||||||
|
Dependencies dependencies = new Dependencies();
|
||||||
|
TodoTaskAdminServiceImpl service = dependencies.createService();
|
||||||
|
when(dependencies.todoTaskMapper.selectTodoById(20L))
|
||||||
|
.thenReturn(task(TodoTaskConstants.STATUS_PENDING));
|
||||||
|
|
||||||
|
assertThrows(ServiceException.class, () -> service.hide(20L, "admin"));
|
||||||
|
|
||||||
|
when(dependencies.todoTaskMapper.selectTodoById(21L))
|
||||||
|
.thenReturn(task(TodoTaskConstants.STATUS_CANCELLED));
|
||||||
|
when(dependencies.todoTaskMapper.hideTodo(21L, "admin")).thenReturn(1);
|
||||||
|
assertTrue(service.hide(21L, "admin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TodoTaskAdminCreateRequest createRequest() {
|
||||||
|
TodoTaskAdminCreateRequest request = new TodoTaskAdminCreateRequest();
|
||||||
|
request.setTaskType("MANUAL_TASK");
|
||||||
|
request.setTitle("人工待办");
|
||||||
|
request.setBusinessType("MANUAL_BUSINESS");
|
||||||
|
request.setBusinessId("B100");
|
||||||
|
request.setRouteName("TodoCenter");
|
||||||
|
request.setAssigneeUserId(8L);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SysTodoTask task(String status) {
|
||||||
|
SysTodoTask task = new SysTodoTask();
|
||||||
|
task.setTodoId(20L);
|
||||||
|
task.setTaskType("MANUAL_TASK");
|
||||||
|
task.setBusinessType("MANUAL_BUSINESS");
|
||||||
|
task.setBusinessId("B100");
|
||||||
|
task.setTaskStatus(status);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SysUser activeUser(Long userId) {
|
||||||
|
SysUser user = new SysUser();
|
||||||
|
user.setUserId(userId);
|
||||||
|
user.setStatus(Constants.ZERO);
|
||||||
|
user.setDelFlag(Constants.ZERO);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
Field field = TodoTaskAdminServiceImpl.class.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Dependencies {
|
||||||
|
private final SysTodoTaskMapper todoTaskMapper = mock(SysTodoTaskMapper.class);
|
||||||
|
private final SysUserMapper userMapper = mock(SysUserMapper.class);
|
||||||
|
private final TodoTaskService todoTaskService = mock(TodoTaskService.class);
|
||||||
|
|
||||||
|
private TodoTaskAdminServiceImpl createService() {
|
||||||
|
TodoTaskAdminServiceImpl service = new TodoTaskAdminServiceImpl();
|
||||||
|
setField(service, "todoTaskMapper", todoTaskMapper);
|
||||||
|
setField(service, "userMapper", userMapper);
|
||||||
|
setField(service, "todoTaskService", todoTaskService);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.jsowell.system.service;
|
||||||
|
|
||||||
|
import com.jsowell.system.mapper.SysUserMapper;
|
||||||
|
import com.jsowell.system.service.impl.TodoTaskAssigneeServiceImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class TodoTaskAssigneeServiceImplTest {
|
||||||
|
@Test
|
||||||
|
void findOperatorAdmins_shouldUseConfiguredRole() {
|
||||||
|
SysUserMapper mapper = mock(SysUserMapper.class);
|
||||||
|
TodoTaskAssigneeServiceImpl service = new TodoTaskAssigneeServiceImpl();
|
||||||
|
setField(service, "userMapper", mapper);
|
||||||
|
setField(service, "operatorAdminRoleId", 7L);
|
||||||
|
when(mapper.selectActiveUserIdsByDeptTreeAndRole(200L, 7L))
|
||||||
|
.thenReturn(Collections.singletonList(8L));
|
||||||
|
|
||||||
|
assertEquals(Collections.singletonList(8L),
|
||||||
|
service.findActiveOperatorAdminUserIdsByDeptTree(200L));
|
||||||
|
verify(mapper).selectActiveUserIdsByDeptTreeAndRole(200L, 7L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findOperatorAdmins_shouldFallbackToExistingMerchantAdminRole() {
|
||||||
|
SysUserMapper mapper = mock(SysUserMapper.class);
|
||||||
|
TodoTaskAssigneeServiceImpl service = new TodoTaskAssigneeServiceImpl();
|
||||||
|
setField(service, "userMapper", mapper);
|
||||||
|
when(mapper.selectActiveUserIdsByDeptTreeAndRole(200L, 3L))
|
||||||
|
.thenReturn(Collections.singletonList(8L));
|
||||||
|
|
||||||
|
service.findActiveOperatorAdminUserIdsByDeptTree(200L);
|
||||||
|
|
||||||
|
verify(mapper).selectActiveUserIdsByDeptTreeAndRole(200L, 3L);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.jsowell.system.service;
|
||||||
|
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult;
|
||||||
|
import com.jsowell.system.mapper.SysTodoTaskMapper;
|
||||||
|
import com.jsowell.system.service.impl.TodoTaskMaintenanceServiceImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class TodoTaskMaintenanceServiceImplTest {
|
||||||
|
@Test
|
||||||
|
void maintain_shouldExpireAndKeepAtLeast180Days() {
|
||||||
|
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||||
|
TodoTaskMaintenanceServiceImpl service = new TodoTaskMaintenanceServiceImpl();
|
||||||
|
setField(service, "todoTaskMapper", mapper);
|
||||||
|
setField(service, "retentionDays", 30);
|
||||||
|
when(mapper.expireDueTasks(any(Date.class), any(String.class))).thenReturn(2);
|
||||||
|
when(mapper.archiveTerminalTasks(any(Date.class), any(String.class))).thenReturn(3);
|
||||||
|
|
||||||
|
TodoTaskMaintenanceResult result = service.maintain();
|
||||||
|
|
||||||
|
assertEquals(2, result.getExpiredCount());
|
||||||
|
assertEquals(3, result.getArchivedCount());
|
||||||
|
ArgumentCaptor<Date> nowCaptor = ArgumentCaptor.forClass(Date.class);
|
||||||
|
ArgumentCaptor<Date> archiveCaptor = ArgumentCaptor.forClass(Date.class);
|
||||||
|
verify(mapper).expireDueTasks(nowCaptor.capture(), any(String.class));
|
||||||
|
verify(mapper).archiveTerminalTasks(archiveCaptor.capture(), any(String.class));
|
||||||
|
long retentionDays = (nowCaptor.getValue().getTime() - archiveCaptor.getValue().getTime())
|
||||||
|
/ (24L * 60 * 60 * 1000);
|
||||||
|
assertTrue(retentionDays >= 179);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
Field field = TodoTaskMaintenanceServiceImpl.class.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createTask_shouldRejectRouteOutsideWhitelist() {
|
||||||
|
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||||
|
TodoTaskServiceImpl service = newService(mapper);
|
||||||
|
TodoTaskCreateCommand command = createCommand();
|
||||||
|
command.setRouteName("UnsafeRoute");
|
||||||
|
|
||||||
|
assertThrows(ServiceException.class, () -> service.createTask(command));
|
||||||
|
verify(mapper, never()).insertTodoTask(any(SysTodoTask.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createTask_shouldRejectNestedRouteParams() {
|
||||||
|
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||||
|
TodoTaskServiceImpl service = newService(mapper);
|
||||||
|
TodoTaskCreateCommand command = createCommand();
|
||||||
|
command.setRouteParams("{\"query\":{\"payload\":{\"admin\":true}}}");
|
||||||
|
|
||||||
|
assertThrows(ServiceException.class, () -> service.createTask(command));
|
||||||
|
verify(mapper, never()).insertTodoTask(any(SysTodoTask.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TodoTaskCreateCommand createCommand() {
|
||||||
|
return TodoTaskCreateCommand.builder()
|
||||||
|
.taskType("INVOICE_REVIEW")
|
||||||
|
.title("充电订单开票申请")
|
||||||
|
.businessType("INVOICE_APPLY")
|
||||||
|
.businessId("100")
|
||||||
|
.routeName("invoiceDetail")
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.jsowell.web.controller.pile;
|
||||||
|
|
||||||
|
import com.jsowell.common.core.domain.AjaxResult;
|
||||||
|
import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest;
|
||||||
|
import com.jsowell.pile.service.MerchantWithdrawInvoiceService;
|
||||||
|
import com.jsowell.pile.vo.web.MerchantWithdrawInvoiceVO;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
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 MerchantWithdrawInvoiceControllerTest {
|
||||||
|
@Test
|
||||||
|
void getInfo_shouldUseCurrentLoginUser() {
|
||||||
|
MerchantWithdrawInvoiceService service = mock(MerchantWithdrawInvoiceService.class);
|
||||||
|
TestController controller = newController(service, 8L, "operator");
|
||||||
|
MerchantWithdrawInvoiceVO view = new MerchantWithdrawInvoiceVO();
|
||||||
|
when(service.getForAssignee("W100", 20L, 8L)).thenReturn(view);
|
||||||
|
|
||||||
|
AjaxResult result = controller.getInfo("W100", 20L);
|
||||||
|
|
||||||
|
assertSame(view, result.get(AjaxResult.DATA_TAG));
|
||||||
|
verify(service).getForAssignee("W100", 20L, 8L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void submit_shouldPassCurrentUserAndOperator() {
|
||||||
|
MerchantWithdrawInvoiceService service = mock(MerchantWithdrawInvoiceService.class);
|
||||||
|
TestController controller = newController(service, 8L, "operator");
|
||||||
|
WithdrawInvoiceSubmitRequest request = new WithdrawInvoiceSubmitRequest();
|
||||||
|
request.setTodoId(20L);
|
||||||
|
request.setInvoiceNumber("INV-100");
|
||||||
|
request.setVoucherUrl("/invoice.pdf");
|
||||||
|
when(service.submit("W100", request, 8L, "operator")).thenReturn(true);
|
||||||
|
|
||||||
|
controller.submit("W100", request);
|
||||||
|
|
||||||
|
verify(service).submit("W100", request, 8L, "operator");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestController newController(MerchantWithdrawInvoiceService service,
|
||||||
|
Long userId, String username) {
|
||||||
|
TestController controller = new TestController(userId, username);
|
||||||
|
try {
|
||||||
|
Field field = MerchantWithdrawInvoiceController.class
|
||||||
|
.getDeclaredField("merchantWithdrawInvoiceService");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(controller, service);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestController extends MerchantWithdrawInvoiceController {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.jsowell.web.controller.system;
|
||||||
|
|
||||||
|
import com.jsowell.system.domain.SysTodoTask;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskCancelRequest;
|
||||||
|
import com.jsowell.system.service.TodoTaskAdminService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class SysTodoTaskAdminControllerTest {
|
||||||
|
@Test
|
||||||
|
void create_shouldPassCurrentOperator() {
|
||||||
|
TodoTaskAdminService service = mock(TodoTaskAdminService.class);
|
||||||
|
TestController controller = newController(service, 1L, "admin");
|
||||||
|
TodoTaskAdminCreateRequest request = new TodoTaskAdminCreateRequest();
|
||||||
|
when(service.create(request, "admin")).thenReturn(new SysTodoTask());
|
||||||
|
|
||||||
|
controller.create(request);
|
||||||
|
|
||||||
|
verify(service).create(request, "admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cancel_shouldPassCurrentUserAndOperator() {
|
||||||
|
TodoTaskAdminService service = mock(TodoTaskAdminService.class);
|
||||||
|
TestController controller = newController(service, 1L, "admin");
|
||||||
|
TodoTaskCancelRequest request = new TodoTaskCancelRequest();
|
||||||
|
request.setReason("异常任务关闭");
|
||||||
|
when(service.cancel(20L, 1L, "admin", "异常任务关闭")).thenReturn(true);
|
||||||
|
|
||||||
|
controller.cancel(20L, request);
|
||||||
|
|
||||||
|
verify(service).cancel(20L, 1L, "admin", "异常任务关闭");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestController newController(TodoTaskAdminService service,
|
||||||
|
Long userId, String username) {
|
||||||
|
TestController controller = new TestController(userId, username);
|
||||||
|
try {
|
||||||
|
Field field = SysTodoTaskAdminController.class.getDeclaredField("todoTaskAdminService");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(controller, service);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new RuntimeException(exception);
|
||||||
|
}
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TestController extends SysTodoTaskAdminController {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.jsowell.pile.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营商提现开票常量。
|
||||||
|
*/
|
||||||
|
public final class MerchantWithdrawInvoiceConstants {
|
||||||
|
public static final String TASK_TYPE = "MERCHANT_WITHDRAW_INVOICE";
|
||||||
|
public static final String BUSINESS_TYPE = "CLEARING_WITHDRAW_INVOICE";
|
||||||
|
public static final String ROUTE_NAME = "financeDetail";
|
||||||
|
|
||||||
|
public static final String STATUS_PENDING = "PENDING";
|
||||||
|
public static final String STATUS_SUBMITTED = "SUBMITTED";
|
||||||
|
public static final String STATUS_CLOSED = "CLOSED";
|
||||||
|
|
||||||
|
private MerchantWithdrawInvoiceConstants() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.jsowell.pile.domain;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.jsowell.common.core.domain.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营商提现开票信息 clearing_withdraw_invoice。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ClearingWithdrawInvoice extends BaseEntity {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String withdrawCode;
|
||||||
|
private String merchantId;
|
||||||
|
private String invoiceStatus;
|
||||||
|
private String invoiceNumber;
|
||||||
|
private String voucherUrl;
|
||||||
|
private Long submittedBy;
|
||||||
|
private String submittedName;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date submittedTime;
|
||||||
|
|
||||||
|
private Long closedBy;
|
||||||
|
private String closedName;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date closedTime;
|
||||||
|
|
||||||
|
private String closeReason;
|
||||||
|
private String delFlag;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.jsowell.pile.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台关闭提现开票任务请求。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WithdrawInvoiceCloseRequest {
|
||||||
|
@NotBlank(message = "关闭原因不能为空")
|
||||||
|
@Size(max = 500, message = "关闭原因不能超过500个字符")
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.jsowell.pile.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交提现发票信息请求。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WithdrawInvoiceSubmitRequest {
|
||||||
|
@NotNull(message = "待办任务 ID 不能为空")
|
||||||
|
private Long todoId;
|
||||||
|
|
||||||
|
@NotBlank(message = "发票号码不能为空")
|
||||||
|
@Size(max = 100, message = "发票号码不能超过100个字符")
|
||||||
|
private String invoiceNumber;
|
||||||
|
|
||||||
|
@NotBlank(message = "发票凭证不能为空")
|
||||||
|
@Size(max = 1000, message = "发票凭证地址不能超过1000个字符")
|
||||||
|
private String voucherUrl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.jsowell.pile.mapper;
|
||||||
|
|
||||||
|
import com.jsowell.pile.domain.ClearingWithdrawInvoice;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营商提现开票信息 Mapper。
|
||||||
|
*/
|
||||||
|
public interface ClearingWithdrawInvoiceMapper {
|
||||||
|
ClearingWithdrawInvoice selectByWithdrawCode(@Param("withdrawCode") String withdrawCode);
|
||||||
|
|
||||||
|
int insertSelective(ClearingWithdrawInvoice invoice);
|
||||||
|
|
||||||
|
int markSubmitted(ClearingWithdrawInvoice invoice);
|
||||||
|
|
||||||
|
int markClosed(ClearingWithdrawInvoice invoice);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.jsowell.pile.service;
|
||||||
|
|
||||||
|
import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest;
|
||||||
|
import com.jsowell.pile.vo.web.MerchantWithdrawInvoiceVO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营商提现向平台开票服务。
|
||||||
|
*/
|
||||||
|
public interface MerchantWithdrawInvoiceService {
|
||||||
|
void handleWithdrawSucceeded(String withdrawCode);
|
||||||
|
|
||||||
|
MerchantWithdrawInvoiceVO getForAssignee(String withdrawCode, Long todoId, Long userId);
|
||||||
|
|
||||||
|
boolean submit(String withdrawCode, WithdrawInvoiceSubmitRequest request,
|
||||||
|
Long userId, String operatorName);
|
||||||
|
|
||||||
|
boolean close(String withdrawCode, String reason, Long operatorId, String operatorName);
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ public interface OrderInvoiceRecordService {
|
|||||||
* @param orderInvoiceRecord 申请开票
|
* @param orderInvoiceRecord 申请开票
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord);
|
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord, Long operatorId, String operatorName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量删除申请开票
|
* 批量删除申请开票
|
||||||
@@ -69,7 +69,7 @@ public interface OrderInvoiceRecordService {
|
|||||||
* @param ids 需要删除的申请开票主键集合
|
* @param ids 需要删除的申请开票主键集合
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public int deleteOrderInvoiceRecordByIds(Integer[] ids);
|
public int deleteOrderInvoiceRecordByIds(Integer[] ids, String operatorName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除申请开票信息
|
* 删除申请开票信息
|
||||||
@@ -77,5 +77,5 @@ public interface OrderInvoiceRecordService {
|
|||||||
* @param id 申请开票主键
|
* @param id 申请开票主键
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
public int deleteOrderInvoiceRecordById(Integer id);
|
public int deleteOrderInvoiceRecordById(Integer id, String operatorName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
package com.jsowell.pile.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.jsowell.common.constant.Constants;
|
||||||
|
import com.jsowell.common.exception.ServiceException;
|
||||||
|
import com.jsowell.common.util.DateUtils;
|
||||||
|
import com.jsowell.common.util.StringUtils;
|
||||||
|
import com.jsowell.pile.constant.MerchantWithdrawInvoiceConstants;
|
||||||
|
import com.jsowell.pile.domain.ClearingBillInfo;
|
||||||
|
import com.jsowell.pile.domain.ClearingWithdrawInfo;
|
||||||
|
import com.jsowell.pile.domain.ClearingWithdrawInvoice;
|
||||||
|
import com.jsowell.pile.domain.PileMerchantInfo;
|
||||||
|
import com.jsowell.pile.dto.WithdrawInvoiceSubmitRequest;
|
||||||
|
import com.jsowell.pile.mapper.ClearingWithdrawInfoMapper;
|
||||||
|
import com.jsowell.pile.mapper.ClearingWithdrawInvoiceMapper;
|
||||||
|
import com.jsowell.pile.service.ClearingBillInfoService;
|
||||||
|
import com.jsowell.pile.service.MerchantWithdrawInvoiceService;
|
||||||
|
import com.jsowell.pile.service.PileMerchantInfoService;
|
||||||
|
import com.jsowell.pile.vo.web.MerchantWithdrawInvoiceVO;
|
||||||
|
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.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营商提现向平台开票服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class MerchantWithdrawInvoiceServiceImpl implements MerchantWithdrawInvoiceService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(MerchantWithdrawInvoiceServiceImpl.class);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClearingWithdrawInfoMapper clearingWithdrawInfoMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ClearingWithdrawInvoiceMapper clearingWithdrawInvoiceMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClearingBillInfoService clearingBillInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PileMerchantInfoService pileMerchantInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TodoTaskAssigneeService todoTaskAssigneeService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TodoTaskService todoTaskService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void handleWithdrawSucceeded(String withdrawCode) {
|
||||||
|
String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64);
|
||||||
|
ClearingWithdrawInfo withdrawInfo = clearingWithdrawInfoMapper.selectByWithdrawCode(safeWithdrawCode);
|
||||||
|
if (withdrawInfo == null) {
|
||||||
|
throw new ServiceException("提现记录不存在,无法创建开票待办");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = DateUtils.getNowDate();
|
||||||
|
if (!Constants.ONE.equals(withdrawInfo.getWithdrawStatus()) || withdrawInfo.getArrivalTime() == null) {
|
||||||
|
withdrawInfo.setWithdrawStatus(Constants.ONE);
|
||||||
|
withdrawInfo.setArrivalTime(now);
|
||||||
|
withdrawInfo.setUpdateTime(now);
|
||||||
|
clearingWithdrawInfoMapper.updateByPrimaryKeySelective(withdrawInfo);
|
||||||
|
}
|
||||||
|
completeClearingBills(safeWithdrawCode);
|
||||||
|
|
||||||
|
ClearingWithdrawInvoice invoice = getOrCreateInvoice(withdrawInfo);
|
||||||
|
if (!MerchantWithdrawInvoiceConstants.STATUS_PENDING.equals(invoice.getInvoiceStatus())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createTodos(withdrawInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MerchantWithdrawInvoiceVO getForAssignee(String withdrawCode, Long todoId, Long userId) {
|
||||||
|
String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64);
|
||||||
|
validateTodo(safeWithdrawCode, todoId, userId);
|
||||||
|
ClearingWithdrawInvoice invoice = requireInvoice(safeWithdrawCode);
|
||||||
|
ClearingWithdrawInfo withdrawInfo = clearingWithdrawInfoMapper.selectByWithdrawCode(safeWithdrawCode);
|
||||||
|
return buildView(invoice, withdrawInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean submit(String withdrawCode, WithdrawInvoiceSubmitRequest request,
|
||||||
|
Long userId, String operatorName) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new ServiceException("发票提交参数不能为空");
|
||||||
|
}
|
||||||
|
String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64);
|
||||||
|
SysTodoTask todoTask = validateTodo(safeWithdrawCode, request.getTodoId(), userId);
|
||||||
|
|
||||||
|
ClearingWithdrawInvoice invoice = requireInvoice(safeWithdrawCode);
|
||||||
|
if (MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED.equals(invoice.getInvoiceStatus())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!TodoTaskConstants.isActiveStatus(todoTask.getTaskStatus())) {
|
||||||
|
throw new ServiceException("当前待办状态不允许提交发票");
|
||||||
|
}
|
||||||
|
if (!MerchantWithdrawInvoiceConstants.STATUS_PENDING.equals(invoice.getInvoiceStatus())) {
|
||||||
|
throw new ServiceException("当前提现开票状态不允许提交");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = DateUtils.getNowDate();
|
||||||
|
invoice.setInvoiceStatus(MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED);
|
||||||
|
invoice.setInvoiceNumber(normalizeRequired(request.getInvoiceNumber(), "发票号码", 100));
|
||||||
|
invoice.setVoucherUrl(normalizeRequired(request.getVoucherUrl(), "发票凭证", 1000));
|
||||||
|
invoice.setSubmittedBy(userId);
|
||||||
|
invoice.setSubmittedName(normalizeOperator(operatorName, userId));
|
||||||
|
invoice.setSubmittedTime(now);
|
||||||
|
invoice.setUpdateBy(invoice.getSubmittedName());
|
||||||
|
invoice.setUpdateTime(now);
|
||||||
|
if (clearingWithdrawInvoiceMapper.markSubmitted(invoice) == 0) {
|
||||||
|
ClearingWithdrawInvoice current = requireInvoice(safeWithdrawCode);
|
||||||
|
if (MerchantWithdrawInvoiceConstants.STATUS_SUBMITTED.equals(current.getInvoiceStatus())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new ServiceException("当前提现开票状态不允许提交");
|
||||||
|
}
|
||||||
|
|
||||||
|
todoTaskService.completeByBusiness(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE,
|
||||||
|
safeWithdrawCode, null, userId, invoice.getSubmittedName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean close(String withdrawCode, String reason, Long operatorId, String operatorName) {
|
||||||
|
String safeWithdrawCode = normalizeRequired(withdrawCode, "提现编号", 64);
|
||||||
|
if (operatorId == null || operatorId <= 0) {
|
||||||
|
throw new ServiceException("关闭操作人不能为空");
|
||||||
|
}
|
||||||
|
ClearingWithdrawInvoice invoice = requireInvoice(safeWithdrawCode);
|
||||||
|
if (MerchantWithdrawInvoiceConstants.STATUS_CLOSED.equals(invoice.getInvoiceStatus())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!MerchantWithdrawInvoiceConstants.STATUS_PENDING.equals(invoice.getInvoiceStatus())) {
|
||||||
|
throw new ServiceException("当前提现开票状态不允许关闭");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = DateUtils.getNowDate();
|
||||||
|
String safeOperator = normalizeOperator(operatorName, operatorId);
|
||||||
|
String safeReason = normalizeRequired(reason, "关闭原因", 500);
|
||||||
|
invoice.setInvoiceStatus(MerchantWithdrawInvoiceConstants.STATUS_CLOSED);
|
||||||
|
invoice.setClosedBy(operatorId);
|
||||||
|
invoice.setClosedName(safeOperator);
|
||||||
|
invoice.setClosedTime(now);
|
||||||
|
invoice.setCloseReason(safeReason);
|
||||||
|
invoice.setUpdateBy(safeOperator);
|
||||||
|
invoice.setUpdateTime(now);
|
||||||
|
if (clearingWithdrawInvoiceMapper.markClosed(invoice) == 0) {
|
||||||
|
ClearingWithdrawInvoice current = requireInvoice(safeWithdrawCode);
|
||||||
|
if (MerchantWithdrawInvoiceConstants.STATUS_CLOSED.equals(current.getInvoiceStatus())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new ServiceException("当前提现开票状态不允许关闭");
|
||||||
|
}
|
||||||
|
|
||||||
|
todoTaskService.cancelByBusiness(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE,
|
||||||
|
safeWithdrawCode, null, safeOperator, safeReason);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeClearingBills(String withdrawCode) {
|
||||||
|
List<ClearingBillInfo> billInfos = clearingBillInfoService.selectByWithdrawCode(withdrawCode);
|
||||||
|
if (CollectionUtils.isEmpty(billInfos)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ClearingBillInfo billInfo : billInfos) {
|
||||||
|
billInfo.setBillStatus("4");
|
||||||
|
}
|
||||||
|
clearingBillInfoService.updateBatchSelective(billInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClearingWithdrawInvoice getOrCreateInvoice(ClearingWithdrawInfo withdrawInfo) {
|
||||||
|
ClearingWithdrawInvoice invoice = clearingWithdrawInvoiceMapper.selectByWithdrawCode(withdrawInfo.getWithdrawCode());
|
||||||
|
if (invoice != null) {
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice = new ClearingWithdrawInvoice();
|
||||||
|
invoice.setWithdrawCode(withdrawInfo.getWithdrawCode());
|
||||||
|
invoice.setMerchantId(withdrawInfo.getMerchantId());
|
||||||
|
invoice.setInvoiceStatus(MerchantWithdrawInvoiceConstants.STATUS_PENDING);
|
||||||
|
invoice.setCreateBy("adapay-callback");
|
||||||
|
invoice.setDelFlag("0");
|
||||||
|
try {
|
||||||
|
clearingWithdrawInvoiceMapper.insertSelective(invoice);
|
||||||
|
return invoice;
|
||||||
|
} catch (DuplicateKeyException exception) {
|
||||||
|
invoice = clearingWithdrawInvoiceMapper.selectByWithdrawCode(withdrawInfo.getWithdrawCode());
|
||||||
|
if (invoice != null) {
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTodos(ClearingWithdrawInfo withdrawInfo) {
|
||||||
|
if (StringUtils.isBlank(withdrawInfo.getMerchantId())) {
|
||||||
|
log.warn("提现记录缺少运营商,无法创建开票待办,withdrawCode={}", withdrawInfo.getWithdrawCode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PileMerchantInfo merchantInfo;
|
||||||
|
try {
|
||||||
|
merchantInfo = pileMerchantInfoService.selectPileMerchantInfoById(Long.parseLong(withdrawInfo.getMerchantId()));
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
log.warn("提现记录运营商 ID 非法,无法创建开票待办,withdrawCode={}, merchantId={}",
|
||||||
|
withdrawInfo.getWithdrawCode(), withdrawInfo.getMerchantId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (merchantInfo == null || StringUtils.isBlank(merchantInfo.getDeptId())) {
|
||||||
|
log.warn("提现记录运营商未配置部门,无法创建开票待办,withdrawCode={}, merchantId={}",
|
||||||
|
withdrawInfo.getWithdrawCode(), withdrawInfo.getMerchantId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long deptId;
|
||||||
|
try {
|
||||||
|
deptId = Long.parseLong(merchantInfo.getDeptId());
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
log.warn("提现记录运营商部门 ID 非法,无法创建开票待办,withdrawCode={}, deptId={}",
|
||||||
|
withdrawInfo.getWithdrawCode(), merchantInfo.getDeptId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Long> assigneeUserIds = todoTaskAssigneeService.findActiveOperatorAdminUserIdsByDeptTree(deptId);
|
||||||
|
if (CollectionUtils.isEmpty(assigneeUserIds)) {
|
||||||
|
log.warn("提现记录运营商部门下没有正常管理员,无法创建开票待办,withdrawCode={}, deptId={}",
|
||||||
|
withdrawInfo.getWithdrawCode(), deptId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String routeParams = buildRouteParams(withdrawInfo);
|
||||||
|
for (Long assigneeUserId : assigneeUserIds) {
|
||||||
|
String idempotentKey = MerchantWithdrawInvoiceConstants.TASK_TYPE + ":"
|
||||||
|
+ MerchantWithdrawInvoiceConstants.BUSINESS_TYPE + ":"
|
||||||
|
+ withdrawInfo.getWithdrawCode() + ":" + assigneeUserId;
|
||||||
|
todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
||||||
|
.taskType(MerchantWithdrawInvoiceConstants.TASK_TYPE)
|
||||||
|
.title("运营商提现向平台开票")
|
||||||
|
.summary("提现已成功,请向平台提交发票,提现编号:" + withdrawInfo.getWithdrawCode())
|
||||||
|
.businessType(MerchantWithdrawInvoiceConstants.BUSINESS_TYPE)
|
||||||
|
.businessId(withdrawInfo.getWithdrawCode())
|
||||||
|
.routeName(MerchantWithdrawInvoiceConstants.ROUTE_NAME)
|
||||||
|
.routeParams(routeParams)
|
||||||
|
.assigneeUserId(assigneeUserId)
|
||||||
|
.assigneeMerchantId(withdrawInfo.getMerchantId())
|
||||||
|
.priority(TodoTaskConstants.PRIORITY_IMPORTANT)
|
||||||
|
.idempotentKey(idempotentKey)
|
||||||
|
.createBy("adapay-callback")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRouteParams(ClearingWithdrawInfo withdrawInfo) {
|
||||||
|
Map<String, Object> query = new HashMap<>(2);
|
||||||
|
query.put("merchantId", withdrawInfo.getMerchantId());
|
||||||
|
query.put("withdrawCode", withdrawInfo.getWithdrawCode());
|
||||||
|
Map<String, Object> routeParams = new HashMap<>(1);
|
||||||
|
routeParams.put("query", query);
|
||||||
|
return JSON.toJSONString(routeParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysTodoTask validateTodo(String withdrawCode, Long todoId, Long userId) {
|
||||||
|
SysTodoTask todoTask = todoTaskService.getForAssignee(todoId, userId);
|
||||||
|
if (!MerchantWithdrawInvoiceConstants.TASK_TYPE.equals(todoTask.getTaskType())
|
||||||
|
|| !MerchantWithdrawInvoiceConstants.BUSINESS_TYPE.equals(todoTask.getBusinessType())
|
||||||
|
|| !withdrawCode.equals(todoTask.getBusinessId())) {
|
||||||
|
throw new ServiceException("待办任务与提现开票信息不匹配");
|
||||||
|
}
|
||||||
|
return todoTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClearingWithdrawInvoice requireInvoice(String withdrawCode) {
|
||||||
|
ClearingWithdrawInvoice invoice = clearingWithdrawInvoiceMapper.selectByWithdrawCode(withdrawCode);
|
||||||
|
if (invoice == null) {
|
||||||
|
throw new ServiceException("提现开票信息不存在");
|
||||||
|
}
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MerchantWithdrawInvoiceVO buildView(ClearingWithdrawInvoice invoice,
|
||||||
|
ClearingWithdrawInfo withdrawInfo) {
|
||||||
|
MerchantWithdrawInvoiceVO view = new MerchantWithdrawInvoiceVO();
|
||||||
|
BeanUtils.copyProperties(invoice, view);
|
||||||
|
if (withdrawInfo != null) {
|
||||||
|
view.setWithdrawAmt(withdrawInfo.getWithdrawAmt());
|
||||||
|
view.setFeeAmt(withdrawInfo.getFeeAmt());
|
||||||
|
view.setCreditedAmt(withdrawInfo.getCreditedAmt());
|
||||||
|
view.setApplicationTime(withdrawInfo.getApplicationTime());
|
||||||
|
view.setArrivalTime(withdrawInfo.getArrivalTime());
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 normalizeOperator(String operatorName, Long operatorId) {
|
||||||
|
String safeOperator = StringUtils.isNotEmpty(operatorName)
|
||||||
|
? operatorName.trim() : String.valueOf(operatorId);
|
||||||
|
if (safeOperator.length() > 64) {
|
||||||
|
throw new ServiceException("操作人名称不能超过64个字符");
|
||||||
|
}
|
||||||
|
return safeOperator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,21 +2,31 @@ package com.jsowell.pile.service.impl;
|
|||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.jsowell.common.core.domain.vo.AuthorizedDeptVO;
|
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.OrderInvoiceRecord;
|
||||||
|
import com.jsowell.pile.domain.PileMerchantInfo;
|
||||||
import com.jsowell.pile.dto.GetInvoiceInfoDTO;
|
import com.jsowell.pile.dto.GetInvoiceInfoDTO;
|
||||||
import com.jsowell.pile.dto.QueryInvoiceRecordDTO;
|
import com.jsowell.pile.dto.QueryInvoiceRecordDTO;
|
||||||
import com.jsowell.pile.mapper.OrderInvoiceRecordMapper;
|
import com.jsowell.pile.mapper.OrderInvoiceRecordMapper;
|
||||||
import com.jsowell.pile.service.MemberInvoiceTitleService;
|
import com.jsowell.pile.service.MemberInvoiceTitleService;
|
||||||
import com.jsowell.pile.service.OrderInvoiceRecordService;
|
import com.jsowell.pile.service.OrderInvoiceRecordService;
|
||||||
import com.jsowell.pile.service.OrderBasicInfoService;
|
import com.jsowell.pile.service.OrderBasicInfoService;
|
||||||
|
import com.jsowell.pile.service.PileMerchantInfoService;
|
||||||
import com.jsowell.pile.util.UserUtils;
|
import com.jsowell.pile.util.UserUtils;
|
||||||
import com.jsowell.pile.vo.base.OrderAmountDetailVO;
|
import com.jsowell.pile.vo.base.OrderAmountDetailVO;
|
||||||
import com.jsowell.pile.vo.uniapp.customer.InvoiceTitleVO;
|
import com.jsowell.pile.vo.uniapp.customer.InvoiceTitleVO;
|
||||||
import com.jsowell.pile.vo.web.InvoiceRecordVO;
|
import com.jsowell.pile.vo.web.InvoiceRecordVO;
|
||||||
import com.jsowell.pile.vo.web.OrderInvoiceRecordVO;
|
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.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -30,6 +40,11 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService {
|
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
|
@Autowired
|
||||||
private OrderInvoiceRecordMapper orderInvoiceRecordMapper;
|
private OrderInvoiceRecordMapper orderInvoiceRecordMapper;
|
||||||
|
|
||||||
@@ -39,6 +54,15 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private OrderBasicInfoService orderBasicInfoService;
|
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 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public int insertOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord) {
|
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 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord) {
|
@Transactional(rollbackFor = Exception.class)
|
||||||
return orderInvoiceRecordMapper.updateOrderInvoiceRecord(orderInvoiceRecord);
|
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 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int deleteOrderInvoiceRecordByIds(Integer[] ids) {
|
@Transactional(rollbackFor = Exception.class)
|
||||||
return orderInvoiceRecordMapper.deleteOrderInvoiceRecordByIds(ids);
|
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 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int deleteOrderInvoiceRecordById(Integer id) {
|
@Transactional(rollbackFor = Exception.class)
|
||||||
return orderInvoiceRecordMapper.deleteOrderInvoiceRecordById(id);
|
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.findActiveOperatorAdminUserIdsByDeptTree(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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.jsowell.pile.vo.web;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运营商提现开票详情。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class MerchantWithdrawInvoiceVO {
|
||||||
|
private Long id;
|
||||||
|
private String withdrawCode;
|
||||||
|
private String merchantId;
|
||||||
|
private String invoiceStatus;
|
||||||
|
private String invoiceNumber;
|
||||||
|
private String voucherUrl;
|
||||||
|
private Long submittedBy;
|
||||||
|
private String submittedName;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date submittedTime;
|
||||||
|
|
||||||
|
private String closedName;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date closedTime;
|
||||||
|
|
||||||
|
private String closeReason;
|
||||||
|
private BigDecimal withdrawAmt;
|
||||||
|
private BigDecimal feeAmt;
|
||||||
|
private BigDecimal creditedAmt;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date applicationTime;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date arrivalTime;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?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.pile.mapper.ClearingWithdrawInvoiceMapper">
|
||||||
|
<resultMap id="BaseResultMap" type="com.jsowell.pile.domain.ClearingWithdrawInvoice">
|
||||||
|
<id column="id" property="id" />
|
||||||
|
<result column="withdraw_code" property="withdrawCode" />
|
||||||
|
<result column="merchant_id" property="merchantId" />
|
||||||
|
<result column="invoice_status" property="invoiceStatus" />
|
||||||
|
<result column="invoice_number" property="invoiceNumber" />
|
||||||
|
<result column="voucher_url" property="voucherUrl" />
|
||||||
|
<result column="submitted_by" property="submittedBy" />
|
||||||
|
<result column="submitted_name" property="submittedName" />
|
||||||
|
<result column="submitted_time" property="submittedTime" />
|
||||||
|
<result column="closed_by" property="closedBy" />
|
||||||
|
<result column="closed_name" property="closedName" />
|
||||||
|
<result column="closed_time" property="closedTime" />
|
||||||
|
<result column="close_reason" property="closeReason" />
|
||||||
|
<result column="create_by" property="createBy" />
|
||||||
|
<result column="create_time" property="createTime" />
|
||||||
|
<result column="update_by" property="updateBy" />
|
||||||
|
<result column="update_time" property="updateTime" />
|
||||||
|
<result column="remark" property="remark" />
|
||||||
|
<result column="del_flag" property="delFlag" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="BaseColumnList">
|
||||||
|
id, withdraw_code, merchant_id, invoice_status, invoice_number, voucher_url,
|
||||||
|
submitted_by, submitted_name, submitted_time, closed_by, closed_name, closed_time,
|
||||||
|
close_reason, create_by, create_time, update_by, update_time, remark, del_flag
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectByWithdrawCode" resultMap="BaseResultMap">
|
||||||
|
select <include refid="BaseColumnList" />
|
||||||
|
from clearing_withdraw_invoice
|
||||||
|
where withdraw_code = #{withdrawCode}
|
||||||
|
and del_flag = '0'
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertSelective" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
|
||||||
|
insert into clearing_withdraw_invoice (
|
||||||
|
withdraw_code, merchant_id, invoice_status, create_by, create_time, del_flag
|
||||||
|
) values (
|
||||||
|
#{withdrawCode}, #{merchantId}, #{invoiceStatus}, #{createBy}, sysdate(), #{delFlag}
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="markSubmitted">
|
||||||
|
update clearing_withdraw_invoice
|
||||||
|
set invoice_status = #{invoiceStatus},
|
||||||
|
invoice_number = #{invoiceNumber},
|
||||||
|
voucher_url = #{voucherUrl},
|
||||||
|
submitted_by = #{submittedBy},
|
||||||
|
submitted_name = #{submittedName},
|
||||||
|
submitted_time = #{submittedTime},
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = #{updateTime}
|
||||||
|
where id = #{id}
|
||||||
|
and invoice_status = 'PENDING'
|
||||||
|
and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="markClosed">
|
||||||
|
update clearing_withdraw_invoice
|
||||||
|
set invoice_status = #{invoiceStatus},
|
||||||
|
closed_by = #{closedBy},
|
||||||
|
closed_name = #{closedName},
|
||||||
|
closed_time = #{closedTime},
|
||||||
|
close_reason = #{closeReason},
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = #{updateTime}
|
||||||
|
where id = #{id}
|
||||||
|
and invoice_status = 'PENDING'
|
||||||
|
and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.jsowell.quartz.task;
|
||||||
|
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult;
|
||||||
|
import com.jsowell.system.service.TodoTaskMaintenanceService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办过期和历史归档定时任务。
|
||||||
|
*/
|
||||||
|
@Component("todoTaskMaintenanceTask")
|
||||||
|
public class TodoTaskMaintenanceTask {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TodoTaskMaintenanceTask.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TodoTaskMaintenanceService todoTaskMaintenanceService;
|
||||||
|
|
||||||
|
public void maintain() {
|
||||||
|
TodoTaskMaintenanceResult result = todoTaskMaintenanceService.maintain();
|
||||||
|
log.info("待办维护任务完成,expiredCount={}, archivedCount={}, archiveBefore={}",
|
||||||
|
result.getExpiredCount(), result.getArchivedCount(), result.getArchiveBefore());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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;
|
||||||
|
/** 现有运营商创建流程使用的运营商管理员角色 ID,正式环境可通过配置覆盖。 */
|
||||||
|
public static final long DEFAULT_OPERATOR_ADMIN_ROLE_ID = 3L;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.jsowell.system.domain.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Max;
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端人工创建待办请求。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TodoTaskAdminCreateRequest implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotBlank(message = "待办类型不能为空")
|
||||||
|
@Size(max = 64, message = "待办类型不能超过64个字符")
|
||||||
|
private String taskType;
|
||||||
|
|
||||||
|
@NotBlank(message = "待办标题不能为空")
|
||||||
|
@Size(max = 200, message = "待办标题不能超过200个字符")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Size(max = 500, message = "待办摘要不能超过500个字符")
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
@Size(max = 20000, message = "待办内容不能超过20000个字符")
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
@NotBlank(message = "业务类型不能为空")
|
||||||
|
@Size(max = 64, message = "业务类型不能超过64个字符")
|
||||||
|
private String businessType;
|
||||||
|
|
||||||
|
@NotBlank(message = "业务主键不能为空")
|
||||||
|
@Size(max = 64, message = "业务主键不能超过64个字符")
|
||||||
|
private String businessId;
|
||||||
|
|
||||||
|
@NotBlank(message = "业务路由不能为空")
|
||||||
|
@Size(max = 100, message = "业务路由不能超过100个字符")
|
||||||
|
private String routeName;
|
||||||
|
|
||||||
|
@Size(max = 1000, message = "路由参数不能超过1000个字符")
|
||||||
|
private String routeParams;
|
||||||
|
|
||||||
|
@NotNull(message = "接收用户不能为空")
|
||||||
|
@Min(value = 1, message = "接收用户不能为空")
|
||||||
|
private Long assigneeUserId;
|
||||||
|
|
||||||
|
@Size(max = 64, message = "运营商 ID 不能超过64个字符")
|
||||||
|
private String assigneeMerchantId;
|
||||||
|
|
||||||
|
@Min(value = 1, message = "优先级只能为1、2或3")
|
||||||
|
@Max(value = 3, message = "优先级只能为1、2或3")
|
||||||
|
private Integer priority;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date dueTime;
|
||||||
|
|
||||||
|
@Size(max = 200, message = "幂等键不能超过200个字符")
|
||||||
|
private String idempotentKey;
|
||||||
|
|
||||||
|
@Size(max = 500, message = "备注不能超过500个字符")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.jsowell.system.domain.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端待办查询条件。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TodoTaskAdminQuery implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long assigneeUserId;
|
||||||
|
private String assigneeMerchantId;
|
||||||
|
private String taskStatus;
|
||||||
|
private String readStatus;
|
||||||
|
private String taskType;
|
||||||
|
private String keyword;
|
||||||
|
private Boolean hideCompleted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.jsowell.system.domain.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.Min;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import javax.validation.constraints.Size;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端待办分配请求。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TodoTaskAssignRequest implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@NotNull(message = "接收用户不能为空")
|
||||||
|
@Min(value = 1, message = "接收用户不能为空")
|
||||||
|
private Long assigneeUserId;
|
||||||
|
|
||||||
|
@Size(max = 64, message = "运营商 ID 不能超过64个字符")
|
||||||
|
private String assigneeMerchantId;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.jsowell.system.domain.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办维护任务执行结果。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TodoTaskMaintenanceResult implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private int expiredCount;
|
||||||
|
private int archivedCount;
|
||||||
|
private Date archiveBefore;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.jsowell.system.mapper;
|
||||||
|
|
||||||
|
import com.jsowell.system.domain.SysTodoTask;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminQuery;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskQuery;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办任务数据层。
|
||||||
|
*
|
||||||
|
* @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> selectAdminTodoList(TodoTaskAdminQuery 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);
|
||||||
|
|
||||||
|
int assignTodo(@Param("todoId") Long todoId,
|
||||||
|
@Param("assigneeUserId") Long assigneeUserId,
|
||||||
|
@Param("assigneeMerchantId") String assigneeMerchantId,
|
||||||
|
@Param("activeIdempotentKey") String activeIdempotentKey,
|
||||||
|
@Param("updateBy") String updateBy);
|
||||||
|
|
||||||
|
int hideTodo(@Param("todoId") Long todoId, @Param("updateBy") String updateBy);
|
||||||
|
|
||||||
|
int expireDueTasks(@Param("now") Date now, @Param("updateBy") String updateBy);
|
||||||
|
|
||||||
|
int archiveTerminalTasks(@Param("archiveBefore") Date archiveBefore,
|
||||||
|
@Param("updateBy") String updateBy);
|
||||||
|
}
|
||||||
@@ -19,6 +19,24 @@ public interface SysUserMapper {
|
|||||||
*/
|
*/
|
||||||
List<SysUser> selectUserList(SysUser sysUser);
|
List<SysUser> selectUserList(SysUser sysUser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询部门及下级部门的正常用户 ID。
|
||||||
|
*
|
||||||
|
* @param deptId 部门 ID
|
||||||
|
* @return 用户 ID 集合
|
||||||
|
*/
|
||||||
|
List<Long> selectActiveUserIdsByDeptTree(@Param("deptId") Long deptId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询部门及下级部门中拥有指定角色的正常用户 ID。
|
||||||
|
*
|
||||||
|
* @param deptId 部门 ID
|
||||||
|
* @param roleId 角色 ID
|
||||||
|
* @return 用户 ID 集合
|
||||||
|
*/
|
||||||
|
List<Long> selectActiveUserIdsByDeptTreeAndRole(@Param("deptId") Long deptId,
|
||||||
|
@Param("roleId") Long roleId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据条件分页查询已配用户角色列表
|
* 根据条件分页查询已配用户角色列表
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.jsowell.system.service;
|
||||||
|
|
||||||
|
import com.jsowell.system.domain.SysTodoTask;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminCreateRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminQuery;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAssignRequest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办管理端服务。
|
||||||
|
*/
|
||||||
|
public interface TodoTaskAdminService {
|
||||||
|
List<SysTodoTask> list(TodoTaskAdminQuery query);
|
||||||
|
|
||||||
|
SysTodoTask create(TodoTaskAdminCreateRequest request, String operatorName);
|
||||||
|
|
||||||
|
boolean assign(Long todoId, TodoTaskAssignRequest request, String operatorName);
|
||||||
|
|
||||||
|
boolean cancel(Long todoId, Long operatorId, String operatorName, String reason);
|
||||||
|
|
||||||
|
boolean hide(Long todoId, String operatorName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.jsowell.system.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办接收人解析服务。
|
||||||
|
*
|
||||||
|
* @author jsowell
|
||||||
|
*/
|
||||||
|
public interface TodoTaskAssigneeService {
|
||||||
|
List<Long> findActiveUserIdsByDeptTree(Long deptId);
|
||||||
|
|
||||||
|
List<Long> findActiveOperatorAdminUserIdsByDeptTree(Long deptId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.jsowell.system.service;
|
||||||
|
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办过期和历史归档维护服务。
|
||||||
|
*/
|
||||||
|
public interface TodoTaskMaintenanceService {
|
||||||
|
TodoTaskMaintenanceResult maintain();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package com.jsowell.system.service.impl;
|
||||||
|
|
||||||
|
import com.jsowell.common.constant.Constants;
|
||||||
|
import com.jsowell.common.core.domain.entity.SysUser;
|
||||||
|
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.TodoTaskAdminCreateRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAdminQuery;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskAssignRequest;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
|
||||||
|
import com.jsowell.system.mapper.SysTodoTaskMapper;
|
||||||
|
import com.jsowell.system.mapper.SysUserMapper;
|
||||||
|
import com.jsowell.system.service.TodoTaskAdminService;
|
||||||
|
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.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办管理端服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TodoTaskAdminServiceImpl implements TodoTaskAdminService {
|
||||||
|
@Autowired
|
||||||
|
private SysTodoTaskMapper todoTaskMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TodoTaskService todoTaskService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysTodoTask> list(TodoTaskAdminQuery query) {
|
||||||
|
TodoTaskAdminQuery safeQuery = query == null ? new TodoTaskAdminQuery() : query;
|
||||||
|
validateQuery(safeQuery);
|
||||||
|
return todoTaskMapper.selectAdminTodoList(safeQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public SysTodoTask create(TodoTaskAdminCreateRequest request, String operatorName) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new ServiceException("待办创建参数不能为空");
|
||||||
|
}
|
||||||
|
validateActiveUser(request.getAssigneeUserId());
|
||||||
|
String idempotentKey = StringUtils.isNotBlank(request.getIdempotentKey())
|
||||||
|
? request.getIdempotentKey().trim()
|
||||||
|
: buildAssignmentKey(request.getTaskType(), request.getBusinessType(),
|
||||||
|
request.getBusinessId(), request.getAssigneeUserId());
|
||||||
|
return todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
||||||
|
.taskType(request.getTaskType())
|
||||||
|
.title(request.getTitle())
|
||||||
|
.summary(request.getSummary())
|
||||||
|
.content(request.getContent())
|
||||||
|
.businessType(request.getBusinessType())
|
||||||
|
.businessId(request.getBusinessId())
|
||||||
|
.routeName(request.getRouteName())
|
||||||
|
.routeParams(request.getRouteParams())
|
||||||
|
.assigneeUserId(request.getAssigneeUserId())
|
||||||
|
.assigneeMerchantId(request.getAssigneeMerchantId())
|
||||||
|
.priority(request.getPriority())
|
||||||
|
.dueTime(request.getDueTime())
|
||||||
|
.idempotentKey(idempotentKey)
|
||||||
|
.createBy(normalizeOperator(operatorName))
|
||||||
|
.remark(request.getRemark())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean assign(Long todoId, TodoTaskAssignRequest request, String operatorName) {
|
||||||
|
if (request == null) {
|
||||||
|
throw new ServiceException("待办分配参数不能为空");
|
||||||
|
}
|
||||||
|
validateTodoId(todoId);
|
||||||
|
validateActiveUser(request.getAssigneeUserId());
|
||||||
|
SysTodoTask todoTask = requireTask(todoId);
|
||||||
|
if (!TodoTaskConstants.isActiveStatus(todoTask.getTaskStatus())) {
|
||||||
|
throw new ServiceException("只有待处理任务可以重新分配");
|
||||||
|
}
|
||||||
|
String activeIdempotentKey = buildAssignmentKey(todoTask.getTaskType(),
|
||||||
|
todoTask.getBusinessType(), todoTask.getBusinessId(), request.getAssigneeUserId());
|
||||||
|
String assigneeMerchantId = StringUtils.isBlank(request.getAssigneeMerchantId())
|
||||||
|
? todoTask.getAssigneeMerchantId()
|
||||||
|
: normalizeOptional(request.getAssigneeMerchantId(), 64);
|
||||||
|
try {
|
||||||
|
int rows = todoTaskMapper.assignTodo(todoId, request.getAssigneeUserId(), assigneeMerchantId,
|
||||||
|
activeIdempotentKey,
|
||||||
|
normalizeOperator(operatorName));
|
||||||
|
if (rows > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (DuplicateKeyException exception) {
|
||||||
|
throw new ServiceException("目标用户已存在同一业务的有效待办");
|
||||||
|
}
|
||||||
|
throw new ServiceException("当前待办状态不允许重新分配");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean cancel(Long todoId, Long operatorId, String operatorName, String reason) {
|
||||||
|
return todoTaskService.cancel(todoId, operatorId, operatorName, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean hide(Long todoId, String operatorName) {
|
||||||
|
validateTodoId(todoId);
|
||||||
|
SysTodoTask todoTask = requireTask(todoId);
|
||||||
|
if (!TodoTaskConstants.isTerminalStatus(todoTask.getTaskStatus())) {
|
||||||
|
throw new ServiceException("待处理任务需先取消后才能隐藏");
|
||||||
|
}
|
||||||
|
if (todoTaskMapper.hideTodo(todoId, normalizeOperator(operatorName)) > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new ServiceException("当前待办状态不允许隐藏");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateQuery(TodoTaskAdminQuery query) {
|
||||||
|
if (query.getAssigneeUserId() != null && query.getAssigneeUserId() <= 0) {
|
||||||
|
throw new ServiceException("接收用户 ID 不合法");
|
||||||
|
}
|
||||||
|
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.setAssigneeMerchantId(normalizeOptional(query.getAssigneeMerchantId(), 64));
|
||||||
|
query.setTaskType(normalizeOptional(query.getTaskType(), 64));
|
||||||
|
query.setKeyword(normalizeOptional(query.getKeyword(), 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysTodoTask requireTask(Long todoId) {
|
||||||
|
SysTodoTask todoTask = todoTaskMapper.selectTodoById(todoId);
|
||||||
|
if (todoTask == null) {
|
||||||
|
throw new ServiceException("待办任务不存在");
|
||||||
|
}
|
||||||
|
return todoTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateActiveUser(Long userId) {
|
||||||
|
if (userId == null || userId <= 0) {
|
||||||
|
throw new ServiceException("接收用户不能为空");
|
||||||
|
}
|
||||||
|
SysUser user = userMapper.selectUserById(userId);
|
||||||
|
if (user == null || !Constants.ZERO.equals(user.getStatus())
|
||||||
|
|| !Constants.ZERO.equals(user.getDelFlag())) {
|
||||||
|
throw new ServiceException("接收用户不存在或已停用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateTodoId(Long todoId) {
|
||||||
|
if (todoId == null || todoId <= 0) {
|
||||||
|
throw new ServiceException("待办任务 ID 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAssignmentKey(String taskType, String businessType,
|
||||||
|
String businessId, Long assigneeUserId) {
|
||||||
|
String source = StringUtils.trim(taskType) + ":" + StringUtils.trim(businessType)
|
||||||
|
+ ":" + StringUtils.trim(businessId) + ":" + assigneeUserId;
|
||||||
|
if (source.length() <= 200) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return "ADMIN_ASSIGN:" + sha256(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sha256(String value) {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder result = new StringBuilder(bytes.length * 2);
|
||||||
|
for (byte item : bytes) {
|
||||||
|
result.append(String.format("%02x", item & 0xff));
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
} catch (NoSuchAlgorithmException exception) {
|
||||||
|
throw new IllegalStateException("当前运行环境不支持 SHA-256", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeOptional(String value, int maxLength) {
|
||||||
|
if (StringUtils.isEmpty(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String safeValue = value.trim();
|
||||||
|
if (safeValue.length() > maxLength) {
|
||||||
|
throw new ServiceException("查询条件长度不能超过" + maxLength + "个字符");
|
||||||
|
}
|
||||||
|
return safeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeOperator(String operatorName) {
|
||||||
|
String safeOperator = StringUtils.isNotEmpty(operatorName) ? operatorName.trim() : "system";
|
||||||
|
if (safeOperator.length() > 64) {
|
||||||
|
throw new ServiceException("操作人名称不能超过64个字符");
|
||||||
|
}
|
||||||
|
return safeOperator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.jsowell.system.service.impl;
|
||||||
|
|
||||||
|
import com.jsowell.common.exception.ServiceException;
|
||||||
|
import com.jsowell.system.constant.TodoTaskConstants;
|
||||||
|
import com.jsowell.system.mapper.SysUserMapper;
|
||||||
|
import com.jsowell.system.service.TodoTaskAssigneeService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办接收人解析服务实现。
|
||||||
|
*
|
||||||
|
* @author jsowell
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TodoTaskAssigneeServiceImpl implements TodoTaskAssigneeService {
|
||||||
|
@Value("${todo.operator-admin-role-id:3}")
|
||||||
|
private Long operatorAdminRoleId;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> findActiveUserIdsByDeptTree(Long deptId) {
|
||||||
|
if (deptId == null || deptId <= 0) {
|
||||||
|
throw new ServiceException("待办接收部门不能为空");
|
||||||
|
}
|
||||||
|
return userMapper.selectActiveUserIdsByDeptTree(deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Long> findActiveOperatorAdminUserIdsByDeptTree(Long deptId) {
|
||||||
|
if (deptId == null || deptId <= 0) {
|
||||||
|
throw new ServiceException("待办接收部门不能为空");
|
||||||
|
}
|
||||||
|
Long roleId = operatorAdminRoleId == null || operatorAdminRoleId <= 0
|
||||||
|
? TodoTaskConstants.DEFAULT_OPERATOR_ADMIN_ROLE_ID : operatorAdminRoleId;
|
||||||
|
return userMapper.selectActiveUserIdsByDeptTreeAndRole(deptId, roleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.jsowell.system.service.impl;
|
||||||
|
|
||||||
|
import com.jsowell.common.util.DateUtils;
|
||||||
|
import com.jsowell.system.domain.dto.TodoTaskMaintenanceResult;
|
||||||
|
import com.jsowell.system.mapper.SysTodoTaskMapper;
|
||||||
|
import com.jsowell.system.service.TodoTaskMaintenanceService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待办过期和历史归档维护服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TodoTaskMaintenanceServiceImpl implements TodoTaskMaintenanceService {
|
||||||
|
private static final int DEFAULT_RETENTION_DAYS = 180;
|
||||||
|
|
||||||
|
@Value("${todo.retention-days:180}")
|
||||||
|
private Integer retentionDays;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysTodoTaskMapper todoTaskMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public TodoTaskMaintenanceResult maintain() {
|
||||||
|
Date now = DateUtils.getNowDate();
|
||||||
|
int safeRetentionDays = retentionDays == null || retentionDays < DEFAULT_RETENTION_DAYS
|
||||||
|
? DEFAULT_RETENTION_DAYS : retentionDays;
|
||||||
|
int expiredCount = todoTaskMapper.expireDueTasks(now, "todo-maintenance");
|
||||||
|
Date archiveBefore = DateUtils.addDays(now, -safeRetentionDays);
|
||||||
|
int archivedCount = todoTaskMapper.archiveTerminalTasks(archiveBefore, "todo-maintenance");
|
||||||
|
return new TodoTaskMaintenanceResult(expiredCount, archivedCount, archiveBefore);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package com.jsowell.system.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
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.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一待办任务服务实现。
|
||||||
|
*
|
||||||
|
* @author jsowell
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class TodoTaskServiceImpl implements TodoTaskService {
|
||||||
|
private static final String SYSTEM_OPERATOR = "system";
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
private static final Set<String> DEFAULT_ROUTE_WHITELIST = new HashSet<>(
|
||||||
|
Arrays.asList("invoiceDetail", "financeDetail"));
|
||||||
|
|
||||||
|
@Value("${todo.route-whitelist:invoiceDetail,financeDetail}")
|
||||||
|
private String routeWhitelist;
|
||||||
|
|
||||||
|
@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));
|
||||||
|
String routeName = normalizeRequired(command.getRouteName(), "业务路由", 100);
|
||||||
|
validateRouteName(routeName);
|
||||||
|
todoTask.setRouteName(routeName);
|
||||||
|
todoTask.setRouteParams(normalizeRouteParams(command.getRouteParams()));
|
||||||
|
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 void validateRouteName(String routeName) {
|
||||||
|
Set<String> allowedRoutes = DEFAULT_ROUTE_WHITELIST;
|
||||||
|
if (StringUtils.isNotBlank(routeWhitelist)) {
|
||||||
|
allowedRoutes = new HashSet<>();
|
||||||
|
for (String configuredRoute : routeWhitelist.split(",")) {
|
||||||
|
if (StringUtils.isNotBlank(configuredRoute)) {
|
||||||
|
allowedRoutes.add(configuredRoute.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!allowedRoutes.contains(routeName)) {
|
||||||
|
throw new ServiceException("待办业务路由未加入白名单");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRouteParams(String routeParams) {
|
||||||
|
String safeRouteParams = normalizeOptional(routeParams, "路由参数", 1000);
|
||||||
|
if (safeRouteParams == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonNode root = OBJECT_MAPPER.readTree(safeRouteParams);
|
||||||
|
if (root == null || !root.isObject()) {
|
||||||
|
throw new ServiceException("待办路由参数必须是 JSON 对象");
|
||||||
|
}
|
||||||
|
boolean structured = root.has("params") || root.has("query");
|
||||||
|
Iterator<Map.Entry<String, JsonNode>> fields = root.fields();
|
||||||
|
while (fields.hasNext()) {
|
||||||
|
Map.Entry<String, JsonNode> field = fields.next();
|
||||||
|
validateRouteParamKey(field.getKey());
|
||||||
|
if (structured) {
|
||||||
|
if (!"params".equals(field.getKey()) && !"query".equals(field.getKey())) {
|
||||||
|
throw new ServiceException("待办路由参数仅允许 params 和 query");
|
||||||
|
}
|
||||||
|
validateRouteParamObject(field.getValue());
|
||||||
|
} else {
|
||||||
|
validateRouteParamValue(field.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return safeRouteParams;
|
||||||
|
} catch (ServiceException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new ServiceException("待办路由参数不是合法 JSON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRouteParamObject(JsonNode node) {
|
||||||
|
if (node == null || !node.isObject()) {
|
||||||
|
throw new ServiceException("待办路由 params 和 query 必须是 JSON 对象");
|
||||||
|
}
|
||||||
|
Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
|
||||||
|
while (fields.hasNext()) {
|
||||||
|
Map.Entry<String, JsonNode> field = fields.next();
|
||||||
|
validateRouteParamKey(field.getKey());
|
||||||
|
validateRouteParamValue(field.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRouteParamKey(String key) {
|
||||||
|
if (StringUtils.isBlank(key) || key.length() > 64) {
|
||||||
|
throw new ServiceException("待办路由参数名称不合法");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRouteParamValue(JsonNode value) {
|
||||||
|
if (value == null || value.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!value.isTextual() && !value.isNumber() && !value.isBoolean()) {
|
||||||
|
throw new ServiceException("待办路由参数值只能是字符串、数字或布尔值");
|
||||||
|
}
|
||||||
|
if (value.isTextual() && value.asText().length() > 500) {
|
||||||
|
throw new ServiceException("待办路由参数值不能超过500个字符");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<?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="selectAdminTodoList" parameterType="com.jsowell.system.domain.dto.TodoTaskAdminQuery"
|
||||||
|
resultMap="SysTodoTaskResult">
|
||||||
|
<include refid="selectTodoVo" />
|
||||||
|
<where>
|
||||||
|
del_flag = '0'
|
||||||
|
<if test="assigneeUserId != null">
|
||||||
|
and assignee_user_id = #{assigneeUserId}
|
||||||
|
</if>
|
||||||
|
<if test="assigneeMerchantId != null and assigneeMerchantId != ''">
|
||||||
|
and assignee_merchant_id = #{assigneeMerchantId}
|
||||||
|
</if>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<update id="assignTodo">
|
||||||
|
update sys_todo_task
|
||||||
|
set assignee_user_id = #{assigneeUserId},
|
||||||
|
assignee_merchant_id = #{assigneeMerchantId},
|
||||||
|
read_status = '0',
|
||||||
|
read_time = null,
|
||||||
|
active_idempotent_key = #{activeIdempotentKey},
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where todo_id = #{todoId}
|
||||||
|
and task_status in ('PENDING', 'PROCESSING')
|
||||||
|
and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="hideTodo">
|
||||||
|
update sys_todo_task
|
||||||
|
set active_idempotent_key = null,
|
||||||
|
del_flag = '2',
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where todo_id = #{todoId}
|
||||||
|
and task_status in ('COMPLETED', 'CANCELLED', 'EXPIRED')
|
||||||
|
and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="expireDueTasks">
|
||||||
|
update sys_todo_task
|
||||||
|
set task_status = 'EXPIRED',
|
||||||
|
completed_time = #{now},
|
||||||
|
active_idempotent_key = null,
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = #{now}
|
||||||
|
where task_status in ('PENDING', 'PROCESSING')
|
||||||
|
and due_time is not null
|
||||||
|
and due_time <= #{now}
|
||||||
|
and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="archiveTerminalTasks">
|
||||||
|
update sys_todo_task
|
||||||
|
set del_flag = '2',
|
||||||
|
active_idempotent_key = null,
|
||||||
|
update_by = #{updateBy},
|
||||||
|
update_time = sysdate()
|
||||||
|
where task_status in ('COMPLETED', 'CANCELLED', 'EXPIRED')
|
||||||
|
and coalesce(completed_time, update_time, create_time) < #{archiveBefore}
|
||||||
|
and del_flag = '0'
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -90,6 +90,45 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<!-- 数据范围过滤 -->
|
<!-- 数据范围过滤 -->
|
||||||
${params.dataScope}
|
${params.dataScope}
|
||||||
</select>
|
</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="selectActiveUserIdsByDeptTreeAndRole" resultType="Long">
|
||||||
|
select distinct u.user_id
|
||||||
|
from sys_user u
|
||||||
|
inner join sys_user_role ur on ur.user_id = u.user_id
|
||||||
|
inner join sys_role r on r.role_id = ur.role_id
|
||||||
|
where u.status = '0'
|
||||||
|
and u.del_flag = '0'
|
||||||
|
and r.status = '0'
|
||||||
|
and r.del_flag = '0'
|
||||||
|
and ur.role_id = #{roleId}
|
||||||
|
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 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
|
select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phone, u.status, u.create_time
|
||||||
@@ -225,4 +264,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</foreach>
|
</foreach>
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user