mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-07-20 22:18:07 +08:00
Compare commits
11 Commits
master
...
de312c934b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de312c934b | ||
|
|
cbc2e3c721 | ||
|
|
974e68cf54 | ||
|
|
f84ec43ceb | ||
|
|
181d4a5b40 | ||
|
|
e65017ab22 | ||
|
|
9cdd984bcd | ||
|
|
345a6efb37 | ||
|
|
b4731d3e85 | ||
|
|
a7d838cdb1 | ||
|
|
4986239cd4 |
364
docs/PRD-待办事项功能.md
Normal file
364
docs/PRD-待办事项功能.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# 待办事项中心 PRD
|
||||
|
||||
## 1. 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 产品名称 | 运营商工作台待办事项中心 |
|
||||
| 文档类型 | 产品需求文档(PRD) |
|
||||
| 版本 | V1.0 |
|
||||
| 状态 | 待评审 |
|
||||
| 适用端 | RuoYi-Vue 管理后台、运营商工作台 |
|
||||
|
||||
## 2. 背景与目标
|
||||
|
||||
当前项目已有 RuoYi 的通知公告功能,支持管理员维护全平台公告,但不支持按用户生成业务任务、展示未处理数量和跳转处理业务。
|
||||
|
||||
截图中的“待办事项”属于业务任务中心:任务面向具体用户,用户需要进入业务页面完成处理,处理后任务应自动完成或由用户确认完成。
|
||||
|
||||
本需求目标:
|
||||
|
||||
1. 在运营商工作台首页增加待办事项卡片。
|
||||
2. 展示当前登录用户未处理的待办数量。
|
||||
3. 支持点击待办跳转到对应业务详情或处理页面。
|
||||
4. 支持待办状态、优先级、已读状态和完成状态管理。
|
||||
5. 建立统一待办能力,后续可接入开票、提现、汇付开户、清分异常等业务。
|
||||
|
||||
## 3. 产品定位
|
||||
|
||||
待办事项与通知公告、站内信的关系如下:
|
||||
|
||||
```text
|
||||
消息中心
|
||||
├── 待办事项:需要用户处理的业务任务
|
||||
├── 业务通知:业务结果或状态变化提醒
|
||||
├── 系统公告:面向全体用户的运营公告
|
||||
└── 系统消息:登录、安全、权限等系统提醒
|
||||
```
|
||||
|
||||
本期只建设“待办事项”,不替换现有 `sys_notice` 通知公告模块。
|
||||
|
||||
## 4. 用户与权限
|
||||
|
||||
### 4.1 用户角色
|
||||
|
||||
| 用户 | 权限 |
|
||||
| --- | --- |
|
||||
| 运营商负责人 | 查看和处理本人或本运营商范围内的待办 |
|
||||
| 运营商财务人员 | 查看和处理财务类待办 |
|
||||
| 平台运营人员 | 创建、分配、转派和关闭待办,可查看全量待办 |
|
||||
| 系统管理员 | 管理待办类型、权限和异常任务 |
|
||||
|
||||
### 4.2 数据权限
|
||||
|
||||
默认只返回当前登录用户的待办。平台管理员是否可以查看全量待办,由现有 RuoYi 权限和数据权限控制,不允许通过修改请求参数绕过接收人过滤。
|
||||
|
||||
## 5. 首期业务范围
|
||||
|
||||
### 5.1 开票待办
|
||||
|
||||
首期优先实现截图中的开票场景:
|
||||
|
||||
| 待办类型 | 触发条件 | 处理页面 | 完成条件 |
|
||||
| --- | --- | --- | --- |
|
||||
| 平台采购开票 | 平台审核拒绝或要求补充材料 | 发票详情/开票申请页 | 用户重新提交或平台关闭任务 |
|
||||
| 司机开票申请 | 收到司机开票申请 | 发票申请处理页 | 审核通过、驳回或关闭 |
|
||||
|
||||
### 5.2 预留业务类型
|
||||
|
||||
| 待办类型 | 触发条件 |
|
||||
| --- | --- |
|
||||
| 汇付企业开户审核 | 企业开户失败或需要补充资料 |
|
||||
| 结算账户异常 | 结算账户创建失败、删除失败或状态不一致 |
|
||||
| 提现处理 | 提现失败、人工审核或补充资料 |
|
||||
| 清分异常 | 清分失败、分账配置错误或账单待处理 |
|
||||
| 订单异常 | 订单退款、结算或支付异常需要人工介入 |
|
||||
|
||||
首期只实现开票待办,其他类型只完成通用模型和扩展接口,不要求全部接入。
|
||||
|
||||
## 6. 业务流程
|
||||
|
||||
### 6.1 创建流程
|
||||
|
||||
```text
|
||||
业务事件发生
|
||||
↓
|
||||
判断是否需要人工处理
|
||||
↓
|
||||
确定接收人和业务权限
|
||||
↓
|
||||
按幂等键创建待办
|
||||
↓
|
||||
首页待办数量增加
|
||||
↓
|
||||
用户点击待办
|
||||
↓
|
||||
进入业务处理页面并标记已读
|
||||
↓
|
||||
业务处理成功后自动完成待办
|
||||
```
|
||||
|
||||
### 6.2 状态定义
|
||||
|
||||
待办状态和阅读状态分开保存:
|
||||
|
||||
| 字段 | 值 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `task_status` | `PENDING` | 待处理 |
|
||||
| `task_status` | `PROCESSING` | 处理中 |
|
||||
| `task_status` | `COMPLETED` | 已完成 |
|
||||
| `task_status` | `CANCELLED` | 已取消 |
|
||||
| `task_status` | `EXPIRED` | 已过期 |
|
||||
| `read_status` | `0` | 未读 |
|
||||
| `read_status` | `1` | 已读 |
|
||||
|
||||
已读只表示用户看过,不能代表业务已经处理完成。只有业务动作完成、平台关闭或管理员取消时,待办才进入完成或取消状态。
|
||||
|
||||
### 6.3 幂等规则
|
||||
|
||||
同一业务任务不能重复生成待办。建议使用以下幂等键:
|
||||
|
||||
```text
|
||||
task_type + business_type + business_id + assignee_user_id
|
||||
```
|
||||
|
||||
例如同一个发票申请给同一个用户只能有一条有效待办。任务完成或取消后,允许业务重新产生新的任务,但必须保留历史记录。
|
||||
|
||||
## 7. 首页交互设计
|
||||
|
||||
### 7.1 待办卡片
|
||||
|
||||
位置:运营商工作台首页右侧,与截图中的“待办事项”位置一致。
|
||||
|
||||
展示规则:
|
||||
|
||||
- 标题:`待办事项`。
|
||||
- 标题右侧显示未完成数量,最多显示 `99+`。
|
||||
- 默认展示最近 3 条未完成待办。
|
||||
- 按优先级倒序、创建时间倒序排列。
|
||||
- 每条显示:类型、标题、简要内容、时间和箭头。
|
||||
- 无待办时显示“暂无待办事项”。
|
||||
- 点击“查看全部”进入待办中心。
|
||||
- 点击单条任务进入业务处理页面。
|
||||
|
||||
### 7.2 待办中心
|
||||
|
||||
页面建议:`src/views/system/todo/index.vue`。
|
||||
|
||||
功能:
|
||||
|
||||
- 全部、待处理、已完成、已取消筛选。
|
||||
- 全部、未读筛选。
|
||||
- 按待办类型筛选。
|
||||
- 关键字搜索标题和业务单号。
|
||||
- 分页查询。
|
||||
- 点击进入业务详情。
|
||||
- 支持批量标记已读。
|
||||
- 支持隐藏已完成待办,不直接物理删除历史任务。
|
||||
|
||||
### 7.3 详情处理
|
||||
|
||||
待办本身不重复实现业务处理表单。点击待办后跳转已有业务页面,并携带:
|
||||
|
||||
```text
|
||||
businessType
|
||||
businessId
|
||||
todoId
|
||||
```
|
||||
|
||||
业务页面处理成功后调用待办完成接口,或由后端业务事务自动完成待办。
|
||||
|
||||
## 8. 数据库设计
|
||||
|
||||
建议新增主表 `sys_todo_task`:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `todo_id` | bigint | 主键 |
|
||||
| `task_type` | varchar(64) | 待办类型编码 |
|
||||
| `title` | varchar(200) | 待办标题 |
|
||||
| `summary` | varchar(500) | 摘要 |
|
||||
| `content` | text | 详细内容 |
|
||||
| `business_type` | varchar(64) | 业务类型 |
|
||||
| `business_id` | varchar(64) | 业务主键或单号 |
|
||||
| `route_name` | varchar(100) | 前端路由名称 |
|
||||
| `route_params` | varchar(1000) | 路由参数 JSON |
|
||||
| `assignee_user_id` | bigint | 接收用户 ID |
|
||||
| `assignee_merchant_id` | varchar(64) | 运营商 ID,可为空 |
|
||||
| `priority` | tinyint | 优先级:1普通、2重要、3紧急 |
|
||||
| `task_status` | varchar(20) | 任务状态 |
|
||||
| `read_status` | char(1) | 0未读、1已读 |
|
||||
| `read_time` | datetime | 阅读时间 |
|
||||
| `due_time` | datetime | 截止时间,可为空 |
|
||||
| `completed_time` | datetime | 完成时间 |
|
||||
| `completed_by` | bigint | 完成人 |
|
||||
| `cancel_reason` | varchar(500) | 取消原因 |
|
||||
| `idempotent_key` | varchar(200) | 幂等键 |
|
||||
| `create_time` | datetime | 创建时间 |
|
||||
| `update_time` | datetime | 更新时间 |
|
||||
| `del_flag` | char(1) | 逻辑删除 |
|
||||
|
||||
建议索引:
|
||||
|
||||
```sql
|
||||
unique key uk_todo_idempotent (idempotent_key)
|
||||
key idx_todo_user_status (assignee_user_id, task_status, read_status, create_time)
|
||||
key idx_todo_merchant_status (assignee_merchant_id, task_status, create_time)
|
||||
key idx_todo_business (business_type, business_id)
|
||||
```
|
||||
|
||||
如果后续需要同一任务分配给多个用户,建议拆分为:
|
||||
|
||||
- `sys_todo_task`:任务主体。
|
||||
- `sys_todo_recipient`:接收人和每个接收人的阅读状态、处理状态。
|
||||
|
||||
V1 只有单接收人时可以先使用单表,避免过早复杂化;但字段和 Service 设计应预留多接收人扩展。
|
||||
|
||||
## 9. 后端接口设计
|
||||
|
||||
### 9.1 用户端接口
|
||||
|
||||
```text
|
||||
GET /system/todo/unreadCount
|
||||
GET /system/todo/homeList?limit=3
|
||||
GET /system/todo/list
|
||||
GET /system/todo/{todoId}
|
||||
PUT /system/todo/{todoId}/read
|
||||
PUT /system/todo/readAll
|
||||
PUT /system/todo/{todoId}/complete
|
||||
PUT /system/todo/{todoId}/cancel
|
||||
```
|
||||
|
||||
接口规则:
|
||||
|
||||
- 所有查询自动使用当前登录用户 ID,不接收任意 `userId` 作为唯一权限依据。
|
||||
- `read` 只能修改当前用户的阅读状态。
|
||||
- `complete` 必须校验当前用户有对应业务处理权限。
|
||||
- `complete` 和具体业务更新建议放在同一事务中,避免业务完成但待办仍为待处理。
|
||||
- 已完成任务默认仍可查询,但不计入未读数量和首页待办数量。
|
||||
|
||||
### 9.2 管理端接口
|
||||
|
||||
```text
|
||||
GET /system/todo/admin/list
|
||||
POST /system/todo/admin/create
|
||||
PUT /system/todo/admin/{todoId}/assign
|
||||
PUT /system/todo/admin/{todoId}/cancel
|
||||
DELETE /system/todo/admin/{todoId}
|
||||
```
|
||||
|
||||
管理端用于异常任务处理和人工补发,普通业务待办优先由业务事件自动生成。
|
||||
|
||||
## 10. 待办服务设计
|
||||
|
||||
新增统一服务接口:
|
||||
|
||||
```java
|
||||
TodoTaskService.createTask(TodoTaskCreateCommand command);
|
||||
TodoTaskService.markRead(Long todoId, Long userId);
|
||||
TodoTaskService.complete(Long todoId, Long userId);
|
||||
TodoTaskService.cancel(Long todoId, Long operatorId, String reason);
|
||||
TodoTaskService.countUnread(Long userId);
|
||||
```
|
||||
|
||||
业务模块只依赖 `TodoTaskService`,不直接操作待办表。
|
||||
|
||||
例如开票业务:
|
||||
|
||||
```java
|
||||
todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
||||
.taskType("INVOICE_REVIEW")
|
||||
.title("司机开票申请")
|
||||
.summary("有新的开票申请需要处理")
|
||||
.businessType("INVOICE_APPLY")
|
||||
.businessId(invoiceId.toString())
|
||||
.assigneeUserId(operatorUserId)
|
||||
.routeName("InvoiceApplyDetail")
|
||||
.idempotentKey("INVOICE_REVIEW:" + invoiceId + ":" + operatorUserId)
|
||||
.build());
|
||||
```
|
||||
|
||||
## 11. 通知刷新策略
|
||||
|
||||
### V1 推荐:轮询
|
||||
|
||||
- 登录后加载未读数量。
|
||||
- 首页每 30~60 秒刷新一次数量和最近待办。
|
||||
- 打开待办中心时立即刷新。
|
||||
- 处理完成后主动刷新当前列表和数量。
|
||||
|
||||
轮询实现简单,不需要改造现有 Netty、RabbitMQ 或新增 WebSocket 基础设施,适合本项目首期落地。
|
||||
|
||||
### V2 可选:实时推送
|
||||
|
||||
当待办实时性要求提高时,再增加 WebSocket 或 SSE。实时推送只负责通知前端刷新,不直接承载任务数据;前端收到事件后仍通过接口重新查询。
|
||||
|
||||
## 12. 非功能要求
|
||||
|
||||
- 接口必须使用当前登录用户做数据权限过滤。
|
||||
- 内容字段需要进行 XSS 处理,详情页禁止直接渲染未经处理的 HTML。
|
||||
- 所有状态变更记录操作人、操作时间和操作原因。
|
||||
- 待办创建必须幂等,避免重复点击或重复回调产生重复任务。
|
||||
- 首页查询应使用索引,目标响应时间不超过 500ms。
|
||||
- 默认保留已完成任务至少 180 天,定时任务归档或清理历史数据。
|
||||
- 待办跳转路由必须使用服务端生成或白名单校验,禁止直接执行前端传入的任意路由。
|
||||
|
||||
## 13. 验收标准
|
||||
|
||||
### 首页
|
||||
|
||||
- 用户登录后可以看到待办事项卡片。
|
||||
- 未完成数量准确,数量为 0 时不显示红色数字或显示 0。
|
||||
- 默认最多展示 3 条待办。
|
||||
- 点击“查看全部”可以进入待办中心。
|
||||
- 点击单条待办可以跳转对应业务页面。
|
||||
|
||||
### 状态
|
||||
|
||||
- 首次进入列表时未读待办显示未读样式。
|
||||
- 点击待办后变为已读,但仍保持待处理状态。
|
||||
- 业务处理成功后变为已完成,不再计入首页数量。
|
||||
- 取消或过期任务不再计入待办数量。
|
||||
- 重复触发同一业务事件不会产生重复有效待办。
|
||||
|
||||
### 权限
|
||||
|
||||
- 用户无法查询其他用户的待办。
|
||||
- 无业务处理权限的用户不能完成待办。
|
||||
- 管理员可以按权限查看和关闭异常待办。
|
||||
|
||||
### 首期业务
|
||||
|
||||
- 平台采购开票待办可以正常生成、展示、跳转和完成。
|
||||
- 司机开票申请待办可以正常生成、展示、跳转和完成。
|
||||
- 原有 `sys_notice` 公告功能不受影响。
|
||||
|
||||
## 14. 实施拆分
|
||||
|
||||
### 第一期:基础能力
|
||||
|
||||
- 建表和索引。
|
||||
- 实体、Mapper、Service、Controller。
|
||||
- 未读数、首页列表、分页列表、已读和完成接口。
|
||||
- 首页待办卡片和待办中心页面。
|
||||
|
||||
### 第二期:开票接入
|
||||
|
||||
- 接入平台采购开票事件。
|
||||
- 接入司机开票申请事件。
|
||||
- 配置业务路由和完成条件。
|
||||
- 增加接口和页面联调测试。
|
||||
|
||||
### 第三期:平台业务扩展
|
||||
|
||||
- 接入汇付开户、结算账户和提现异常。
|
||||
- 接入清分异常和订单异常。
|
||||
- 评估 WebSocket/SSE 实时推送。
|
||||
|
||||
## 15. 与现有 RuoYi 模块的关系
|
||||
|
||||
- `sys_notice` 继续作为全平台通知公告管理。
|
||||
- 新增 `sys_todo_task` 作为用户待办任务管理。
|
||||
- 沿用 RuoYi 的 `SysUser`、权限注解、数据权限和分页组件。
|
||||
- 沿用现有 `BaseController`、`AjaxResult`、`TableDataInfo` 和前端 `request` 封装。
|
||||
- 不建议把待办字段直接塞入 `sys_notice`,避免公告和业务任务互相污染。
|
||||
190
docs/plan/2026-07-16-todo-center-task-tracker.md
Normal file
190
docs/plan/2026-07-16-todo-center-task-tracker.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# 待办事项中心开发任务跟踪
|
||||
|
||||
## 任务概述
|
||||
|
||||
- 需求来源:`docs/PRD-待办事项功能.md`
|
||||
- 开发目标:先交付面向当前登录用户的通用待办闭环,再接入首期开票业务,最后补充管理和扩展能力。
|
||||
- 后端仓库:当前根仓库,分支 `feature-待办中心`
|
||||
- 前端仓库:`jsowell-charge-ui/` 独立 Git 仓库,当前分支 `dev`
|
||||
- 状态定义:`todo` 未开始、`in_progress` 进行中、`blocked` 阻塞、`done` 已完成
|
||||
|
||||
## 优先级说明
|
||||
|
||||
- P0:首期上线必需,缺失时无法形成可用闭环。
|
||||
- P1:首期开票业务落地和运营管理必需,可在通用能力稳定后接入。
|
||||
- P2:安全、运维和体验增强,不阻塞首期核心流程。
|
||||
- P3:后续业务扩展和实时化能力。
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
### 幂等键生命周期
|
||||
|
||||
PRD 同时要求“幂等键唯一”和“任务完成后允许同一业务重新产生任务”。MySQL 普通唯一索引无法同时满足这两个要求,因此采用双字段方案:
|
||||
|
||||
- `idempotent_key`:保存业务方传入的原始幂等键,历史记录不修改。
|
||||
- `active_idempotent_key`:仅非终态任务保存原始幂等键,并建立唯一索引。
|
||||
- 任务进入 `COMPLETED`、`CANCELLED` 或 `EXPIRED` 后,将 `active_idempotent_key` 置空。
|
||||
|
||||
该方案利用 MySQL 唯一索引允许多个 `NULL` 的特性,保证并发创建时只有一条有效待办,同时允许终态后再次创建。
|
||||
|
||||
### 数据权限边界
|
||||
|
||||
- 用户端接口不接收接收人 ID,统一从当前登录态获取用户 ID。
|
||||
- 用户端详情、已读、完成和取消更新均在 SQL 条件中携带 `assignee_user_id`,避免先查后改产生越权窗口。
|
||||
- 管理端能力使用独立路径和权限标识,不复用用户端接口绕过接收人过滤。
|
||||
|
||||
### 前后端仓库边界
|
||||
|
||||
`jsowell-charge-ui/` 是独立 Git 仓库。后端和前端分别提交中文 commit,不将前端目录作为未跟踪目录加入后端仓库。
|
||||
|
||||
## 执行批次
|
||||
|
||||
| 批次 | 范围 | 优先级 | 状态 | 验收结果 | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| B01 | 任务拆分、数据表和索引 | P0 | done | SQL 结构和索引评审通过 | `e65017ab2` |
|
||||
| B02 | 实体、命令模型、枚举、Mapper、Service | P0 | done | 模块编译和 Mapper XML 校验通过 | `181d4a5b4` |
|
||||
| B03 | 当前用户查询、已读、完成、取消接口及测试 | P0 | done | 全量依赖编译、9 个专项测试通过 | `f84ec43ce` |
|
||||
| B04 | 前端 API、待办中心页面和路由跳转 | P0 | done | ESLint 和生产构建通过 | 前端 `ff6b890` |
|
||||
| B05 | 首页待办卡片、数量展示和轮询刷新 | P0 | done | ESLint 和生产构建通过 | 前端 `0627061` |
|
||||
| B06 | 菜单权限 SQL、联调和首期回归 | P0 | done | 菜单 SQL、构建和专项回归通过;待测试库执行 SQL | `974e68cf5` |
|
||||
| B07 | 平台采购开票待办接入 | P1 | blocked | 仓库内未找到平台采购开票模型、审核状态或接口入口 | - |
|
||||
| B08 | 司机开票申请待办接入 | P1 | done | 全依赖编译、12 个专项测试通过 | `cbc2e3c72` |
|
||||
| B09 | 管理端创建、分配、取消和隐藏能力 | P1 | todo | - | - |
|
||||
| B10 | 归档清理、过期任务和操作审计增强 | P2 | todo | - | - |
|
||||
| B11 | 路由白名单、性能压测和安全专项验证 | P2 | todo | - | - |
|
||||
| B12 | 汇付、提现、清分、订单异常和实时推送扩展 | P3 | todo | - | - |
|
||||
|
||||
## 任务清单
|
||||
|
||||
### TODO-001 建表和索引
|
||||
|
||||
- 优先级:P0
|
||||
- 批次:B01
|
||||
- 状态:done
|
||||
- 目标:创建 `sys_todo_task` 表,覆盖任务状态、阅读状态、业务路由、接收人、幂等和审计字段。
|
||||
- 验收标准:
|
||||
- 有效任务幂等键具备数据库唯一约束。
|
||||
- 当前用户首页和列表查询具备组合索引。
|
||||
- 业务单据、运营商范围和历史清理具备查询索引。
|
||||
- SQL 可重复交付,不包含真实环境配置或数据。
|
||||
|
||||
### TODO-002 通用领域模型和服务
|
||||
|
||||
- 优先级:P0
|
||||
- 批次:B02
|
||||
- 状态:done
|
||||
- 目标:新增实体、创建命令、状态常量、Mapper 和统一 `TodoTaskService`。
|
||||
- 验收标准:
|
||||
- 创建接口校验必要字段并设置默认状态、优先级和阅读状态。
|
||||
- 并发重复创建返回已有有效任务,不产生重复记录。
|
||||
- 完成、取消和过期操作释放有效幂等键并记录操作信息。
|
||||
- 业务模块仅依赖 Service,不直接依赖 Mapper。
|
||||
|
||||
### TODO-003 当前用户后端接口
|
||||
|
||||
- 优先级:P0
|
||||
- 批次:B03
|
||||
- 状态:done
|
||||
- 目标:实现未完成数量、首页列表、分页列表、详情、已读、全部已读、完成和取消接口。
|
||||
- 验收标准:
|
||||
- 所有用户端查询和更新强制使用当前登录用户 ID。
|
||||
- 首页最多返回 10 条,默认 3 条,按优先级和创建时间倒序。
|
||||
- 已完成、已取消和已过期任务不计入首页数量。
|
||||
- 状态流转非法时返回明确业务错误。
|
||||
- 核心 Service 和 Controller 权限边界有自动化测试。
|
||||
|
||||
### TODO-004 待办中心前端页面
|
||||
|
||||
- 优先级:P0
|
||||
- 批次:B04
|
||||
- 状态:done
|
||||
- 目标:增加 API 封装、待办中心列表、筛选、分页、批量已读和安全跳转。
|
||||
- 验收标准:
|
||||
- 支持任务状态、阅读状态、任务类型和关键字筛选。
|
||||
- 未读任务有清晰样式,点击后标记已读。
|
||||
- 跳转只使用后端返回的已配置路由名称,并附带 `businessType`、`businessId`、`todoId`。
|
||||
- 已完成记录默认保留展示,不执行物理删除。
|
||||
|
||||
### TODO-005 首页待办卡片
|
||||
|
||||
- 优先级:P0
|
||||
- 批次:B05
|
||||
- 状态:done
|
||||
- 目标:在运营商工作台首页展示未完成数量和最近 3 条任务。
|
||||
- 验收标准:
|
||||
- 数量超过 99 显示 `99+`。
|
||||
- 无任务时显示空状态。
|
||||
- 页面打开立即加载,并按 60 秒轮询刷新。
|
||||
- 完成任务或返回首页后主动刷新。
|
||||
|
||||
### TODO-006 菜单权限和联调
|
||||
|
||||
- 优先级:P0
|
||||
- 批次:B06
|
||||
- 状态:done
|
||||
- 当前进展:
|
||||
- 菜单、查询、已读、完成和取消权限 SQL 已完成。
|
||||
- 列表、查询和已读权限默认授予正常角色;完成和取消权限保留为按业务角色授权。
|
||||
- 后端 9 个专项测试、前端专项 ESLint 和生产构建已通过。
|
||||
- 待在测试库执行 `docs/sql/sys_todo_menu.sql` 后进行登录态 HTTP 联调。
|
||||
- 目标:补充菜单与权限 SQL,完成前后端构建和核心流程联调。
|
||||
- 验收标准:
|
||||
- 普通用户只看到本人待办。
|
||||
- 无权限用户不能访问管理接口或完成无权业务。
|
||||
- 原 `sys_notice` 功能和菜单不受影响。
|
||||
- 后端编译、相关测试、前端 lint/build 通过或记录环境阻塞原因。
|
||||
|
||||
### TODO-007 平台采购开票接入
|
||||
|
||||
- 优先级:P1
|
||||
- 批次:B07
|
||||
- 状态:blocked
|
||||
- 阻塞原因:当前仓库检索不到平台采购开票对应的数据库模型、审核拒绝/补充材料状态和业务接口,无法安全判断创建与完成时机。
|
||||
- 目标:在平台采购开票审核拒绝或要求补充材料时创建待办,重新提交或平台关闭时完成。
|
||||
- 验收标准:同一开票事件对同一接收人只产生一条有效待办,跳转参数和完成条件正确。
|
||||
|
||||
### TODO-008 司机开票申请接入
|
||||
|
||||
- 优先级:P1
|
||||
- 批次:B08
|
||||
- 状态:in_progress
|
||||
- 目标:收到司机开票申请时创建待办,审核通过、驳回或关闭时完成。
|
||||
- 验收标准:创建和完成与开票业务事务一致,异常回滚时不产生脏任务状态。
|
||||
|
||||
### TODO-009 管理端异常任务处理
|
||||
|
||||
- 优先级:P1
|
||||
- 批次:B09
|
||||
- 状态:todo
|
||||
- 目标:实现管理员列表、人工创建、分配、取消和逻辑隐藏。
|
||||
- 验收标准:接口具备独立权限标识和操作日志,普通用户不能访问。
|
||||
|
||||
### TODO-010 运维与安全增强
|
||||
|
||||
- 优先级:P2
|
||||
- 批次:B10-B11
|
||||
- 状态:todo
|
||||
- 目标:补充过期、180 天归档清理、操作审计、路由白名单和性能验证。
|
||||
- 验收标准:首页查询目标响应时间小于 500ms,路由不能被请求参数任意覆盖,历史任务按策略保留。
|
||||
|
||||
### TODO-011 后续业务扩展
|
||||
|
||||
- 优先级:P3
|
||||
- 批次:B12
|
||||
- 状态:todo
|
||||
- 目标:接入汇付开户、结算账户、提现、清分和订单异常,并评估 WebSocket/SSE。
|
||||
- 验收标准:新增业务只通过统一待办 Service 接入,不修改通用数据权限和状态模型。
|
||||
|
||||
## 提交记录
|
||||
|
||||
每完成一个批次,在此追加:完成时间、验证命令、结果和 commit hash。
|
||||
|
||||
- 2026-07-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,B07 分析:当前仓库未发现平台采购开票业务落点,需补充对应模块/表/接口信息后继续。
|
||||
- 2026-07-16,B08:司机开票记录创建后,按运营商部门树为正常后台用户幂等创建待办;开票状态更新为已开票时自动完成,删除/关闭记录时自动取消;业务与待办状态处于同一事务;全依赖编译和 12 个专项测试通过;commit `cbc2e3c72`。
|
||||
142
docs/sql/sys_todo_menu.sql
Normal file
142
docs/sql/sys_todo_menu.sql
Normal file
@@ -0,0 +1,142 @@
|
||||
-- 待办中心菜单与基础权限
|
||||
-- 执行效果:
|
||||
-- 1. 新增可访问 /system/todo 的顶级菜单。
|
||||
-- 2. 为所有正常角色授予列表、详情和已读权限;接口始终只返回当前登录用户数据。
|
||||
-- 3. 完成和取消权限默认不授予普通角色,需按业务职责显式分配。
|
||||
|
||||
SET @todoMenuId := (
|
||||
SELECT menu_id
|
||||
FROM sys_menu
|
||||
WHERE path = 'system/todo'
|
||||
AND menu_type = 'C'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||
)
|
||||
SELECT
|
||||
'待办中心', 0, 8, 'system/todo', 'system/todo/index', 1, 0,
|
||||
'C', '0', '0', 'system:todo:list', 'list', 'system', sysdate(), '当前登录用户待办中心'
|
||||
FROM DUAL
|
||||
WHERE @todoMenuId IS NULL;
|
||||
|
||||
SET @todoMenuId := COALESCE(@todoMenuId, LAST_INSERT_ID());
|
||||
|
||||
SET @todoQueryMenuId := (
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE parent_id = @todoMenuId AND perms = 'system:todo:query'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||
)
|
||||
SELECT
|
||||
'待办查询', @todoMenuId, 1, '#', '', 1, 0,
|
||||
'F', '0', '0', 'system:todo:query', '#', 'system', sysdate(), ''
|
||||
FROM DUAL
|
||||
WHERE @todoQueryMenuId IS NULL;
|
||||
|
||||
SET @todoQueryMenuId := COALESCE(@todoQueryMenuId, LAST_INSERT_ID());
|
||||
|
||||
SET @todoReadMenuId := (
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE parent_id = @todoMenuId AND perms = 'system:todo:read'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||
)
|
||||
SELECT
|
||||
'待办已读', @todoMenuId, 2, '#', '', 1, 0,
|
||||
'F', '0', '0', 'system:todo:read', '#', 'system', sysdate(), ''
|
||||
FROM DUAL
|
||||
WHERE @todoReadMenuId IS NULL;
|
||||
|
||||
SET @todoReadMenuId := COALESCE(@todoReadMenuId, LAST_INSERT_ID());
|
||||
|
||||
SET @todoCompleteMenuId := (
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE parent_id = @todoMenuId AND perms = 'system:todo:complete'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||
)
|
||||
SELECT
|
||||
'待办完成', @todoMenuId, 3, '#', '', 1, 0,
|
||||
'F', '0', '0', 'system:todo:complete', '#', 'system', sysdate(), '仅分配给具备对应业务处理权限的角色'
|
||||
FROM DUAL
|
||||
WHERE @todoCompleteMenuId IS NULL;
|
||||
|
||||
SET @todoCompleteMenuId := COALESCE(@todoCompleteMenuId, LAST_INSERT_ID());
|
||||
|
||||
SET @todoCancelMenuId := (
|
||||
SELECT menu_id FROM sys_menu
|
||||
WHERE parent_id = @todoMenuId AND perms = 'system:todo:cancel'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_name, parent_id, order_num, path, component, is_frame, is_cache,
|
||||
menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||
)
|
||||
SELECT
|
||||
'待办取消', @todoMenuId, 4, '#', '', 1, 0,
|
||||
'F', '0', '0', 'system:todo:cancel', '#', 'system', sysdate(), '仅分配给具备关闭任务权限的角色'
|
||||
FROM DUAL
|
||||
WHERE @todoCancelMenuId IS NULL;
|
||||
|
||||
SET @todoCancelMenuId := COALESCE(@todoCancelMenuId, LAST_INSERT_ID());
|
||||
|
||||
-- 所有正常角色均可查看、查询并标记本人的待办。
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT role_id, @todoMenuId
|
||||
FROM sys_role role_info
|
||||
WHERE role_info.status = '0'
|
||||
AND role_info.del_flag = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu relation
|
||||
WHERE relation.role_id = role_info.role_id
|
||||
AND relation.menu_id = @todoMenuId
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT role_id, @todoQueryMenuId
|
||||
FROM sys_role role_info
|
||||
WHERE role_info.status = '0'
|
||||
AND role_info.del_flag = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu relation
|
||||
WHERE relation.role_id = role_info.role_id
|
||||
AND relation.menu_id = @todoQueryMenuId
|
||||
);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT role_id, @todoReadMenuId
|
||||
FROM sys_role role_info
|
||||
WHERE role_info.status = '0'
|
||||
AND role_info.del_flag = '0'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu relation
|
||||
WHERE relation.role_id = role_info.role_id
|
||||
AND relation.menu_id = @todoReadMenuId
|
||||
);
|
||||
|
||||
-- 完成/取消权限按业务角色手工授权示例:
|
||||
-- INSERT INTO sys_role_menu (role_id, menu_id) VALUES (<业务处理角色ID>, @todoCompleteMenuId);
|
||||
-- INSERT INTO sys_role_menu (role_id, menu_id) VALUES (<任务关闭角色ID>, @todoCancelMenuId);
|
||||
|
||||
SELECT
|
||||
@todoMenuId AS todo_menu_id,
|
||||
@todoQueryMenuId AS todo_query_menu_id,
|
||||
@todoReadMenuId AS todo_read_menu_id,
|
||||
@todoCompleteMenuId AS todo_complete_menu_id,
|
||||
@todoCancelMenuId AS todo_cancel_menu_id;
|
||||
39
docs/sql/sys_todo_task.sql
Normal file
39
docs/sql/sys_todo_task.sql
Normal file
@@ -0,0 +1,39 @@
|
||||
-- 待办事项中心任务表
|
||||
-- 说明:active_idempotent_key 仅在非终态任务中保留,终态时置空,
|
||||
-- 用于同时满足“有效任务唯一”和“终态后允许重新创建”的要求。
|
||||
|
||||
CREATE TABLE `sys_todo_task` (
|
||||
`todo_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '待办任务主键',
|
||||
`task_type` varchar(64) NOT NULL COMMENT '待办类型编码',
|
||||
`title` varchar(200) NOT NULL COMMENT '待办标题',
|
||||
`summary` varchar(500) DEFAULT NULL COMMENT '待办摘要',
|
||||
`content` text COMMENT '待办详细内容(纯文本)',
|
||||
`business_type` varchar(64) NOT NULL COMMENT '业务类型编码',
|
||||
`business_id` varchar(64) NOT NULL COMMENT '业务主键或单号',
|
||||
`route_name` varchar(100) NOT NULL COMMENT '前端白名单路由名称',
|
||||
`route_params` varchar(1000) DEFAULT NULL COMMENT '前端路由参数 JSON',
|
||||
`assignee_user_id` bigint(20) NOT NULL COMMENT '接收用户 ID',
|
||||
`assignee_merchant_id` varchar(64) DEFAULT NULL COMMENT '接收运营商 ID',
|
||||
`priority` tinyint(3) NOT NULL DEFAULT '1' COMMENT '优先级(1普通 2重要 3紧急)',
|
||||
`task_status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态(PENDING PROCESSING COMPLETED CANCELLED EXPIRED)',
|
||||
`read_status` char(1) NOT NULL DEFAULT '0' COMMENT '阅读状态(0未读 1已读)',
|
||||
`read_time` datetime DEFAULT NULL COMMENT '阅读时间',
|
||||
`due_time` datetime DEFAULT NULL COMMENT '截止时间',
|
||||
`completed_time` datetime DEFAULT NULL COMMENT '完成时间',
|
||||
`completed_by` bigint(20) DEFAULT NULL COMMENT '完成人用户 ID',
|
||||
`cancel_reason` varchar(500) DEFAULT NULL COMMENT '取消原因',
|
||||
`idempotent_key` varchar(200) NOT NULL COMMENT '业务原始幂等键',
|
||||
`active_idempotent_key` varchar(200) DEFAULT NULL COMMENT '有效任务幂等键,终态置空',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除(0正常 2删除)',
|
||||
PRIMARY KEY (`todo_id`) USING BTREE,
|
||||
UNIQUE KEY `uk_todo_active_idempotent` (`active_idempotent_key`) USING BTREE,
|
||||
KEY `idx_todo_user_status` (`assignee_user_id`, `task_status`, `read_status`, `priority`, `create_time`) USING BTREE,
|
||||
KEY `idx_todo_merchant_status` (`assignee_merchant_id`, `task_status`, `create_time`) USING BTREE,
|
||||
KEY `idx_todo_business` (`business_type`, `business_id`) USING BTREE,
|
||||
KEY `idx_todo_history_cleanup` (`del_flag`, `task_status`, `completed_time`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户待办任务表';
|
||||
@@ -237,9 +237,12 @@ public class AdapayMemberController extends BaseController {
|
||||
try {
|
||||
adapayService.updateSettleAccountConfig(dto);
|
||||
result = AjaxResult.success();
|
||||
} catch (BusinessException e) {
|
||||
logger.warn("修改汇付结算配置warn", e);
|
||||
result = AjaxResult.error(e.getMessage());
|
||||
} catch (BaseAdaPayException e) {
|
||||
logger.error("查询汇付账户余额error", e);
|
||||
result = AjaxResult.error();
|
||||
logger.error("修改汇付结算配置error", e);
|
||||
result = AjaxResult.error("修改汇付结算配置异常");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -254,9 +257,12 @@ public class AdapayMemberController extends BaseController {
|
||||
try {
|
||||
adapayService.changeBankCard(dto);
|
||||
result = AjaxResult.success();
|
||||
} catch (BusinessException e) {
|
||||
logger.warn("换绑银行卡warn", e);
|
||||
result = AjaxResult.error(e.getMessage());
|
||||
} catch (BaseAdaPayException e) {
|
||||
logger.error("换绑银行卡error", e);
|
||||
result = AjaxResult.error();
|
||||
result = AjaxResult.error("换绑银行卡异常");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -292,12 +298,14 @@ public class AdapayMemberController extends BaseController {
|
||||
public AjaxResult deleteSettleAccount(@RequestBody AdapayMemberInfoDTO dto) {
|
||||
AjaxResult result = null;
|
||||
try {
|
||||
// 新写删除方法
|
||||
adapayService.deleteSettleAccount(dto);
|
||||
result = AjaxResult.success();
|
||||
} catch (BusinessException e) {
|
||||
logger.warn("删除结算账户warn", e);
|
||||
result = AjaxResult.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("删除结算账户 error,", e);
|
||||
result = AjaxResult.error();
|
||||
result = AjaxResult.error("删除结算账户异常");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -314,9 +322,13 @@ public class AdapayMemberController extends BaseController {
|
||||
AjaxResult result = null;
|
||||
try {
|
||||
adapayService.createBankAccount(dto);
|
||||
result = AjaxResult.success();
|
||||
} catch (BusinessException e) {
|
||||
logger.warn("重新创建结算账户warn", e);
|
||||
result = AjaxResult.error(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("重新创建结算账户 error, ", e);
|
||||
result = AjaxResult.error();
|
||||
result = AjaxResult.error("重新创建结算账户异常");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ public class OrderInvoiceRecordController extends BaseController {
|
||||
@Log(title = "申请开票", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody OrderInvoiceRecord orderInvoiceRecord) {
|
||||
return toAjax(orderInvoiceRecordService.updateOrderInvoiceRecord(orderInvoiceRecord));
|
||||
return toAjax(orderInvoiceRecordService.updateOrderInvoiceRecord(orderInvoiceRecord, getUserId(), getUsername()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,6 +100,6 @@ public class OrderInvoiceRecordController extends BaseController {
|
||||
@Log(title = "申请开票", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable Integer[] ids) {
|
||||
return toAjax(orderInvoiceRecordService.deleteOrderInvoiceRecordByIds(ids));
|
||||
return toAjax(orderInvoiceRecordService.deleteOrderInvoiceRecordByIds(ids, getUsername()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.jsowell.pile.service;
|
||||
|
||||
import com.jsowell.pile.domain.OrderInvoiceRecord;
|
||||
import com.jsowell.pile.domain.PileMerchantInfo;
|
||||
import com.jsowell.pile.mapper.OrderInvoiceRecordMapper;
|
||||
import com.jsowell.pile.service.impl.OrderInvoiceRecordServiceImpl;
|
||||
import com.jsowell.system.constant.TodoTaskConstants;
|
||||
import com.jsowell.system.domain.SysTodoTask;
|
||||
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
|
||||
import com.jsowell.system.service.TodoTaskAssigneeService;
|
||||
import com.jsowell.system.service.TodoTaskService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class OrderInvoiceRecordTodoTest {
|
||||
@Test
|
||||
void insert_shouldCreateTodoForEachMerchantUser() {
|
||||
OrderInvoiceRecordMapper mapper = mock(OrderInvoiceRecordMapper.class);
|
||||
PileMerchantInfoService merchantService = mock(PileMerchantInfoService.class);
|
||||
TodoTaskAssigneeService assigneeService = mock(TodoTaskAssigneeService.class);
|
||||
TodoTaskService todoTaskService = mock(TodoTaskService.class);
|
||||
OrderInvoiceRecordServiceImpl service = newService(mapper, merchantService, assigneeService, todoTaskService);
|
||||
|
||||
OrderInvoiceRecord invoiceRecord = new OrderInvoiceRecord();
|
||||
invoiceRecord.setMerchantId("100");
|
||||
doAnswer(invocation -> {
|
||||
OrderInvoiceRecord record = invocation.getArgument(0);
|
||||
record.setId(55);
|
||||
return 1;
|
||||
}).when(mapper).insertOrderInvoiceRecord(invoiceRecord);
|
||||
|
||||
PileMerchantInfo merchantInfo = new PileMerchantInfo();
|
||||
merchantInfo.setDeptId("200");
|
||||
when(merchantService.selectPileMerchantInfoById(100L)).thenReturn(merchantInfo);
|
||||
when(assigneeService.findActiveUserIdsByDeptTree(200L)).thenReturn(Arrays.asList(8L, 9L));
|
||||
when(todoTaskService.createTask(any(TodoTaskCreateCommand.class))).thenReturn(new SysTodoTask());
|
||||
|
||||
service.insertOrderInvoiceRecord(invoiceRecord);
|
||||
|
||||
ArgumentCaptor<TodoTaskCreateCommand> captor = ArgumentCaptor.forClass(TodoTaskCreateCommand.class);
|
||||
verify(todoTaskService, times(2)).createTask(captor.capture());
|
||||
List<TodoTaskCreateCommand> commands = captor.getAllValues();
|
||||
assertEquals(Arrays.asList(8L, 9L), Arrays.asList(
|
||||
commands.get(0).getAssigneeUserId(), commands.get(1).getAssigneeUserId()));
|
||||
assertEquals("ORDER_INVOICE_RECORD", commands.get(0).getBusinessType());
|
||||
assertEquals("55", commands.get(0).getBusinessId());
|
||||
assertEquals("invoiceDetail", commands.get(0).getRouteName());
|
||||
assertEquals(TodoTaskConstants.PRIORITY_IMPORTANT, commands.get(0).getPriority());
|
||||
assertEquals("INVOICE_REVIEW:ORDER_INVOICE_RECORD:55:8", commands.get(0).getIdempotentKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateCompleted_shouldCompleteBusinessTodos() {
|
||||
OrderInvoiceRecordMapper mapper = mock(OrderInvoiceRecordMapper.class);
|
||||
TodoTaskService todoTaskService = mock(TodoTaskService.class);
|
||||
OrderInvoiceRecordServiceImpl service = newService(mapper, mock(PileMerchantInfoService.class),
|
||||
mock(TodoTaskAssigneeService.class), todoTaskService);
|
||||
OrderInvoiceRecord invoiceRecord = new OrderInvoiceRecord();
|
||||
invoiceRecord.setId(55);
|
||||
invoiceRecord.setStatus("1");
|
||||
when(mapper.updateOrderInvoiceRecord(invoiceRecord)).thenReturn(1);
|
||||
|
||||
service.updateOrderInvoiceRecord(invoiceRecord, 1L, "admin");
|
||||
|
||||
verify(todoTaskService).completeByBusiness("ORDER_INVOICE_RECORD", "55", null, 1L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete_shouldCancelBusinessTodos() {
|
||||
OrderInvoiceRecordMapper mapper = mock(OrderInvoiceRecordMapper.class);
|
||||
TodoTaskService todoTaskService = mock(TodoTaskService.class);
|
||||
OrderInvoiceRecordServiceImpl service = newService(mapper, mock(PileMerchantInfoService.class),
|
||||
mock(TodoTaskAssigneeService.class), todoTaskService);
|
||||
Integer[] ids = { 55, 56 };
|
||||
when(mapper.deleteOrderInvoiceRecordByIds(ids)).thenReturn(2);
|
||||
|
||||
service.deleteOrderInvoiceRecordByIds(ids, "admin");
|
||||
|
||||
verify(todoTaskService).cancelByBusiness("ORDER_INVOICE_RECORD", "55", null,
|
||||
"admin", "开票申请已关闭");
|
||||
verify(todoTaskService).cancelByBusiness("ORDER_INVOICE_RECORD", "56", null,
|
||||
"admin", "开票申请已关闭");
|
||||
}
|
||||
|
||||
private static OrderInvoiceRecordServiceImpl newService(OrderInvoiceRecordMapper mapper,
|
||||
PileMerchantInfoService merchantService, TodoTaskAssigneeService assigneeService,
|
||||
TodoTaskService todoTaskService) {
|
||||
OrderInvoiceRecordServiceImpl service = new OrderInvoiceRecordServiceImpl();
|
||||
setField(service, "orderInvoiceRecordMapper", mapper);
|
||||
setField(service, "pileMerchantInfoService", merchantService);
|
||||
setField(service, "todoTaskAssigneeService", assigneeService);
|
||||
setField(service, "todoTaskService", todoTaskService);
|
||||
return service;
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.jsowell.system.service;
|
||||
|
||||
import com.jsowell.common.exception.ServiceException;
|
||||
import com.jsowell.system.constant.TodoTaskConstants;
|
||||
import com.jsowell.system.domain.SysTodoTask;
|
||||
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
|
||||
import com.jsowell.system.domain.dto.TodoTaskQuery;
|
||||
import com.jsowell.system.mapper.SysTodoTaskMapper;
|
||||
import com.jsowell.system.service.impl.TodoTaskServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class TodoTaskServiceImplTest {
|
||||
@Test
|
||||
void createTask_shouldSetDefaultsAndInsert() {
|
||||
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||
TodoTaskServiceImpl service = newService(mapper);
|
||||
TodoTaskCreateCommand command = createCommand();
|
||||
when(mapper.selectActiveByIdempotentKey("INVOICE:100:8")).thenReturn(null);
|
||||
doAnswer(invocation -> {
|
||||
SysTodoTask task = invocation.getArgument(0);
|
||||
task.setTodoId(11L);
|
||||
return 1;
|
||||
}).when(mapper).insertTodoTask(any(SysTodoTask.class));
|
||||
|
||||
SysTodoTask result = service.createTask(command);
|
||||
|
||||
ArgumentCaptor<SysTodoTask> captor = ArgumentCaptor.forClass(SysTodoTask.class);
|
||||
verify(mapper).insertTodoTask(captor.capture());
|
||||
SysTodoTask inserted = captor.getValue();
|
||||
assertEquals(11L, result.getTodoId());
|
||||
assertEquals(TodoTaskConstants.PRIORITY_NORMAL, inserted.getPriority());
|
||||
assertEquals(TodoTaskConstants.STATUS_PENDING, inserted.getTaskStatus());
|
||||
assertEquals(TodoTaskConstants.READ_UNREAD, inserted.getReadStatus());
|
||||
assertEquals(inserted.getIdempotentKey(), inserted.getActiveIdempotentKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createTask_shouldReturnExistingTask() {
|
||||
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||
TodoTaskServiceImpl service = newService(mapper);
|
||||
SysTodoTask existing = new SysTodoTask();
|
||||
existing.setTodoId(12L);
|
||||
when(mapper.selectActiveByIdempotentKey("INVOICE:100:8")).thenReturn(existing);
|
||||
|
||||
SysTodoTask result = service.createTask(createCommand());
|
||||
|
||||
assertSame(existing, result);
|
||||
verify(mapper, never()).insertTodoTask(any(SysTodoTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createTask_shouldRecoverFromConcurrentDuplicate() {
|
||||
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||
TodoTaskServiceImpl service = newService(mapper);
|
||||
SysTodoTask existing = new SysTodoTask();
|
||||
existing.setTodoId(13L);
|
||||
when(mapper.selectActiveByIdempotentKey("INVOICE:100:8")).thenReturn(null, existing);
|
||||
doThrow(new DuplicateKeyException("duplicate"))
|
||||
.when(mapper).insertTodoTask(any(SysTodoTask.class));
|
||||
|
||||
SysTodoTask result = service.createTask(createCommand());
|
||||
|
||||
assertSame(existing, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listForAssignee_shouldOverrideRequestedUser() {
|
||||
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||
TodoTaskServiceImpl service = newService(mapper);
|
||||
TodoTaskQuery query = new TodoTaskQuery();
|
||||
query.setAssigneeUserId(999L);
|
||||
when(mapper.selectTodoList(query)).thenReturn(Collections.emptyList());
|
||||
|
||||
service.listForAssignee(query, 8L);
|
||||
|
||||
assertEquals(8L, query.getAssigneeUserId());
|
||||
verify(mapper).selectTodoList(query);
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_shouldBeIdempotentWhenAlreadyCompleted() {
|
||||
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||
TodoTaskServiceImpl service = newService(mapper);
|
||||
SysTodoTask completed = new SysTodoTask();
|
||||
completed.setTaskStatus(TodoTaskConstants.STATUS_COMPLETED);
|
||||
when(mapper.completeTodoForAssignee(20L, 8L, 8L, "tester")).thenReturn(0);
|
||||
when(mapper.selectTodoByIdForAssignee(20L, 8L)).thenReturn(completed);
|
||||
|
||||
assertTrue(service.complete(20L, 8L, "tester"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void complete_shouldRejectCancelledTask() {
|
||||
SysTodoTaskMapper mapper = mock(SysTodoTaskMapper.class);
|
||||
TodoTaskServiceImpl service = newService(mapper);
|
||||
SysTodoTask cancelled = new SysTodoTask();
|
||||
cancelled.setTaskStatus(TodoTaskConstants.STATUS_CANCELLED);
|
||||
when(mapper.completeTodoForAssignee(20L, 8L, 8L, "tester")).thenReturn(0);
|
||||
when(mapper.selectTodoByIdForAssignee(20L, 8L)).thenReturn(cancelled);
|
||||
|
||||
assertThrows(ServiceException.class, () -> service.complete(20L, 8L, "tester"));
|
||||
}
|
||||
|
||||
private static TodoTaskCreateCommand createCommand() {
|
||||
return TodoTaskCreateCommand.builder()
|
||||
.taskType("INVOICE_REVIEW")
|
||||
.title("司机开票申请")
|
||||
.businessType("INVOICE_APPLY")
|
||||
.businessId("100")
|
||||
.routeName("InvoiceApplyDetail")
|
||||
.assigneeUserId(8L)
|
||||
.idempotentKey("INVOICE:100:8")
|
||||
.createBy("tester")
|
||||
.build();
|
||||
}
|
||||
|
||||
private static TodoTaskServiceImpl newService(SysTodoTaskMapper mapper) {
|
||||
TodoTaskServiceImpl service = new TodoTaskServiceImpl();
|
||||
setField(service, "todoTaskMapper", mapper);
|
||||
return service;
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,9 @@ public enum AdapayOpenActionEnum {
|
||||
UPDATE_CORP_MEMBER("UPDATE_CORP_MEMBER", "更新企业资料"),
|
||||
RESUBMIT_CORP_MEMBER("RESUBMIT_CORP_MEMBER", "重新提交企业开户"),
|
||||
CREATE_SETTLE_ACCOUNT("CREATE_SETTLE_ACCOUNT", "创建结算账户"),
|
||||
UPDATE_SETTLE_ACCOUNT_CONFIG("UPDATE_SETTLE_ACCOUNT_CONFIG", "修改结算配置"),
|
||||
DELETE_SETTLE_ACCOUNT("DELETE_SETTLE_ACCOUNT", "删除结算账户"),
|
||||
DELETE_MEMBER("DELETE_MEMBER", "删除汇付用户");
|
||||
DELETE_MEMBER("DELETE_MEMBER", "删除账户(本地解绑)");
|
||||
|
||||
private final String value;
|
||||
private final String label;
|
||||
|
||||
@@ -59,5 +59,8 @@ public class QueryMemberResponse extends AdapayBaseResponse{
|
||||
private String bank_acct_type;
|
||||
private String prov_code;
|
||||
private String card_id;
|
||||
private String min_amt;
|
||||
private String remained_amt;
|
||||
private String channel_remark;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ public class AdapayService {
|
||||
@Autowired
|
||||
private PileMerchantInfoService pileMerchantInfoService;
|
||||
|
||||
@Autowired
|
||||
private StationSplitConfigService stationSplitConfigService;
|
||||
|
||||
@Autowired
|
||||
private MemberBasicInfoService memberBasicInfoService;
|
||||
|
||||
@@ -324,8 +327,11 @@ public class AdapayService {
|
||||
String wechatAppId = pileMerchantInfoService.queryAppIdByMerchantId(dto.getMerchantId());
|
||||
SettleAccountDTO request = buildSettleAccountDTO(dto);
|
||||
Map<String, Object> settleCount = createSettleAccountRequest(request, adapayMemberAccount.getAdapayMemberId(), wechatAppId);
|
||||
if (settleCount == null || StringUtils.equals((String) settleCount.get("status"), "failed")) {
|
||||
String errorMsg = settleCount == null ? "创建汇付结算账户失败" : (String) settleCount.get("error_msg");
|
||||
if (settleCount == null
|
||||
|| !AdapayStatusEnum.SUCCEEDED.getValue().equals(settleCount.get("status"))
|
||||
|| StringUtils.isBlank((String) settleCount.get("id"))) {
|
||||
String errorMsg = settleCount == null ? null : (String) settleCount.get("error_msg");
|
||||
errorMsg = StringUtils.isNotBlank(errorMsg) ? errorMsg : "创建汇付结算账户失败";
|
||||
throw new BusinessException("00500001", errorMsg);
|
||||
}
|
||||
|
||||
@@ -345,13 +351,7 @@ public class AdapayService {
|
||||
return Lists.newArrayList();
|
||||
}
|
||||
String wechatAppId = pileMerchantInfoService.queryAppIdByMerchantId(merchantId);
|
||||
String settleAccountId = adapayMemberAccount.getSettleAccountId();
|
||||
if (StringUtils.isBlank(settleAccountId) && isCorpMember(adapayMemberAccount.getAdapayMemberId())) {
|
||||
AdapayCorpMemberVO corpMemberVO = queryCorpAdapayMemberInfo(adapayMemberAccount.getAdapayMemberId(), wechatAppId);
|
||||
if (corpMemberVO != null) {
|
||||
settleAccountId = corpMemberVO.getSettleAccountId();
|
||||
}
|
||||
}
|
||||
String settleAccountId = resolveSettleAccountId(adapayMemberAccount, wechatAppId);
|
||||
AdapaySettleAccountVO adapaySettleAccountVO = queryAdapaySettleAccount(adapayMemberAccount.getAdapayMemberId(), settleAccountId, wechatAppId);
|
||||
if (adapaySettleAccountVO == null) {
|
||||
return Lists.newArrayList();
|
||||
@@ -360,6 +360,35 @@ public class AdapayService {
|
||||
return Lists.newArrayList(adapaySettleAccountVO);
|
||||
}
|
||||
|
||||
private String resolveSettleAccountId(AdapayMemberAccount account, String wechatAppId) throws BaseAdaPayException {
|
||||
if (StringUtils.isNotBlank(account.getSettleAccountId())) {
|
||||
return account.getSettleAccountId();
|
||||
}
|
||||
|
||||
String settleAccountId = null;
|
||||
if (isPersonalMember(account.getAdapayMemberId())) {
|
||||
AdapayMemberInfoVO memberInfo = queryAdapayMemberInfo(account.getAdapayMemberId(), wechatAppId);
|
||||
if (memberInfo != null) {
|
||||
settleAccountId = memberInfo.getSettleAccountId();
|
||||
}
|
||||
} else {
|
||||
AdapayCorpMemberVO corpMember = queryCorpAdapayMemberInfo(account.getAdapayMemberId(), wechatAppId);
|
||||
if (corpMember != null) {
|
||||
settleAccountId = corpMember.getSettleAccountId();
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(settleAccountId)) {
|
||||
AdapayMemberAccount updateRecord = new AdapayMemberAccount();
|
||||
updateRecord.setMerchantId(account.getMerchantId());
|
||||
updateRecord.setAdapayMemberId(account.getAdapayMemberId());
|
||||
updateRecord.setSettleAccountId(settleAccountId);
|
||||
adapayMemberAccountService.updateAdapayMemberAccountByMemberId(updateRecord);
|
||||
account.setSettleAccountId(settleAccountId);
|
||||
}
|
||||
return settleAccountId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建汇付会员
|
||||
*
|
||||
@@ -498,12 +527,21 @@ public class AdapayService {
|
||||
}
|
||||
|
||||
QueryMemberResponse queryMemberResponse = JSON.parseObject(JSON.toJSONString(member), QueryMemberResponse.class);
|
||||
String settleAccountId = null;
|
||||
if (CollectionUtils.isNotEmpty(queryMemberResponse.getSettle_accounts())) {
|
||||
settleAccountId = queryMemberResponse.getSettle_accounts().stream()
|
||||
.map(QueryMemberResponse.SettleAccount::getId)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
AdapayMemberInfoVO resultVO = AdapayMemberInfoVO.builder()
|
||||
.memberId(adapayMemberId)
|
||||
.nickname(queryMemberResponse.getNickname())
|
||||
.gender(queryMemberResponse.getGender())
|
||||
.email(queryMemberResponse.getEmail())
|
||||
.location(queryMemberResponse.getLocation())
|
||||
.settleAccountId(settleAccountId)
|
||||
.build();
|
||||
return resultVO;
|
||||
}
|
||||
@@ -635,6 +673,11 @@ public class AdapayService {
|
||||
settleCountParams.put("app_id", config.getAdapayAppId());
|
||||
Map<String, Object> settleCount = SettleAccount.delete(settleCountParams, wechatAppId);
|
||||
log.info("创建删除结算账户请求param:{}, result:{}", JSON.toJSONString(settleCountParams), JSON.toJSONString(settleCount));
|
||||
if (settleCount == null
|
||||
|| !AdapayStatusEnum.SUCCEEDED.getValue().equals(settleCount.get("status"))) {
|
||||
String errorMsg = settleCount == null ? null : (String) settleCount.get("error_msg");
|
||||
throw new BusinessException("", StringUtils.isNotBlank(errorMsg) ? errorMsg : "删除汇付结算账户失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,6 +721,9 @@ public class AdapayService {
|
||||
.bankAcctType(accountInfo.getBank_acct_type())
|
||||
.provCode(accountInfo.getProv_code())
|
||||
.areaCode(accountInfo.getArea_code())
|
||||
.minAmt(accountInfo.getMin_amt())
|
||||
.remainedAmt(accountInfo.getRemained_amt())
|
||||
.channelRemark(accountInfo.getChannel_remark())
|
||||
.build();
|
||||
return resultVO;
|
||||
}
|
||||
@@ -768,6 +814,7 @@ public class AdapayService {
|
||||
* @throws BaseAdaPayException
|
||||
*/
|
||||
public void updateSettleAccountConfig(UpdateAccountConfigDTO dto) throws BaseAdaPayException {
|
||||
assertActionAllowed(dto.getMerchantId(), AdapayOpenActionEnum.UPDATE_SETTLE_ACCOUNT_CONFIG);
|
||||
String wechatAppId = pileMerchantInfoService.queryAppIdByMerchantId(dto.getMerchantId());
|
||||
// 获取汇付支付配置
|
||||
AbstractAdapayConfig config = AdapayConfigFactory.getConfig(wechatAppId);
|
||||
@@ -776,16 +823,19 @@ public class AdapayService {
|
||||
}
|
||||
|
||||
// 通过merchantId 查询出汇付会员id 和 结算账户id,用来查询余额
|
||||
AdapayMemberAccount adapayMemberAccount = adapayMemberAccountService.selectByMerchantId(dto.getMerchantId());
|
||||
if (adapayMemberAccount == null) {
|
||||
log.error("通过merchantId:{}, 没有查询到结算账户配置", dto.getMerchantId());
|
||||
return;
|
||||
AdapayMemberAccount adapayMemberAccount = requireCurrentMemberAccount(dto.getMerchantId());
|
||||
String settleAccountId = resolveSettleAccountId(adapayMemberAccount, wechatAppId);
|
||||
if (StringUtils.isBlank(settleAccountId)) {
|
||||
throw new BusinessException("", "未查询到结算账户");
|
||||
}
|
||||
if (StringUtils.isAllBlank(dto.getMinAmt(), dto.getRemainedAmt(), dto.getChannelRemark())) {
|
||||
throw new BusinessException("", "结算起始金额、结算留存金额和结算摘要至少填写一项");
|
||||
}
|
||||
// 修改账户配置
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
params.put("app_id", config.getAdapayAppId());
|
||||
params.put("member_id", adapayMemberAccount.getAdapayMemberId());
|
||||
params.put("settle_account_id", adapayMemberAccount.getSettleAccountId());
|
||||
params.put("settle_account_id", settleAccountId);
|
||||
if (StringUtils.isNotBlank(dto.getMinAmt())) {
|
||||
params.put("min_amt", dto.getMinAmt());
|
||||
}
|
||||
@@ -795,8 +845,13 @@ public class AdapayService {
|
||||
if (StringUtils.isNotBlank(dto.getChannelRemark())) {
|
||||
params.put("channel_remark", dto.getChannelRemark());
|
||||
}
|
||||
Map<String, Object> settleCount = SettleAccount.update(params);
|
||||
Map<String, Object> settleCount = SettleAccount.modify(params, config.getWechatAppId());
|
||||
log.info("更新结算账户设置 param:{}, result:{}", JSON.toJSONString(params), JSON.toJSONString(settleCount));
|
||||
if (settleCount == null
|
||||
|| !AdapayStatusEnum.SUCCEEDED.getValue().equals(settleCount.get("status"))) {
|
||||
String errorMsg = settleCount == null ? null : (String) settleCount.get("error_msg");
|
||||
throw new BusinessException("", StringUtils.isNotBlank(errorMsg) ? errorMsg : "修改汇付结算配置失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1675,38 +1730,20 @@ public class AdapayService {
|
||||
* 用户需要换绑银行卡 1-删除结算账户信息,2-使用新账户信息创建结算账户】
|
||||
*/
|
||||
public void changeBankCard(ChangeBankCardDTO dto) throws BaseAdaPayException {
|
||||
String wechatAppId = pileMerchantInfoService.queryAppIdByMerchantId(dto.getMerchantId());
|
||||
// 获取汇付支会员信息
|
||||
AdapayMemberAccount account = adapayMemberAccountService.selectByMerchantId(dto.getMerchantId());
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1-删除结算账户信息
|
||||
String adapayMemberId = account.getAdapayMemberId();
|
||||
String settleAccountId = null;
|
||||
AdapayCorpMemberVO adapayCorpMemberVO = this.queryCorpAdapayMemberInfo(adapayMemberId, wechatAppId);
|
||||
if (adapayCorpMemberVO != null) {
|
||||
settleAccountId = adapayCorpMemberVO.getSettleAccountId();
|
||||
}
|
||||
this.createDeleteSettleAccountRequest(adapayMemberId, settleAccountId, wechatAppId);
|
||||
// 2-使用新账户信息创建结算账户
|
||||
SettleAccountDTO settleAccountDTO = new SettleAccountDTO();
|
||||
settleAccountDTO.setCardId(dto.getCardId());
|
||||
settleAccountDTO.setCardName(dto.getCardName());
|
||||
settleAccountDTO.setTelNo(dto.getTelNo());
|
||||
settleAccountDTO.setBankCode(dto.getBankCode());
|
||||
settleAccountDTO.setBankAcctType(dto.getBankAcctType());
|
||||
settleAccountDTO.setProvCode(dto.getProvCode());
|
||||
settleAccountDTO.setAreaCode(dto.getAreaCode());
|
||||
Map<String, Object> settleAccount = this.createSettleAccountRequest(settleAccountDTO, adapayMemberId, wechatAppId);
|
||||
if (settleAccount != null && !StringUtils.equals((String) settleAccount.get("status"), "failed")) {
|
||||
AdapayMemberAccount updateRecord = new AdapayMemberAccount();
|
||||
updateRecord.setAdapayMemberId(adapayMemberId);
|
||||
updateRecord.setMerchantId(dto.getMerchantId());
|
||||
updateRecord.setSettleAccountId((String) settleAccount.get("id"));
|
||||
adapayMemberAccountService.updateAdapayMemberAccountByMemberId(updateRecord);
|
||||
}
|
||||
deleteSettleAccount(AdapayMemberInfoDTO.builder().merchantId(dto.getMerchantId()).build());
|
||||
CreateSettleAccountDTO createRequest = CreateSettleAccountDTO.builder()
|
||||
.merchantId(dto.getMerchantId())
|
||||
.bankAcctType(dto.getBankAcctType())
|
||||
.cardId(dto.getCardId())
|
||||
.cardName(dto.getCardName())
|
||||
.certId(dto.getCertId())
|
||||
.certType(dto.getCertType())
|
||||
.telNo(dto.getTelNo())
|
||||
.bankCode(dto.getBankCode())
|
||||
.provCode(dto.getProvCode())
|
||||
.areaCode(dto.getAreaCode())
|
||||
.build();
|
||||
createSettleAccount(createRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1797,21 +1834,27 @@ public class AdapayService {
|
||||
assertActionAllowed(dto.getMerchantId(), AdapayOpenActionEnum.DELETE_SETTLE_ACCOUNT);
|
||||
AdapayMemberAccount adapayMemberAccount = requireCurrentMemberAccount(dto.getMerchantId());
|
||||
String wechatAppId = pileMerchantInfoService.queryAppIdByMerchantId(dto.getMerchantId());
|
||||
String adapayMemberId = StringUtils.isNotBlank(dto.getAdapayMemberId()) ? dto.getAdapayMemberId() : adapayMemberAccount.getAdapayMemberId();
|
||||
String settleAccountId = StringUtils.isNotBlank(dto.getSettleAccountId()) ? dto.getSettleAccountId() : adapayMemberAccount.getSettleAccountId();
|
||||
if (StringUtils.isBlank(settleAccountId) && isCorpMember(adapayMemberId)) {
|
||||
AdapayCorpMemberVO corpMemberVO = queryCorpAdapayMemberInfo(adapayMemberId, wechatAppId);
|
||||
if (corpMemberVO != null) {
|
||||
settleAccountId = corpMemberVO.getSettleAccountId();
|
||||
}
|
||||
String adapayMemberId = adapayMemberAccount.getAdapayMemberId();
|
||||
if (StringUtils.isNotBlank(dto.getAdapayMemberId())
|
||||
&& !StringUtils.equals(dto.getAdapayMemberId(), adapayMemberId)) {
|
||||
throw new BusinessException("", "汇付用户信息不匹配");
|
||||
}
|
||||
String settleAccountId = resolveSettleAccountId(adapayMemberAccount, wechatAppId);
|
||||
if (StringUtils.isBlank(settleAccountId)) {
|
||||
throw new BusinessException("", "未查询到结算账户");
|
||||
}
|
||||
if (StringUtils.isNotBlank(dto.getSettleAccountId())
|
||||
&& !StringUtils.equals(dto.getSettleAccountId(), settleAccountId)) {
|
||||
throw new BusinessException("", "结算账户信息不匹配");
|
||||
}
|
||||
|
||||
assertSettleAccountCanDelete(dto.getMerchantId());
|
||||
this.createDeleteSettleAccountRequest(adapayMemberId, settleAccountId, wechatAppId);
|
||||
adapayMemberAccountService.clearSettleAccountByMerchantId(dto.getMerchantId());
|
||||
int cleared = adapayMemberAccountService.clearCurrentSettleAccount(
|
||||
adapayMemberAccount.getId(), dto.getMerchantId(), adapayMemberId, settleAccountId);
|
||||
if (cleared != 1) {
|
||||
throw new BusinessException("", "汇付结算账户已删除,但本地账户状态更新失败,请联系管理员处理");
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteAdapayMember(DeleteAdapayMemberDTO dto) throws BaseAdaPayException {
|
||||
@@ -1825,7 +1868,16 @@ public class AdapayService {
|
||||
if (CollectionUtils.isNotEmpty(selectSettleAccount(dto.getMerchantId()))) {
|
||||
throw new BusinessException("", "请先删除结算账户");
|
||||
}
|
||||
adapayMemberAccountService.deleteAccountByMerchantId(dto.getMerchantId());
|
||||
assertSettleAccountCanDelete(dto.getMerchantId());
|
||||
int splitStationCount = stationSplitConfigService.countActiveByAdapayMemberId(adapayMemberAccount.getAdapayMemberId());
|
||||
if (splitStationCount > 0) {
|
||||
throw new BusinessException("", String.format("当前账户仍被%d个站点的分账配置引用,请先调整分账配置", splitStationCount));
|
||||
}
|
||||
int deleted = adapayMemberAccountService.deleteCurrentAccount(
|
||||
adapayMemberAccount.getId(), dto.getMerchantId(), adapayMemberAccount.getAdapayMemberId());
|
||||
if (deleted != 1) {
|
||||
throw new BusinessException("", "账户状态已变化,请刷新后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2001,6 +2053,7 @@ public class AdapayService {
|
||||
break;
|
||||
case PERSONAL_COMPLETED:
|
||||
actions.add(AdapayOpenActionEnum.UPDATE_PERSONAL_MEMBER.getValue());
|
||||
actions.add(AdapayOpenActionEnum.UPDATE_SETTLE_ACCOUNT_CONFIG.getValue());
|
||||
actions.add(AdapayOpenActionEnum.DELETE_SETTLE_ACCOUNT.getValue());
|
||||
break;
|
||||
case CORP_AUDITING:
|
||||
@@ -2014,9 +2067,11 @@ public class AdapayService {
|
||||
case CORP_OPENED_NO_SETTLE:
|
||||
actions.add(AdapayOpenActionEnum.CREATE_SETTLE_ACCOUNT.getValue());
|
||||
actions.add(AdapayOpenActionEnum.UPDATE_CORP_MEMBER.getValue());
|
||||
actions.add(AdapayOpenActionEnum.DELETE_MEMBER.getValue());
|
||||
break;
|
||||
case CORP_COMPLETED:
|
||||
actions.add(AdapayOpenActionEnum.UPDATE_CORP_MEMBER.getValue());
|
||||
actions.add(AdapayOpenActionEnum.UPDATE_SETTLE_ACCOUNT_CONFIG.getValue());
|
||||
actions.add(AdapayOpenActionEnum.DELETE_SETTLE_ACCOUNT.getValue());
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -20,4 +20,6 @@ public class AdapayMemberInfoVO {
|
||||
private String gender;
|
||||
// 昵称
|
||||
private String nickname;
|
||||
// 结算账户id
|
||||
private String settleAccountId;
|
||||
}
|
||||
|
||||
@@ -73,4 +73,10 @@ public class AdapaySettleAccountVO {
|
||||
* 银行账户开户银行所在地区编码(省市编码),银行账户类型为对公时,必填
|
||||
*/
|
||||
private String areaCode;
|
||||
|
||||
private String minAmt;
|
||||
|
||||
private String remainedAmt;
|
||||
|
||||
private String channelRemark;
|
||||
}
|
||||
|
||||
@@ -64,4 +64,9 @@ public class InvoiceTitleDTO {
|
||||
* 接收方式
|
||||
*/
|
||||
private String reception;
|
||||
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
private String email;
|
||||
}
|
||||
|
||||
@@ -120,13 +120,14 @@ public interface AdapayMemberAccountMapper {
|
||||
|
||||
AdapayMemberAccount selectByMemberId(String memberId);
|
||||
|
||||
/**
|
||||
* 通过运营商id删除账户信息
|
||||
* @param merchantId
|
||||
*/
|
||||
void deleteAccountByMerchantId(String merchantId);
|
||||
int deleteCurrentAccount(@Param("id") Integer id,
|
||||
@Param("merchantId") String merchantId,
|
||||
@Param("adapayMemberId") String adapayMemberId);
|
||||
|
||||
void clearSettleAccountByMerchantId(String merchantId);
|
||||
int clearCurrentSettleAccount(@Param("id") Integer id,
|
||||
@Param("merchantId") String merchantId,
|
||||
@Param("adapayMemberId") String adapayMemberId,
|
||||
@Param("settleAccountId") String settleAccountId);
|
||||
|
||||
/**
|
||||
* 根据运营商id查询最近一条的信息
|
||||
|
||||
@@ -24,4 +24,9 @@ public interface ChargeParkingDiscountMapper {
|
||||
int insertOrUpdateSelective(ChargeParkingDiscount record);
|
||||
|
||||
ChargeParkingDiscount selectByStationId(String stationId);
|
||||
}
|
||||
|
||||
int logicDeleteByStationIdExcludeId(@Param("stationId") String stationId,
|
||||
@Param("keepId") Integer keepId,
|
||||
@Param("updateBy") String updateBy,
|
||||
@Param("updateTime") java.util.Date updateTime);
|
||||
}
|
||||
|
||||
@@ -35,4 +35,6 @@ public interface StationSplitConfigMapper {
|
||||
* @return
|
||||
*/
|
||||
List<SplitConfigStationVO> queryStationList(String adapayMemberId);
|
||||
|
||||
int countActiveByAdapayMemberId(String adapayMemberId);
|
||||
}
|
||||
|
||||
@@ -111,12 +111,11 @@ public interface AdapayMemberAccountService {
|
||||
String selectMerchantNameByAdapayMemberId(String adapayMemberId);
|
||||
|
||||
/**
|
||||
* 根据运营商id删除记录
|
||||
* @param merchantId
|
||||
* 精确逻辑删除当前有效账户,避免并发开户时误删新记录。
|
||||
*/
|
||||
void deleteAccountByMerchantId(String merchantId);
|
||||
int deleteCurrentAccount(Integer id, String merchantId, String adapayMemberId);
|
||||
|
||||
void clearSettleAccountByMerchantId(String merchantId);
|
||||
int clearCurrentSettleAccount(Integer id, String merchantId, String adapayMemberId, String settleAccountId);
|
||||
|
||||
/**
|
||||
* 根据运营商Id查询最近一条的信息
|
||||
|
||||
@@ -61,7 +61,7 @@ public interface OrderInvoiceRecordService {
|
||||
* @param orderInvoiceRecord 申请开票
|
||||
* @return 结果
|
||||
*/
|
||||
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord);
|
||||
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord, Long operatorId, String operatorName);
|
||||
|
||||
/**
|
||||
* 批量删除申请开票
|
||||
@@ -69,7 +69,7 @@ public interface OrderInvoiceRecordService {
|
||||
* @param ids 需要删除的申请开票主键集合
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteOrderInvoiceRecordByIds(Integer[] ids);
|
||||
public int deleteOrderInvoiceRecordByIds(Integer[] ids, String operatorName);
|
||||
|
||||
/**
|
||||
* 删除申请开票信息
|
||||
@@ -77,5 +77,5 @@ public interface OrderInvoiceRecordService {
|
||||
* @param id 申请开票主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteOrderInvoiceRecordById(Integer id);
|
||||
public int deleteOrderInvoiceRecordById(Integer id, String operatorName);
|
||||
}
|
||||
|
||||
@@ -59,4 +59,6 @@ public interface StationSplitConfigService {
|
||||
* @return
|
||||
*/
|
||||
Map<String,List<SplitConfigStationVO>> queryStationList(SplitConfigStationDTO splitConfigStationDTO);
|
||||
|
||||
int countActiveByAdapayMemberId(String adapayMemberId);
|
||||
}
|
||||
|
||||
@@ -270,15 +270,21 @@ public class AdapayMemberAccountServiceImpl implements AdapayMemberAccountServic
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAccountByMerchantId(String merchantId) {
|
||||
adapayMemberAccountMapper.deleteAccountByMerchantId(merchantId);
|
||||
redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + merchantId);
|
||||
public int deleteCurrentAccount(Integer id, String merchantId, String adapayMemberId) {
|
||||
int result = adapayMemberAccountMapper.deleteCurrentAccount(id, merchantId, adapayMemberId);
|
||||
if (result > 0) {
|
||||
redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + merchantId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearSettleAccountByMerchantId(String merchantId) {
|
||||
adapayMemberAccountMapper.clearSettleAccountByMerchantId(merchantId);
|
||||
redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + merchantId);
|
||||
public int clearCurrentSettleAccount(Integer id, String merchantId, String adapayMemberId, String settleAccountId) {
|
||||
int result = adapayMemberAccountMapper.clearCurrentSettleAccount(id, merchantId, adapayMemberId, settleAccountId);
|
||||
if (result > 0) {
|
||||
redisCache.deleteObject(CacheConstants.ADAPAY_MEMBER_ACCOUNT + merchantId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.jsowell.pile.vo.web.ChargeParkingDiscountVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@Service
|
||||
public class ChargeParkingDiscountServiceImpl implements ChargeParkingDiscountService{
|
||||
@@ -81,12 +82,26 @@ public class ChargeParkingDiscountServiceImpl implements ChargeParkingDiscountSe
|
||||
|
||||
@Override
|
||||
public int insertOrUpdateSelective(ChargeParkingDiscount record) {
|
||||
if (record.getId() == null) {
|
||||
record.setCreateBy(SecurityUtils.getUsername());
|
||||
} else {
|
||||
record.setUpdateBy(SecurityUtils.getUsername());
|
||||
if (record == null || StringUtils.isBlank(record.getStationId())) {
|
||||
return 0;
|
||||
}
|
||||
return chargeParkingDiscountMapper.insertOrUpdateSelective(record);
|
||||
String username = SecurityUtils.getUsername();
|
||||
Date now = new Date();
|
||||
record.setDelFlag("0");
|
||||
|
||||
ChargeParkingDiscount latestRecord = this.selectByStationId(record.getStationId());
|
||||
if (latestRecord == null) {
|
||||
record.setCreateBy(username);
|
||||
record.setCreateTime(now);
|
||||
return chargeParkingDiscountMapper.insertSelective(record);
|
||||
}
|
||||
|
||||
record.setId(latestRecord.getId());
|
||||
record.setUpdateBy(username);
|
||||
record.setUpdateTime(now);
|
||||
int result = chargeParkingDiscountMapper.updateByPrimaryKeySelective(record);
|
||||
chargeParkingDiscountMapper.logicDeleteByStationIdExcludeId(record.getStationId(), latestRecord.getId(), username, now);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,21 +2,31 @@ package com.jsowell.pile.service.impl;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.jsowell.common.core.domain.vo.AuthorizedDeptVO;
|
||||
import com.jsowell.common.util.StringUtils;
|
||||
import com.jsowell.pile.domain.OrderInvoiceRecord;
|
||||
import com.jsowell.pile.domain.PileMerchantInfo;
|
||||
import com.jsowell.pile.dto.GetInvoiceInfoDTO;
|
||||
import com.jsowell.pile.dto.QueryInvoiceRecordDTO;
|
||||
import com.jsowell.pile.mapper.OrderInvoiceRecordMapper;
|
||||
import com.jsowell.pile.service.MemberInvoiceTitleService;
|
||||
import com.jsowell.pile.service.OrderInvoiceRecordService;
|
||||
import com.jsowell.pile.service.OrderBasicInfoService;
|
||||
import com.jsowell.pile.service.PileMerchantInfoService;
|
||||
import com.jsowell.pile.util.UserUtils;
|
||||
import com.jsowell.pile.vo.base.OrderAmountDetailVO;
|
||||
import com.jsowell.pile.vo.uniapp.customer.InvoiceTitleVO;
|
||||
import com.jsowell.pile.vo.web.InvoiceRecordVO;
|
||||
import com.jsowell.pile.vo.web.OrderInvoiceRecordVO;
|
||||
import com.jsowell.system.constant.TodoTaskConstants;
|
||||
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
|
||||
import com.jsowell.system.service.TodoTaskAssigneeService;
|
||||
import com.jsowell.system.service.TodoTaskService;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -30,6 +40,11 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService {
|
||||
private static final Logger log = LoggerFactory.getLogger(OrderInvoiceRecordServiceImpl.class);
|
||||
private static final String TODO_TASK_TYPE = "INVOICE_REVIEW";
|
||||
private static final String TODO_BUSINESS_TYPE = "ORDER_INVOICE_RECORD";
|
||||
private static final String TODO_ROUTE_NAME = "invoiceDetail";
|
||||
|
||||
@Autowired
|
||||
private OrderInvoiceRecordMapper orderInvoiceRecordMapper;
|
||||
|
||||
@@ -39,6 +54,15 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
|
||||
@Autowired
|
||||
private OrderBasicInfoService orderBasicInfoService;
|
||||
|
||||
@Autowired
|
||||
private PileMerchantInfoService pileMerchantInfoService;
|
||||
|
||||
@Autowired
|
||||
private TodoTaskAssigneeService todoTaskAssigneeService;
|
||||
|
||||
@Autowired
|
||||
private TodoTaskService todoTaskService;
|
||||
|
||||
/**
|
||||
* 查询申请开票
|
||||
*
|
||||
@@ -132,8 +156,13 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int insertOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord) {
|
||||
return orderInvoiceRecordMapper.insertOrderInvoiceRecord(orderInvoiceRecord);
|
||||
int rows = orderInvoiceRecordMapper.insertOrderInvoiceRecord(orderInvoiceRecord);
|
||||
if (rows > 0) {
|
||||
createInvoiceReviewTodos(orderInvoiceRecord);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,8 +172,14 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord) {
|
||||
return orderInvoiceRecordMapper.updateOrderInvoiceRecord(orderInvoiceRecord);
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateOrderInvoiceRecord(OrderInvoiceRecord orderInvoiceRecord, Long operatorId, String operatorName) {
|
||||
int rows = orderInvoiceRecordMapper.updateOrderInvoiceRecord(orderInvoiceRecord);
|
||||
if (rows > 0 && "1".equals(orderInvoiceRecord.getStatus())) {
|
||||
todoTaskService.completeByBusiness(TODO_BUSINESS_TYPE, String.valueOf(orderInvoiceRecord.getId()),
|
||||
null, operatorId, operatorName);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,8 +189,18 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteOrderInvoiceRecordByIds(Integer[] ids) {
|
||||
return orderInvoiceRecordMapper.deleteOrderInvoiceRecordByIds(ids);
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteOrderInvoiceRecordByIds(Integer[] ids, String operatorName) {
|
||||
int rows = orderInvoiceRecordMapper.deleteOrderInvoiceRecordByIds(ids);
|
||||
if (rows > 0 && ids != null) {
|
||||
for (Integer id : ids) {
|
||||
if (id != null) {
|
||||
todoTaskService.cancelByBusiness(TODO_BUSINESS_TYPE, String.valueOf(id), null,
|
||||
operatorName, "开票申请已关闭");
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,7 +210,71 @@ public class OrderInvoiceRecordServiceImpl implements OrderInvoiceRecordService
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteOrderInvoiceRecordById(Integer id) {
|
||||
return orderInvoiceRecordMapper.deleteOrderInvoiceRecordById(id);
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteOrderInvoiceRecordById(Integer id, String operatorName) {
|
||||
int rows = orderInvoiceRecordMapper.deleteOrderInvoiceRecordById(id);
|
||||
if (rows > 0) {
|
||||
todoTaskService.cancelByBusiness(TODO_BUSINESS_TYPE, String.valueOf(id), null,
|
||||
operatorName, "开票申请已关闭");
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void createInvoiceReviewTodos(OrderInvoiceRecord invoiceRecord) {
|
||||
if (invoiceRecord.getId() == null || StringUtils.isBlank(invoiceRecord.getMerchantId())) {
|
||||
log.warn("开票申请缺少主键或运营商,无法创建待办,invoiceId={}, merchantId={}",
|
||||
invoiceRecord.getId(), invoiceRecord.getMerchantId());
|
||||
return;
|
||||
}
|
||||
|
||||
PileMerchantInfo merchantInfo;
|
||||
try {
|
||||
merchantInfo = pileMerchantInfoService.selectPileMerchantInfoById(Long.parseLong(invoiceRecord.getMerchantId()));
|
||||
} catch (NumberFormatException exception) {
|
||||
log.warn("开票申请运营商 ID 非法,无法创建待办,invoiceId={}, merchantId={}",
|
||||
invoiceRecord.getId(), invoiceRecord.getMerchantId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (merchantInfo == null || StringUtils.isBlank(merchantInfo.getDeptId())) {
|
||||
log.warn("开票申请运营商未配置部门,无法创建待办,invoiceId={}, merchantId={}",
|
||||
invoiceRecord.getId(), invoiceRecord.getMerchantId());
|
||||
return;
|
||||
}
|
||||
|
||||
Long merchantDeptId;
|
||||
try {
|
||||
merchantDeptId = Long.parseLong(merchantInfo.getDeptId());
|
||||
} catch (NumberFormatException exception) {
|
||||
log.warn("开票申请运营商部门 ID 非法,无法创建待办,invoiceId={}, deptId={}",
|
||||
invoiceRecord.getId(), merchantInfo.getDeptId());
|
||||
return;
|
||||
}
|
||||
|
||||
List<Long> assigneeUserIds = todoTaskAssigneeService.findActiveUserIdsByDeptTree(merchantDeptId);
|
||||
if (CollectionUtils.isEmpty(assigneeUserIds)) {
|
||||
log.warn("开票申请运营商部门下没有正常用户,无法创建待办,invoiceId={}, deptId={}",
|
||||
invoiceRecord.getId(), merchantInfo.getDeptId());
|
||||
return;
|
||||
}
|
||||
|
||||
for (Long assigneeUserId : assigneeUserIds) {
|
||||
String idempotentKey = TODO_TASK_TYPE + ":" + TODO_BUSINESS_TYPE + ":"
|
||||
+ invoiceRecord.getId() + ":" + assigneeUserId;
|
||||
todoTaskService.createTask(TodoTaskCreateCommand.builder()
|
||||
.taskType(TODO_TASK_TYPE)
|
||||
.title("司机开票申请")
|
||||
.summary("有新的司机开票申请需要处理,申请单号:" + invoiceRecord.getId())
|
||||
.businessType(TODO_BUSINESS_TYPE)
|
||||
.businessId(String.valueOf(invoiceRecord.getId()))
|
||||
.routeName(TODO_ROUTE_NAME)
|
||||
.routeParams("{\"params\":{\"id\":\"" + invoiceRecord.getId() + "\"}}")
|
||||
.assigneeUserId(assigneeUserId)
|
||||
.assigneeMerchantId(invoiceRecord.getMerchantId())
|
||||
.priority(TodoTaskConstants.PRIORITY_IMPORTANT)
|
||||
.idempotentKey(idempotentKey)
|
||||
.createBy("invoice-system")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,4 +316,9 @@ public class StationSplitConfigServiceImpl implements StationSplitConfigService{
|
||||
return stationMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countActiveByAdapayMemberId(String adapayMemberId) {
|
||||
return stationSplitConfigMapper.countActiveByAdapayMemberId(adapayMemberId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -572,20 +572,26 @@
|
||||
and adapay_member_id = #{memberId,jdbcType=VARCHAR}
|
||||
</select>
|
||||
|
||||
<update id="deleteAccountByMerchantId">
|
||||
<update id="deleteCurrentAccount">
|
||||
update
|
||||
adapay_member_account
|
||||
set del_flag = '1'
|
||||
where merchant_id = #{merchantId,jdbcType=VARCHAR}
|
||||
set del_flag = '1',
|
||||
update_time = now()
|
||||
where id = #{id,jdbcType=INTEGER}
|
||||
and merchant_id = #{merchantId,jdbcType=VARCHAR}
|
||||
and adapay_member_id = #{adapayMemberId,jdbcType=VARCHAR}
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="clearSettleAccountByMerchantId">
|
||||
<update id="clearCurrentSettleAccount">
|
||||
update
|
||||
adapay_member_account
|
||||
set settle_account_id = null,
|
||||
update_time = now()
|
||||
where merchant_id = #{merchantId,jdbcType=VARCHAR}
|
||||
where id = #{id,jdbcType=INTEGER}
|
||||
and merchant_id = #{merchantId,jdbcType=VARCHAR}
|
||||
and adapay_member_id = #{adapayMemberId,jdbcType=VARCHAR}
|
||||
and settle_account_id = #{settleAccountId,jdbcType=VARCHAR}
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
|
||||
@@ -485,11 +485,23 @@
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<update id="logicDeleteByStationIdExcludeId">
|
||||
update charge_parking_discount
|
||||
set del_flag = '1',
|
||||
update_by = #{updateBy,jdbcType=VARCHAR},
|
||||
update_time = #{updateTime,jdbcType=TIMESTAMP}
|
||||
where station_id = #{stationId,jdbcType=VARCHAR}
|
||||
and del_flag = '0'
|
||||
and id != #{keepId,jdbcType=INTEGER}
|
||||
</update>
|
||||
|
||||
<select id="selectByStationId" resultMap="BaseResultMap">
|
||||
select
|
||||
<include refid="Base_Column_List" />
|
||||
from charge_parking_discount
|
||||
where del_flag = '0'
|
||||
and station_id = #{stationId,jdbcType=VARCHAR}
|
||||
order by ifnull(update_time, create_time) desc, id desc
|
||||
limit 1
|
||||
</select>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -41,6 +41,13 @@
|
||||
where t1.adapay_member_id = #{adapayMemberId}
|
||||
</select>
|
||||
|
||||
<select id="countActiveByAdapayMemberId" resultType="int">
|
||||
select count(distinct station_id)
|
||||
from station_split_config
|
||||
where adapay_member_id = #{adapayMemberId}
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<insert id="batchInsert" parameterType="map">
|
||||
<!--@mbg.generated-->
|
||||
insert into station_split_config
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.jsowell.system.constant;
|
||||
|
||||
/**
|
||||
* 待办任务常量。
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
public final class TodoTaskConstants {
|
||||
private TodoTaskConstants() {
|
||||
}
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_PROCESSING = "PROCESSING";
|
||||
public static final String STATUS_COMPLETED = "COMPLETED";
|
||||
public static final String STATUS_CANCELLED = "CANCELLED";
|
||||
public static final String STATUS_EXPIRED = "EXPIRED";
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
|
||||
public static final String READ_UNREAD = "0";
|
||||
public static final String READ_READ = "1";
|
||||
|
||||
public static final int PRIORITY_NORMAL = 1;
|
||||
public static final int PRIORITY_IMPORTANT = 2;
|
||||
public static final int PRIORITY_URGENT = 3;
|
||||
|
||||
public static final int DEFAULT_HOME_LIMIT = 3;
|
||||
public static final int MAX_HOME_LIMIT = 10;
|
||||
|
||||
public static boolean isActiveStatus(String status) {
|
||||
return STATUS_PENDING.equals(status) || STATUS_PROCESSING.equals(status);
|
||||
}
|
||||
|
||||
public static boolean isTerminalStatus(String status) {
|
||||
return STATUS_COMPLETED.equals(status)
|
||||
|| STATUS_CANCELLED.equals(status)
|
||||
|| STATUS_EXPIRED.equals(status);
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatus(String status) {
|
||||
return STATUS_ACTIVE.equals(status)
|
||||
|| isActiveStatus(status)
|
||||
|| isTerminalStatus(status);
|
||||
}
|
||||
}
|
||||
@@ -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,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,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,65 @@
|
||||
package com.jsowell.system.mapper;
|
||||
|
||||
import com.jsowell.system.domain.SysTodoTask;
|
||||
import com.jsowell.system.domain.dto.TodoTaskQuery;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 待办任务数据层。
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
public interface SysTodoTaskMapper {
|
||||
SysTodoTask selectTodoById(@Param("todoId") Long todoId);
|
||||
|
||||
SysTodoTask selectTodoByIdForAssignee(@Param("todoId") Long todoId,
|
||||
@Param("assigneeUserId") Long assigneeUserId);
|
||||
|
||||
SysTodoTask selectActiveByIdempotentKey(@Param("activeIdempotentKey") String activeIdempotentKey);
|
||||
|
||||
List<SysTodoTask> selectTodoList(TodoTaskQuery query);
|
||||
|
||||
List<SysTodoTask> selectHomeList(@Param("assigneeUserId") Long assigneeUserId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
int countActive(@Param("assigneeUserId") Long assigneeUserId);
|
||||
|
||||
int countUnread(@Param("assigneeUserId") Long assigneeUserId);
|
||||
|
||||
int insertTodoTask(SysTodoTask todoTask);
|
||||
|
||||
int markTodoRead(@Param("todoId") Long todoId,
|
||||
@Param("assigneeUserId") Long assigneeUserId,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
int markAllRead(@Param("assigneeUserId") Long assigneeUserId,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
int completeTodoForAssignee(@Param("todoId") Long todoId,
|
||||
@Param("assigneeUserId") Long assigneeUserId,
|
||||
@Param("completedBy") Long completedBy,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
int completeTodoByBusiness(@Param("businessType") String businessType,
|
||||
@Param("businessId") String businessId,
|
||||
@Param("assigneeUserId") Long assigneeUserId,
|
||||
@Param("completedBy") Long completedBy,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
int cancelTodoForAssignee(@Param("todoId") Long todoId,
|
||||
@Param("assigneeUserId") Long assigneeUserId,
|
||||
@Param("reason") String reason,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
int cancelTodo(@Param("todoId") Long todoId,
|
||||
@Param("reason") String reason,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
int cancelTodoByBusiness(@Param("businessType") String businessType,
|
||||
@Param("businessId") String businessId,
|
||||
@Param("assigneeUserId") Long assigneeUserId,
|
||||
@Param("reason") String reason,
|
||||
@Param("updateBy") String updateBy);
|
||||
}
|
||||
@@ -19,6 +19,14 @@ public interface SysUserMapper {
|
||||
*/
|
||||
List<SysUser> selectUserList(SysUser sysUser);
|
||||
|
||||
/**
|
||||
* 查询部门及下级部门的正常用户 ID。
|
||||
*
|
||||
* @param deptId 部门 ID
|
||||
* @return 用户 ID 集合
|
||||
*/
|
||||
List<Long> selectActiveUserIdsByDeptTree(@Param("deptId") Long deptId);
|
||||
|
||||
/**
|
||||
* 根据条件分页查询已配用户角色列表
|
||||
*
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.jsowell.system.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 待办接收人解析服务。
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
public interface TodoTaskAssigneeService {
|
||||
List<Long> findActiveUserIdsByDeptTree(Long deptId);
|
||||
}
|
||||
@@ -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,28 @@
|
||||
package com.jsowell.system.service.impl;
|
||||
|
||||
import com.jsowell.common.exception.ServiceException;
|
||||
import com.jsowell.system.mapper.SysUserMapper;
|
||||
import com.jsowell.system.service.TodoTaskAssigneeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 待办接收人解析服务实现。
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Service
|
||||
public class TodoTaskAssigneeServiceImpl implements TodoTaskAssigneeService {
|
||||
@Autowired
|
||||
private SysUserMapper userMapper;
|
||||
|
||||
@Override
|
||||
public List<Long> findActiveUserIdsByDeptTree(Long deptId) {
|
||||
if (deptId == null || deptId <= 0) {
|
||||
throw new ServiceException("待办接收部门不能为空");
|
||||
}
|
||||
return userMapper.selectActiveUserIdsByDeptTree(deptId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package com.jsowell.system.service.impl;
|
||||
|
||||
import com.jsowell.common.exception.ServiceException;
|
||||
import com.jsowell.common.util.StringUtils;
|
||||
import com.jsowell.system.constant.TodoTaskConstants;
|
||||
import com.jsowell.system.domain.SysTodoTask;
|
||||
import com.jsowell.system.domain.dto.TodoTaskCreateCommand;
|
||||
import com.jsowell.system.domain.dto.TodoTaskQuery;
|
||||
import com.jsowell.system.mapper.SysTodoTaskMapper;
|
||||
import com.jsowell.system.service.TodoTaskService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 统一待办任务服务实现。
|
||||
*
|
||||
* @author jsowell
|
||||
*/
|
||||
@Service
|
||||
public class TodoTaskServiceImpl implements TodoTaskService {
|
||||
private static final String SYSTEM_OPERATOR = "system";
|
||||
|
||||
@Autowired
|
||||
private SysTodoTaskMapper todoTaskMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysTodoTask createTask(TodoTaskCreateCommand command) {
|
||||
if (command == null) {
|
||||
throw new ServiceException("待办创建参数不能为空");
|
||||
}
|
||||
|
||||
SysTodoTask todoTask = buildTask(command);
|
||||
SysTodoTask existingTask = todoTaskMapper.selectActiveByIdempotentKey(todoTask.getActiveIdempotentKey());
|
||||
if (existingTask != null) {
|
||||
return existingTask;
|
||||
}
|
||||
|
||||
try {
|
||||
todoTaskMapper.insertTodoTask(todoTask);
|
||||
return todoTask;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
existingTask = todoTaskMapper.selectActiveByIdempotentKey(todoTask.getActiveIdempotentKey());
|
||||
if (existingTask != null) {
|
||||
return existingTask;
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysTodoTask getForAssignee(Long todoId, Long assigneeUserId) {
|
||||
validateIdentity(todoId, assigneeUserId);
|
||||
SysTodoTask todoTask = todoTaskMapper.selectTodoByIdForAssignee(todoId, assigneeUserId);
|
||||
if (todoTask == null) {
|
||||
throw new ServiceException("待办任务不存在或无权访问");
|
||||
}
|
||||
return todoTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysTodoTask> listForAssignee(TodoTaskQuery query, Long assigneeUserId) {
|
||||
validateUserId(assigneeUserId);
|
||||
TodoTaskQuery safeQuery = query == null ? new TodoTaskQuery() : query;
|
||||
validateQuery(safeQuery);
|
||||
safeQuery.setAssigneeUserId(assigneeUserId);
|
||||
return todoTaskMapper.selectTodoList(safeQuery);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysTodoTask> homeList(Long assigneeUserId, Integer limit) {
|
||||
validateUserId(assigneeUserId);
|
||||
int safeLimit = limit == null ? TodoTaskConstants.DEFAULT_HOME_LIMIT : limit;
|
||||
if (safeLimit < 1) {
|
||||
safeLimit = TodoTaskConstants.DEFAULT_HOME_LIMIT;
|
||||
}
|
||||
if (safeLimit > TodoTaskConstants.MAX_HOME_LIMIT) {
|
||||
safeLimit = TodoTaskConstants.MAX_HOME_LIMIT;
|
||||
}
|
||||
return todoTaskMapper.selectHomeList(assigneeUserId, safeLimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countActive(Long assigneeUserId) {
|
||||
validateUserId(assigneeUserId);
|
||||
return todoTaskMapper.countActive(assigneeUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countUnread(Long assigneeUserId) {
|
||||
validateUserId(assigneeUserId);
|
||||
return todoTaskMapper.countUnread(assigneeUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean markRead(Long todoId, Long assigneeUserId, String operatorName) {
|
||||
validateIdentity(todoId, assigneeUserId);
|
||||
int rows = todoTaskMapper.markTodoRead(todoId, assigneeUserId, normalizeOperator(operatorName, assigneeUserId));
|
||||
if (rows > 0) {
|
||||
return true;
|
||||
}
|
||||
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
|
||||
return TodoTaskConstants.READ_READ.equals(todoTask.getReadStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int markAllRead(Long assigneeUserId, String operatorName) {
|
||||
validateUserId(assigneeUserId);
|
||||
return todoTaskMapper.markAllRead(assigneeUserId, normalizeOperator(operatorName, assigneeUserId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean complete(Long todoId, Long assigneeUserId, String operatorName) {
|
||||
validateIdentity(todoId, assigneeUserId);
|
||||
int rows = todoTaskMapper.completeTodoForAssignee(todoId, assigneeUserId, assigneeUserId,
|
||||
normalizeOperator(operatorName, assigneeUserId));
|
||||
if (rows > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
|
||||
if (TodoTaskConstants.STATUS_COMPLETED.equals(todoTask.getTaskStatus())) {
|
||||
return true;
|
||||
}
|
||||
throw new ServiceException("当前待办状态不允许完成");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int completeByBusiness(String businessType, String businessId, Long assigneeUserId,
|
||||
Long operatorId, String operatorName) {
|
||||
String safeBusinessType = normalizeRequired(businessType, "业务类型", 64);
|
||||
String safeBusinessId = normalizeRequired(businessId, "业务主键", 64);
|
||||
if (assigneeUserId != null) {
|
||||
validateUserId(assigneeUserId);
|
||||
}
|
||||
if (operatorId == null || operatorId <= 0) {
|
||||
throw new ServiceException("待办完成人不能为空");
|
||||
}
|
||||
return todoTaskMapper.completeTodoByBusiness(safeBusinessType, safeBusinessId, assigneeUserId,
|
||||
operatorId, normalizeOperator(operatorName, operatorId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean cancelForAssignee(Long todoId, Long assigneeUserId, String operatorName, String reason) {
|
||||
validateIdentity(todoId, assigneeUserId);
|
||||
String safeReason = normalizeRequired(reason, "取消原因", 500);
|
||||
int rows = todoTaskMapper.cancelTodoForAssignee(todoId, assigneeUserId, safeReason,
|
||||
normalizeOperator(operatorName, assigneeUserId));
|
||||
if (rows > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SysTodoTask todoTask = getForAssignee(todoId, assigneeUserId);
|
||||
if (TodoTaskConstants.STATUS_CANCELLED.equals(todoTask.getTaskStatus())) {
|
||||
return true;
|
||||
}
|
||||
throw new ServiceException("当前待办状态不允许取消");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean cancel(Long todoId, Long operatorId, String operatorName, String reason) {
|
||||
if (todoId == null || todoId <= 0) {
|
||||
throw new ServiceException("待办任务 ID 不能为空");
|
||||
}
|
||||
if (operatorId == null || operatorId <= 0) {
|
||||
throw new ServiceException("取消操作人不能为空");
|
||||
}
|
||||
String safeReason = normalizeRequired(reason, "取消原因", 500);
|
||||
int rows = todoTaskMapper.cancelTodo(todoId, safeReason, normalizeOperator(operatorName, operatorId));
|
||||
if (rows > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SysTodoTask todoTask = todoTaskMapper.selectTodoById(todoId);
|
||||
if (todoTask == null) {
|
||||
throw new ServiceException("待办任务不存在");
|
||||
}
|
||||
if (TodoTaskConstants.STATUS_CANCELLED.equals(todoTask.getTaskStatus())) {
|
||||
return true;
|
||||
}
|
||||
throw new ServiceException("当前待办状态不允许取消");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int cancelByBusiness(String businessType, String businessId, Long assigneeUserId,
|
||||
String operatorName, String reason) {
|
||||
String safeBusinessType = normalizeRequired(businessType, "业务类型", 64);
|
||||
String safeBusinessId = normalizeRequired(businessId, "业务主键", 64);
|
||||
if (assigneeUserId != null) {
|
||||
validateUserId(assigneeUserId);
|
||||
}
|
||||
String safeReason = normalizeRequired(reason, "取消原因", 500);
|
||||
return todoTaskMapper.cancelTodoByBusiness(safeBusinessType, safeBusinessId, assigneeUserId,
|
||||
safeReason, normalizeOperator(operatorName, null));
|
||||
}
|
||||
|
||||
private SysTodoTask buildTask(TodoTaskCreateCommand command) {
|
||||
if (command.getAssigneeUserId() == null || command.getAssigneeUserId() <= 0) {
|
||||
throw new ServiceException("待办接收用户不能为空");
|
||||
}
|
||||
|
||||
Integer priority = command.getPriority() == null
|
||||
? TodoTaskConstants.PRIORITY_NORMAL : command.getPriority();
|
||||
if (priority < TodoTaskConstants.PRIORITY_NORMAL || priority > TodoTaskConstants.PRIORITY_URGENT) {
|
||||
throw new ServiceException("待办优先级只能为 1、2 或 3");
|
||||
}
|
||||
|
||||
SysTodoTask todoTask = new SysTodoTask();
|
||||
todoTask.setTaskType(normalizeRequired(command.getTaskType(), "待办类型", 64));
|
||||
todoTask.setTitle(normalizeRequired(command.getTitle(), "待办标题", 200));
|
||||
todoTask.setSummary(normalizeOptional(command.getSummary(), "待办摘要", 500));
|
||||
todoTask.setContent(normalizeOptional(command.getContent(), "待办内容", 20000));
|
||||
todoTask.setBusinessType(normalizeRequired(command.getBusinessType(), "业务类型", 64));
|
||||
todoTask.setBusinessId(normalizeRequired(command.getBusinessId(), "业务主键", 64));
|
||||
todoTask.setRouteName(normalizeRequired(command.getRouteName(), "业务路由", 100));
|
||||
todoTask.setRouteParams(normalizeOptional(command.getRouteParams(), "路由参数", 1000));
|
||||
todoTask.setAssigneeUserId(command.getAssigneeUserId());
|
||||
todoTask.setAssigneeMerchantId(normalizeOptional(command.getAssigneeMerchantId(), "运营商 ID", 64));
|
||||
todoTask.setPriority(priority);
|
||||
todoTask.setTaskStatus(TodoTaskConstants.STATUS_PENDING);
|
||||
todoTask.setReadStatus(TodoTaskConstants.READ_UNREAD);
|
||||
todoTask.setDueTime(command.getDueTime());
|
||||
todoTask.setIdempotentKey(normalizeRequired(command.getIdempotentKey(), "幂等键", 200));
|
||||
todoTask.setActiveIdempotentKey(todoTask.getIdempotentKey());
|
||||
todoTask.setCreateBy(normalizeOperator(command.getCreateBy(), null));
|
||||
todoTask.setRemark(normalizeOptional(command.getRemark(), "备注", 500));
|
||||
todoTask.setDelFlag("0");
|
||||
return todoTask;
|
||||
}
|
||||
|
||||
private void validateQuery(TodoTaskQuery query) {
|
||||
if (StringUtils.isNotEmpty(query.getTaskStatus())
|
||||
&& !TodoTaskConstants.isSupportedStatus(query.getTaskStatus())) {
|
||||
throw new ServiceException("不支持的待办状态筛选条件");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(query.getReadStatus())
|
||||
&& !TodoTaskConstants.READ_UNREAD.equals(query.getReadStatus())
|
||||
&& !TodoTaskConstants.READ_READ.equals(query.getReadStatus())) {
|
||||
throw new ServiceException("不支持的阅读状态筛选条件");
|
||||
}
|
||||
query.setTaskType(normalizeOptional(query.getTaskType(), "待办类型", 64));
|
||||
query.setKeyword(normalizeOptional(query.getKeyword(), "关键字", 100));
|
||||
}
|
||||
|
||||
private void validateIdentity(Long todoId, Long assigneeUserId) {
|
||||
if (todoId == null || todoId <= 0) {
|
||||
throw new ServiceException("待办任务 ID 不能为空");
|
||||
}
|
||||
validateUserId(assigneeUserId);
|
||||
}
|
||||
|
||||
private void validateUserId(Long userId) {
|
||||
if (userId == null || userId <= 0) {
|
||||
throw new ServiceException("待办接收用户不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeRequired(String value, String fieldName, int maxLength) {
|
||||
String safeValue = StringUtils.trim(value);
|
||||
if (StringUtils.isEmpty(safeValue)) {
|
||||
throw new ServiceException(fieldName + "不能为空");
|
||||
}
|
||||
if (safeValue.length() > maxLength) {
|
||||
throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符");
|
||||
}
|
||||
return safeValue;
|
||||
}
|
||||
|
||||
private String normalizeOptional(String value, String fieldName, int maxLength) {
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
String safeValue = value.trim();
|
||||
if (safeValue.length() > maxLength) {
|
||||
throw new ServiceException(fieldName + "不能超过" + maxLength + "个字符");
|
||||
}
|
||||
return safeValue;
|
||||
}
|
||||
|
||||
private String normalizeOperator(String operatorName, Long operatorId) {
|
||||
String safeOperator = StringUtils.isNotEmpty(operatorName)
|
||||
? operatorName.trim()
|
||||
: (operatorId == null ? SYSTEM_OPERATOR : String.valueOf(operatorId));
|
||||
if (safeOperator.length() > 64) {
|
||||
throw new ServiceException("操作人名称不能超过64个字符");
|
||||
}
|
||||
return safeOperator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.jsowell.system.mapper.SysTodoTaskMapper">
|
||||
|
||||
<resultMap type="SysTodoTask" id="SysTodoTaskResult">
|
||||
<id property="todoId" column="todo_id" />
|
||||
<result property="taskType" column="task_type" />
|
||||
<result property="title" column="title" />
|
||||
<result property="summary" column="summary" />
|
||||
<result property="content" column="content" />
|
||||
<result property="businessType" column="business_type" />
|
||||
<result property="businessId" column="business_id" />
|
||||
<result property="routeName" column="route_name" />
|
||||
<result property="routeParams" column="route_params" />
|
||||
<result property="assigneeUserId" column="assignee_user_id" />
|
||||
<result property="assigneeMerchantId" column="assignee_merchant_id" />
|
||||
<result property="priority" column="priority" />
|
||||
<result property="taskStatus" column="task_status" />
|
||||
<result property="readStatus" column="read_status" />
|
||||
<result property="readTime" column="read_time" />
|
||||
<result property="dueTime" column="due_time" />
|
||||
<result property="completedTime" column="completed_time" />
|
||||
<result property="completedBy" column="completed_by" />
|
||||
<result property="cancelReason" column="cancel_reason" />
|
||||
<result property="idempotentKey" column="idempotent_key" />
|
||||
<result property="activeIdempotentKey" column="active_idempotent_key" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectTodoVo">
|
||||
select todo_id, task_type, title, summary, cast(content as char) as content,
|
||||
business_type, business_id, route_name, route_params,
|
||||
assignee_user_id, assignee_merchant_id, priority, task_status,
|
||||
read_status, read_time, due_time, completed_time, completed_by,
|
||||
cancel_reason, idempotent_key, active_idempotent_key,
|
||||
create_by, create_time, update_by, update_time, remark, del_flag
|
||||
from sys_todo_task
|
||||
</sql>
|
||||
|
||||
<select id="selectTodoById" parameterType="Long" resultMap="SysTodoTaskResult">
|
||||
<include refid="selectTodoVo" />
|
||||
where todo_id = #{todoId}
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectTodoByIdForAssignee" resultMap="SysTodoTaskResult">
|
||||
<include refid="selectTodoVo" />
|
||||
where todo_id = #{todoId}
|
||||
and assignee_user_id = #{assigneeUserId}
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectActiveByIdempotentKey" parameterType="String" resultMap="SysTodoTaskResult">
|
||||
<include refid="selectTodoVo" />
|
||||
where active_idempotent_key = #{activeIdempotentKey}
|
||||
and del_flag = '0'
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectTodoList" parameterType="com.jsowell.system.domain.dto.TodoTaskQuery"
|
||||
resultMap="SysTodoTaskResult">
|
||||
<include refid="selectTodoVo" />
|
||||
<where>
|
||||
del_flag = '0'
|
||||
and assignee_user_id = #{assigneeUserId}
|
||||
<if test="taskStatus != null and taskStatus == 'ACTIVE'">
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
</if>
|
||||
<if test="taskStatus != null and taskStatus != '' and taskStatus != 'ACTIVE'">
|
||||
and task_status = #{taskStatus}
|
||||
</if>
|
||||
<if test="readStatus != null and readStatus != ''">
|
||||
and read_status = #{readStatus}
|
||||
</if>
|
||||
<if test="taskType != null and taskType != ''">
|
||||
and task_type = #{taskType}
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (title like concat('%', #{keyword}, '%')
|
||||
or business_id like concat('%', #{keyword}, '%'))
|
||||
</if>
|
||||
<if test="hideCompleted != null and hideCompleted">
|
||||
and task_status != 'COMPLETED'
|
||||
</if>
|
||||
</where>
|
||||
order by priority desc, create_time desc, todo_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectHomeList" resultMap="SysTodoTaskResult">
|
||||
<include refid="selectTodoVo" />
|
||||
where assignee_user_id = #{assigneeUserId}
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and del_flag = '0'
|
||||
order by priority desc, create_time desc, todo_id desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countActive" parameterType="Long" resultType="int">
|
||||
select count(1)
|
||||
from sys_todo_task
|
||||
where assignee_user_id = #{assigneeUserId}
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countUnread" parameterType="Long" resultType="int">
|
||||
select count(1)
|
||||
from sys_todo_task
|
||||
where assignee_user_id = #{assigneeUserId}
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and read_status = '0'
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<insert id="insertTodoTask" parameterType="SysTodoTask" useGeneratedKeys="true" keyProperty="todoId">
|
||||
insert into sys_todo_task (
|
||||
task_type, title, summary, content, business_type, business_id,
|
||||
route_name, route_params, assignee_user_id, assignee_merchant_id,
|
||||
priority, task_status, read_status, due_time, idempotent_key,
|
||||
active_idempotent_key, create_by, create_time, remark, del_flag
|
||||
) values (
|
||||
#{taskType}, #{title}, #{summary}, #{content}, #{businessType}, #{businessId},
|
||||
#{routeName}, #{routeParams}, #{assigneeUserId}, #{assigneeMerchantId},
|
||||
#{priority}, #{taskStatus}, #{readStatus}, #{dueTime}, #{idempotentKey},
|
||||
#{activeIdempotentKey}, #{createBy}, sysdate(), #{remark}, #{delFlag}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="markTodoRead">
|
||||
update sys_todo_task
|
||||
set read_status = '1',
|
||||
read_time = sysdate(),
|
||||
update_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where todo_id = #{todoId}
|
||||
and assignee_user_id = #{assigneeUserId}
|
||||
and read_status = '0'
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="markAllRead">
|
||||
update sys_todo_task
|
||||
set read_status = '1',
|
||||
read_time = sysdate(),
|
||||
update_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where assignee_user_id = #{assigneeUserId}
|
||||
and read_status = '0'
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="completeTodoForAssignee">
|
||||
update sys_todo_task
|
||||
set task_status = 'COMPLETED',
|
||||
completed_time = sysdate(),
|
||||
completed_by = #{completedBy},
|
||||
active_idempotent_key = null,
|
||||
update_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where todo_id = #{todoId}
|
||||
and assignee_user_id = #{assigneeUserId}
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="completeTodoByBusiness">
|
||||
update sys_todo_task
|
||||
set task_status = 'COMPLETED',
|
||||
completed_time = sysdate(),
|
||||
completed_by = #{completedBy},
|
||||
active_idempotent_key = null,
|
||||
update_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where business_type = #{businessType}
|
||||
and business_id = #{businessId}
|
||||
<if test="assigneeUserId != null">
|
||||
and assignee_user_id = #{assigneeUserId}
|
||||
</if>
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="cancelTodoForAssignee">
|
||||
update sys_todo_task
|
||||
set task_status = 'CANCELLED',
|
||||
cancel_reason = #{reason},
|
||||
active_idempotent_key = null,
|
||||
update_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where todo_id = #{todoId}
|
||||
and assignee_user_id = #{assigneeUserId}
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="cancelTodo">
|
||||
update sys_todo_task
|
||||
set task_status = 'CANCELLED',
|
||||
cancel_reason = #{reason},
|
||||
active_idempotent_key = null,
|
||||
update_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where todo_id = #{todoId}
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="cancelTodoByBusiness">
|
||||
update sys_todo_task
|
||||
set task_status = 'CANCELLED',
|
||||
cancel_reason = #{reason},
|
||||
active_idempotent_key = null,
|
||||
update_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where business_type = #{businessType}
|
||||
and business_id = #{businessId}
|
||||
<if test="assigneeUserId != null">
|
||||
and assignee_user_id = #{assigneeUserId}
|
||||
</if>
|
||||
and task_status in ('PENDING', 'PROCESSING')
|
||||
and del_flag = '0'
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -90,6 +90,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<!-- 数据范围过滤 -->
|
||||
${params.dataScope}
|
||||
</select>
|
||||
|
||||
<select id="selectActiveUserIdsByDeptTree" resultType="Long">
|
||||
select distinct u.user_id
|
||||
from sys_user u
|
||||
where u.status = '0'
|
||||
and u.del_flag = '0'
|
||||
and (
|
||||
u.dept_id = #{deptId}
|
||||
or u.dept_id in (
|
||||
select d.dept_id
|
||||
from sys_dept d
|
||||
where find_in_set(#{deptId}, d.ancestors)
|
||||
and d.status = '0'
|
||||
)
|
||||
)
|
||||
order by u.user_id
|
||||
</select>
|
||||
|
||||
<select id="selectAllocatedList" parameterType="com.jsowell.common.core.domain.entity.SysUser" resultMap="SysUserResult">
|
||||
select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phone, u.status, u.create_time
|
||||
@@ -225,4 +242,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user