Compare commits

22 Commits

Author SHA1 Message Date
1350250847@qq.com
8a22ad771a Merge branch 'dev' into dev_xm 2026-04-28 15:21:05 +08:00
1350250847@qq.com
68d2a99b77 秒杀活动增加商品图片、单位信息 2026-04-28 14:31:29 +08:00
1350250847@qq.com
359c080023 增加活动底图功能 2026-04-28 09:43:58 +08:00
1350250847@qq.com
70b299eda6 代码生成调整ID查询回退为Integer类型 2026-04-27 17:47:04 +08:00
1350250847@qq.com
9eeb0c5682 秒杀活动主键类型切换 2026-04-27 17:45:00 +08:00
1350250847@qq.com
818be01c7c 1.商品下单优化秒杀订单以秒杀价格为准
2.修改水票套票释放逻辑,个人水票发放以次月以1日凌晨为时间节点
3.增加以订单号形式发送水票套票信息
2026-04-27 17:23:08 +08:00
1350250847@qq.com
1ae7a76901 调整秒杀业务ID类型 2026-04-27 17:09:39 +08:00
1350250847@qq.com
95964219a5 优化秒杀活动限购数量业务 2026-04-23 17:17:05 +08:00
1350250847@qq.com
9344f3750c 1.修改数据库链接配置
2.增加系统异常码常量池
3.调整代码生产业务
4.增加秒杀活动业务功能
2026-04-23 15:59:10 +08:00
1575bf504c fix(payment): 修复支付回调地址配置逻辑
- 新增 apiUrl 配置属性,支持通过 API 网关地址访问回调
- 优先使用 apiUrl 拼接回调地址,确保回调服务公网可访问
- 兼容原有 serverUrl 配置,作为备用回调地址使用
- 移除默认注释,明确支付回调地址的选择逻辑
2026-04-21 13:04:35 +08:00
47ae81ca9f fix(wxlogin): 修复scene参数解析与多租户用户查询异常问题
- 修改UserMapper接口,selectByIdIgnoreTenant由返回单用户改为返回用户列表,避免多结果异常
- UserService新增listByIdIgnoreTenant方法,兼容多条用户记录查询
- WxLoginController优先从scene参数直接解析tenantId,兼容旧格式时使用list接口查询用户
- 调整website.getRunning判空,避免空指针异常
- 多处调用处修改获取用户信息的逻辑,防止因多租户导致的查询失败
- 前端三处vue组件调整scene格式为uid_userId_tenantId,确保tenantId正确传递
2026-04-21 12:44:25 +08:00
d9e4371735 feat(shop): 实现分销订单手动触发佣金解冻接口
- 新增ShopDealerOrderController.manualUnfreeze接口,支持通过订单号手动触发佣金解冻
- 在ShopDealerOrderService及实现类添加manualUnfreeze方法,实现手动解冻业务逻辑
- 手动解冻包含分销订单查询、状态校验、关联商城订单与水票套餐校验
- 补充手动解冻中配送奖励发放逻辑,保证幂等,记录详细处理信息
- 丰富手动解冻的异常处理与业务日志,方便排查与追踪
- 优化DealerCommissionUnfreeze10584Task定时任务,增强日志详尽度和流程步骤清晰性
- 对送水订单和非送水订单条件进行精确分类处理,避免误判
- 调整配送奖励发放逻辑,确保任务和手动触发路径一致
- 引入多处日志打印提升监控和调试能力,包括订单过滤、佣金解冻和配送奖励发放情况
2026-04-21 00:04:41 +08:00
eadaa8c4dd docs(memory): 补充分销佣金解冻任务分析与排查日志
- 新增 DealerCommissionUnfreeze10584Task 解冻规则详解
- 说明送水套餐与非送水套餐的不同解冻触发条件
- 解析“已送达”与“已完成”状态区别及影响
- 列出常见未解冻原因及排查优先级
- 增加长期记忆文件中分销佣金解冻相关业务规则和状态流转说明
- 添加2026-04-18排查解冻任务未触发的问题及可能原因
- 更新专家历史记录,新增高级开发工程师吴八哥信息
- 新增生产环境 application-test.yml 配置文件示例
2026-04-18 10:58:38 +08:00
fa5260d583 fix(order): 修改配送员提成直接入账逻辑
- 配送员提成由先入冻结金额 freeze_money 改为直接加入可提现余额 money
- 更新两个 LambdaUpdateWrapper SQL 语句,修改相关字段及注释
- total_money 字段保持累计不变
- 修复 Transaction 类路径和字段结构导致的回调字段映射失败问题
- 优化回调通知配置缓存,避免重复初始化带来的网络请求失败风险
2026-04-16 01:17:23 +08:00
0c4bdc3031 fix(shop-order): 修复支付回调签名验证失败及状态更新问题
- 修正导入 Transaction 类为直连商户模式路径,解决签名验证失败
- 新增按 mchId 缓存 NotificationConfig,避免重复拉取平台证书和重复初始化
- 更新 ShopOrderMapper.xml,增加 update_time 和 expiration_time 字段更新
- 删除 ShopOrderServiceImpl.updateByOutTradeNo 中重置 expirationTime 的代码,确保回调传递值生效
- 补充日志,完善异步通知证书配置流程监控
2026-04-16 00:33:20 +08:00
47ef45054a fix(shop): 修复支付回调状态判断逻辑,确保订单状态更新
- 将支付成功状态判断由字符串比较改为枚举值比较
- 使用 Transaction.TradeStateEnum.SUCCESS 替代 "支付成功" 字符串判断
- 避免因状态描述字符串不一致导致支付回调处理失败
- 保证支付成功后订单状态能够正确更新
2026-04-13 02:16:00 +08:00
9297d13045 fix(shop): 修复支付回调状态判断逻辑,确保订单状态更新
- 将支付成功状态判断由字符串比较改为枚举值比较
- 使用 Transaction.TradeStateEnum.SUCCESS 替代 "支付成功" 字符串判断
- 避免因状态描述字符串不一致导致支付回调处理失败
- 保证支付成功后订单状态能够正确更新
2026-04-13 02:14:34 +08:00
701a135edd chore(config): 更新数据库和Redis连接配置
- 修改application-glt.yml的数据源URL、用户名和密码
- 更新application-prod.yml的数据源URL和密码
- 调整application-prod.yml中Redis主机地址和密码配置
2026-04-13 02:03:37 +08:00
6781374c1e fix(system): 修正登录记录时间格式和更新专家数据
- 为LoginRecord实体的createTime和updateTime字段添加时区配置GMT+8
- 更新.expert-history.json文件,新增高级开发工程师Will的专家信息
- 同步更新lastUpdated时间戳以反映最新变更
2026-04-12 22:09:27 +08:00
7c90f5e8af fix(system): 修正登录记录时间格式和更新专家数据
- 为LoginRecord实体的createTime和updateTime字段添加时区配置GMT+8
- 更新.expert-history.json文件,新增高级开发工程师Will的专家信息
- 同步更新lastUpdated时间戳以反映最新变更
2026-04-12 22:09:24 +08:00
721ce5a595 feat(order): 添加配送方式及相关配送费用字段
- 新增deliveryMethod字段支持配送方式选择(电梯/步梯/一楼商铺)
- 新增deliveryFloor字段记录步梯送上楼时的楼层
- 新增deliveryFee字段计算并保存配送费用
- 在数据库表glt_ticket_order中增加对应字段及注释说明
- 丰富订单实体GltTicketOrder类以支持新配送信息存储和传输
2026-04-12 21:55:16 +08:00
506505bb46 chore(config): 更新开发环境数据库和Redis配置
- 将application.yml中的active profile由glt2改为dev
- 更新application-dev.yml中的MySQL连接信息,包括url、用户名和密码
- 修改Redis服务器地址以匹配新的环境设置
- 添加新文件expert-history.json和MEMORY.md用于记录扩展历史和内存使用情况
2026-04-12 21:31:55 +08:00
59 changed files with 2800 additions and 279 deletions

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

View 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`
**状态**:已修复

View 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`
**状态**:已修复

View 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避免重复初始化。

View 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

View File

@@ -0,0 +1,18 @@
# 2026-04-21 日志
## WxLoginController.getOrderQRCodeUnlimited 修复(完整)
### 根因
1. `extractTenantIdFromScene` 通过 `selectByIdIgnoreTenant` 反查用户获取 tenantIduserId=35280 在多租户下有2条记录 → `TooManyResultsException`
2. 异常被 catch 后 fallback 到默认租户 10550Redis 中无 `mp-weixin:10550` 缓存 → 最终失败
3. 第 452 行 `website.getRunning().equals(2)` 存在 NPE 风险
### 修复内容
- **后端 WxLoginController**: scene 格式改为 `uid_userId_tenantId`,优先从 scene 直接解析 tenantId兼容旧 `uid_userId` 格式时改用 `selectList` 避免多条记录异常
- **后端 UserMapper/UserService**: `selectByIdIgnoreTenant` 返回类型从 `User` 改为 `List<User>`;新增 `listByIdIgnoreTenant` 方法
- **后端 NPE 修复**: `website.getRunning().equals(2)``website != null && Integer.valueOf(2).equals(website.getRunning())`
- **前端 3 个 vue**: scene 从 `uid_${userId}` 改为 `uid_${userId}_${tenantId}`(从 tenantStore.company.tenantId 获取)
- shopDealerUser/index.vue
- shopDealerUserShop/index.vue
- shopDealerUserDelivery/index.vue

View File

@@ -0,0 +1,32 @@
# MEMORY.md - 长期记忆
## 项目概况
- 后端:/Users/gxwebsoft/JAVA/java-10584Spring 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() 不为空,否则整个任务跳过

View File

@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"modern-webapp@cb_teams_marketplace": true
}
}

View File

@@ -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>

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

View File

@@ -2,4 +2,7 @@ package com.gxwebsoft.common.core.constants;
public class BaseConstants { public class BaseConstants {
public static final String[] STATUS = {"未定义","显示","隐藏"}; public static final String[] STATUS = {"未定义","显示","隐藏"};
public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
} }

View File

@@ -0,0 +1,43 @@
package com.gxwebsoft.common.core.enums;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 全局用户类型枚举
*/
@AllArgsConstructor
@Getter
public enum UserTypeEnum {
// 面向 a 端,管理后台
RIDER(0, "骑手"),
// 面向 c 端,普通用户
MEMBER(1, "会员"),
STORE(3, "门店"),
// 面向 b 端,管理后台
ADMIN(2, "管理员"),
CHAT(4, "群聊");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray();
/**
* 类型
*/
private final Integer value;
/**
* 类型名
*/
private final String name;
public static UserTypeEnum valueOf(Integer value) {
return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values());
}
}

View File

@@ -0,0 +1,27 @@
package com.gxwebsoft.common.core.exception;
import lombok.Data;
/**
* 错误码对象
*
* 全局错误码,占用 [0, 999], 参见 {@link com.gxwebsoft.common.core.exception.enums.GlobalErrorCodeConstants}
*
*/
@Data
public class ErrorCode {
/**
* 错误码
*/
private final Integer code;
/**
* 错误提示
*/
private final String msg;
public ErrorCode(Integer code, String message) {
this.code = code;
this.msg = message;
}
}

View File

@@ -0,0 +1,40 @@
package com.gxwebsoft.common.core.exception.enums;
import com.gxwebsoft.common.core.exception.ErrorCode;
/**
* 全局错误码枚举
* 0-999 系统异常编码保留
*
* 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
* 虽然说HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的
* 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。
*
* @author xm
*/
public interface GlobalErrorCodeConstants {
ErrorCode SUCCESS = new ErrorCode(0, "成功");
// ========== 客户端错误段 ==========
ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确");
ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录");
ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限");
ErrorCode NOT_FOUND = new ErrorCode(404, "查询无此数据");
ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确");
ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试");
// ========== 服务端错误段 ==========
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启");
// ========== 自定义错误段 ==========
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作");
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
ErrorCode FINANCE_BILL_NOT_EXISTS = new ErrorCode(600, "门店财务账单不存在");
}

View File

@@ -1,5 +1,6 @@
package com.gxwebsoft.common.core.service; package com.gxwebsoft.common.core.service;
import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.common.system.entity.Payment; import com.gxwebsoft.common.system.entity.Payment;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +27,9 @@ public class EnvironmentAwarePaymentService {
@Value("${config.server-url:}") @Value("${config.server-url:}")
private String serverUrl; private String serverUrl;
@Value("${config.api-url:}")
private String apiUrl;
// 开发环境回调地址配置 // 开发环境回调地址配置
@Value("${payment.dev.notify-url:http://frps-10550.s209.websoft.top/api/shop/shop-order/notify}") @Value("${payment.dev.notify-url:http://frps-10550.s209.websoft.top/api/shop/shop-order/notify}")
private String devNotifyUrl; private String devNotifyUrl;
@@ -73,8 +77,8 @@ public class EnvironmentAwarePaymentService {
// 生产环境使用生产回调地址 // 生产环境使用生产回调地址
return prodNotifyUrl; return prodNotifyUrl;
} else { } else {
// 默认使用配置的服务器地址 // 默认使用 API 网关地址(支付回调需要公网可访问的 API 地址
return serverUrl + "/shop/shop-order/notify"; return (StrUtil.isNotBlank(apiUrl) ? apiUrl : serverUrl) + "/shop/shop-order/notify";
} }
} }

View File

@@ -0,0 +1,29 @@
package com.gxwebsoft.common.core.utils;
import com.gxwebsoft.common.system.entity.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class LoginUserUtil {
/**
* 获取当前登录的user
*
* @return User
*/
public static User getLoginUser() {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Object object = authentication.getPrincipal();
if (object instanceof User) {
return (User) object;
}
}
} catch (Exception e) {
System.out.println(e.getMessage());
return null;
}
return null;
}
}

View File

@@ -40,6 +40,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -449,7 +450,7 @@ public class WxLoginController extends BaseController {
// 判断应用运行状态 // 判断应用运行状态
final CmsWebsite website = cmsWebsiteService.getByTenantId(tenantId); final CmsWebsite website = cmsWebsiteService.getByTenantId(tenantId);
if(website.getRunning().equals(2)){ if(website != null && Integer.valueOf(2).equals(website.getRunning())){
map.put("check_path",false); map.put("check_path",false);
map.put("env_version","trial"); map.put("env_version","trial");
} }
@@ -725,26 +726,43 @@ public class WxLoginController extends BaseController {
/** /**
* 从scene参数中提取租户ID * 从scene参数中提取租户ID
* scene格式可能是: uid_33103 或其他包含用户ID的格式 * scene格式: uid_userId_tenantId优先或 uid_userId兼容旧格式
*/ */
private Integer extractTenantIdFromScene(String scene) { private Integer extractTenantIdFromScene(String scene) {
try { try {
System.out.println("解析scene参数: " + scene); System.out.println("解析scene参数: " + scene);
// 如果scene包含uid_前缀提取用户ID
if (scene != null && scene.startsWith("uid_")) { if (scene != null && scene.startsWith("uid_")) {
String userIdStr = scene.substring(4); // 去掉"uid_"前缀 String content = scene.substring(4); // 去掉"uid_"前缀
Integer userId = Integer.parseInt(userIdStr);
System.out.println("userId = " + userId);
// 根据用户ID查询用户信息获取租户ID // 优先解析 uid_userId_tenantId 格式
User user = userService.getByIdIgnoreTenant(userId); String[] parts = content.split("_");
System.out.println("user = " + user); if (parts.length >= 2) {
if (user != null) { try {
System.out.println("从用户ID " + userId + " 获取到租户ID: " + user.getTenantId()); Integer tenantId = Integer.parseInt(parts[1]);
return user.getTenantId(); System.out.println("从scene直接解析到tenantId = " + tenantId);
} else { return tenantId;
System.err.println("未找到用户ID: " + userId); } catch (NumberFormatException e) {
System.err.println("scene中tenantId格式异常: " + parts[1]);
}
}
// 兼容旧格式 uid_userId根据用户ID查询租户ID
if (parts.length == 1) {
Integer userId = Integer.parseInt(parts[0]);
System.out.println("userId = " + userId);
try {
List<User> users = userService.listByIdIgnoreTenant(userId);
System.out.println("查询到用户数量 = " + (users != null ? users.size() : 0));
if (users != null && !users.isEmpty()) {
System.out.println("从用户ID " + userId + " 获取到租户ID: " + users.get(0).getTenantId());
return users.get(0).getTenantId();
} else {
System.err.println("未找到用户ID: " + userId);
}
} catch (Exception ex) {
System.err.println("查询用户异常: " + ex.getMessage());
}
} }
} }

View File

@@ -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")

View File

@@ -60,7 +60,7 @@ public interface UserMapper extends BaseMapper<User> {
* @return User * @return User
*/ */
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
User selectByIdIgnoreTenant(@Param("userId") Integer userId); List<User> selectByIdIgnoreTenant(@Param("userId") Integer userId);
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
List<User> pageAdminByPhone(@Param("param") UserParam param); List<User> pageAdminByPhone(@Param("param") UserParam param);

View File

@@ -117,6 +117,11 @@ public interface UserService extends IService<User>, UserDetailsService {
*/ */
User getByIdIgnoreTenant(Integer userId); User getByIdIgnoreTenant(Integer userId);
/**
* 根据用户ID查询用户列表忽略租户隔离
*/
List<User> listByIdIgnoreTenant(Integer userId);
List<User> pageAdminByPhone(UserParam param); List<User> pageAdminByPhone(UserParam param);
List<User> listByAlert(); List<User> listByAlert();

View File

@@ -2,6 +2,9 @@ package com.gxwebsoft.common.system.service.impl;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import java.util.Collections;
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -224,6 +227,15 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
if (userId == null) { if (userId == null) {
return null; return null;
} }
List<User> users = baseMapper.selectByIdIgnoreTenant(userId);
return users != null && !users.isEmpty() ? users.get(0) : null;
}
@Override
public List<User> listByIdIgnoreTenant(Integer userId) {
if (userId == null) {
return Collections.emptyList();
}
return baseMapper.selectByIdIgnoreTenant(userId); return baseMapper.selectByIdIgnoreTenant(userId);
} }

View File

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

View File

@@ -2,6 +2,7 @@ package com.gxwebsoft.glt.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.glt.entity.GltTicketTemplate; import com.gxwebsoft.glt.entity.GltTicketTemplate;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog; import com.gxwebsoft.glt.entity.GltUserTicketLog;
@@ -12,17 +13,18 @@ import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.ShopOrderService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.util.ArrayList; import java.util.*;
import java.util.HashSet; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/** /**
* 套票发放(从订单生成用户套票 + 释放计划)的业务逻辑。 * 套票发放(从订单生成用户套票 + 释放计划)的业务逻辑。
@@ -128,6 +130,57 @@ public class GltTicketIssueService {
tenantId, uniqueGoodsIds, orders.size(), success, skipped, failed); tenantId, uniqueGoodsIds, orders.size(), success, skipped, failed);
} }
@Async
// @Scheduled(cron = "0/1 * 4-22 * * ?") 没秒钟执行一次
public void paySuccessExecute(String orderNo, Integer tenantId){
suerTicketRelease(orderNo, tenantId);
}
/**
* 订单支付成功,直接发送水票【后期优化订单类型,为水票的订单才需要执行此业务】
* @param orderNo 订单号
* @param tenantId 租户ID
*/
public void suerTicketRelease(String orderNo, Integer tenantId){
//1.订单为空跳过执行
ShopOrder shopOrder = shopOrderService.getByOrderNo(orderNo, tenantId);
if(shopOrder == null){
return;
}
//2.跳过已完成发放套票订单
if(shopOrder.getOrderStatus() == 1){
return;
}
//3.订单商品为空跳过执行
List<ShopOrderGoods> goodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(shopOrder.getOrderId());
if (CollectionUtils.isEmpty(goodsList)) {
return;
}
//4.执行水票发放业务【】
AtomicBoolean release = new AtomicBoolean(false);
goodsList.forEach(orderGood ->{
IssueOutcome outcome = transactionTemplate.execute(status -> doIssueOne(tenantId, shopOrder, orderGood));
if(Arrays.asList(IssueOutcome.ISSUED, IssueOutcome.ALREADY_ISSUED).contains(outcome)){
release.set(true);
}
});
//5.更新商品订单为已完成、已收到赠品状态
if (release.get()) {
shopOrderService.update(new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, shopOrder.getOrderId())
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getOrderStatus, 0)
.set(ShopOrder::getOrderStatus, 1)
.set(ShopOrder::getHasTakeGift, true)
.set(ShopOrder::getUpdateTime, LocalDateTime.now())
);
}
}
private int issueForOrder(Integer tenantId, Set<Integer> goodsIds, ShopOrder order) { private int issueForOrder(Integer tenantId, Set<Integer> goodsIds, ShopOrder order) {
List<ShopOrderGoods> goodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(order.getOrderId()); List<ShopOrderGoods> goodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(order.getOrderId());
if (goodsList == null || goodsList.isEmpty()) { if (goodsList == null || goodsList.isEmpty()) {
@@ -304,12 +357,14 @@ public class GltTicketIssueService {
// 若启用了 releasePeriods 且首期释放时机为“支付成功当刻”,则将首期释放量直接计入可用, // 若启用了 releasePeriods 且首期释放时机为“支付成功当刻”,则将首期释放量直接计入可用,
// 避免用户刚购买后短时间内无可用水票;后续期数仍由自动释放任务按 release_time 释放。 // 避免用户刚购买后短时间内无可用水票;后续期数仍由自动释放任务按 release_time 释放。
if (useReleasePeriods && !releases.isEmpty() && !Objects.equals(template.getFirstReleaseMode(), 1)) { // if (useReleasePeriods && !releases.isEmpty() && !Objects.equals(template.getFirstReleaseMode(), 1)) {
if (!releases.isEmpty() && !Objects.equals(template.getFirstReleaseMode(), 1)) {
GltUserTicketRelease first = releases.get(0); GltUserTicketRelease first = releases.get(0);
Integer firstQtyObj = first.getReleaseQty(); Integer firstQtyObj = first.getReleaseQty();
LocalDateTime firstTime = first.getReleaseTime(); LocalDateTime firstTime = first.getReleaseTime();
int firstQty = firstQtyObj != null ? firstQtyObj : 0; int firstQty = firstQtyObj != null ? firstQtyObj : 0;
if (firstQty > 0 && (firstTime == null || !firstTime.isAfter(now))) { // if (firstQty > 0 && (firstTime == null || !firstTime.isAfter(now))) {
if (firstQty > 0) {
first.setStatus(1); first.setStatus(1);
first.setUpdateTime(now); first.setUpdateTime(now);
@@ -376,10 +431,13 @@ public class GltTicketIssueService {
// 首期释放时间 // 首期释放时间
LocalDateTime firstReleaseTime; LocalDateTime firstReleaseTime;
LocalDateTime referenceTime;
if (Objects.equals(template.getFirstReleaseMode(), 1)) { if (Objects.equals(template.getFirstReleaseMode(), 1)) {
firstReleaseTime = nextMonthSameDay(baseTime); firstReleaseTime = nextMonthSameDay(baseTime);
referenceTime = firstReleaseTime.withDayOfMonth(1).toLocalDate().atStartOfDay();
} else { } else {
firstReleaseTime = baseTime; firstReleaseTime = baseTime;
referenceTime = firstReleaseTime.withDayOfMonth(1).toLocalDate().atStartOfDay();
} }
// 每期释放数量计算 // 每期释放数量计算
@@ -393,7 +451,11 @@ public class GltTicketIssueService {
if (qty <= 0) { if (qty <= 0) {
continue; continue;
} }
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now)); if(i == 0){
list.add(buildRelease(userTicket, i, qty, firstReleaseTime, now));
}else {
list.add(buildRelease(userTicket, i, qty, referenceTime.plusMonths(i), now));
}
} }
return list; return list;
} }
@@ -410,7 +472,11 @@ public class GltTicketIssueService {
break; break;
} }
remaining -= qty; remaining -= qty;
list.add(buildRelease(userTicket, i, qty, firstReleaseTime.plusMonths(i), now)); if(i == 0){
list.add(buildRelease(userTicket, i, qty, firstReleaseTime, now));
}else {
list.add(buildRelease(userTicket, i, qty, referenceTime.plusMonths(i), now));
}
} }
return list; return list;

View File

@@ -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)
); );
@@ -878,7 +878,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
newDealerUser.setFreezeMoney(BigDecimal.ZERO); newDealerUser.setFreezeMoney(BigDecimal.ZERO);
newDealerUser.setTotalMoney(BigDecimal.ZERO); newDealerUser.setTotalMoney(BigDecimal.ZERO);
try { try {
User sysUser = userMapper.selectByIdIgnoreTenant(riderId); List<User> sysUsers = userMapper.selectByIdIgnoreTenant(riderId);
User sysUser = (sysUsers != null && !sysUsers.isEmpty()) ? sysUsers.get(0) : null;
if (sysUser != null) { if (sysUser != null) {
newDealerUser.setRealName(sysUser.getRealName() != null ? sysUser.getRealName() : sysUser.getNickname()); newDealerUser.setRealName(sysUser.getRealName() != null ? sysUser.getRealName() : sysUser.getNickname());
newDealerUser.setMobile(sysUser.getPhone()); newDealerUser.setMobile(sysUser.getPhone());
@@ -895,7 +896,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)
); );

View File

@@ -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);

View File

@@ -620,7 +620,8 @@ public class DealerOrderSettlement10584Task {
newDealerUser.setTotalMoney(BigDecimal.ZERO); newDealerUser.setTotalMoney(BigDecimal.ZERO);
// 尽量补齐基础信息,避免表字段 NOT NULL 导致插入失败(插入失败会让门店分佣“找到了人但入不了账”)。 // 尽量补齐基础信息,避免表字段 NOT NULL 导致插入失败(插入失败会让门店分佣“找到了人但入不了账”)。
try { try {
User sysUser = userMapper.selectByIdIgnoreTenant(dealerUserId); List<User> sysUsers = userMapper.selectByIdIgnoreTenant(dealerUserId);
User sysUser = (sysUsers != null && !sysUsers.isEmpty()) ? sysUsers.get(0) : null;
if (sysUser != null) { if (sysUser != null) {
newDealerUser.setRealName(sysUser.getRealName() != null ? sysUser.getRealName() : sysUser.getNickname()); newDealerUser.setRealName(sysUser.getRealName() != null ? sysUser.getRealName() : sysUser.getNickname());
newDealerUser.setMobile(sysUser.getPhone()); newDealerUser.setMobile(sysUser.getPhone());

View File

@@ -0,0 +1,127 @@
package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.shop.service.ShopActiveImageService;
import com.gxwebsoft.shop.entity.ShopActiveImage;
import com.gxwebsoft.shop.param.ShopActiveImageParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* 推广码底图控制器
*
* @author xm
* @since 2026-04-27 18:02:18
*/
@Tag(name = "推广码底图管理")
@RestController
@RequestMapping("/api/shop/shop-active-image")
public class ShopActiveImageController extends BaseController {
@Resource
private ShopActiveImageService shopActiveImageService;
// @PreAuthorize("hasAuthority('shop:shopActiveImage:list')")
@Operation(summary = "分页查询推广码底图")
@GetMapping("/page")
public ApiResult<PageResult<ShopActiveImage>> page(ShopActiveImageParam param) {
// 使用关联查询
return success(shopActiveImageService.pageRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopActiveImage:list')")
@Operation(summary = "查询全部推广码底图")
@GetMapping()
public ApiResult<List<ShopActiveImage>> list(ShopActiveImageParam param) {
// 使用关联查询
return success(shopActiveImageService.listRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopActiveImage:list')")
@Operation(summary = "根据id查询推广码底图")
@GetMapping("/{id}")
public ApiResult<ShopActiveImage> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopActiveImageService.getByIdRel(id));
}
// @PreAuthorize("hasAuthority('shop:shopActiveImage:save')")
@OperationLog
@Operation(summary = "添加推广码底图")
@PostMapping()
public ApiResult<?> save(@RequestBody ShopActiveImage shopActiveImage) {
shopActiveImage.setCreator(String.valueOf(getLoginUserId()));
shopActiveImage.setCreateTime(LocalDateTime.now());
if (shopActiveImageService.save(shopActiveImage)) {
return success("添加成功");
}
return fail("添加失败");
}
// @PreAuthorize("hasAuthority('shop:shopActiveImage:update')")
@OperationLog
@Operation(summary = "修改推广码底图")
@PutMapping()
public ApiResult<?> update(@RequestBody ShopActiveImage shopActiveImage) {
shopActiveImage.setUpdater(String.valueOf(getLoginUserId()));
shopActiveImage.setUpdateTime(LocalDateTime.now());
if (shopActiveImageService.updateById(shopActiveImage)) {
return success("修改成功");
}
return fail("修改失败");
}
// @PreAuthorize("hasAuthority('shop:shopActiveImage:remove')")
@OperationLog
@Operation(summary = "删除推广码底图")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (shopActiveImageService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('shop:shopActiveImage:save')")
@OperationLog
@Operation(summary = "批量添加推广码底图")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ShopActiveImage> list) {
if (shopActiveImageService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('shop:shopActiveImage:update')")
@OperationLog
@Operation(summary = "批量修改推广码底图")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopActiveImage> batchParam) {
if (batchParam.update(shopActiveImageService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
// @PreAuthorize("hasAuthority('shop:shopActiveImage:remove')")
@OperationLog
@Operation(summary = "批量删除推广码底图")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (shopActiveImageService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

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

View File

@@ -0,0 +1,156 @@
package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopFlashSaleActivity;
import com.gxwebsoft.shop.param.ShopFlashSaleActivityParam;
import com.gxwebsoft.shop.service.ShopFlashSaleActivityService;
import com.gxwebsoft.shop.vo.ShopFlashSaleActivityVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 秒杀活动控制器
*
* @author xm
* @since 2026-04-22 17:18:18
*/
@Tag(name = "秒杀活动管理")
@RestController
@RequestMapping("/api/shop/shop-flash-sale-activity")
public class ShopFlashSaleActivityController extends BaseController {
@Resource
private ShopFlashSaleActivityService shopFlashSaleActivityService;
// @PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:list')")
@Operation(summary = "后台分页查询秒杀活动")
@GetMapping("/page")
public ApiResult<PageResult<ShopFlashSaleActivityVO>> page(ShopFlashSaleActivityParam param) {
// 使用关联查询
return success(shopFlashSaleActivityService.pageRel(param));
}
@Operation(summary = "个人获取秒杀活动数据")
@GetMapping("/getMyActive")
public ApiResult<List<ShopFlashSaleActivityVO>> getMyActive(@RequestParam("tenantId") Integer tenantId) {
// 使用关联查询
return success(shopFlashSaleActivityService.getMyActive(tenantId));
}
// @PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:list')")
@Operation(summary = "查询全部秒杀活动")
@GetMapping()
public ApiResult<List<ShopFlashSaleActivity>> list(ShopFlashSaleActivityParam param) {
// 使用关联查询
return success(shopFlashSaleActivityService.listRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:list')")
@Operation(summary = "根据id查询秒杀活动")
@GetMapping("/{id}")
public ApiResult<ShopFlashSaleActivity> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopFlashSaleActivityService.getByIdRel(id));
}
// @PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:save')")
@OperationLog
@Operation(summary = "添加秒杀活动")
@PostMapping()
public ApiResult<?> save(@RequestBody ShopFlashSaleActivity shopFlashSaleActivity) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
shopFlashSaleActivity.setCreator(loginUser.getUserId().toString());
}
if (shopFlashSaleActivityService.save(shopFlashSaleActivity)) {
return success("添加成功");
}
return fail("添加失败");
}
// @PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:update')")
@OperationLog
@Operation(summary = "修改秒杀活动")
@PutMapping()
public ApiResult<?> update(@RequestBody ShopFlashSaleActivity shopFlashSaleActivity) {
shopFlashSaleActivity.setUpdater(String.valueOf(getLoginUserId()));
if (shopFlashSaleActivityService.updateById(shopFlashSaleActivity)) {
return success("修改成功");
}
return fail("修改失败");
}
@OperationLog
@Operation(summary = "开启/关闭秒杀活动状态")
@PutMapping("/updateStatus")
public ApiResult<?> updateStatus(@RequestParam("id") Integer id) {
return success(shopFlashSaleActivityService.updateStatus(id));
}
@Operation(summary = "修改秒杀活动排序")
@PutMapping("/updateSortNumber")
@Parameters({
@Parameter(name = "id", description = "活动ID", required = true, example = "1"),
@Parameter(name = "sortNumber", description = "排序", required = true, example = "2")
})
public ApiResult<?> updateSortNumber(@RequestParam("id") Integer id, @RequestParam("sortNumber") Integer sortNumber) {
return success(shopFlashSaleActivityService.updateSortNumber(id, sortNumber));
}
// @PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:remove')")
@OperationLog
@Operation(summary = "删除秒杀活动")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (shopFlashSaleActivityService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:save')")
@OperationLog
@Operation(summary = "批量添加秒杀活动")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<ShopFlashSaleActivity> list) {
if (shopFlashSaleActivityService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:update')")
@OperationLog
@Operation(summary = "批量修改秒杀活动")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopFlashSaleActivity> batchParam) {
if (batchParam.update(shopFlashSaleActivityService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('shop:shopFlashSaleActivity:remove')")
@OperationLog
@Operation(summary = "批量删除秒杀活动")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (shopFlashSaleActivityService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -13,6 +13,7 @@ import com.gxwebsoft.common.core.utils.WechatCertAutoConfig;
import com.gxwebsoft.common.core.utils.WechatPayConfigValidator; import com.gxwebsoft.common.core.utils.WechatPayConfigValidator;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.Payment; import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.glt.service.GltTicketIssueService;
import com.gxwebsoft.shop.entity.ShopOrderDelivery; import com.gxwebsoft.shop.entity.ShopOrderDelivery;
import com.gxwebsoft.shop.entity.ShopUserAddress; import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.*; import com.gxwebsoft.shop.service.*;
@@ -35,7 +36,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 +54,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 +67,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
@@ -107,6 +111,8 @@ public class ShopOrderController extends BaseController {
private GltTicketRevokeService gltTicketRevokeService; private GltTicketRevokeService gltTicketRevokeService;
@Resource @Resource
private ShopDealerCommissionRollbackService shopDealerCommissionRollbackService; private ShopDealerCommissionRollbackService shopDealerCommissionRollbackService;
@Resource
private GltTicketIssueService gltTicketIssueService;
@Operation(summary = "分页查询订单") @Operation(summary = "分页查询订单")
@GetMapping("/page") @GetMapping("/page")
@@ -760,7 +766,7 @@ public class ShopOrderController extends BaseController {
@Schema(description = "异步通知11") @Schema(description = "异步通知11")
@PostMapping("/notify/{tenantId}") @PostMapping("/notify/{tenantId}")
public String wxNotify(@RequestHeader Map<String, String> header, @RequestBody String body, @PathVariable("tenantId") Integer tenantId) { public String wxNotify(@RequestHeader Map<String, String> header, @RequestBody String body, @PathVariable("tenantId") Integer tenantId) {
logger.info("异步通知*************** = " + tenantId); logger.info("异步通知*************** = " + body + ",租户:" +tenantId);
// 获取支付配置信息用于解密 // 获取支付配置信息用于解密
String key = "Payment:1:".concat(tenantId.toString()); String key = "Payment:1:".concat(tenantId.toString());
@@ -791,90 +797,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
@@ -886,9 +902,10 @@ public class ShopOrderController extends BaseController {
logger.info("开始解析微信支付异步通知..."); logger.info("开始解析微信支付异步通知...");
Transaction transaction = parser.parse(requestParam, Transaction.class); Transaction transaction = parser.parse(requestParam, Transaction.class);
logger.info("✅ 异步通知解析成功 - 交易状态: {}, 商户订单号: {}", logger.info("✅ 异步通知解析成功 - 交易状态: {}, 商户订单号: {}",
transaction.getTradeStateDesc(), transaction.getOutTradeNo()); transaction.getTradeState(), 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();
@@ -920,6 +937,10 @@ public class ShopOrderController extends BaseController {
System.out.println("实际付款金额 = " + order.getPayPrice()); System.out.println("实际付款金额 = " + order.getPayPrice());
// 更新订单状态并处理支付成功后的业务逻辑(包括累加商品销量) // 更新订单状态并处理支付成功后的业务逻辑(包括累加商品销量)
shopOrderService.updateByOutTradeNo(order); shopOrderService.updateByOutTradeNo(order);
// //支付成功执行一步任务
// gltTicketIssueService.paySuccessExecute(order.getOrderNo(), tenantId);
return "SUCCESS"; return "SUCCESS";
} }
} }

View File

@@ -147,6 +147,9 @@ public class OrderCreateRequest {
@NotNull(message = "租户ID不能为空") @NotNull(message = "租户ID不能为空")
private Integer tenantId; private Integer tenantId;
@Schema(description = "秒杀活动ID")
private Integer activityId;
@Schema(description = "订单商品列表") @Schema(description = "订单商品列表")
@Valid @Valid
@NotEmpty(message = "订单商品列表不能为空") @NotEmpty(message = "订单商品列表不能为空")
@@ -158,6 +161,9 @@ public class OrderCreateRequest {
@Data @Data
@Schema(name = "OrderGoodsItem", description = "订单商品项") @Schema(name = "OrderGoodsItem", description = "订单商品项")
public static class OrderGoodsItem { public static class OrderGoodsItem {
@Schema(description = "秒杀活动ID")
private Integer activityId;
@Schema(description = "商品ID", required = true) @Schema(description = "商品ID", required = true)
@NotNull(message = "商品ID不能为空") @NotNull(message = "商品ID不能为空")
private Integer goodsId; private Integer goodsId;

View File

@@ -0,0 +1,71 @@
package com.gxwebsoft.shop.entity;
import com.baomidou.mybatisplus.annotation.*;
import java.time.LocalDateTime;
import java.io.Serializable;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
/**
* 推广码底图
*
* @author xm
* @since 2026-04-27 18:02:17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ShopActiveImage对象", description = "推广码底图")
@TableName("shop_active_image")
public class ShopActiveImage implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "名称")
private String name;
@Schema(description = "类型 0-推广底图 1-其他")
private Integer type;
@Schema(description = "图片地址,多个以‘,’隔开")
private String imgUrl;
@Schema(description = "图片地址集合")
@TableField(exist = false)
private List<String> imgUrlList;
@Schema(description = "启用状态 0-启用 1-禁用")
private Integer status;
@Schema(description = "排序")
private Integer sortNumber;
@Schema(description = "租户ID")
@NotNull(message = "租户ID不能为空")
private Integer tenantId;
@Schema(description = "创建人")
private String creator;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新人")
private String updater;
@Schema(description = "修改时间")
private LocalDateTime updateTime;
@Schema(description = "是否删除 0-未删 1-已删")
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,101 @@
package com.gxwebsoft.shop.entity;
import com.baomidou.mybatisplus.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.gxwebsoft.common.core.constants.BaseConstants;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 秒杀活动
*
* @author xm
* @since 2026-04-22 17:18:17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "ShopFlashSaleActivity对象", description = "秒杀活动")
@TableName("shop_flash_sale_activity")
public class ShopFlashSaleActivity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "秒杀活动编号")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "秒杀活动名称")
private String name;
@Schema(description = "秒杀活动商品")
private Integer goodsId;
@Schema(description = "商品skuId")
private Integer skuId;
@Schema(description = "商品数量")
private Integer num;
@Schema(description = "秒杀活动商品价格")
private BigDecimal price;
@Schema(description = "活动状态 0-开启 1-关闭")
private Integer status;
@Schema(
description = "活动开始时间",
type = "string",
pattern = BaseConstants.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND,
example = "2026-04-01 12:00:00"
)
private LocalDateTime startTime;
@Schema(
description = "活动结束时间",
type = "string",
pattern = BaseConstants.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND,
example = "2027-01-01 12:00:00"
)
private LocalDateTime endTime;
@Schema(description = "活动限购数量")
private Integer saleLimit;
@Schema(description = "库存")
private Integer stock;
@Schema(description = "展示类型0普通用户1新用户")
private Integer displayType;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sortNumber;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建者")
private String creator;
@Schema(description = "创建时间", pattern = BaseConstants.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime createTime;
@Schema(description = "更新者")
private String updater;
@Schema(description = "更新时间")
@DateTimeFormat(pattern = BaseConstants.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime updateTime;
@Schema(description = "是否删除")
@TableLogic
private Integer deleted;
}

View File

@@ -306,6 +306,9 @@ public class ShopOrder implements Serializable {
@NotNull(message = "租户ID不能为空") @NotNull(message = "租户ID不能为空")
private Integer tenantId; private Integer tenantId;
@Schema(description = "秒杀活动ID")
private Integer activityId;
@Schema(description = "修改时间") @Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private LocalDateTime updateTime;

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.shop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.shop.entity.ShopActiveImage;
import com.gxwebsoft.shop.param.ShopActiveImageParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 推广码底图Mapper
*
* @author xm
* @since 2026-04-27 18:02:17
*/
public interface ShopActiveImageMapper extends BaseMapper<ShopActiveImage> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ShopActiveImage>
*/
List<ShopActiveImage> selectPageRel(@Param("page") IPage<ShopActiveImage> page,
@Param("param") ShopActiveImageParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ShopActiveImage> selectListRel(@Param("param") ShopActiveImageParam param);
}

View File

@@ -0,0 +1,38 @@
package com.gxwebsoft.shop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.shop.entity.ShopFlashSaleActivity;
import com.gxwebsoft.shop.param.ShopFlashSaleActivityParam;
import com.gxwebsoft.shop.vo.ShopFlashSaleActivityVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 秒杀活动Mapper
*
* @author xm
* @since 2026-04-22 17:18:17
*/
public interface ShopFlashSaleActivityMapper extends BaseMapper<ShopFlashSaleActivity> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<ShopFlashSaleActivity>
*/
List<ShopFlashSaleActivityVO> selectPageRel(@Param("page") IPage<ShopFlashSaleActivity> page,
@Param("param") ShopFlashSaleActivityParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<ShopFlashSaleActivity> selectListRel(@Param("param") ShopFlashSaleActivityParam param);
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.shop.mapper.ShopActiveImageMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM shop_active_image a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.name != null">
AND a.name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.type != null">
AND a.type = #{param.type}
</if>
<if test="param.imgUrl != null">
AND a.img_url LIKE CONCAT('%', #{param.imgUrl}, '%')
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.creator != null">
AND a.creator LIKE CONCAT('%', #{param.creator}, '%')
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.updater != null">
AND a.updater LIKE CONCAT('%', #{param.updater}, '%')
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.shop.entity.ShopActiveImage">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.shop.entity.ShopActiveImage">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.shop.mapper.ShopFlashSaleActivityMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*
FROM shop_flash_sale_activity a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.name != null">
AND a.name LIKE CONCAT('%', #{param.name}, '%')
</if>
<if test="param.goodsId != null">
AND a.goods_id LIKE CONCAT('%', #{param.goodsId}, '%')
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.startTime != null">
AND a.start_time LIKE CONCAT('%', #{param.startTime}, '%')
</if>
<if test="param.endTime != null">
AND a.end_time LIKE CONCAT('%', #{param.endTime}, '%')
</if>
<if test="param.stock != null">
AND a.stock = #{param.stock}
</if>
<if test="param.displayType != null">
AND a.display_type = #{param.displayType}
</if>
<if test="param.remark != null">
AND a.remark LIKE CONCAT('%', #{param.remark}, '%')
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.creator != null">
AND a.creator LIKE CONCAT('%', #{param.creator}, '%')
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.updater != null">
AND a.updater LIKE CONCAT('%', #{param.updater}, '%')
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.shop.vo.ShopFlashSaleActivityVO">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.shop.entity.ShopFlashSaleActivity">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -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>

View File

@@ -0,0 +1,56 @@
package com.gxwebsoft.shop.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 推广码底图查询参数
*
* @author xm
* @since 2026-04-27 18:02:17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "ShopActiveImageParam对象", description = "推广码底图查询参数")
public class ShopActiveImageParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "名称")
private String name;
@Schema(description = "类型 0-推广底图 1-其他")
@QueryField(type = QueryType.EQ)
private Integer type;
@Schema(description = "图片地址")
private String imgUrl;
@Schema(description = "启用状态 0-启用 1-禁用")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "排序")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "创建人")
private String creator;
@Schema(description = "更新人")
private String updater;
@Schema(description = "是否删除 0-未删 1-已删")
@QueryField(type = QueryType.EQ)
private Integer deleted;
}

View File

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

View File

@@ -0,0 +1,82 @@
package com.gxwebsoft.shop.param;
import java.math.BigDecimal;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 秒杀活动查询参数
*
* @author xm
* @since 2026-04-22 17:18:17
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "ShopFlashSaleActivityParam对象", description = "秒杀活动查询参数")
public class ShopFlashSaleActivityParam extends BaseParam {
private static final long serialVersionUID = 1L;
@Schema(description = "秒杀活动编号")
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "秒杀活动名称")
private String name;
@Schema(description = "秒杀活动商品")
private Integer goodsId;
@Schema(description = "商品skuId")
private Integer skuId;
@Schema(description = "商品数量")
private Integer num;
@Schema(description = "秒杀活动商品价格")
private BigDecimal price;
@Schema(description = "活动状态 0-开启 1-关闭")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "活动开始时间")
private String startTime;
@Schema(description = "活动结束时间")
private String endTime;
@Schema(description = "活动限购数量")
private Integer saleLimit;
@Schema(description = "库存")
@QueryField(type = QueryType.EQ)
private Integer stock;
@Schema(description = "展示类型0普通用户1新用户")
@QueryField(type = QueryType.EQ)
private Integer displayType;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "创建者")
private String creator;
@Schema(description = "更新者")
private String updater;
@Schema(description = "是否删除")
@QueryField(type = QueryType.EQ)
private Integer deleted;
}

View File

@@ -3,10 +3,12 @@ package com.gxwebsoft.shop.service;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.gxwebsoft.common.core.exception.BusinessException; import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.exception.enums.GlobalErrorCodeConstants;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.config.OrderConfigProperties; import com.gxwebsoft.shop.config.OrderConfigProperties;
import com.gxwebsoft.shop.dto.OrderCreateRequest; import com.gxwebsoft.shop.dto.OrderCreateRequest;
import com.gxwebsoft.shop.entity.*; import com.gxwebsoft.shop.entity.*;
import com.gxwebsoft.shop.mapper.ShopFlashSaleActivityMapper;
import com.gxwebsoft.shop.service.ShopStoreFenceService; import com.gxwebsoft.shop.service.ShopStoreFenceService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
@@ -52,11 +54,16 @@ public class OrderBusinessService {
@Resource @Resource
private ShopUserAddressService shopUserAddressService; private ShopUserAddressService shopUserAddressService;
@Resource @Resource
private ShopUserCouponService shopUserCouponService; private ShopUserCouponService shopUserCouponService;
@Resource @Resource
private ShopStoreFenceService shopStoreFenceService; private ShopStoreFenceService shopStoreFenceService;
@Resource
private ShopFlashSaleActivityMapper shopFlashSaleActivityMapper;
/** /**
* 创建订单 * 创建订单
* *
@@ -167,6 +174,8 @@ public class OrderBusinessService {
BigDecimal total = BigDecimal.ZERO; BigDecimal total = BigDecimal.ZERO;
for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) { for (OrderCreateRequest.OrderGoodsItem item : request.getGoodsItems()) {
Integer activityId = item.getActivityId();
// 验证商品ID // 验证商品ID
if (item.getGoodsId() == null) { if (item.getGoodsId() == null) {
throw new BusinessException("商品ID不能为空"); throw new BusinessException("商品ID不能为空");
@@ -216,6 +225,25 @@ public class OrderBusinessService {
productName = goods.getName() + "(" + (item.getSpecInfo() != null ? item.getSpecInfo() : sku.getSku()) + ")"; productName = goods.getName() + "(" + (item.getSpecInfo() != null ? item.getSpecInfo() : sku.getSku()) + ")";
} }
//秒杀商品价格以秒杀价为准
if(activityId != null){
request.setActivityId(activityId);
ShopFlashSaleActivity saleActivity = shopFlashSaleActivityMapper.selectById(activityId);
if(saleActivity == null){
throw new BusinessException("秒杀活动数据查询失败!");
}
if(saleActivity.getStatus() != 0){
throw new BusinessException("当前秒杀活动已失效!");
}
if(saleActivity.getStock() <= 0){
throw new BusinessException("当前秒杀活动商品已售罄!");
}
if(item.getQuantity() > saleActivity.getSaleLimit()){
throw new BusinessException("选购数量已超秒杀活动限购数量!");
}
actualPrice = saleActivity.getPrice();
}
// 验证实际价格 // 验证实际价格
if (actualPrice == null || actualPrice.compareTo(BigDecimal.ZERO) <= 0) { if (actualPrice == null || actualPrice.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("商品价格异常:" + productName); throw new BusinessException("商品价格异常:" + productName);
@@ -651,6 +679,13 @@ public class OrderBusinessService {
} }
} }
//秒杀商品价格以秒杀价为准
Integer activityId = item.getActivityId();
if(activityId != null){
ShopFlashSaleActivity saleActivity = shopFlashSaleActivityMapper.selectById(activityId);
actualPrice = saleActivity.getPrice();
}
// 验证库存 // 验证库存
if (actualStock == null || actualStock < item.getQuantity()) { if (actualStock == null || actualStock < item.getQuantity()) {
String stockMsg = sku != null ? "商品规格库存不足" : "商品库存不足"; String stockMsg = sku != null ? "商品规格库存不足" : "商品库存不足";

View File

@@ -0,0 +1,42 @@
package com.gxwebsoft.shop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopActiveImage;
import com.gxwebsoft.shop.param.ShopActiveImageParam;
import java.util.List;
/**
* 推广码底图Service
*
* @author xm
* @since 2026-04-27 18:02:17
*/
public interface ShopActiveImageService extends IService<ShopActiveImage> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<ShopActiveImage>
*/
PageResult<ShopActiveImage> pageRel(ShopActiveImageParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<ShopActiveImage>
*/
List<ShopActiveImage> listRel(ShopActiveImageParam param);
/**
* 根据id查询
*
* @param id 主键ID
* @return ShopActiveImage
*/
ShopActiveImage getByIdRel(Integer id);
}

View File

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

View File

@@ -0,0 +1,65 @@
package com.gxwebsoft.shop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopFlashSaleActivity;
import com.gxwebsoft.shop.param.ShopFlashSaleActivityParam;
import com.gxwebsoft.shop.vo.ShopFlashSaleActivityVO;
import java.util.List;
/**
* 秒杀活动Service
*
* @author xm
* @since 2026-04-22 17:18:17
*/
public interface ShopFlashSaleActivityService extends IService<ShopFlashSaleActivity> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<ShopFlashSaleActivity>
*/
PageResult<ShopFlashSaleActivityVO> pageRel(ShopFlashSaleActivityParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<ShopFlashSaleActivity>
*/
List<ShopFlashSaleActivity> listRel(ShopFlashSaleActivityParam param);
/**
* 根据id查询
*
* @param id 秒杀活动编号
* @return ShopFlashSaleActivity
*/
ShopFlashSaleActivity getByIdRel(Integer id);
/**
* 查询个人可参与的活动数据
* @param tenantId 租户ID
* @return
*/
List<ShopFlashSaleActivityVO> getMyActive(Integer tenantId);
/**
* 修改秒杀活动状态
* @param id
* @return
*/
Boolean updateStatus(Integer id);
/**
* 修改秒杀活动排序
* @param id
* @param sortNumber
* @return
*/
Boolean updateSortNumber(Integer id, Integer sortNumber);
}

View File

@@ -0,0 +1,70 @@
package com.gxwebsoft.shop.service.impl;
import com.aliyuncs.utils.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.shop.entity.ShopActiveImage;
import com.gxwebsoft.shop.mapper.ShopActiveImageMapper;
import com.gxwebsoft.shop.param.ShopActiveImageParam;
import com.gxwebsoft.shop.service.ShopActiveImageService;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
/**
* 推广码底图Service实现
*
* @author xm
* @since 2026-04-27 17:53:00
*/
@Service
@AllArgsConstructor
public class ShopActiveImageServiceImpl extends ServiceImpl<ShopActiveImageMapper, ShopActiveImage> implements ShopActiveImageService {
@Override
public PageResult<ShopActiveImage> pageRel(ShopActiveImageParam param) {
PageParam<ShopActiveImage, ShopActiveImageParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<ShopActiveImage> list = baseMapper.selectPageRel(page, param);
if(CollectionUtils.isNotEmpty(list)){
list.forEach(shopActiveImage -> {
if(!StringUtils.isEmpty(shopActiveImage.getImgUrl())){
shopActiveImage.setImgUrlList(Arrays.asList(shopActiveImage.getImgUrl().split(",")));
}
});
}
return new PageResult<>(list, page.getTotal());
}
@Override
public List<ShopActiveImage> listRel(ShopActiveImageParam param) {
List<ShopActiveImage> list = baseMapper.selectListRel(param);
if(CollectionUtils.isNotEmpty(list)){
list.forEach(shopActiveImage -> {
if(!StringUtils.isEmpty(shopActiveImage.getImgUrl())){
shopActiveImage.setImgUrlList(Arrays.asList(shopActiveImage.getImgUrl().split(",")));
}
});
}
// 排序
PageParam<ShopActiveImage, ShopActiveImageParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public ShopActiveImage getByIdRel(Integer id) {
ShopActiveImageParam param = new ShopActiveImageParam();
param.setId(id);
ShopActiveImage activeImage = param.getOne(baseMapper.selectListRel(param));
if(activeImage != null && !StringUtils.isEmpty(activeImage.getImgUrl())){
activeImage.setImgUrlList(Arrays.asList(activeImage.getImgUrl().split(",")));
}
return activeImage;
}
}

View File

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

View File

@@ -0,0 +1,183 @@
package com.gxwebsoft.shop.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.exception.enums.GlobalErrorCodeConstants;
import com.gxwebsoft.common.core.utils.LoginUserUtil;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopFlashSaleActivity;
import com.gxwebsoft.shop.entity.ShopGoods;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.mapper.ShopFlashSaleActivityMapper;
import com.gxwebsoft.shop.mapper.ShopGoodsMapper;
import com.gxwebsoft.shop.mapper.ShopOrderMapper;
import com.gxwebsoft.shop.param.ShopFlashSaleActivityParam;
import com.gxwebsoft.shop.service.ShopFlashSaleActivityService;
import com.gxwebsoft.shop.vo.ShopFlashSaleActivityVO;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 秒杀活动Service实现
*
* @author xm
* @since 2026-04-22 17:18:17
*/
@Service
@AllArgsConstructor
public class ShopFlashSaleActivityServiceImpl extends ServiceImpl<ShopFlashSaleActivityMapper, ShopFlashSaleActivity> implements ShopFlashSaleActivityService {
private ShopGoodsMapper shopGoodsMapper;
private ShopOrderMapper shopOrderMapper;
@Override
public PageResult<ShopFlashSaleActivityVO> pageRel(ShopFlashSaleActivityParam param) {
PageParam<ShopFlashSaleActivity, ShopFlashSaleActivityParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<ShopFlashSaleActivityVO> list = baseMapper.selectPageRel(page, param);
if(CollectionUtils.isNotEmpty(list)){
List<Integer> goodsIdList = list.stream().map(ShopFlashSaleActivityVO::getGoodsId).distinct().collect(Collectors.toList());
List<ShopGoods> shopGoods = shopGoodsMapper.selectBatchIds(goodsIdList);
list.forEach(shopFlashSaleActivityVO -> {
ShopGoods shopGood = shopGoods.stream().filter(goods -> shopFlashSaleActivityVO.getGoodsId().equals(goods.getGoodsId())).findFirst().orElse(null);
if(shopGood != null){
shopFlashSaleActivityVO.setGoodsPrice(shopGood.getPrice());
shopFlashSaleActivityVO.setGoodsTotalPrice(shopGood.getPrice().multiply(new BigDecimal(shopFlashSaleActivityVO.getNum())));
shopFlashSaleActivityVO.setGoodsName(shopGood.getName());
shopFlashSaleActivityVO.setImage(shopGood.getImage());
shopFlashSaleActivityVO.setUnitName(shopGood.getUnitName());
}
});
}
return new PageResult<>(list, page.getTotal());
}
@Override
public List<ShopFlashSaleActivity> listRel(ShopFlashSaleActivityParam param) {
List<ShopFlashSaleActivity> list = baseMapper.selectListRel(param);
// 排序
PageParam<ShopFlashSaleActivity, ShopFlashSaleActivityParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public ShopFlashSaleActivity getByIdRel(Integer id) {
ShopFlashSaleActivityParam param = new ShopFlashSaleActivityParam();
param.setId(id);
return param.getOne(baseMapper.selectListRel(param));
}
@Override
public List<ShopFlashSaleActivityVO> getMyActive(Integer tenantId) {
List<ShopFlashSaleActivityVO> resultVOList = new ArrayList<>();
User loginUser = LoginUserUtil.getLoginUser();
if(loginUser == null){
throw new BusinessException(GlobalErrorCodeConstants.UNAUTHORIZED.getMsg());
}
Boolean newUser = true;
//判断是否为新用户【只要未成功下单都判定为新用户】
LambdaQueryWrapper<ShopOrder> shopOrderLambdaQueryWrapper = new LambdaQueryWrapper<ShopOrder>().eq(ShopOrder::getUserId, loginUser.getUserId()).eq(ShopOrder::getPayStatus, 1)
.in(ShopOrder::getOrderStatus, Arrays.asList(0, 1));
List<ShopOrder> shopOrderList = shopOrderMapper.selectList(shopOrderLambdaQueryWrapper);
if(CollectionUtils.isNotEmpty(shopOrderList)){
newUser = false;
}
//查询满足条件的活动数据
LambdaQueryChainWrapper<ShopFlashSaleActivity> activityWrapper = lambdaQuery().eq(ShopFlashSaleActivity::getStatus, 0).gt(ShopFlashSaleActivity::getStock, 0).eq(ShopFlashSaleActivity::getTenantId, tenantId)
.apply("NOW() BETWEEN start_time AND end_time");
Map<Integer, Integer> activityMap = new HashMap<>();
if(!newUser){
//查询当前用户是否有下过秒杀活动订单数据【判断下单数量是否超过限制】
List<Integer> activityIdList = activityWrapper.list().stream().map(ShopFlashSaleActivity::getId).collect(Collectors.toList());
LambdaQueryWrapper<ShopOrder> shopOrderWrapper = new LambdaQueryWrapper<ShopOrder>().select(ShopOrder::getOrderId, ShopOrder::getActivityId, ShopOrder::getTotalNum)
.eq(ShopOrder::getUserId, loginUser.getUserId()).in(ShopOrder::getActivityId, activityIdList).in(ShopOrder::getOrderStatus, Arrays.asList(0, 1));
activityMap = shopOrderMapper.selectList(shopOrderWrapper).stream().collect(Collectors.groupingBy(ShopOrder::getActivityId, Collectors.summingInt(ShopOrder::getTotalNum)));
activityWrapper.eq(ShopFlashSaleActivity::getDisplayType, 0);
}
activityWrapper.orderByAsc(ShopFlashSaleActivity::getSortNumber);
List<ShopFlashSaleActivity> activityList = activityWrapper.list();
if(CollectionUtils.isNotEmpty(activityList)){
resultVOList = BeanUtil.copyToList(activityList, ShopFlashSaleActivityVO.class);
List<Integer> goodsIdList = resultVOList.stream().map(ShopFlashSaleActivityVO::getGoodsId).distinct().collect(Collectors.toList());
List<ShopGoods> shopGoods = shopGoodsMapper.selectBatchIds(goodsIdList);
resultVOList.forEach(activity -> {
ShopGoods shopGood = shopGoods.stream().filter(goods -> activity.getGoodsId().equals(goods.getGoodsId())).findFirst().orElse(null);
if(shopGood != null){
activity.setGoodsPrice(shopGood.getPrice());
activity.setGoodsTotalPrice(shopGood.getPrice().multiply(new BigDecimal(activity.getNum())));
activity.setGoodsName(shopGood.getName());
activity.setImage(shopGood.getImage());
activity.setUnitName(shopGood.getUnitName());
}
});
}
//过滤超下单数量活动
if(!activityMap.isEmpty()){
Iterator<ShopFlashSaleActivityVO> iterator = resultVOList.iterator();
while (iterator.hasNext()){
ShopFlashSaleActivityVO vo = iterator.next();
Integer orderTotalNum = activityMap.get(vo.getId());
if(orderTotalNum != null && orderTotalNum >= vo.getSaleLimit()){
iterator.remove();
}
}
}
return resultVOList;
}
@Override
public Boolean updateStatus(Integer id) {
ShopFlashSaleActivity saleActivity = baseMapper.selectById(id);
if(saleActivity != null){
if (saleActivity.getStatus() == 0){
saleActivity.setStatus(1);
}else {
saleActivity.setStatus(0);
}
User loginUser = LoginUserUtil.getLoginUser();
if(loginUser != null){
saleActivity.setUpdater(loginUser.getUserId().toString());
}
saleActivity.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(saleActivity);
return Boolean.TRUE;
}else {
throw new BusinessException(GlobalErrorCodeConstants.NOT_FOUND.getMsg());
}
}
@Override
public Boolean updateSortNumber(Integer id, Integer sortNumber) {
ShopFlashSaleActivity saleActivity = baseMapper.selectById(id);
if(saleActivity != null){
saleActivity.setSortNumber(sortNumber);
saleActivity.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(saleActivity);
return Boolean.TRUE;
}else {
throw new BusinessException(GlobalErrorCodeConstants.NOT_FOUND.getMsg());
}
}
}

View File

@@ -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);
// 处理支付成功后的业务逻辑 // 处理支付成功后的业务逻辑

View File

@@ -0,0 +1,96 @@
package com.gxwebsoft.shop.vo;
import com.baomidou.mybatisplus.annotation.TableLogic;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 秒杀活动
*
* @author xm
* @since 2026-04-22 17:18:17
*/
@Data
public class ShopFlashSaleActivityVO implements Serializable {
@Schema(description = "秒杀活动编号")
private Integer id;
@Schema(description = "秒杀活动名称")
private String name;
@Schema(description = "秒杀活动商品")
private Integer goodsId;
@Schema(description = "秒杀活动商品名称")
private String goodsName;
@Schema(description = "商品skuId")
private Integer skuId;
@Schema(description = "商品图片地址")
private String image;
@Schema(description = "单位")
private String unitName;
@Schema(description = "商品数量")
private Integer num;
@Schema(description = "秒杀价")
private BigDecimal price;
@Schema(description = "商品单价")
private BigDecimal goodsPrice;
@Schema(description = "商品总价")
private BigDecimal goodsTotalPrice;
@Schema(description = "活动状态 0-开启 1-关闭")
private Integer status;
@Schema(description = "活动开始时间")
private LocalDateTime startTime;
@Schema(description = "活动结束时间")
private LocalDateTime endTime;
@Schema(description = "活动限购数量")
private Integer saleLimit;
@Schema(description = "库存")
private Integer stock;
@Schema(description = "展示类型0普通用户1新用户")
private Integer displayType;
@Schema(description = "备注")
private String remark;
@Schema(description = "排序")
private Integer sortNumber;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建者")
private String creator;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新者")
private String updater;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
@Schema(description = "是否删除")
@TableLogic
private Integer deleted;
}

View File

@@ -7,7 +7,7 @@ server:
# 数据源配置 # 数据源配置
spring: spring:
datasource: datasource:
url: jdbc:mysql://8.134.55.105:13306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8 url: jdbc:mysql://47.107.249.41:13306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: modules username: modules
password: tYmmMGh5wpwXR3ae password: tYmmMGh5wpwXR3ae
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver

View File

@@ -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:

View File

@@ -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:

View 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月份配送费"}]'

View File

@@ -38,14 +38,14 @@ public class ShopGenerator {
// Vue文件输出目录 // Vue文件输出目录
private static final String OUTPUT_DIR_VUE = "/src"; private static final String OUTPUT_DIR_VUE = "/src";
// 作者名称 // 作者名称
private static final String AUTHOR = "科技小王子"; private static final String AUTHOR = "xm";
// 是否在xml中添加二级缓存配置 // 是否在xml中添加二级缓存配置
private static final boolean ENABLE_CACHE = false; private static final boolean ENABLE_CACHE = false;
// 数据库连接配置 // 数据库连接配置
private static final String DB_URL = "jdbc:mysql://8.134.169.209:13306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8"; private static final String DB_URL = "jdbc:mysql://47.107.249.41:13306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8";
private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String DB_USERNAME = "modules"; private static final String DB_USERNAME = "modules";
private static final String DB_PASSWORD = "8YdLnk7KsPAyDXGA"; private static final String DB_PASSWORD = "tYmmMGh5wpwXR3ae";
// 包名 // 包名
private static final String PACKAGE_NAME = "com.gxwebsoft"; private static final String PACKAGE_NAME = "com.gxwebsoft";
// 模块名 // 模块名
@@ -105,7 +105,8 @@ public class ShopGenerator {
// "shop_express_template", // "shop_express_template",
// "shop_express_template_detail", // "shop_express_template_detail",
// "shop_gift" // "shop_gift"
"shop_article" // "shop_flash_sale_activity"
// "shop_active_image"
}; };
// 需要去除的表前缀 // 需要去除的表前缀
private static final String[] TABLE_PREFIX = new String[]{ private static final String[] TABLE_PREFIX = new String[]{

View File

@@ -19,10 +19,8 @@ import ${cfg.packageName!}.${package.ModuleName}.entity.${entity};
import ${cfg.packageName!}.${package.ModuleName}.param.${entity}Param; import ${cfg.packageName!}.${package.ModuleName}.param.${entity}Param;
import ${cfg.packageName!}.common.core.web.ApiResult; import ${cfg.packageName!}.common.core.web.ApiResult;
import ${cfg.packageName!}.common.core.web.PageResult; import ${cfg.packageName!}.common.core.web.PageResult;
import ${cfg.packageName!}.common.core.web.PageParam;
import ${cfg.packageName!}.common.core.web.BatchParam; import ${cfg.packageName!}.common.core.web.BatchParam;
import ${cfg.packageName!}.common.core.annotation.OperationLog; import ${cfg.packageName!}.common.core.annotation.OperationLog;
import ${cfg.packageName!}.common.system.entity.User;
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 org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;

View File

@@ -3,6 +3,7 @@ package ${package.Entity};
<% for(pkg in table.importPackages) { %> <% for(pkg in table.importPackages) { %>
import ${pkg}; import ${pkg};
<% } %> <% } %>
import com.baomidou.mybatisplus.annotation.TableName;
<% if(swagger2) { %> <% if(swagger2) { %>
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
<% } %> <% } %>
@@ -34,6 +35,7 @@ import lombok.experimental.Accessors;
<% if(swagger2) { %> <% if(swagger2) { %>
@Schema(name = "${entity}对象", description = "${table.comment!''}") @Schema(name = "${entity}对象", description = "${table.comment!''}")
<% } %> <% } %>
@TableName("${table.name}")
<% if(table.convert) { %> <% if(table.convert) { %>
@TableName("${table.name}") @TableName("${table.name}")
<% } %> <% } %>

View File

@@ -16,6 +16,7 @@ import ${package.Entity}.${entity};
import ${cfg.packageName!}.${package.ModuleName}.param.${entity}Param; import ${cfg.packageName!}.${package.ModuleName}.param.${entity}Param;
import ${cfg.packageName!}.common.core.web.PageParam; import ${cfg.packageName!}.common.core.web.PageParam;
import ${cfg.packageName!}.common.core.web.PageResult; import ${cfg.packageName!}.common.core.web.PageResult;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
@@ -27,6 +28,7 @@ import java.util.List;
* @since ${date(), 'yyyy-MM-dd HH:mm:ss'} * @since ${date(), 'yyyy-MM-dd HH:mm:ss'}
*/ */
@Service @Service
@AllArgsConstructor
<% if(kotlin){ %> <% if(kotlin){ %>
open class ${table.serviceImplName} : ${superServiceImplClass}<${table.mapperName}, ${entity}>(), ${table.serviceName} { open class ${table.serviceImplName} : ${superServiceImplClass}<${table.mapperName}, ${entity}>(), ${table.serviceName} {