Compare commits
14 Commits
glt_xm
...
d9e4371735
| Author | SHA1 | Date | |
|---|---|---|---|
| d9e4371735 | |||
| eadaa8c4dd | |||
| fa5260d583 | |||
| 0c4bdc3031 | |||
| 47ef45054a | |||
| 9297d13045 | |||
| 701a135edd | |||
| 6781374c1e | |||
| 7c90f5e8af | |||
| 721ce5a595 | |||
| 506505bb46 | |||
| 8b83e4862f | |||
| 800b4f6f93 | |||
| 498a47977e |
61
.workbuddy/expert-history.json
Normal file
61
.workbuddy/expert-history.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"sessions": {
|
||||||
|
"7759a9e57f984a0bb5af2ffd05be2f63": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1775972794982,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"e7c3c15a2556446884e56ce4d588e133": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1776000797914,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"44c34a14b6dc4139b39ff61239e259ea": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "Will",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1776000797914,
|
||||||
|
"industryId": "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"d11a5ebd8e064cc19ff4a85b8d931dac": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "吴八哥",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1776443595917,
|
||||||
|
"industryId": "02-Engineering"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"e339ec20b1ef45479756bdfdf93c3654": [
|
||||||
|
{
|
||||||
|
"expertId": "SeniorDeveloper",
|
||||||
|
"name": "吴八哥",
|
||||||
|
"profession": "高级开发工程师",
|
||||||
|
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
|
||||||
|
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
|
||||||
|
"usedAt": 1776696820692,
|
||||||
|
"industryId": "02-Engineering"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lastUpdated": 1776699418893
|
||||||
|
}
|
||||||
13
.workbuddy/memory/2026-04-12.md
Normal file
13
.workbuddy/memory/2026-04-12.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 2026-04-12 工作日志
|
||||||
|
|
||||||
|
## 修复登录日志时间显示问题
|
||||||
|
|
||||||
|
**问题描述**:小程序后台登录日志中的登录时间显示不正确,实际登录时间 9:20:20,显示为 17:16:31,相差约 8 小时。
|
||||||
|
|
||||||
|
**问题原因**:`LoginRecord` 实体类中的 `createTime` 和 `updateTime` 字段使用了 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")` 注解,但没有指定 `timezone` 属性。对于 `LocalDateTime` 类型,Jackson 序列化时未正确应用全局时区配置,导致时间多了 8 小时。
|
||||||
|
|
||||||
|
**修复方案**:为 `@JsonFormat` 注解添加 `timezone = "GMT+8"` 属性。
|
||||||
|
|
||||||
|
**修改文件**:`src/main/java/com/gxwebsoft/common/system/entity/LoginRecord.java`
|
||||||
|
|
||||||
|
**状态**:已修复
|
||||||
15
.workbuddy/memory/2026-04-13.md
Normal file
15
.workbuddy/memory/2026-04-13.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 2026-04-13 工作日志
|
||||||
|
|
||||||
|
## 修复支付回调订单状态不更新问题
|
||||||
|
|
||||||
|
**问题描述**:支付成功后,订单支付状态没有更新,回调地址 `https://glt-api.websoft.top/api/shop/shop-order/notify` 接收到了通知但订单状态未改变。
|
||||||
|
|
||||||
|
**问题原因**:`ShopOrderController.java` 的 `wxNotify` 方法中,使用 `StrUtil.equals("支付成功", transaction.getTradeStateDesc())` 来判断支付状态。但微信返回的 `tradeStateDesc` 可能不是固定的 "支付成功" 字符串(可能是 "SUCCESS" 或其他描述),导致支付成功的回调没有被正确处理。
|
||||||
|
|
||||||
|
**修复方案**:将状态判断从字符串比较改为枚举值比较:
|
||||||
|
- 原代码:`if (StrUtil.equals("支付成功", transaction.getTradeStateDesc()))`
|
||||||
|
- 修复后:`if (Transaction.TradeStateEnum.SUCCESS.equals(transaction.getTradeState()))`
|
||||||
|
|
||||||
|
**修改文件**:`src/main/java/com/gxwebsoft/shop/controller/ShopOrderController.java`
|
||||||
|
|
||||||
|
**状态**:已修复
|
||||||
75
.workbuddy/memory/2026-04-16.md
Normal file
75
.workbuddy/memory/2026-04-16.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# 2026-04-16 工作记录
|
||||||
|
|
||||||
|
## 支付回调状态不更新问题诊断与修复
|
||||||
|
|
||||||
|
**问题接口**: `POST /api/shop/shop-order/notify/{tenantId}`
|
||||||
|
|
||||||
|
### 发现的 Bug
|
||||||
|
|
||||||
|
1. **根因 Bug**:`ShopOrderServiceImpl.updateByOutTradeNo()` 第837行有 `order.setExpirationTime(null)`,强制覆盖了 Controller 中设置的 `expirationTime`(`LocalDateTime.now().plusYears(10)`),导致 XML 中 expirationTime 条件不生效。**已修复**:删除了该行。
|
||||||
|
|
||||||
|
2. **XML 缺少 `update_time`**:`ShopOrderMapper.xml` 的 `updateByOutTradeNo` SQL 的 `<set>` 块中没有 `update_time = NOW()` 和 `expiration_time` 字段。**已修复**:新增了这两个字段更新。
|
||||||
|
|
||||||
|
3. **回调地址路由问题**:Controller 路由为 `/notify/{tenantId}`,但测试访问的 `/notify`(无 tenantId)不存在,返回 fail。正确回调地址格式为 `https://glt-api.websoft.top/api/shop/shop-order/notify/{tenantId}`(需带租户ID)。**待检查**:数据库 Payment 表的 `notify_url` 字段是否正确配置了带 tenantId 的完整路径。
|
||||||
|
|
||||||
|
### 修复文件
|
||||||
|
- `src/main/java/com/gxwebsoft/shop/service/impl/ShopOrderServiceImpl.java`
|
||||||
|
- `src/main/java/com/gxwebsoft/shop/mapper/xml/ShopOrderMapper.xml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 支付回调签名验证失败:Transaction 类错误(00:29修复)
|
||||||
|
|
||||||
|
**错误日志关键信息**:
|
||||||
|
```
|
||||||
|
signature verification failed, signType[WECHATPAY2-SHA256-RSA2048]
|
||||||
|
serial[test] message[test\ntest\n{"test":"test"}] sign[test]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 根本原因(最致命)
|
||||||
|
|
||||||
|
`ShopOrderController.java` 导入了 **服务商模式** 的 Transaction 类:
|
||||||
|
```java
|
||||||
|
// 错误(服务商模式)
|
||||||
|
import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction;
|
||||||
|
```
|
||||||
|
而 `ShopOrderServiceImpl.java` 下单时用的是**直连商户模式**:
|
||||||
|
```java
|
||||||
|
// 正确(直连商户模式)
|
||||||
|
import com.wechat.pay.java.service.payments.model.Transaction;
|
||||||
|
```
|
||||||
|
两个 Transaction 包路径不同,字段结构有差异(服务商 Transaction 有 spAppid/spMchid 等字段),用错误的类解析回调会导致字段映射失败,交易状态无法正确读取。**已修复**:改为正确的直连商户模式 Transaction。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配送员提成直接入账(01:15修改)
|
||||||
|
|
||||||
|
**文件**:`src/main/java/com/gxwebsoft/glt/service/impl/GltTicketOrderServiceImpl.java`
|
||||||
|
|
||||||
|
**变更**:配送员提成(ticketOrderId 关联送水订单)从进入 `freeze_money` 改为直接进入 `money`(可提现余额)。修改了 2 处 `LambdaUpdateWrapper` SQL(`freeze_money` → `money`),注释同步更新。`total_money` 不变(仍累计)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分销佣金解冻任务分析(DealerCommissionUnfreeze10584Task)
|
||||||
|
|
||||||
|
**订单号**:2038841514750459904
|
||||||
|
|
||||||
|
### 解冻规则
|
||||||
|
- **送水套餐**(shop_order.form_id IN 水票模板的 goods_id):该订单关联的水票第一条送水订单 deliveryStatus=40(已完成)才触发解冻
|
||||||
|
- **非送水套餐**(form_id 不在水票模板中):shop_order.order_status=1 即触发解冻
|
||||||
|
|
||||||
|
### "已送达"≠"已完成"的关键区别
|
||||||
|
- deliveryStatus=30(送达待确认):配送员拍照确认送达,此时**不触发解冻**
|
||||||
|
- deliveryStatus=40(已完成):需用户手动确认收货 OR 超时24h自动确认后才到达此状态
|
||||||
|
|
||||||
|
### 常见未解冻原因(按排查优先级)
|
||||||
|
1. 送水订单停在 deliveryStatus=30(送达待确认),未到 40(已完成)
|
||||||
|
2. shop_order.form_id 在水票模板里,走的是"送水套餐"逻辑,但没有找到对应的 glt_user_ticket 记录
|
||||||
|
3. glt_user_ticket 记录缺失或 order_no 字段为空
|
||||||
|
4. 已有 flowType=50 的解冻 marker(说明已解冻)
|
||||||
|
|
||||||
|
### 次要原因
|
||||||
|
|
||||||
|
`RSAAutoCertificateConfig` 每次回调都重新 `build()`,SDK 内部会发一次 `serial=test` 的探测验签,网络问题或并发场景下可能导致首次回调失败。**已优化**:添加 `notifyConfigCache`(ConcurrentHashMap)按 mchId 缓存 config,避免重复初始化。
|
||||||
13
.workbuddy/memory/2026-04-18.md
Normal file
13
.workbuddy/memory/2026-04-18.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 2026-04-18 工作日志
|
||||||
|
|
||||||
|
## 排查解冻任务未触发问题
|
||||||
|
- 用户反馈:GltTicketOrder订单已完成配送,但部分订单未触发解冻(freezeMoney未转到money)
|
||||||
|
- 完整梳理了资金流转链路:结算→冻结→解冻
|
||||||
|
- 识别出5个可能原因:
|
||||||
|
1. GltTicketOrder.userTicketId为NULL(解冻任务硬性过滤条件)
|
||||||
|
2. GltUserTicket.orderNo缺失导致关联断裂
|
||||||
|
3. isFirstTicketOrderFinished()"第一条"逻辑阻断后续订单解冻
|
||||||
|
4. loadWaterFormIds()返回空集导致整个解冻任务跳过
|
||||||
|
5. 配送员提成orderNo格式不匹配(非bug,配送员提成本身不经过冻结)
|
||||||
|
- 提供了5条排查SQL和修复建议
|
||||||
|
- 关键文件:DealerCommissionUnfreeze10584Task.java、GltTicketOrderServiceImpl.java
|
||||||
32
.workbuddy/memory/MEMORY.md
Normal file
32
.workbuddy/memory/MEMORY.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# MEMORY.md - 长期记忆
|
||||||
|
|
||||||
|
## 项目概况
|
||||||
|
- 后端:/Users/gxwebsoft/JAVA/java-10584(Spring Boot + MyBatis-Plus)
|
||||||
|
- 后台管理:/Users/gxwebsoft/VUE/mp-10584
|
||||||
|
- 小程序端:/Users/gxwebsoft/VUE/template-10584
|
||||||
|
- 多租户架构(tenantId 隔离),主力租户 10584
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
- 后端:Spring Boot + MyBatis-Plus + FastJSON 2.x
|
||||||
|
- 前端:Nuxt/Vue3 + TypeScript + Ant Design Vue4 + Tailwind
|
||||||
|
- 小程序:Uni-app/Taro
|
||||||
|
- 开发环境:Mac + Node.js v22 + JetBrains + Docker + pnpm
|
||||||
|
|
||||||
|
## 业务规则备忘
|
||||||
|
|
||||||
|
### 分销佣金解冻规则(10584)
|
||||||
|
- 结算:DealerOrderSettlement10584Task 每10秒,佣金先入 freezeMoney
|
||||||
|
- 解冻:DealerCommissionUnfreeze10584Task 每20秒,freezeMoney→money
|
||||||
|
- 送水套餐解冻条件:同一userTicketId下第一条送水订单deliveryStatus=40
|
||||||
|
- 非送水套餐解冻条件:ShopOrder.orderStatus=1 且 payStatus=true
|
||||||
|
- 幂等标记:ShopDealerCapital(flowType=50, comments="佣金解冻(capitalId=xxx)")
|
||||||
|
- 配送员提成:直接入money(不经过冻结),orderNo格式="gltTicketOrder:"+id
|
||||||
|
|
||||||
|
### 送水订单状态流转
|
||||||
|
- 10(待配送)→20(配送中)→30(待客户确认)→40(已完成)
|
||||||
|
- delivered()配送员确认送达时就会同步ShopOrder.orderStatus=1
|
||||||
|
- confirmReceive()/autoConfirmTimeout()也会同步
|
||||||
|
|
||||||
|
### 已知排查问题
|
||||||
|
- 解冻任务可能因 userTicketId为空、GltUserTicket.orderNo缺失、"第一条未完成"阻断等原因未触发
|
||||||
|
- 解冻任务依赖 loadWaterFormIds() 不为空,否则整个任务跳过
|
||||||
5
.workbuddy/settings.local.json
Normal file
5
.workbuddy/settings.local.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"modern-webapp@cb_teams_marketplace": true
|
||||||
|
}
|
||||||
|
}
|
||||||
4
pom.xml
4
pom.xml
@@ -4,10 +4,10 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>com.gxwebsoft</groupId>
|
<groupId>com.gxwebsoft</groupId>
|
||||||
<artifactId>glt-api</artifactId>
|
<artifactId>mp-api</artifactId>
|
||||||
<version>1.0</version>
|
<version>1.0</version>
|
||||||
|
|
||||||
<name>glt-api</name>
|
<name>mp-api</name>
|
||||||
<description>WebSoftApi project for Spring Boot</description>
|
<description>WebSoftApi project for Spring Boot</description>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
|
|||||||
7
sql/glt_ticket_order_delivery_fields.sql
Normal file
7
sql/glt_ticket_order_delivery_fields.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- 配送方式、楼层、配送费字段
|
||||||
|
-- 对应需求:送水订单下单时选择配送方式(电梯/步梯/一楼商铺),步梯送上楼需选楼层,配送费 = 数量 × (楼层-1)
|
||||||
|
|
||||||
|
ALTER TABLE glt_ticket_order
|
||||||
|
ADD COLUMN delivery_method VARCHAR(32) DEFAULT NULL COMMENT '配送方式:elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)' AFTER buyer_remarks,
|
||||||
|
ADD COLUMN delivery_floor INT DEFAULT NULL COMMENT '楼层(步梯+送上楼时有值,从2开始)' AFTER delivery_method,
|
||||||
|
ADD COLUMN delivery_fee DECIMAL(10,2) DEFAULT NULL COMMENT '配送费(数量 × (楼层-1))' AFTER delivery_floor;
|
||||||
@@ -58,11 +58,11 @@ public class LoginRecord implements Serializable {
|
|||||||
private Integer tenantId;
|
private Integer tenantId;
|
||||||
|
|
||||||
@Schema(description = "操作时间")
|
@Schema(description = "操作时间")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
@Schema(description = "修改时间")
|
@Schema(description = "修改时间")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
@Schema(description = "用户id")
|
@Schema(description = "用户id")
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.gxwebsoft.glt.controller;
|
package com.gxwebsoft.glt.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||||
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
import com.gxwebsoft.common.core.web.ApiResult;
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
import com.gxwebsoft.common.core.web.BaseController;
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
import com.gxwebsoft.common.core.web.BatchParam;
|
import com.gxwebsoft.common.core.web.BatchParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
|
import com.gxwebsoft.common.system.mapper.UserMapper;
|
||||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
import com.gxwebsoft.glt.param.GltTicketOrderDeliveredParam;
|
import com.gxwebsoft.glt.param.GltTicketOrderDeliveredParam;
|
||||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||||
|
import com.gxwebsoft.glt.service.GltSubscribeMessageService;
|
||||||
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||||
import com.gxwebsoft.shop.entity.ShopStoreRider;
|
import com.gxwebsoft.shop.entity.ShopStoreRider;
|
||||||
import com.gxwebsoft.shop.entity.ShopUserAddress;
|
import com.gxwebsoft.shop.entity.ShopUserAddress;
|
||||||
@@ -19,12 +22,13 @@ import com.gxwebsoft.shop.service.ShopUserAddressService;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 送水订单控制器
|
* 送水订单控制器
|
||||||
@@ -32,6 +36,7 @@ import java.util.List;
|
|||||||
* @author 科技小王子
|
* @author 科技小王子
|
||||||
* @since 2026-02-05 18:50:21
|
* @since 2026-02-05 18:50:21
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Tag(name = "送水订单管理")
|
@Tag(name = "送水订单管理")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/glt/glt-ticket-order")
|
@RequestMapping("/api/glt/glt-ticket-order")
|
||||||
@@ -44,6 +49,10 @@ public class GltTicketOrderController extends BaseController {
|
|||||||
private ShopStoreFenceService shopStoreFenceService;
|
private ShopStoreFenceService shopStoreFenceService;
|
||||||
@Resource
|
@Resource
|
||||||
private ShopStoreRiderService shopStoreRiderService;
|
private ShopStoreRiderService shopStoreRiderService;
|
||||||
|
@Resource
|
||||||
|
private GltSubscribeMessageService gltSubscribeMessageService;
|
||||||
|
@Resource
|
||||||
|
private UserMapper userMapper;
|
||||||
|
|
||||||
@Operation(summary = "分页查询送水订单")
|
@Operation(summary = "分页查询送水订单")
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@@ -167,9 +176,76 @@ public class GltTicketOrderController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId());
|
gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId());
|
||||||
|
|
||||||
|
// 订单创建成功后,异步通知所有在线配送员有新订单
|
||||||
|
try {
|
||||||
|
notifyRidersOfNewOrder(gltTicketOrder, loginUser.getTenantId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("通知配送员失败(不影响下单): {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
return success("下单成功");
|
return success("下单成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知所有在线配送员有新订单
|
||||||
|
*/
|
||||||
|
private void notifyRidersOfNewOrder(GltTicketOrder order, Integer tenantId) {
|
||||||
|
if (order == null || tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询所有启用且在线的配送员
|
||||||
|
List<ShopStoreRider> onlineRiders = shopStoreRiderService.list(
|
||||||
|
new LambdaQueryWrapper<ShopStoreRider>()
|
||||||
|
.eq(ShopStoreRider::getTenantId, tenantId)
|
||||||
|
.eq(ShopStoreRider::getIsDelete, 0)
|
||||||
|
.eq(ShopStoreRider::getStatus, 1)
|
||||||
|
.eq(ShopStoreRider::getWorkStatus, 1) // 在线状态
|
||||||
|
.or()
|
||||||
|
.eq(ShopStoreRider::getTenantId, tenantId)
|
||||||
|
.eq(ShopStoreRider::getIsDelete, 0)
|
||||||
|
.eq(ShopStoreRider::getStatus, 1)
|
||||||
|
.isNull(ShopStoreRider::getWorkStatus) // 兼容未设置状态的配送员
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onlineRiders == null || onlineRiders.isEmpty()) {
|
||||||
|
log.info("当前无在线配送员,无需发送订阅消息");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配送员的 userId 列表
|
||||||
|
List<Integer> riderUserIds = onlineRiders.stream()
|
||||||
|
.map(ShopStoreRider::getUserId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (riderUserIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量查询配送员的 openId
|
||||||
|
List<User> riders = userMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<User>()
|
||||||
|
.select(User::getUserId, User::getOpenid)
|
||||||
|
.in(User::getUserId, riderUserIds)
|
||||||
|
.isNotNull(User::getOpenid)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送订阅消息
|
||||||
|
for (User rider : riders) {
|
||||||
|
if (StrUtil.isNotBlank(rider.getOpenid())) {
|
||||||
|
try {
|
||||||
|
gltSubscribeMessageService.sendNewOrderNotice(order, rider.getOpenid(), tenantId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("发送订阅消息给配送员失败 - userId={}, error={}", rider.getUserId(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("已向 {} 位配送员发送新订单通知", riders.size());
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
@Operation(summary = "配送员接单")
|
@Operation(summary = "配送员接单")
|
||||||
@PostMapping("/{id}/accept")
|
@PostMapping("/{id}/accept")
|
||||||
|
|||||||
@@ -191,6 +191,15 @@ public class GltTicketOrder implements Serializable {
|
|||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String warehouseLngAndLat;
|
private String warehouseLngAndLat;
|
||||||
|
|
||||||
|
@Schema(description = "配送方式:elevator(电梯) / stairs(步梯) / groundFloor(一楼商铺/其他)")
|
||||||
|
private String deliveryMethod;
|
||||||
|
|
||||||
|
@Schema(description = "楼层(步梯+送上楼时有值,从2开始)")
|
||||||
|
private Integer deliveryFloor;
|
||||||
|
|
||||||
|
@Schema(description = "配送费(步梯+送上楼时计算:数量 × (楼层-1))")
|
||||||
|
private BigDecimal deliveryFee;
|
||||||
|
|
||||||
@Schema(description = "排序(数字越小越靠前)")
|
@Schema(description = "排序(数字越小越靠前)")
|
||||||
private Integer sortNumber;
|
private Integer sortNumber;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.gxwebsoft.glt.service;
|
||||||
|
|
||||||
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息服务接口
|
||||||
|
*/
|
||||||
|
public interface GltSubscribeMessageService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送新订单通知给配送员
|
||||||
|
* @param order 订单信息
|
||||||
|
* @param riderOpenId 配送员微信openId
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return 是否发送成功
|
||||||
|
*/
|
||||||
|
boolean sendNewOrderNotice(GltTicketOrder order, String riderOpenId, Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送订单状态变更通知
|
||||||
|
* @param order 订单信息
|
||||||
|
* @param riderOpenId 配送员微信openId
|
||||||
|
* @param statusText 状态描述
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return 是否发送成功
|
||||||
|
*/
|
||||||
|
boolean sendOrderStatusNotice(GltTicketOrder order, String riderOpenId, String statusText, Integer tenantId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package com.gxwebsoft.glt.service.impl;
|
||||||
|
|
||||||
|
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||||
|
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
|
||||||
|
import cn.binarywang.wx.miniapp.config.WxMaConfig;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
|
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||||
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
|
import com.gxwebsoft.glt.service.GltSubscribeMessageService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息服务实现
|
||||||
|
*
|
||||||
|
* <p>功能:
|
||||||
|
* <ul>
|
||||||
|
* <li>新订单通知配送员</li>
|
||||||
|
* <li>订单状态变更通知</li>
|
||||||
|
* </ul>
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class GltSubscribeMessageServiceImpl implements GltSubscribeMessageService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅消息模板ID(需在微信公众平台配置)
|
||||||
|
* 模板名称:订单配送通知
|
||||||
|
* 关键词:订单编号、订单内容、配送地址、订单金额
|
||||||
|
*/
|
||||||
|
private static final String SUBSCRIBE_TEMPLATE_ID = "YOUR_TEMPLATE_ID"; // TODO: 替换为实际模板ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送新订单通知给配送员
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean sendNewOrderNotice(GltTicketOrder order, String riderOpenId, Integer tenantId) {
|
||||||
|
if (order == null || StrUtil.isBlank(riderOpenId) || tenantId == null) {
|
||||||
|
log.warn("发送订阅消息参数不完整");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String accessToken = getAccessToken(tenantId);
|
||||||
|
if (StrUtil.isBlank(accessToken)) {
|
||||||
|
log.warn("获取access_token失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建消息内容
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("phrase1", Map.of("value", "待配送")); // 订单状态
|
||||||
|
data.put("character_string2", Map.of("value", String.valueOf(order.getId()))); // 订单编号
|
||||||
|
data.put("thing3", Map.of("value", truncateStr(order.getAddress(), 20))); // 配送地址
|
||||||
|
data.put("number4", Map.of("value", String.valueOf(order.getTotalNum()))); // 商品数量
|
||||||
|
data.put("time5", Map.of("value", formatTime(order.getSendTime()))); // 期望送达时间
|
||||||
|
|
||||||
|
// 发送订阅消息
|
||||||
|
return sendSubscribeMessage(accessToken, riderOpenId, data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送新订单订阅消息失败 - orderId={}, riderOpenId={}, error={}",
|
||||||
|
order.getId(), riderOpenId, e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送订单状态变更通知
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean sendOrderStatusNotice(GltTicketOrder order, String riderOpenId, String statusText, Integer tenantId) {
|
||||||
|
if (order == null || StrUtil.isBlank(riderOpenId) || tenantId == null) {
|
||||||
|
log.warn("发送订阅消息参数不完整");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String accessToken = getAccessToken(tenantId);
|
||||||
|
if (StrUtil.isBlank(accessToken)) {
|
||||||
|
log.warn("获取access_token失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建消息内容
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("phrase1", Map.of("value", truncateStr(statusText, 5))); // 状态描述
|
||||||
|
data.put("character_string2", Map.of("value", String.valueOf(order.getId()))); // 订单编号
|
||||||
|
data.put("time3", Map.of("value", formatTime(null))); // 通知时间
|
||||||
|
|
||||||
|
// 发送订阅消息
|
||||||
|
return sendSubscribeMessage(accessToken, riderOpenId, data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("发送订单状态变更订阅消息失败 - orderId={}, riderOpenId={}, error={}",
|
||||||
|
order.getId(), riderOpenId, e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取小程序的 access_token
|
||||||
|
*/
|
||||||
|
private String getAccessToken(Integer tenantId) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new BusinessException("tenantId 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String tokenCacheKey = ACCESS_TOKEN_KEY + ":" + tenantId;
|
||||||
|
|
||||||
|
// 1) 优先从缓存取
|
||||||
|
String cachedValue = stringRedisTemplate.opsForValue().get(tokenCacheKey);
|
||||||
|
if (StrUtil.isNotBlank(cachedValue)) {
|
||||||
|
try {
|
||||||
|
JSONObject cachedJson = JSON.parseObject(cachedValue);
|
||||||
|
String accessToken = cachedJson.getString("access_token");
|
||||||
|
if (StrUtil.isNotBlank(accessToken)) {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// 旧格式:直接存 token
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 缓存没有则从租户配置获取 appId/appSecret
|
||||||
|
final String wxConfigKey = MP_WX_KEY + tenantId;
|
||||||
|
final String wxConfigValue = stringRedisTemplate.opsForValue().get(wxConfigKey);
|
||||||
|
if (StrUtil.isBlank(wxConfigValue)) {
|
||||||
|
log.warn("未找到微信小程序配置,请检查缓存key: {}", wxConfigKey);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject wxConfig;
|
||||||
|
try {
|
||||||
|
wxConfig = JSON.parseObject(wxConfigValue);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("微信小程序配置格式错误: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String appId = wxConfig.getString("appId");
|
||||||
|
final String appSecret = wxConfig.getString("appSecret");
|
||||||
|
if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
|
||||||
|
log.error("微信小程序配置不完整(appId/appSecret)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 调用微信接口获取 token
|
||||||
|
final String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
|
||||||
|
+ "&appid=" + appId + "&secret=" + appSecret;
|
||||||
|
String result = HttpUtil.get(apiUrl);
|
||||||
|
|
||||||
|
JSONObject json = JSON.parseObject(result);
|
||||||
|
if (json.containsKey("errcode") && json.getIntValue("errcode") != 0) {
|
||||||
|
log.error("获取小程序access_token失败: {}", json.getString("errmsg"));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String accessToken = json.getString("access_token");
|
||||||
|
Integer expiresIn = json.getInteger("expires_in");
|
||||||
|
if (StrUtil.isBlank(accessToken)) {
|
||||||
|
log.error("获取小程序access_token失败: access_token为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 缓存,提前5分钟过期
|
||||||
|
long ttlSeconds = 7000L;
|
||||||
|
if (expiresIn != null && expiresIn > 300) {
|
||||||
|
ttlSeconds = expiresIn - 300L;
|
||||||
|
}
|
||||||
|
stringRedisTemplate.opsForValue().set(tokenCacheKey, result, ttlSeconds, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
log.info("获取小程序access_token成功 - tenantId={}, ttlSeconds={}", tenantId, ttlSeconds);
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送订阅消息
|
||||||
|
*/
|
||||||
|
private boolean sendSubscribeMessage(String accessToken, String openId, Map<String, Object> data) {
|
||||||
|
String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken;
|
||||||
|
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("touser", openId); // 用户 openid
|
||||||
|
params.put("template_id", SUBSCRIBE_TEMPLATE_ID); // 模板ID
|
||||||
|
params.put("page", "pages/rider/orders/index"); // 点击后跳转的页面
|
||||||
|
params.put("data", data);
|
||||||
|
|
||||||
|
String response = HttpUtil.createPost(url)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body(JSON.toJSONString(params))
|
||||||
|
.timeout(10000)
|
||||||
|
.execute()
|
||||||
|
.body();
|
||||||
|
|
||||||
|
JSONObject result = JSON.parseObject(response);
|
||||||
|
int errcode = result.getIntValue("errcode");
|
||||||
|
|
||||||
|
if (errcode == 0) {
|
||||||
|
log.info("订阅消息发送成功 - openId={}", openId);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn("订阅消息发送失败 - openId={}, errcode={}, errmsg={}",
|
||||||
|
openId, errcode, result.getString("errmsg"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截断字符串
|
||||||
|
*/
|
||||||
|
private String truncateStr(String str, int maxLen) {
|
||||||
|
if (str == null) return "";
|
||||||
|
return str.length() > maxLen ? str.substring(0, maxLen) : str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间
|
||||||
|
*/
|
||||||
|
private String formatTime(String timeStr) {
|
||||||
|
if (StrUtil.isBlank(timeStr)) {
|
||||||
|
return cn.hutool.core.date.DateUtil.now();
|
||||||
|
}
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -845,13 +845,13 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 送水订单提成:先入冻结金额 freeze_money(与分销订单佣金一致)
|
// 送水订单提成:直接入账可提现余额 money(配送员提成即时可用,无需冻结)
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
boolean updated = shopDealerUserService.update(
|
boolean updated = shopDealerUserService.update(
|
||||||
new LambdaUpdateWrapper<ShopDealerUser>()
|
new LambdaUpdateWrapper<ShopDealerUser>()
|
||||||
.eq(ShopDealerUser::getTenantId, tenantId)
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
.eq(ShopDealerUser::getUserId, riderId)
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString())
|
.setSql("money = IFNULL(money,0) + " + money.toPlainString())
|
||||||
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
||||||
.set(ShopDealerUser::getUpdateTime, now)
|
.set(ShopDealerUser::getUpdateTime, now)
|
||||||
);
|
);
|
||||||
@@ -895,7 +895,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
|
|||||||
new LambdaUpdateWrapper<ShopDealerUser>()
|
new LambdaUpdateWrapper<ShopDealerUser>()
|
||||||
.eq(ShopDealerUser::getTenantId, tenantId)
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
.eq(ShopDealerUser::getUserId, riderId)
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString())
|
.setSql("money = IFNULL(money,0) + " + money.toPlainString())
|
||||||
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
||||||
.set(ShopDealerUser::getUpdateTime, now)
|
.set(ShopDealerUser::getUpdateTime, now)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import java.math.RoundingMode;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -48,7 +49,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
* <p>1) 送水套餐(formId in 水票模板 goodsId):订单号关联的水票产生了第一次送水订单,且该第一次送水订单状态=已完成(40) -> 解冻。</p>
|
* <p>1) 送水套餐(formId in 水票模板 goodsId):订单号关联的水票产生了第一次送水订单,且该第一次送水订单状态=已完成(40) -> 解冻。</p>
|
||||||
* <p>2) 非送水套餐(formId not in 水票模板 goodsId):订单已确认收货(orderStatus=1) -> 解冻。</p>
|
* <p>2) 非送水套餐(formId not in 水票模板 goodsId):订单已确认收货(orderStatus=1) -> 解冻。</p>
|
||||||
*
|
*
|
||||||
* <p>实现策略:以 ShopDealerCapital(flowType=10) 的“佣金明细”为解冻粒度,
|
* <p>实现策略:以 ShopDealerCapital(flowType=10) 的"佣金明细"为解冻粒度,
|
||||||
* 每条佣金明细对应生成一条 ShopDealerCapital(flowType=50) 作为幂等标记,并执行
|
* 每条佣金明细对应生成一条 ShopDealerCapital(flowType=50) 作为幂等标记,并执行
|
||||||
* ShopDealerUser.freezeMoney -> ShopDealerUser.money 的转移。</p>
|
* ShopDealerUser.freezeMoney -> ShopDealerUser.money 的转移。</p>
|
||||||
*/
|
*/
|
||||||
@@ -76,7 +77,7 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
if (rawRate == null || rawRate.signum() <= 0) {
|
if (rawRate == null || rawRate.signum() <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 如果录入 >= 1,按“百分比”处理(1 => 1%)
|
// 如果录入 >= 1,按"百分比"处理(1 => 1%)
|
||||||
if (rawRate.compareTo(BigDecimal.ONE) >= 0) {
|
if (rawRate.compareTo(BigDecimal.ONE) >= 0) {
|
||||||
return rawRate.movePointLeft(2);
|
return rawRate.movePointLeft(2);
|
||||||
}
|
}
|
||||||
@@ -115,7 +116,7 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
|
|
||||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
|
||||||
@Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/20 * * * * ?}")
|
@Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/50 * * * * ?}")
|
||||||
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
|
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
|
||||||
public void run() {
|
public void run() {
|
||||||
if (!running.compareAndSet(false, true)) {
|
if (!running.compareAndSet(false, true)) {
|
||||||
@@ -124,46 +125,77 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// ========== 步骤1: 加载水票模板 ==========
|
||||||
Set<Integer> waterFormIds = loadWaterFormIds();
|
Set<Integer> waterFormIds = loadWaterFormIds();
|
||||||
|
log.info("【步骤1】加载水票模板 - tenantId={}, waterFormIds={}", TENANT_ID, waterFormIds);
|
||||||
if (waterFormIds.isEmpty()) {
|
if (waterFormIds.isEmpty()) {
|
||||||
// 送水/非送水的判断依赖模板 goodsId;拿不到会导致误判,宁可跳过本轮。
|
// 送水/非送水的判断依赖模板 goodsId;拿不到会导致误判,宁可跳过本轮。
|
||||||
log.warn("分销佣金解冻任务跳过:未找到水票模板 goodsId - tenantId={}", TENANT_ID);
|
log.warn("分销佣金解冻任务跳过:未找到水票模板 goodsId - tenantId={}", TENANT_ID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先按“最近确认收货”的订单扫描,避免总是卡在很早的历史订单上。
|
// ========== 步骤2: 扫描非送水订单(优先最新) ==========
|
||||||
Set<String> eligibleOrderNos = new HashSet<>();
|
Set<String> eligibleOrderNos = new HashSet<>();
|
||||||
eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, true));
|
List<String> nonWaterOrders = findEligibleNonWaterOrderNos(waterFormIds, true);
|
||||||
eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds));
|
log.info("【步骤2】扫描非送水订单(最新优先)- tenantId={}, count={}, orderNos={}", TENANT_ID, nonWaterOrders.size(), nonWaterOrders.size() <= 20 ? nonWaterOrders : nonWaterOrders.subList(0, 20));
|
||||||
|
eligibleOrderNos.addAll(nonWaterOrders);
|
||||||
|
|
||||||
|
// ========== 步骤3: 扫描送水订单(第一条送水完成) ==========
|
||||||
|
Set<String> waterOrders = findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds);
|
||||||
|
log.info("【步骤3】扫描送水订单(第一条送水完成)- tenantId={}, count={}, orderNos={}", TENANT_ID, waterOrders.size(), waterOrders);
|
||||||
|
eligibleOrderNos.addAll(waterOrders);
|
||||||
|
|
||||||
if (eligibleOrderNos.isEmpty()) {
|
if (eligibleOrderNos.isEmpty()) {
|
||||||
|
log.info("【步骤4-9】无可处理订单,本轮结束 - tenantId={}", TENANT_ID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 订单太多时不打印完整列表
|
||||||
|
String orderNosSummary = eligibleOrderNos.size() <= 30 ? eligibleOrderNos.toString() : eligibleOrderNos.size() + " orders (too many to show)";
|
||||||
|
log.info("【步骤4】汇总待处理订单 - tenantId={}, totalCount={}, orderNos={}", TENANT_ID, eligibleOrderNos.size(), orderNosSummary);
|
||||||
|
|
||||||
// 配送奖励(与佣金解冻独立):按订单发放,幂等保证不会重复入账
|
// ========== 步骤5: 发放配送奖励 ==========
|
||||||
|
log.info("【步骤5】开始发放配送奖励 - tenantId={}, orderCount={}", TENANT_ID, eligibleOrderNos.size());
|
||||||
int rewarded = 0;
|
int rewarded = 0;
|
||||||
|
List<String> rewardedOrders = new ArrayList<>();
|
||||||
for (String orderNo : eligibleOrderNos) {
|
for (String orderNo : eligibleOrderNos) {
|
||||||
try {
|
try {
|
||||||
if (settleDeliveryRewardIfNeeded(orderNo)) {
|
if (settleDeliveryRewardIfNeeded(orderNo)) {
|
||||||
rewarded++;
|
rewarded++;
|
||||||
|
rewardedOrders.add(orderNo);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
|
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("【步骤5】配送奖励发放完成 - tenantId={}, rewardedCount={}, rewardedOrders={}", TENANT_ID, rewarded, rewardedOrders);
|
||||||
|
|
||||||
|
// ========== 步骤6: 查询佣金明细 ==========
|
||||||
|
log.info("【步骤6】查询佣金明细(flowType=10)- tenantId={}, orderCount={}", TENANT_ID, eligibleOrderNos.size());
|
||||||
List<ShopDealerCapital> capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
|
List<ShopDealerCapital> capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
|
||||||
|
String capitalsSummary = capitals.size() <= 50
|
||||||
|
? capitals.stream().map(c -> "capitalId=" + c.getId() + ", orderNo=" + c.getOrderNo() + ", amount=" + c.getMoney()).toList().toString()
|
||||||
|
: capitals.size() + " capitals (too many to show)";
|
||||||
|
log.info("【步骤6】查询到佣金明细 - tenantId={}, count={}, capitals={}", TENANT_ID, capitals.size(), capitalsSummary);
|
||||||
|
|
||||||
if (capitals.isEmpty()) {
|
if (capitals.isEmpty()) {
|
||||||
// 若本轮没有取到佣金明细,回退再按“最早确认收货”的订单扫一轮,尽量覆盖历史遗留未解冻。
|
// ========== 步骤6.1: 兜底扫描历史订单 ==========
|
||||||
|
log.info("【步骤6.1】本轮未取到佣金明细,执行兜底扫描(最早确认收货) - tenantId={}", TENANT_ID);
|
||||||
eligibleOrderNos.clear();
|
eligibleOrderNos.clear();
|
||||||
eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, false));
|
List<String> fallbackNonWater = findEligibleNonWaterOrderNos(waterFormIds, false);
|
||||||
eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds));
|
log.info("【步骤6.1】兜底-非送水订单 - tenantId={}, count={}", TENANT_ID, fallbackNonWater.size());
|
||||||
|
eligibleOrderNos.addAll(fallbackNonWater);
|
||||||
|
|
||||||
|
Set<String> fallbackWater = findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds);
|
||||||
|
log.info("【步骤6.1】兜底-送水订单 - tenantId={}, count={}", TENANT_ID, fallbackWater.size());
|
||||||
|
eligibleOrderNos.addAll(fallbackWater);
|
||||||
|
|
||||||
// 兜底扫描出来的订单也补发配送奖励(幂等)
|
// 兜底扫描出来的订单也补发配送奖励(幂等)
|
||||||
|
log.info("【步骤6.1】兜底扫描后补发配送奖励 - tenantId={}, orderCount={}", TENANT_ID, eligibleOrderNos.size());
|
||||||
for (String orderNo : eligibleOrderNos) {
|
for (String orderNo : eligibleOrderNos) {
|
||||||
try {
|
try {
|
||||||
if (settleDeliveryRewardIfNeeded(orderNo)) {
|
if (settleDeliveryRewardIfNeeded(orderNo)) {
|
||||||
rewarded++;
|
rewarded++;
|
||||||
|
rewardedOrders.add(orderNo + "(兜底)");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
|
log.error("发放配送奖励失败,将在下次任务重试 - tenantId={}, orderNo={}", TENANT_ID, orderNo, e);
|
||||||
@@ -171,32 +203,41 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
|
capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
|
||||||
|
log.info("【步骤6.1】兜底扫描到佣金明细 - tenantId={}, count={}", TENANT_ID, capitals.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capitals.isEmpty()) {
|
if (capitals.isEmpty()) {
|
||||||
|
log.info("【步骤7-9】仍未查到佣金明细,本轮结束 - tenantId={}", TENANT_ID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 步骤7: 执行佣金解冻 ==========
|
||||||
|
log.info("【步骤7】开始执行佣金解冻 - tenantId={}, totalCount={}", TENANT_ID, capitals.size());
|
||||||
int unfrozen = 0;
|
int unfrozen = 0;
|
||||||
|
List<String> unfrozenDetails = new ArrayList<>();
|
||||||
for (ShopDealerCapital cap : capitals) {
|
for (ShopDealerCapital cap : capitals) {
|
||||||
try {
|
try {
|
||||||
boolean ok = unfreezeOneCapitalIfNeeded(cap);
|
boolean ok = unfreezeOneCapitalIfNeeded(cap);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
unfrozen++;
|
unfrozen++;
|
||||||
|
unfrozenDetails.add("capitalId=" + cap.getId() + ", orderNo=" + cap.getOrderNo() + ", amount=" + cap.getMoney());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解冻佣金失败,将在下次任务重试 - tenantId={}, capitalId={}, orderNo={}, userId={}",
|
log.error("解冻佣金失败,将在下次任务重试 - tenantId={}, capitalId={}, orderNo={}, userId={}",
|
||||||
TENANT_ID, cap != null ? cap.getId() : null, cap != null ? cap.getOrderNo() : null, cap != null ? cap.getUserId() : null, e);
|
TENANT_ID, cap != null ? cap.getId() : null, cap != null ? cap.getOrderNo() : null, cap != null ? cap.getUserId() : null, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("【步骤7】佣金解冻完成 - tenantId={}, unfrozenDetails={}", TENANT_ID, unfrozenDetails);
|
||||||
|
|
||||||
if (unfrozen > 0) {
|
// ========== 步骤8: 更新分销订单状态 ==========
|
||||||
log.info("分销佣金解冻完成 - tenantId={}, eligibleOrderNos={}, scannedCapitals={}, unfrozen={}",
|
log.info("【步骤8】检查并更新分销订单状态(isUnfreeze=1)- tenantId={}, unfrozenCount={}", TENANT_ID, unfrozen);
|
||||||
TENANT_ID, eligibleOrderNos.size(), capitals.size(), unfrozen);
|
|
||||||
}
|
// ========== 步骤9: 汇总报告 ==========
|
||||||
if (rewarded > 0) {
|
log.info("========================================");
|
||||||
log.info("配送奖励发放完成 - tenantId={}, eligibleOrderNos={}, rewarded={}", TENANT_ID, eligibleOrderNos.size(), rewarded);
|
log.info("【步骤9】本轮任务执行完毕 - tenantId={}", TENANT_ID);
|
||||||
}
|
log.info(" - 配送奖励: rewarded={}, orders={}", rewarded, rewardedOrders);
|
||||||
|
log.info(" - 佣金解冻: unfrozen={}, details={}", unfrozen, unfrozenDetails);
|
||||||
|
log.info("========================================");
|
||||||
} finally {
|
} finally {
|
||||||
running.set(false);
|
running.set(false);
|
||||||
}
|
}
|
||||||
@@ -204,8 +245,10 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
|
|
||||||
private boolean settleDeliveryRewardIfNeeded(String orderNo) {
|
private boolean settleDeliveryRewardIfNeeded(String orderNo) {
|
||||||
if (orderNo == null || orderNo.isBlank()) {
|
if (orderNo == null || orderNo.isBlank()) {
|
||||||
|
log.debug("【步骤5.X】配送奖励跳过:orderNo为空 - tenantId={}", TENANT_ID);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
log.debug("【步骤5.X】开始处理配送奖励 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
|
|
||||||
ShopOrder order = shopOrderService.getOne(
|
ShopOrder order = shopOrderService.getOne(
|
||||||
new LambdaQueryWrapper<ShopOrder>()
|
new LambdaQueryWrapper<ShopOrder>()
|
||||||
@@ -215,13 +258,16 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
.last("limit 1")
|
.last("limit 1")
|
||||||
);
|
);
|
||||||
if (order == null) {
|
if (order == null) {
|
||||||
|
log.debug("【步骤5.X】配送奖励跳过:订单不存在 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer riderId = order.getRiderId();
|
Integer riderId = order.getRiderId();
|
||||||
if (riderId == null || riderId <= 0) {
|
if (riderId == null || riderId <= 0) {
|
||||||
|
log.debug("【步骤5.X】配送奖励跳过:无配送员 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
log.debug("【步骤5.X】找到配送员 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
|
||||||
|
|
||||||
// 快速幂等检查:已发放则跳过(事务内仍会二次校验避免并发重复)
|
// 快速幂等检查:已发放则跳过(事务内仍会二次校验避免并发重复)
|
||||||
boolean already = shopDealerCapitalService.count(
|
boolean already = shopDealerCapitalService.count(
|
||||||
@@ -231,147 +277,162 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
) > 0;
|
) > 0;
|
||||||
if (already) {
|
if (already) {
|
||||||
|
log.debug("【步骤5.X】配送奖励跳过:已发放(幂等)- tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
log.debug("【步骤5.X】进入事务 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
|
|
||||||
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
try {
|
||||||
LocalDateTime now = LocalDateTime.now();
|
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
log.debug("【步骤5.X】事务内开始 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
|
|
||||||
// 锁定配送员资金明细 marker,确保并发幂等
|
// 锁定配送员资金明细 marker,确保并发幂等
|
||||||
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
|
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
|
||||||
new LambdaQueryWrapper<ShopDealerCapital>()
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
|
||||||
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
.last("limit 1 for update")
|
.last("limit 1 for update")
|
||||||
);
|
);
|
||||||
if (existedMarker != null) {
|
if (existedMarker != null) {
|
||||||
return false;
|
log.debug("【步骤5.X】配送奖励跳过:事务内检测已存在 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
}
|
return false;
|
||||||
|
|
||||||
Integer orderId = order.getOrderId();
|
|
||||||
if (orderId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.list(
|
|
||||||
new LambdaQueryWrapper<ShopOrderGoods>()
|
|
||||||
.eq(ShopOrderGoods::getTenantId, TENANT_ID)
|
|
||||||
.eq(ShopOrderGoods::getOrderId, orderId)
|
|
||||||
);
|
|
||||||
if (orderGoodsList == null || orderGoodsList.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Integer> goodsIds = orderGoodsList.stream()
|
|
||||||
.map(ShopOrderGoods::getGoodsId)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.distinct()
|
|
||||||
.toList();
|
|
||||||
if (goodsIds.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<Integer, BigDecimal> goodsDeliveryMoneyMap = shopGoodsService.list(
|
|
||||||
new LambdaQueryWrapper<ShopGoods>()
|
|
||||||
.eq(ShopGoods::getTenantId, TENANT_ID)
|
|
||||||
.in(ShopGoods::getGoodsId, goodsIds)
|
|
||||||
).stream().collect(java.util.stream.Collectors.toMap(
|
|
||||||
ShopGoods::getGoodsId,
|
|
||||||
g -> g.getDeliveryMoney() != null ? g.getDeliveryMoney() : BigDecimal.ZERO,
|
|
||||||
(a, b) -> a
|
|
||||||
));
|
|
||||||
|
|
||||||
BigDecimal reward = BigDecimal.ZERO;
|
|
||||||
for (ShopOrderGoods og : orderGoodsList) {
|
|
||||||
Integer goodsId = og.getGoodsId();
|
|
||||||
if (goodsId == null) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
|
|
||||||
if (qty <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
BigDecimal rawRate = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO);
|
|
||||||
BigDecimal rate = normalizeDeliveryRate(rawRate);
|
|
||||||
if (rate == null || rate.signum() <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
BigDecimal unitPrice = og.getPrice() != null ? og.getPrice() : BigDecimal.ZERO;
|
|
||||||
if (unitPrice.signum() <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
BigDecimal lineAmount = unitPrice.multiply(BigDecimal.valueOf(qty));
|
|
||||||
reward = reward.add(lineAmount.multiply(rate));
|
|
||||||
}
|
|
||||||
|
|
||||||
reward = reward.setScale(2, RoundingMode.HALF_UP);
|
Integer orderId = order.getOrderId();
|
||||||
if (reward.signum() <= 0) {
|
if (orderId == null) {
|
||||||
return false;
|
log.debug("【步骤5.X】配送奖励跳过:orderId为空 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 锁定/创建配送员分销账户
|
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.list(
|
||||||
ShopDealerUser dealerUser = shopDealerUserService.getOne(
|
new LambdaQueryWrapper<ShopOrderGoods>()
|
||||||
new LambdaQueryWrapper<ShopDealerUser>()
|
.eq(ShopOrderGoods::getTenantId, TENANT_ID)
|
||||||
.eq(ShopDealerUser::getTenantId, TENANT_ID)
|
.eq(ShopOrderGoods::getOrderId, orderId)
|
||||||
.eq(ShopDealerUser::getUserId, riderId)
|
);
|
||||||
.last("limit 1 for update")
|
if (orderGoodsList == null || orderGoodsList.isEmpty()) {
|
||||||
);
|
log.debug("【步骤5.X】配送奖励跳过:订单商品为空 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
if (dealerUser == null) {
|
return false;
|
||||||
ShopDealerUser newDealerUser = new ShopDealerUser();
|
}
|
||||||
newDealerUser.setTenantId(TENANT_ID);
|
|
||||||
newDealerUser.setUserId(riderId);
|
|
||||||
newDealerUser.setType(0);
|
|
||||||
newDealerUser.setIsDelete(0);
|
|
||||||
newDealerUser.setSortNumber(0);
|
|
||||||
newDealerUser.setFirstNum(0);
|
|
||||||
newDealerUser.setSecondNum(0);
|
|
||||||
newDealerUser.setThirdNum(0);
|
|
||||||
newDealerUser.setMoney(BigDecimal.ZERO);
|
|
||||||
newDealerUser.setFreezeMoney(BigDecimal.ZERO);
|
|
||||||
newDealerUser.setTotalMoney(BigDecimal.ZERO);
|
|
||||||
newDealerUser.setCreateTime(now);
|
|
||||||
newDealerUser.setUpdateTime(now);
|
|
||||||
shopDealerUserService.save(newDealerUser);
|
|
||||||
|
|
||||||
dealerUser = shopDealerUserService.getOne(
|
List<Integer> goodsIds = orderGoodsList.stream()
|
||||||
|
.map(ShopOrderGoods::getGoodsId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (goodsIds.isEmpty()) {
|
||||||
|
log.debug("【步骤5.X】配送奖励跳过:商品ID列表为空 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, BigDecimal> goodsDeliveryMoneyMap = shopGoodsService.list(
|
||||||
|
new LambdaQueryWrapper<ShopGoods>()
|
||||||
|
.eq(ShopGoods::getTenantId, TENANT_ID)
|
||||||
|
.in(ShopGoods::getGoodsId, goodsIds)
|
||||||
|
).stream().collect(java.util.stream.Collectors.toMap(
|
||||||
|
ShopGoods::getGoodsId,
|
||||||
|
g -> g.getDeliveryMoney() != null ? g.getDeliveryMoney() : BigDecimal.ZERO,
|
||||||
|
(a, b) -> a
|
||||||
|
));
|
||||||
|
|
||||||
|
BigDecimal reward = BigDecimal.ZERO;
|
||||||
|
for (ShopOrderGoods og : orderGoodsList) {
|
||||||
|
Integer goodsId = og.getGoodsId();
|
||||||
|
if (goodsId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
|
||||||
|
if (qty <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal rawRate = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO);
|
||||||
|
BigDecimal rate = normalizeDeliveryRate(rawRate);
|
||||||
|
if (rate == null || rate.signum() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal unitPrice = og.getPrice() != null ? og.getPrice() : BigDecimal.ZERO;
|
||||||
|
if (unitPrice.signum() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal lineAmount = unitPrice.multiply(BigDecimal.valueOf(qty));
|
||||||
|
reward = reward.add(lineAmount.multiply(rate));
|
||||||
|
}
|
||||||
|
|
||||||
|
reward = reward.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
if (reward.signum() <= 0) {
|
||||||
|
log.debug("【步骤5.X】配送奖励跳过:计算金额为0 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.debug("【步骤5.X】计算配送奖励 - tenantId={}, orderNo={}, reward={}", TENANT_ID, orderNo, reward);
|
||||||
|
|
||||||
|
// 锁定/创建配送员分销账户
|
||||||
|
ShopDealerUser dealerUser = shopDealerUserService.getOne(
|
||||||
new LambdaQueryWrapper<ShopDealerUser>()
|
new LambdaQueryWrapper<ShopDealerUser>()
|
||||||
.eq(ShopDealerUser::getTenantId, TENANT_ID)
|
.eq(ShopDealerUser::getTenantId, TENANT_ID)
|
||||||
.eq(ShopDealerUser::getUserId, riderId)
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
.last("limit 1 for update")
|
.last("limit 1 for update")
|
||||||
);
|
);
|
||||||
if (dealerUser == null) {
|
if (dealerUser == null) {
|
||||||
log.warn("配送奖励入账失败:未找到/创建分销账户 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
|
log.info("【步骤5.X】创建配送员分销账户 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
|
||||||
|
ShopDealerUser newDealerUser = new ShopDealerUser();
|
||||||
|
newDealerUser.setTenantId(TENANT_ID);
|
||||||
|
newDealerUser.setUserId(riderId);
|
||||||
|
newDealerUser.setType(0);
|
||||||
|
newDealerUser.setIsDelete(0);
|
||||||
|
newDealerUser.setSortNumber(0);
|
||||||
|
newDealerUser.setFirstNum(0);
|
||||||
|
newDealerUser.setSecondNum(0);
|
||||||
|
newDealerUser.setThirdNum(0);
|
||||||
|
newDealerUser.setMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setFreezeMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setTotalMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setCreateTime(now);
|
||||||
|
newDealerUser.setUpdateTime(now);
|
||||||
|
shopDealerUserService.save(newDealerUser);
|
||||||
|
|
||||||
|
dealerUser = shopDealerUserService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (dealerUser == null) {
|
||||||
|
log.warn("配送奖励入账失败:未找到/创建分销账户 - tenantId={}, orderNo={}, riderId={}", TENANT_ID, orderNo, riderId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
|
||||||
|
BigDecimal totalMoneyVal = dealerUser.getTotalMoney() != null ? dealerUser.getTotalMoney() : BigDecimal.ZERO;
|
||||||
|
dealerUser.setMoney(moneyVal.add(reward));
|
||||||
|
dealerUser.setTotalMoney(totalMoneyVal.add(reward));
|
||||||
|
dealerUser.setUpdateTime(now);
|
||||||
|
if (!shopDealerUserService.updateById(dealerUser)) {
|
||||||
|
log.warn("配送奖励入账失败:更新分销账户失败 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
|
ShopDealerCapital cap = new ShopDealerCapital();
|
||||||
BigDecimal totalMoneyVal = dealerUser.getTotalMoney() != null ? dealerUser.getTotalMoney() : BigDecimal.ZERO;
|
cap.setUserId(riderId);
|
||||||
dealerUser.setMoney(moneyVal.add(reward));
|
cap.setOrderNo(orderNo);
|
||||||
dealerUser.setTotalMoney(totalMoneyVal.add(reward));
|
cap.setFlowType(FLOW_TYPE_DELIVERY_REWARD);
|
||||||
dealerUser.setUpdateTime(now);
|
cap.setMoney(reward);
|
||||||
if (!shopDealerUserService.updateById(dealerUser)) {
|
cap.setComments("配送奖励");
|
||||||
log.warn("配送奖励入账失败:更新分销账户失败 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
|
cap.setToUserId(order.getUserId());
|
||||||
return false;
|
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||||
}
|
cap.setTenantId(TENANT_ID);
|
||||||
|
cap.setCreateTime(now);
|
||||||
|
cap.setUpdateTime(now);
|
||||||
|
shopDealerCapitalService.save(cap);
|
||||||
|
|
||||||
ShopDealerCapital cap = new ShopDealerCapital();
|
log.info("【步骤5.X】配送奖励发放成功 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
|
||||||
cap.setUserId(riderId);
|
return true;
|
||||||
cap.setOrderNo(orderNo);
|
}));
|
||||||
cap.setFlowType(FLOW_TYPE_DELIVERY_REWARD);
|
} catch (Exception e) {
|
||||||
cap.setMoney(reward);
|
log.error("【步骤5.X】配送奖励发放异常 - tenantId={}, orderNo={}, error={}", TENANT_ID, orderNo, e.getMessage(), e);
|
||||||
cap.setComments("配送奖励");
|
return false;
|
||||||
cap.setToUserId(order.getUserId());
|
}
|
||||||
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
|
||||||
cap.setTenantId(TENANT_ID);
|
|
||||||
cap.setCreateTime(now);
|
|
||||||
cap.setUpdateTime(now);
|
|
||||||
shopDealerCapitalService.save(cap);
|
|
||||||
|
|
||||||
log.info("配送奖励发放成功 - tenantId={}, orderNo={}, riderId={}, reward={}", TENANT_ID, orderNo, riderId, reward);
|
|
||||||
return true;
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<Integer> loadWaterFormIds() {
|
private Set<Integer> loadWaterFormIds() {
|
||||||
@@ -408,10 +469,11 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> findEligibleWaterOrderNosByFirstFinishedTicketOrder(Set<Integer> waterFormIds) {
|
private Set<String> findEligibleWaterOrderNosByFirstFinishedTicketOrder(Set<Integer> waterFormIds) {
|
||||||
// 缓存减少 DB 往返:userTicketId -> 是否“第一次送水单已完成”
|
// 缓存减少 DB 往返:userTicketId -> 是否"第一次送水单已完成"
|
||||||
Map<Integer, Boolean> firstFinishedCache = new HashMap<>();
|
Map<Integer, Boolean> firstFinishedCache = new HashMap<>();
|
||||||
Map<Integer, String> userTicketOrderNoCache = new HashMap<>();
|
Map<Integer, String> userTicketOrderNoCache = new HashMap<>();
|
||||||
|
|
||||||
|
log.info("【步骤3.1】查询已完成的送水订单 - tenantId={}, limit={}", TENANT_ID, MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN);
|
||||||
List<GltTicketOrder> finishedTicketOrders = gltTicketOrderService.list(
|
List<GltTicketOrder> finishedTicketOrders = gltTicketOrderService.list(
|
||||||
new LambdaQueryWrapper<GltTicketOrder>()
|
new LambdaQueryWrapper<GltTicketOrder>()
|
||||||
.eq(GltTicketOrder::getTenantId, TENANT_ID)
|
.eq(GltTicketOrder::getTenantId, TENANT_ID)
|
||||||
@@ -421,21 +483,27 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
.orderByDesc(GltTicketOrder::getId)
|
.orderByDesc(GltTicketOrder::getId)
|
||||||
.last("limit " + MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN)
|
.last("limit " + MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN)
|
||||||
);
|
);
|
||||||
|
log.info("【步骤3.1】查到已完成的送水订单数量 - tenantId={}, count={}", TENANT_ID, finishedTicketOrders.size());
|
||||||
|
|
||||||
Set<String> orderNos = new HashSet<>();
|
Set<String> orderNos = new HashSet<>();
|
||||||
|
int checked = 0, skippedNull = 0, skippedNotFirst = 0, skippedNoOrderNo = 0, skippedNotWater = 0;
|
||||||
for (GltTicketOrder ticketOrder : finishedTicketOrders) {
|
for (GltTicketOrder ticketOrder : finishedTicketOrders) {
|
||||||
Integer userTicketId = ticketOrder.getUserTicketId();
|
Integer userTicketId = ticketOrder.getUserTicketId();
|
||||||
|
checked++;
|
||||||
if (userTicketId == null) {
|
if (userTicketId == null) {
|
||||||
|
skippedNull++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean firstFinished = firstFinishedCache.computeIfAbsent(userTicketId, id -> isFirstTicketOrderFinished(id));
|
boolean firstFinished = firstFinishedCache.computeIfAbsent(userTicketId, id -> isFirstTicketOrderFinished(id));
|
||||||
if (!firstFinished) {
|
if (!firstFinished) {
|
||||||
|
skippedNotFirst++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String orderNo = userTicketOrderNoCache.computeIfAbsent(userTicketId, id -> findOrderNoByUserTicketId(id));
|
String orderNo = userTicketOrderNoCache.computeIfAbsent(userTicketId, id -> findOrderNoByUserTicketId(id));
|
||||||
if (orderNo == null || orderNo.isBlank()) {
|
if (orderNo == null || orderNo.isBlank()) {
|
||||||
|
skippedNoOrderNo++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,6 +517,7 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
.last("limit 1")
|
.last("limit 1")
|
||||||
);
|
);
|
||||||
if (shopOrder == null || shopOrder.getFormId() == null || !waterFormIds.contains(shopOrder.getFormId())) {
|
if (shopOrder == null || shopOrder.getFormId() == null || !waterFormIds.contains(shopOrder.getFormId())) {
|
||||||
|
skippedNotWater++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +526,8 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("【步骤3.2】送水订单筛选完成 - tenantId={}, checked={}, skippedNull={}, skippedNotFirst={}, skippedNoOrderNo={}, skippedNotWater={}, matched={}, orderNos={}",
|
||||||
|
TENANT_ID, checked, skippedNull, skippedNotFirst, skippedNoOrderNo, skippedNotWater, orderNos.size(), orderNos);
|
||||||
return orderNos;
|
return orderNos;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +564,9 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
if (eligibleOrderNos == null || eligibleOrderNos.isEmpty()) {
|
if (eligibleOrderNos == null || eligibleOrderNos.isEmpty()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return shopDealerCapitalService.list(
|
// 先按 eligibleOrderNos 集合的顺序(最新订单优先)处理佣金
|
||||||
|
// 注意:MyBatis-Plus 的 in 查询会保持 in() 列表的顺序
|
||||||
|
List<ShopDealerCapital> capitals = shopDealerCapitalService.list(
|
||||||
new LambdaQueryWrapper<ShopDealerCapital>()
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
.eq(ShopDealerCapital::getFlowType, 10)
|
.eq(ShopDealerCapital::getFlowType, 10)
|
||||||
@@ -502,6 +575,14 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
.orderByAsc(ShopDealerCapital::getId)
|
.orderByAsc(ShopDealerCapital::getId)
|
||||||
.last("limit " + MAX_CAPITALS_PER_RUN)
|
.last("limit " + MAX_CAPITALS_PER_RUN)
|
||||||
);
|
);
|
||||||
|
// 按 eligibleOrderNos 的顺序重新排序(最新订单优先)
|
||||||
|
List<String> orderList = List.copyOf(eligibleOrderNos);
|
||||||
|
capitals.sort((a, b) -> {
|
||||||
|
int idxA = orderList.indexOf(a.getOrderNo());
|
||||||
|
int idxB = orderList.indexOf(b.getOrderNo());
|
||||||
|
return Integer.compare(idxA, idxB);
|
||||||
|
});
|
||||||
|
return capitals;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean unfreezeOneCapitalIfNeeded(ShopDealerCapital cap) {
|
private boolean unfreezeOneCapitalIfNeeded(ShopDealerCapital cap) {
|
||||||
@@ -546,7 +627,7 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RR 隔离级别下,先锁 user 行,再用锁定读检查 marker,避免“读到旧快照”导致重复解冻。
|
// RR 隔离级别下,先锁 user 行,再用锁定读检查 marker,避免"读到旧快照"导致重复解冻。
|
||||||
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
|
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
|
||||||
new LambdaQueryWrapper<ShopDealerCapital>()
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
@@ -590,7 +671,7 @@ public class DealerCommissionUnfreeze10584Task {
|
|||||||
marker.setUpdateTime(now);
|
marker.setUpdateTime(now);
|
||||||
shopDealerCapitalService.save(marker);
|
shopDealerCapitalService.save(marker);
|
||||||
|
|
||||||
// 佣金全部解冻完成后,将分销订单状态置为“已解冻”(0)。
|
// 佣金全部解冻完成后,将分销订单状态置为"已解冻"(0)。
|
||||||
// 以当前任务生成的 flowType=50 marker 数量作为完成度判断,避免提前将订单置为已解冻。
|
// 以当前任务生成的 flowType=50 marker 数量作为完成度判断,避免提前将订单置为已解冻。
|
||||||
setDealerOrderUnfrozenIfCompleted(orderNo, now);
|
setDealerOrderUnfrozenIfCompleted(orderNo, now);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分销商订单记录表控制器
|
* 分销商订单记录表控制器
|
||||||
@@ -168,4 +169,22 @@ public class ShopDealerOrderController extends BaseController {
|
|||||||
}
|
}
|
||||||
return fail("导入失败", null);
|
return fail("导入失败", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopDealerOrder:update')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "手动触发单条订单佣金解冻")
|
||||||
|
@PostMapping("/unfreeze")
|
||||||
|
public ApiResult<String> manualUnfreeze(@RequestBody Map<String, Object> body) {
|
||||||
|
String orderNo = (String) body.get("orderNo");
|
||||||
|
if (orderNo == null || orderNo.isBlank()) {
|
||||||
|
return fail("订单编号不能为空", null);
|
||||||
|
}
|
||||||
|
Integer tenantId = getTenantId();
|
||||||
|
try {
|
||||||
|
String detail = shopDealerOrderService.manualUnfreeze(orderNo, tenantId);
|
||||||
|
return success("解冻执行完成", detail);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail(e.getMessage(),null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import com.wechat.pay.java.core.notification.NotificationConfig;
|
|||||||
import com.wechat.pay.java.core.notification.NotificationParser;
|
import com.wechat.pay.java.core.notification.NotificationParser;
|
||||||
import com.wechat.pay.java.core.notification.RequestParam;
|
import com.wechat.pay.java.core.notification.RequestParam;
|
||||||
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
|
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
|
||||||
import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction;
|
import com.wechat.pay.java.service.payments.model.Transaction;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -53,6 +53,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单控制器
|
* 订单控制器
|
||||||
@@ -65,6 +66,8 @@ import java.util.Objects;
|
|||||||
@RequestMapping("/api/shop/shop-order")
|
@RequestMapping("/api/shop/shop-order")
|
||||||
public class ShopOrderController extends BaseController {
|
public class ShopOrderController extends BaseController {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ShopOrderController.class);
|
private static final Logger logger = LoggerFactory.getLogger(ShopOrderController.class);
|
||||||
|
/** 按商户号缓存 NotificationConfig,避免每次回调都重新拉取平台证书 */
|
||||||
|
private final ConcurrentHashMap<String, NotificationConfig> notifyConfigCache = new ConcurrentHashMap<>();
|
||||||
@Resource
|
@Resource
|
||||||
private ShopOrderService shopOrderService;
|
private ShopOrderService shopOrderService;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -791,90 +794,100 @@ public class ShopOrderController extends BaseController {
|
|||||||
.body(body)
|
.body(body)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 创建通知配置 - 使用与下单方法相同的证书配置逻辑
|
// 创建通知配置 - 使用与下单方法相同的证书配置逻辑(按 mchId 缓存,避免重复拉取平台证书)
|
||||||
NotificationConfig config;
|
NotificationConfig config;
|
||||||
try {
|
final String mchId = payment.getMchId();
|
||||||
if (active.equals("dev")) {
|
config = notifyConfigCache.get(mchId);
|
||||||
// 开发环境 - 构建包含租户号的私钥路径
|
if (config == null) {
|
||||||
String tenantCertPath = "dev/wechat/" + tenantId;
|
try {
|
||||||
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
|
NotificationConfig newConfig;
|
||||||
|
if (active.equals("dev")) {
|
||||||
|
// 开发环境 - 构建包含租户号的私钥路径
|
||||||
|
String tenantCertPath = "dev/wechat/" + tenantId;
|
||||||
|
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
|
||||||
|
|
||||||
logger.info("开发环境异步通知证书路径: {}", privateKeyPath);
|
logger.info("开发环境异步通知证书路径: {}", privateKeyPath);
|
||||||
logger.info("租户ID: {}, 证书目录: {}", tenantId, tenantCertPath);
|
logger.info("租户ID: {}, 证书目录: {}", tenantId, tenantCertPath);
|
||||||
|
|
||||||
// 检查证书文件是否存在
|
// 检查证书文件是否存在
|
||||||
if (!certificateLoader.certificateExists(privateKeyPath)) {
|
if (!certificateLoader.certificateExists(privateKeyPath)) {
|
||||||
logger.error("证书文件不存在: {}", privateKeyPath);
|
logger.error("证书文件不存在: {}", privateKeyPath);
|
||||||
throw new RuntimeException("证书文件不存在: " + privateKeyPath);
|
throw new RuntimeException("证书文件不存在: " + privateKeyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
String privateKey = certificateLoader.loadCertificatePath(privateKeyPath);
|
||||||
|
|
||||||
|
// 使用验证器获取有效的 APIv3 密钥
|
||||||
|
String apiV3Key = wechatPayConfigValidator.getValidApiV3Key(payment);
|
||||||
|
|
||||||
|
logger.info("私钥文件加载成功: {}", privateKey);
|
||||||
|
logger.info("使用APIv3密钥来源: {}", payment.getApiKey() != null && !payment.getApiKey().trim().isEmpty() ? "数据库配置" : "配置文件默认");
|
||||||
|
logger.info("APIv3密钥长度: {}", apiV3Key != null ? apiV3Key.length() : 0);
|
||||||
|
logger.info("商户证书序列号: {}", payment.getMerchantSerialNumber());
|
||||||
|
|
||||||
|
// 使用自动证书配置
|
||||||
|
newConfig = new RSAAutoCertificateConfig.Builder()
|
||||||
|
.merchantId(mchId)
|
||||||
|
.privateKeyFromPath(privateKey)
|
||||||
|
.merchantSerialNumber(payment.getMerchantSerialNumber())
|
||||||
|
.apiV3Key(apiV3Key)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
logger.info("✅ 开发环境使用自动证书配置创建通知解析器成功");
|
||||||
|
} else {
|
||||||
|
// 生产环境 - 使用自动证书配置
|
||||||
|
final String certRootPath = certConfig.getCertRootPath();
|
||||||
|
logger.info("生产环境证书根路径: {}", certRootPath);
|
||||||
|
|
||||||
|
String privateKeyRelativePath = payment.getApiclientKey();
|
||||||
|
logger.info("数据库中的私钥相对路径: {}", privateKeyRelativePath);
|
||||||
|
|
||||||
|
// 生产环境已经没有/file目录,所有路径都直接拼接到根路径
|
||||||
|
String privateKeyFullPath;
|
||||||
|
// 处理数据库中可能存在的历史路径格式
|
||||||
|
String cleanPath = privateKeyRelativePath;
|
||||||
|
if (privateKeyRelativePath.startsWith("/file/")) {
|
||||||
|
// 去掉历史的 /file/ 前缀
|
||||||
|
cleanPath = privateKeyRelativePath.substring(6);
|
||||||
|
} else if (privateKeyRelativePath.startsWith("file/")) {
|
||||||
|
// 去掉历史的 file/ 前缀
|
||||||
|
cleanPath = privateKeyRelativePath.substring(5);
|
||||||
|
}
|
||||||
|
// 确保路径以 / 开头
|
||||||
|
if (!cleanPath.startsWith("/")) {
|
||||||
|
cleanPath = "/" + cleanPath;
|
||||||
|
}
|
||||||
|
privateKeyFullPath = certRootPath + cleanPath;
|
||||||
|
|
||||||
|
logger.info("生产环境私钥完整路径: {}", privateKeyFullPath);
|
||||||
|
String privateKey = certificateLoader.loadCertificatePath(privateKeyFullPath);
|
||||||
|
String apiV3Key = payment.getApiKey();
|
||||||
|
|
||||||
|
// 使用自动证书配置
|
||||||
|
newConfig = new RSAAutoCertificateConfig.Builder()
|
||||||
|
.merchantId(mchId)
|
||||||
|
.privateKeyFromPath(privateKey)
|
||||||
|
.merchantSerialNumber(payment.getMerchantSerialNumber())
|
||||||
|
.apiV3Key(apiV3Key)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
logger.info("✅ 生产环境使用自动证书配置创建通知解析器成功");
|
||||||
}
|
}
|
||||||
|
// 放入缓存
|
||||||
String privateKey = certificateLoader.loadCertificatePath(privateKeyPath);
|
notifyConfigCache.putIfAbsent(mchId, newConfig);
|
||||||
|
config = notifyConfigCache.get(mchId);
|
||||||
// 使用验证器获取有效的 APIv3 密钥
|
} catch (Exception e) {
|
||||||
String apiV3Key = wechatPayConfigValidator.getValidApiV3Key(payment);
|
logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, mchId, e);
|
||||||
|
logger.error("🔍 错误详情: {}", e.getMessage());
|
||||||
logger.info("私钥文件加载成功: {}", privateKey);
|
logger.error("💡 请检查:");
|
||||||
logger.info("使用APIv3密钥来源: {}", payment.getApiKey() != null && !payment.getApiKey().trim().isEmpty() ? "数据库配置" : "配置文件默认");
|
logger.error("1. 证书文件是否存在且路径正确");
|
||||||
logger.info("APIv3密钥长度: {}", apiV3Key != null ? apiV3Key.length() : 0);
|
logger.error("2. APIv3密钥是否配置正确");
|
||||||
logger.info("商户证书序列号: {}", payment.getMerchantSerialNumber());
|
logger.error("3. 商户证书序列号是否正确");
|
||||||
|
logger.error("4. 网络连接是否正常");
|
||||||
// 使用自动证书配置
|
throw new RuntimeException("微信支付通知配置失败: " + e.getMessage(), e);
|
||||||
config = new RSAAutoCertificateConfig.Builder()
|
|
||||||
.merchantId(payment.getMchId())
|
|
||||||
.privateKeyFromPath(privateKey)
|
|
||||||
.merchantSerialNumber(payment.getMerchantSerialNumber())
|
|
||||||
.apiV3Key(apiV3Key)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
logger.info("✅ 开发环境使用自动证书配置创建通知解析器成功");
|
|
||||||
} else {
|
|
||||||
// 生产环境 - 使用自动证书配置
|
|
||||||
final String certRootPath = certConfig.getCertRootPath();
|
|
||||||
logger.info("生产环境证书根路径: {}", certRootPath);
|
|
||||||
|
|
||||||
String privateKeyRelativePath = payment.getApiclientKey();
|
|
||||||
logger.info("数据库中的私钥相对路径: {}", privateKeyRelativePath);
|
|
||||||
|
|
||||||
// 生产环境已经没有/file目录,所有路径都直接拼接到根路径
|
|
||||||
String privateKeyFullPath;
|
|
||||||
// 处理数据库中可能存在的历史路径格式
|
|
||||||
String cleanPath = privateKeyRelativePath;
|
|
||||||
if (privateKeyRelativePath.startsWith("/file/")) {
|
|
||||||
// 去掉历史的 /file/ 前缀
|
|
||||||
cleanPath = privateKeyRelativePath.substring(6);
|
|
||||||
} else if (privateKeyRelativePath.startsWith("file/")) {
|
|
||||||
// 去掉历史的 file/ 前缀
|
|
||||||
cleanPath = privateKeyRelativePath.substring(5);
|
|
||||||
}
|
|
||||||
// 确保路径以 / 开头
|
|
||||||
if (!cleanPath.startsWith("/")) {
|
|
||||||
cleanPath = "/" + cleanPath;
|
|
||||||
}
|
|
||||||
privateKeyFullPath = certRootPath + cleanPath;
|
|
||||||
|
|
||||||
logger.info("生产环境私钥完整路径: {}", privateKeyFullPath);
|
|
||||||
String privateKey = certificateLoader.loadCertificatePath(privateKeyFullPath);
|
|
||||||
String apiV3Key = payment.getApiKey();
|
|
||||||
|
|
||||||
// 使用自动证书配置
|
|
||||||
config = new RSAAutoCertificateConfig.Builder()
|
|
||||||
.merchantId(payment.getMchId())
|
|
||||||
.privateKeyFromPath(privateKey)
|
|
||||||
.merchantSerialNumber(payment.getMerchantSerialNumber())
|
|
||||||
.apiV3Key(apiV3Key)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
logger.info("✅ 生产环境使用自动证书配置创建通知解析器成功");
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
logger.error("❌ 创建通知配置失败 - 租户ID: {}, 商户号: {}", tenantId, payment.getMchId(), e);
|
logger.info("✅ 使用缓存的通知配置 - 商户号: {}", mchId);
|
||||||
logger.error("🔍 错误详情: {}", e.getMessage());
|
|
||||||
logger.error("💡 请检查:");
|
|
||||||
logger.error("1. 证书文件是否存在且路径正确");
|
|
||||||
logger.error("2. APIv3密钥是否配置正确");
|
|
||||||
logger.error("3. 商户证书序列号是否正确");
|
|
||||||
logger.error("4. 网络连接是否正常");
|
|
||||||
throw new RuntimeException("微信支付通知配置失败: " + e.getMessage(), e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 NotificationParser
|
// 初始化 NotificationParser
|
||||||
@@ -888,7 +901,8 @@ public class ShopOrderController extends BaseController {
|
|||||||
logger.info("✅ 异步通知解析成功 - 交易状态: {}, 商户订单号: {}",
|
logger.info("✅ 异步通知解析成功 - 交易状态: {}, 商户订单号: {}",
|
||||||
transaction.getTradeStateDesc(), transaction.getOutTradeNo());
|
transaction.getTradeStateDesc(), transaction.getOutTradeNo());
|
||||||
|
|
||||||
if (StrUtil.equals("支付成功", transaction.getTradeStateDesc())) {
|
// 使用枚举值判断支付状态,避免依赖状态描述字符串
|
||||||
|
if (Transaction.TradeStateEnum.SUCCESS.equals(transaction.getTradeState())) {
|
||||||
final String outTradeNo = transaction.getOutTradeNo();
|
final String outTradeNo = transaction.getOutTradeNo();
|
||||||
final String transactionId = transaction.getTransactionId();
|
final String transactionId = transaction.getTransactionId();
|
||||||
final Integer total = transaction.getAmount().getTotal();
|
final Integer total = transaction.getAmount().getTotal();
|
||||||
|
|||||||
@@ -116,4 +116,7 @@ public class ShopDealerUser implements Serializable {
|
|||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
@Schema(description = "分销商等级:0-普通用户 1-超级管理员 2-合伙人(总店) 3-合伙人(分店)")
|
||||||
|
private Integer dealerLevel;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,12 @@ public class ShopGoods implements Serializable {
|
|||||||
@Schema(description = "状态, 0上架 1待上架 2待审核 3审核不通过")
|
@Schema(description = "状态, 0上架 1待上架 2待审核 3审核不通过")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "活动方式: 0全平台 1新用户专享")
|
||||||
|
private Integer activityType;
|
||||||
|
|
||||||
|
@Schema(description = "配送方式: 0送上门 1限自提")
|
||||||
|
private Integer deliveryMode;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String comments;
|
private String comments;
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@
|
|||||||
<if test="param.sortNumber != null">
|
<if test="param.sortNumber != null">
|
||||||
AND a.sort_number = #{param.sortNumber}
|
AND a.sort_number = #{param.sortNumber}
|
||||||
</if>
|
</if>
|
||||||
|
<if test="param.dealerLevel != null">
|
||||||
|
AND a.dealer_level = #{param.dealerLevel}
|
||||||
|
</if>
|
||||||
<if test="param.keywords != null">
|
<if test="param.keywords != null">
|
||||||
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
|
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
OR a.user_id = #{param.keywords} OR a.dealer_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.real_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.mobile LIKE CONCAT('%', #{param.keywords}, '%')
|
OR a.user_id = #{param.keywords} OR a.dealer_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.real_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.mobile LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
|||||||
@@ -135,6 +135,12 @@
|
|||||||
OR a.comments LIKE CONCAT('%', #{param.keywords}, '%')
|
OR a.comments LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
|
<if test="param.activityType != null">
|
||||||
|
AND a.activity_type = #{param.activityType}
|
||||||
|
</if>
|
||||||
|
<if test="param.deliveryMode != null">
|
||||||
|
AND a.delivery_mode = #{param.deliveryMode}
|
||||||
|
</if>
|
||||||
</where>
|
</where>
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
|
|||||||
@@ -329,6 +329,10 @@
|
|||||||
<update id="updateByOutTradeNo" parameterType="com.gxwebsoft.cms.entity.CmsWebsite">
|
<update id="updateByOutTradeNo" parameterType="com.gxwebsoft.cms.entity.CmsWebsite">
|
||||||
UPDATE shop_order
|
UPDATE shop_order
|
||||||
<set>
|
<set>
|
||||||
|
update_time = NOW(),
|
||||||
|
<if test="param.expirationTime != null">
|
||||||
|
expiration_time = #{param.expirationTime},
|
||||||
|
</if>
|
||||||
<if test="param.payType != null">
|
<if test="param.payType != null">
|
||||||
pay_type = #{param.payType},
|
pay_type = #{param.payType},
|
||||||
</if>
|
</if>
|
||||||
|
|||||||
@@ -86,4 +86,8 @@ public class ShopDealerUserParam extends BaseParam {
|
|||||||
@QueryField(type = QueryType.EQ)
|
@QueryField(type = QueryType.EQ)
|
||||||
private Integer isDelete;
|
private Integer isDelete;
|
||||||
|
|
||||||
|
@Schema(description = "分销商等级:0-普通用户 1-超级管理员 2-合伙人(总店) 3-合伙人(分店)")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer dealerLevel;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,4 +150,12 @@ public class ShopGoodsParam extends BaseParam {
|
|||||||
@QueryField(type = QueryType.EQ)
|
@QueryField(type = QueryType.EQ)
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
|
|
||||||
|
@Schema(description = "活动方式: 0全平台 1新用户专享")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer activityType;
|
||||||
|
|
||||||
|
@Schema(description = "配送方式: 0送上门 1限自提")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer deliveryMode;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,13 @@ public interface ShopDealerOrderService extends IService<ShopDealerOrder> {
|
|||||||
*/
|
*/
|
||||||
ShopDealerOrder getByIdRel(Integer id);
|
ShopDealerOrder getByIdRel(Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发单条订单的佣金解冻(保留与定时任务相同的前置条件检查)
|
||||||
|
*
|
||||||
|
* @param orderNo 分销订单号
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return 解冻结果描述(包含成功/失败详情)
|
||||||
|
*/
|
||||||
|
String manualUnfreeze(String orderNo, Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,43 @@
|
|||||||
package com.gxwebsoft.shop.service.impl;
|
package com.gxwebsoft.shop.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.gxwebsoft.shop.mapper.ShopDealerOrderMapper;
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
import com.gxwebsoft.shop.service.ShopDealerOrderService;
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
|
import com.gxwebsoft.glt.entity.GltTicketTemplate;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||||
|
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||||
|
import com.gxwebsoft.glt.service.GltTicketTemplateService;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketService;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopDealerCapital;
|
||||||
import com.gxwebsoft.shop.entity.ShopDealerOrder;
|
import com.gxwebsoft.shop.entity.ShopDealerOrder;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopDealerUser;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopGoods;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopOrderGoods;
|
||||||
|
import com.gxwebsoft.shop.mapper.ShopDealerOrderMapper;
|
||||||
|
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopDealerOrderService;
|
||||||
import com.gxwebsoft.shop.param.ShopDealerOrderParam;
|
import com.gxwebsoft.shop.param.ShopDealerOrderParam;
|
||||||
import com.gxwebsoft.common.core.web.PageParam;
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分销商订单记录表Service实现
|
* 分销商订单记录表Service实现
|
||||||
@@ -18,8 +46,41 @@ import java.util.List;
|
|||||||
* @since 2025-08-12 11:55:18
|
* @since 2025-08-12 11:55:18
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
public class ShopDealerOrderServiceImpl extends ServiceImpl<ShopDealerOrderMapper, ShopDealerOrder> implements ShopDealerOrderService {
|
public class ShopDealerOrderServiceImpl extends ServiceImpl<ShopDealerOrderMapper, ShopDealerOrder> implements ShopDealerOrderService {
|
||||||
|
|
||||||
|
private static final int ORDER_STATUS_CONFIRMED_RECEIVE = 1;
|
||||||
|
private static final int FLOW_TYPE_COMMISSION = 10;
|
||||||
|
private static final int FLOW_TYPE_UNFREEZE = 50;
|
||||||
|
private static final int FLOW_TYPE_DELIVERY_REWARD = 60;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TransactionTemplate transactionTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.gxwebsoft.shop.service.ShopOrderService shopOrderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.gxwebsoft.shop.service.ShopOrderGoodsService shopOrderGoodsService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.gxwebsoft.shop.service.ShopGoodsService shopGoodsService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopDealerCapitalService shopDealerCapitalService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.gxwebsoft.shop.service.ShopDealerUserService shopDealerUserService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketService gltUserTicketService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltTicketOrderService gltTicketOrderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltTicketTemplateService gltTicketTemplateService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageResult<ShopDealerOrder> pageRel(ShopDealerOrderParam param) {
|
public PageResult<ShopDealerOrder> pageRel(ShopDealerOrderParam param) {
|
||||||
PageParam<ShopDealerOrder, ShopDealerOrderParam> page = new PageParam<>(param);
|
PageParam<ShopDealerOrder, ShopDealerOrderParam> page = new PageParam<>(param);
|
||||||
@@ -44,4 +105,436 @@ public class ShopDealerOrderServiceImpl extends ServiceImpl<ShopDealerOrderMappe
|
|||||||
return param.getOne(baseMapper.selectListRel(param));
|
return param.getOne(baseMapper.selectListRel(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 手动解冻 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String manualUnfreeze(String orderNo, Integer tenantId) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.append("【手动解冻】orderNo=").append(orderNo).append(", tenantId=").append(tenantId).append("\n");
|
||||||
|
|
||||||
|
// 1) 查分销订单
|
||||||
|
ShopDealerOrder dealerOrder = this.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerOrder>()
|
||||||
|
.eq(ShopDealerOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerOrder::getOrderNo, orderNo)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (dealerOrder == null) {
|
||||||
|
throw new BusinessException("分销订单不存在: " + orderNo);
|
||||||
|
}
|
||||||
|
result.append(" 分销订单ID=").append(dealerOrder.getId())
|
||||||
|
.append(", isSettled=").append(dealerOrder.getIsSettled())
|
||||||
|
.append(", isUnfreeze=").append(dealerOrder.getIsUnfreeze()).append("\n");
|
||||||
|
|
||||||
|
if (dealerOrder.getIsSettled() == null || dealerOrder.getIsSettled() != 1) {
|
||||||
|
throw new BusinessException("订单未结算,无法解冻");
|
||||||
|
}
|
||||||
|
if (dealerOrder.getIsUnfreeze() != null && dealerOrder.getIsUnfreeze() == 1) {
|
||||||
|
throw new BusinessException("订单已解冻,无需重复操作");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 查商城订单,判断解冻前置条件
|
||||||
|
ShopOrder shopOrder = shopOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getOrderNo, orderNo)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (shopOrder == null) {
|
||||||
|
throw new BusinessException("商城订单不存在: " + orderNo);
|
||||||
|
}
|
||||||
|
result.append(" 商城订单: payStatus=").append(shopOrder.getPayStatus())
|
||||||
|
.append(", orderStatus=").append(shopOrder.getOrderStatus())
|
||||||
|
.append(", formId=").append(shopOrder.getFormId()).append("\n");
|
||||||
|
|
||||||
|
// 3) 加载水票模板 goodsId 集合(用于判断是否送水套餐)
|
||||||
|
Set<Integer> waterFormIds = gltTicketTemplateService.list(
|
||||||
|
new LambdaQueryWrapper<GltTicketTemplate>()
|
||||||
|
.eq(GltTicketTemplate::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketTemplate::getDeleted, 0)
|
||||||
|
.isNotNull(GltTicketTemplate::getGoodsId)
|
||||||
|
).stream()
|
||||||
|
.map(GltTicketTemplate::getGoodsId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
result.append(" 水票模板goodsId集合: ").append(waterFormIds).append("\n");
|
||||||
|
|
||||||
|
boolean isWaterOrder = shopOrder.getFormId() != null && waterFormIds.contains(shopOrder.getFormId());
|
||||||
|
result.append(" 是否送水套餐: ").append(isWaterOrder).append("\n");
|
||||||
|
|
||||||
|
// 4) 检查解冻前置条件
|
||||||
|
if (isWaterOrder) {
|
||||||
|
// 送水套餐:至少有一条送水订单 deliveryStatus=40(已完成)
|
||||||
|
GltUserTicket userTicket = gltUserTicketService.getOne(
|
||||||
|
new LambdaQueryWrapper<GltUserTicket>()
|
||||||
|
.eq(GltUserTicket::getTenantId, tenantId)
|
||||||
|
.eq(GltUserTicket::getDeleted, 0)
|
||||||
|
.eq(GltUserTicket::getOrderNo, orderNo)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (userTicket == null) {
|
||||||
|
// 兜底:通过 orderId 反查
|
||||||
|
userTicket = gltUserTicketService.getOne(
|
||||||
|
new LambdaQueryWrapper<GltUserTicket>()
|
||||||
|
.eq(GltUserTicket::getTenantId, tenantId)
|
||||||
|
.eq(GltUserTicket::getDeleted, 0)
|
||||||
|
.eq(GltUserTicket::getOrderId, shopOrder.getOrderId())
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (userTicket == null) {
|
||||||
|
throw new BusinessException("未找到关联的水票记录,无法确认送水状态");
|
||||||
|
}
|
||||||
|
|
||||||
|
GltTicketOrder firstTicketOrder = gltTicketOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<GltTicketOrder>()
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getUserTicketId, userTicket.getId())
|
||||||
|
.orderByAsc(GltTicketOrder::getId)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
|
||||||
|
boolean firstFinished = firstTicketOrder != null
|
||||||
|
&& firstTicketOrder.getDeliveryStatus() != null
|
||||||
|
&& firstTicketOrder.getDeliveryStatus() == GltTicketOrderService.DELIVERY_STATUS_FINISHED;
|
||||||
|
|
||||||
|
result.append(" 送水订单检查: userTicketId=").append(userTicket.getId())
|
||||||
|
.append(", firstTicketOrderId=").append(firstTicketOrder != null ? firstTicketOrder.getId() : "null")
|
||||||
|
.append(", deliveryStatus=").append(firstTicketOrder != null ? firstTicketOrder.getDeliveryStatus() : "null")
|
||||||
|
.append(", firstFinished=").append(firstFinished).append("\n");
|
||||||
|
|
||||||
|
if (!firstFinished) {
|
||||||
|
throw new BusinessException("送水套餐订单尚未完成第一次配送确认收货,不满足解冻条件");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非送水套餐:商城订单 orderStatus=1(已确认收货)
|
||||||
|
boolean orderCompleted = shopOrder.getOrderStatus() != null
|
||||||
|
&& shopOrder.getOrderStatus() == ORDER_STATUS_CONFIRMED_RECEIVE
|
||||||
|
&& shopOrder.getPayStatus() != null
|
||||||
|
&& shopOrder.getPayStatus();
|
||||||
|
|
||||||
|
result.append(" 非送水订单检查: orderStatus=").append(shopOrder.getOrderStatus())
|
||||||
|
.append(", payStatus=").append(shopOrder.getPayStatus())
|
||||||
|
.append(", completed=").append(orderCompleted).append("\n");
|
||||||
|
|
||||||
|
if (!orderCompleted) {
|
||||||
|
throw new BusinessException("订单尚未确认收货,不满足解冻条件 (orderStatus=" + shopOrder.getOrderStatus() + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) 发放配送奖励(幂等)
|
||||||
|
result.append(" 开始发放配送奖励...\n");
|
||||||
|
try {
|
||||||
|
boolean rewarded = settleDeliveryRewardIfNeeded(orderNo, tenantId);
|
||||||
|
result.append(" 配送奖励结果: ").append(rewarded ? "已发放" : "跳过(无配送员/已发放)").append("\n");
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.append(" 配送奖励异常: ").append(e.getMessage()).append("\n");
|
||||||
|
log.warn("手动解冻-配送奖励发放失败 - orderNo={}, error={}", orderNo, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) 查询该订单的佣金明细(flowType=10)
|
||||||
|
List<ShopDealerCapital> capitals = shopDealerCapitalService.list(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_COMMISSION)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
);
|
||||||
|
result.append(" 佣金明细(flowType=10)数量: ").append(capitals.size()).append("\n");
|
||||||
|
|
||||||
|
if (capitals.isEmpty()) {
|
||||||
|
throw new BusinessException("未找到佣金明细(flowType=10),无法解冻");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) 逐条执行解冻
|
||||||
|
int unfrozen = 0;
|
||||||
|
for (ShopDealerCapital cap : capitals) {
|
||||||
|
try {
|
||||||
|
boolean ok = unfreezeOneCapitalIfNeeded(cap, tenantId);
|
||||||
|
if (ok) {
|
||||||
|
unfrozen++;
|
||||||
|
result.append(" 解冻成功: capitalId=").append(cap.getId())
|
||||||
|
.append(", userId=").append(cap.getUserId())
|
||||||
|
.append(", amount=").append(cap.getMoney()).append("\n");
|
||||||
|
} else {
|
||||||
|
result.append(" 解冻跳过(已解冻/幂等): capitalId=").append(cap.getId()).append("\n");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.append(" 解冻失败: capitalId=").append(cap.getId())
|
||||||
|
.append(", error=").append(e.getMessage()).append("\n");
|
||||||
|
log.error("手动解冻-单条佣金解冻失败 - orderNo={}, capitalId={}, error={}", orderNo, cap.getId(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8) 检查是否全部解冻完成
|
||||||
|
if (unfrozen > 0) {
|
||||||
|
long totalCommissions = shopDealerCapitalService.count(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_COMMISSION)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
);
|
||||||
|
long unfrozenMarkers = shopDealerCapitalService.count(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_UNFREEZE)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.like(ShopDealerCapital::getComments, "佣金解冻(capitalId=")
|
||||||
|
);
|
||||||
|
result.append(" 解冻进度: ").append(unfrozenMarkers).append("/").append(totalCommissions).append("\n");
|
||||||
|
|
||||||
|
if (unfrozenMarkers >= totalCommissions) {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
boolean updated = this.update(
|
||||||
|
new LambdaUpdateWrapper<ShopDealerOrder>()
|
||||||
|
.eq(ShopDealerOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerOrder::getOrderNo, orderNo)
|
||||||
|
.set(ShopDealerOrder::getIsUnfreeze, 1)
|
||||||
|
.set(ShopDealerOrder::getUnfreezeTime, now)
|
||||||
|
.set(ShopDealerOrder::getUpdateTime, now)
|
||||||
|
);
|
||||||
|
result.append(" 订单解冻状态更新: ").append(updated ? "成功(isUnfreeze=1)" : "失败").append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append("【手动解冻完成】共解冻 ").append(unfrozen).append("/").append(capitals.size()).append(" 条佣金");
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发放配送奖励(从定时任务复制,支持 tenantId 参数)
|
||||||
|
*/
|
||||||
|
private boolean settleDeliveryRewardIfNeeded(String orderNo, Integer tenantId) {
|
||||||
|
if (orderNo == null || orderNo.isBlank() || tenantId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShopOrder order = shopOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getOrderNo, orderNo)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (order == null) return false;
|
||||||
|
|
||||||
|
Integer riderId = order.getRiderId();
|
||||||
|
if (riderId == null || riderId <= 0) return false;
|
||||||
|
|
||||||
|
// 幂等检查
|
||||||
|
boolean already = shopDealerCapitalService.count(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
) > 0;
|
||||||
|
if (already) return false;
|
||||||
|
|
||||||
|
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
// 锁定 marker 幂等
|
||||||
|
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_DELIVERY_REWARD)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (existedMarker != null) return false;
|
||||||
|
|
||||||
|
Integer orderId = order.getOrderId();
|
||||||
|
if (orderId == null) return false;
|
||||||
|
|
||||||
|
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.list(
|
||||||
|
new LambdaQueryWrapper<ShopOrderGoods>()
|
||||||
|
.eq(ShopOrderGoods::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrderGoods::getOrderId, orderId)
|
||||||
|
);
|
||||||
|
if (orderGoodsList == null || orderGoodsList.isEmpty()) return false;
|
||||||
|
|
||||||
|
List<Integer> goodsIds = orderGoodsList.stream()
|
||||||
|
.map(ShopOrderGoods::getGoodsId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (goodsIds.isEmpty()) return false;
|
||||||
|
|
||||||
|
Map<Integer, BigDecimal> goodsDeliveryMoneyMap = shopGoodsService.list(
|
||||||
|
new LambdaQueryWrapper<ShopGoods>()
|
||||||
|
.eq(ShopGoods::getTenantId, tenantId)
|
||||||
|
.in(ShopGoods::getGoodsId, goodsIds)
|
||||||
|
).stream().collect(Collectors.toMap(
|
||||||
|
ShopGoods::getGoodsId,
|
||||||
|
g -> g.getDeliveryMoney() != null ? g.getDeliveryMoney() : BigDecimal.ZERO,
|
||||||
|
(a, b) -> a
|
||||||
|
));
|
||||||
|
|
||||||
|
BigDecimal reward = BigDecimal.ZERO;
|
||||||
|
for (ShopOrderGoods og : orderGoodsList) {
|
||||||
|
Integer goodsId = og.getGoodsId();
|
||||||
|
if (goodsId == null) continue;
|
||||||
|
int qty = og.getTotalNum() == null ? 0 : og.getTotalNum();
|
||||||
|
if (qty <= 0) continue;
|
||||||
|
BigDecimal rawRate = goodsDeliveryMoneyMap.getOrDefault(goodsId, BigDecimal.ZERO);
|
||||||
|
BigDecimal rate = normalizeDeliveryRate(rawRate);
|
||||||
|
if (rate == null || rate.signum() <= 0) continue;
|
||||||
|
BigDecimal unitPrice = og.getPrice() != null ? og.getPrice() : BigDecimal.ZERO;
|
||||||
|
if (unitPrice.signum() <= 0) continue;
|
||||||
|
BigDecimal lineAmount = unitPrice.multiply(BigDecimal.valueOf(qty));
|
||||||
|
reward = reward.add(lineAmount.multiply(rate));
|
||||||
|
}
|
||||||
|
|
||||||
|
reward = reward.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
if (reward.signum() <= 0) return false;
|
||||||
|
|
||||||
|
// 锁定/创建配送员分销账户
|
||||||
|
ShopDealerUser dealerUser = shopDealerUserService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (dealerUser == null) {
|
||||||
|
ShopDealerUser newDealerUser = new ShopDealerUser();
|
||||||
|
newDealerUser.setTenantId(tenantId);
|
||||||
|
newDealerUser.setUserId(riderId);
|
||||||
|
newDealerUser.setType(0);
|
||||||
|
newDealerUser.setIsDelete(0);
|
||||||
|
newDealerUser.setSortNumber(0);
|
||||||
|
newDealerUser.setFirstNum(0);
|
||||||
|
newDealerUser.setSecondNum(0);
|
||||||
|
newDealerUser.setThirdNum(0);
|
||||||
|
newDealerUser.setMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setFreezeMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setTotalMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setCreateTime(now);
|
||||||
|
newDealerUser.setUpdateTime(now);
|
||||||
|
shopDealerUserService.save(newDealerUser);
|
||||||
|
dealerUser = shopDealerUserService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (dealerUser == null) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
|
||||||
|
BigDecimal totalMoneyVal = dealerUser.getTotalMoney() != null ? dealerUser.getTotalMoney() : BigDecimal.ZERO;
|
||||||
|
dealerUser.setMoney(moneyVal.add(reward));
|
||||||
|
dealerUser.setTotalMoney(totalMoneyVal.add(reward));
|
||||||
|
dealerUser.setUpdateTime(now);
|
||||||
|
if (!shopDealerUserService.updateById(dealerUser)) return false;
|
||||||
|
|
||||||
|
ShopDealerCapital cap = new ShopDealerCapital();
|
||||||
|
cap.setUserId(riderId);
|
||||||
|
cap.setOrderNo(orderNo);
|
||||||
|
cap.setFlowType(FLOW_TYPE_DELIVERY_REWARD);
|
||||||
|
cap.setMoney(reward);
|
||||||
|
cap.setComments("配送奖励");
|
||||||
|
cap.setToUserId(order.getUserId());
|
||||||
|
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||||
|
cap.setTenantId(tenantId);
|
||||||
|
cap.setCreateTime(now);
|
||||||
|
cap.setUpdateTime(now);
|
||||||
|
shopDealerCapitalService.save(cap);
|
||||||
|
|
||||||
|
log.info("手动解冻-配送奖励发放成功 - orderNo={}, riderId={}, reward={}", orderNo, riderId, reward);
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解冻单条佣金(从定时任务复制,支持 tenantId 参数)
|
||||||
|
*/
|
||||||
|
private boolean unfreezeOneCapitalIfNeeded(ShopDealerCapital cap, Integer tenantId) {
|
||||||
|
if (cap == null) return false;
|
||||||
|
Integer capitalId = cap.getId();
|
||||||
|
Integer dealerUserId = cap.getUserId();
|
||||||
|
String orderNo = cap.getOrderNo();
|
||||||
|
BigDecimal amount = cap.getMoney();
|
||||||
|
if (capitalId == null || dealerUserId == null || orderNo == null || orderNo.isBlank() || amount == null || amount.signum() <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String markerComment = "佣金解冻(capitalId=" + capitalId + ")";
|
||||||
|
|
||||||
|
// 快速幂等检查
|
||||||
|
boolean already = shopDealerCapitalService.count(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_UNFREEZE)
|
||||||
|
.eq(ShopDealerCapital::getUserId, dealerUserId)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.eq(ShopDealerCapital::getComments, markerComment)
|
||||||
|
) > 0;
|
||||||
|
if (already) return false;
|
||||||
|
|
||||||
|
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
ShopDealerUser dealerUser = shopDealerUserService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerUser::getUserId, dealerUserId)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (dealerUser == null) {
|
||||||
|
log.warn("手动解冻失败:未找到分销账户 - orderNo={}, dealerUserId={}, amount={}", orderNo, dealerUserId, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二次幂等检查
|
||||||
|
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, FLOW_TYPE_UNFREEZE)
|
||||||
|
.eq(ShopDealerCapital::getUserId, dealerUserId)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.eq(ShopDealerCapital::getComments, markerComment)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (existedMarker != null) return false;
|
||||||
|
|
||||||
|
BigDecimal freezeMoney = dealerUser.getFreezeMoney() != null ? dealerUser.getFreezeMoney() : BigDecimal.ZERO;
|
||||||
|
if (freezeMoney.compareTo(amount) < 0) {
|
||||||
|
log.warn("手动解冻失败:冻结金额不足 - orderNo={}, dealerUserId={}, freezeMoney={}, amount={}",
|
||||||
|
orderNo, dealerUserId, freezeMoney, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
|
||||||
|
dealerUser.setFreezeMoney(freezeMoney.subtract(amount));
|
||||||
|
dealerUser.setMoney(moneyVal.add(amount));
|
||||||
|
dealerUser.setUpdateTime(now);
|
||||||
|
if (!shopDealerUserService.updateById(dealerUser)) {
|
||||||
|
log.warn("手动解冻失败:更新分销账户失败 - orderNo={}, dealerUserId={}, amount={}", orderNo, dealerUserId, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShopDealerCapital marker = new ShopDealerCapital();
|
||||||
|
marker.setUserId(dealerUserId);
|
||||||
|
marker.setOrderNo(orderNo);
|
||||||
|
marker.setFlowType(FLOW_TYPE_UNFREEZE);
|
||||||
|
marker.setMoney(amount);
|
||||||
|
marker.setComments(markerComment);
|
||||||
|
marker.setToUserId(cap.getToUserId());
|
||||||
|
marker.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||||
|
marker.setTenantId(tenantId);
|
||||||
|
marker.setCreateTime(now);
|
||||||
|
marker.setUpdateTime(now);
|
||||||
|
shopDealerCapitalService.save(marker);
|
||||||
|
|
||||||
|
log.info("手动解冻成功 - orderNo={}, dealerUserId={}, amount={}, capitalId={}", orderNo, dealerUserId, amount, capitalId);
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal normalizeDeliveryRate(BigDecimal rawRate) {
|
||||||
|
if (rawRate == null || rawRate.signum() <= 0) return null;
|
||||||
|
if (rawRate.compareTo(BigDecimal.ONE) >= 0) return rawRate.movePointLeft(2);
|
||||||
|
return rawRate;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -834,7 +834,6 @@ public class ShopOrderServiceImpl extends ServiceImpl<ShopOrderMapper, ShopOrder
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateByOutTradeNo(ShopOrder order) {
|
public void updateByOutTradeNo(ShopOrder order) {
|
||||||
order.setExpirationTime(null);
|
|
||||||
baseMapper.updateByOutTradeNo(order);
|
baseMapper.updateByOutTradeNo(order);
|
||||||
|
|
||||||
// 处理支付成功后的业务逻辑
|
// 处理支付成功后的业务逻辑
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ server:
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://47.107.249.41:13306/gltdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
url: jdbc:mysql://8.134.55.105:13306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
|
||||||
username: gltdb
|
username: modules
|
||||||
password: EeD4FtzyA5ksj7Bk
|
password: tYmmMGh5wpwXR3ae
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
|
||||||
# redis
|
# redis
|
||||||
redis:
|
redis:
|
||||||
database: 0
|
database: 0
|
||||||
host: 47.107.249.41
|
host: 8.134.55.105
|
||||||
port: 16379
|
port: 16379
|
||||||
password: redis_t74P8C
|
password: redis_t74P8C
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://1Panel-mysql-XsWW:3306/gltdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
url: jdbc:mysql://1Panel-mysql-XsWW:3306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
username: gltdb
|
username: modules
|
||||||
password: EeD4FtzyA5ksj7Bk
|
password: tYmmMGh5wpwXR3ae
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
druid:
|
druid:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ server:
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://47.107.249.41:13306/gltdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
url: jdbc:mysql://1Panel-mysql-XsWW:3306/gltdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
username: gltdb
|
username: gltdb
|
||||||
password: EeD4FtzyA5ksj7Bk
|
password: EeD4FtzyA5ksj7Bk
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
@@ -16,8 +16,8 @@ spring:
|
|||||||
# redis
|
# redis
|
||||||
redis:
|
redis:
|
||||||
database: 0
|
database: 0
|
||||||
host: 8.134.55.105
|
host: 1Panel-redis-GmNr
|
||||||
port: 16379
|
port: 6379
|
||||||
password: redis_t74P8C
|
password: redis_t74P8C
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
# 数据源配置
|
# 数据源配置
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://1Panel-mysql-Bqdt:3306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
url: jdbc:mysql://1Panel-mysql-XsWW:3306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
username: modules
|
username: modules
|
||||||
password: P7KsAyDXG8YdLnkA
|
password: tYmmMGh5wpwXR3ae
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
druid:
|
druid:
|
||||||
@@ -14,9 +14,9 @@ spring:
|
|||||||
# redis
|
# redis
|
||||||
redis:
|
redis:
|
||||||
database: 0
|
database: 0
|
||||||
host: 1Panel-redis-Q1LE
|
host: 1Panel-redis-GmNr
|
||||||
port: 6379
|
port: 6379
|
||||||
password: redis_WSDb88
|
password: redis_t74P8C
|
||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
85
src/main/resources/application-test.yml
Normal file
85
src/main/resources/application-test.yml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 生产环境配置
|
||||||
|
|
||||||
|
# 数据源配置
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://47.107.249.41:13306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
|
username: modules
|
||||||
|
password: tYmmMGh5wpwXR3ae
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
druid:
|
||||||
|
remove-abandoned: true
|
||||||
|
|
||||||
|
# redis
|
||||||
|
redis:
|
||||||
|
database: 0
|
||||||
|
host: 47.107.249.41
|
||||||
|
port: 16379
|
||||||
|
password: redis_t74P8C
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
file:
|
||||||
|
name: websoft-modules.log
|
||||||
|
level:
|
||||||
|
root: WARN
|
||||||
|
com.gxwebsoft: ERROR
|
||||||
|
com.baomidou.mybatisplus: ERROR
|
||||||
|
|
||||||
|
socketio:
|
||||||
|
host: 0.0.0.0 #IP地址
|
||||||
|
|
||||||
|
# MQTT配置
|
||||||
|
mqtt:
|
||||||
|
enabled: false # 启用MQTT服务
|
||||||
|
host: tcp://132.232.214.96:1883
|
||||||
|
username: swdev
|
||||||
|
password: Sw20250523
|
||||||
|
client-id-prefix: hjm_car_
|
||||||
|
topic: /SW_GPS/#
|
||||||
|
qos: 2
|
||||||
|
connection-timeout: 10
|
||||||
|
keep-alive-interval: 20
|
||||||
|
auto-reconnect: true
|
||||||
|
|
||||||
|
# 框架配置
|
||||||
|
config:
|
||||||
|
# 文件服务器
|
||||||
|
file-server: https://file-s209.shoplnk.cn
|
||||||
|
# 生产环境接口
|
||||||
|
server-url: https://glt-server.websoft.top/api
|
||||||
|
# 业务模块接口
|
||||||
|
api-url: https://glt-api.websoft.top/api
|
||||||
|
upload-path: /www/wwwroot/file.ws
|
||||||
|
|
||||||
|
# 阿里云OSS云存储
|
||||||
|
endpoint: https://oss-cn-shenzhen.aliyuncs.com
|
||||||
|
accessKeyId: LTAI4GKGZ9Z2Z8JZ77c3GNZP
|
||||||
|
accessKeySecret: BiDkpS7UXj72HWwDWaFZxiXjNFBNCM
|
||||||
|
bucketName: oss-gxwebsoft
|
||||||
|
bucketDomain: https://oss.wsdns.cn
|
||||||
|
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
|
||||||
|
|
||||||
|
# 生产环境证书配置
|
||||||
|
certificate:
|
||||||
|
load-mode: VOLUME # 生产环境从Docker挂载卷加载
|
||||||
|
cert-root-path: /www/wwwroot/file.ws
|
||||||
|
|
||||||
|
# 支付配置缓存
|
||||||
|
payment:
|
||||||
|
cache:
|
||||||
|
# 支付配置缓存键前缀,生产环境使用 Payment:1* 格式
|
||||||
|
key-prefix: "Payment:1"
|
||||||
|
# 缓存过期时间(小时)
|
||||||
|
expire-hours: 24
|
||||||
|
# 阿里云翻译配置
|
||||||
|
aliyun:
|
||||||
|
translate:
|
||||||
|
access-key-id: LTAI5tEsyhW4GCKbds1qsopg
|
||||||
|
access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO
|
||||||
|
endpoint: mt.cn-hangzhou.aliyuncs.com
|
||||||
|
wechatpay:
|
||||||
|
transfer:
|
||||||
|
scene-id: 1005
|
||||||
|
scene-report-infos-json: '[{"info_type":"岗位类型","info_content":"配送员"},{"info_type":"报酬说明","info_content":"12月份配送费"}]'
|
||||||
@@ -4,7 +4,7 @@ server:
|
|||||||
# 多环境配置
|
# 多环境配置
|
||||||
spring:
|
spring:
|
||||||
profiles:
|
profiles:
|
||||||
active: glt2
|
active: dev
|
||||||
|
|
||||||
application:
|
application:
|
||||||
name: server
|
name: server
|
||||||
|
|||||||
Reference in New Issue
Block a user