mirror of
https://codeup.aliyun.com/67c68d4e484ca2f0a13ac3c1/ydc/jsowell-charger-web.git
synced 2026-07-21 14:38:07 +08:00
Compare commits
3 Commits
feature-in
...
9cdd984bcd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cdd984bcd | ||
|
|
345a6efb37 | ||
|
|
b4731d3e85 |
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`,避免公告和业务任务互相污染。
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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查询最近一条的信息
|
||||
|
||||
@@ -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查询最近一条的信息
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user