Compare commits

...

66 Commits

Author SHA1 Message Date
xm
c3a50306ba 优化秒杀活动用户下单能力业务 2026-05-30 15:26:39 +08:00
xm
8d1a91cee7 Merge remote-tracking branch 'origin/dev_xm' into glt_xm
# Conflicts:
#	src/main/resources/application-dev.yml
#	src/main/resources/application-local.yml
2026-05-30 14:50:16 +08:00
xm
61b1d16937 1、优化配送派单消息推送功能
2、优化送水订单配送状态、水票标识查询,回显品名、水票标识、订单状态业务
3、优化送水订单接单、送达业务
4、优化资金表、资金流水表资金变更同步业务
2026-05-28 17:01:35 +08:00
xm
30363735df 增加商品分润基础业务功能 2026-05-27 10:59:00 +08:00
xm
b9d0d6b036 1、增加配送师傅订单派送发送消息通知功能
2、增加普通商品配完成送结算步梯费用给到配送师傅业务功能
3、增加分销商查询个人资金流水功能
2026-05-27 10:56:55 +08:00
xm
ba3748d2f9 1、调整微信消息推送模板业务
2、增加普通商品配送自动发单功能
3、增加核销员管理功能
4、开发配送员转单功能
5、优化送水订单查询业务
6、优化配送订单自动分配配送师傅业务
7、增加普通商品下单配送支付步梯费功能
2026-05-25 18:08:47 +08:00
xm
18148ddb8d 1、优化配送费分佣计算方式,按百分比/固定金额计算
2、增加自提订单核销数据统计功能
3、增加现场核销推广指定人分佣业务
2026-05-21 18:14:35 +08:00
xm
4d455e2ba2 增加分销员推广核销能力业务 2026-05-20 16:54:57 +08:00
xm
74bf6dd8d2 1、推广码底图增加分享底图业务
2、优化订单核销码、核销人查询业务
2026-05-20 14:28:30 +08:00
xm
5b876d29eb 1、关闭原有定时结算、手动结算业务,因新业务已完成流程结算功能
2、优化秒杀订单查询活动名称、核销码业务
3、优化订单状态查询业务
2026-05-20 11:29:24 +08:00
xm
032c936f42 local启动配置修改 2026-05-19 17:52:19 +08:00
xm
bbb26b44a9 优化配送订单完成结算配送金业务 2026-05-19 17:40:08 +08:00
xm
52af00170a 1、优化送水订单同步记录水票订单号业务
2、分销用户分销明细增加结算标识业务
3、增加分销用户个人当日收益数据统计
2026-05-19 16:47:42 +08:00
xm
b5d4274d97 1、修改测试、本地环境证书加载路径业务
2、优化配送结算业务,配送完成即计算
3、优化门店、服务商结算任务
4、秒杀活动增加弹窗业务
2026-05-19 10:32:17 +08:00
xm
2fbcf16a48 增加门店、服务商佣金定时结算功能,默认每天凌晨一点执行 2026-05-16 15:10:14 +08:00
xm
7de53327d6 修改配置 2026-05-14 17:45:04 +08:00
xm
f86bf8df4b 修改配置了 2026-05-14 08:21:10 +08:00
xm
65f1fa861d 配置修改 2026-05-14 01:33:58 +08:00
xm
cdabd5d446 优化水票释放业务 2026-05-13 17:29:45 +08:00
xm
86d27db76d Merge branch 'dev' into dev_xm
# Conflicts:
#	src/test/java/com/gxwebsoft/generator/ShopGenerator.java
2026-05-13 16:33:42 +08:00
xm
c3fcb36f66 增加退款按订单号回退分销用户钱包流水及同步生产回退流水功能 2026-05-13 16:32:09 +08:00
xm
f3d6bbef63 1.优化分销员计算一级、二级分销佣金业务
2.优化送水订单获取用户收货地址业务
3.优化收益明细订单号、用户ID查询业务
4.提现业务增加订单号,避免使用订单ID传入会与生产ID重复,导致都是已使用订单号
2026-05-13 09:55:49 +08:00
xm
92ca45a5c1 1.优化分佣算法业务
2.修改配置
2026-05-12 16:47:41 +08:00
xm
991b6fe529 1.调整订单分销、分润、分红结算算法功能
2.商品订单支付成功增加执行分销员分销、统计门店/服务商分销业务、执行总分红业务功能
3.配送员完成配送增加解冻商品订单业务功能
4.订单分销记录增加关联结算单号、结算状态字段,方便门店/服务商做计算做准备
2026-05-11 17:55:48 +08:00
xm
bc88f54c24 优化商品下单业务、水票解冻业务 2026-05-09 14:56:53 +08:00
xm
c6cec21d12 1.优化链接配置
2.关闭套票发放任务【支付成功回调取代】
3.优化定时解冻水票业务,每月1号即可释放本月水票
2026-05-09 14:36:40 +08:00
xm
81a9974e64 1.优化配置
2.商品订单支付成功后增加执行发放水票业务功能
3.送水订单、商品订单优化生产订单号业务
2026-05-08 15:00:06 +08:00
xm
3c7ee5057f swagger增加glt包扫描,获取接口文档 2026-05-08 09:58:23 +08:00
xm
82ce775807 1.调整开发端微信支付回调参数,保证测试环境能正常使用微信支付功能
2.调整送水订单显示用户具体收货地址业务
2026-05-08 09:36:05 +08:00
xm
f3dc242b5c 1.增加更新分销用户资金变更api,此功能提供统一分销资金结算、分销资金解冻、扫码直返佣等业务功能,变更账户同时生成对应流水
2.分销资金明细表增加:订单号、变动类型、变更后金额、创建人、删除等字段及相关功能
3.商品增加推广核验佣金比率业务
4.商品订单增加统计推广核销佣金、查询用户收货详细地址业务
5.增加订单扫码核销功能
6.新增统一获取订单工具类
2026-05-07 16:09:34 +08:00
a306f53336 feat(cms): 在CmsNavigation实体中添加文章列表关联
- 在CmsNavigation类中新增articles字段用于存储栏目文章列表
- 在CmsNavigationServiceImpl的pageRel方法中关联查询每个栏目对应的最多5篇文章
- 调用CmsArticleService获取文章列表并设置到CmsNavigation实体中
- 优化栏目分页查询,增加文章数据支持展示
2026-05-06 19:39:03 +08:00
8fca992e37 fix(shopOrder): 支持订单拒绝退款状态校验
- 修改退款操作接口,增加对拒绝退款状态(orderStatus=5)的支持
- 更新接口描述,明确支持申请、拒绝、同意及客户端申请退款四种状态
- 优化校验逻辑,确保orderStatus只能为4、5、6或7
- 完善参数非空判断并返回相应失败信息
2026-05-06 17:06:40 +08:00
xm
1a68b70591 调整订单取消任务每5分钟执行一次 2026-05-06 09:52:45 +08:00
xm
587caa78d7 1.优化小程序端用户待付款、待发货、待收货、已完成、退货/售后总单量数据及对应订单明细查询功能
2.新增分销用户查询团队成员订单数、订单金额、团队成员数查询业务功能
2026-05-05 17:39:26 +08:00
xm
a575907623 1.下单增加商品判断是否水票优化、默认订单类型功能
2.订单增加生成核销码功能
3.自提订单下单不校验电子围栏;配送订单如在电子围栏内默认自配送,在电子围栏外默认发快递
4.商品增加:配送方式、水票标识业务;商品订单增加:订单类型、水票标识、核销码业务
2026-05-05 09:31:45 +08:00
xm
044bb24b57 优化用户收货地址修改默认属性业务,一个地址设置为默认其他地址同步设置非默认 2026-04-30 10:39:58 +08:00
xm
53c0dc9cd7 常用实体类增加表名称对应名称,方便快速查询业务 2026-04-29 15:34:35 +08:00
026824d31d chore(config): remove obsolete test and environment configuration files
- 删除了 application-glt2.yml、application-glt3.yml 和 application-test.yml 配置文件
- 移除了所有与证书路径相关的测试类,包括 CertificatePathConcatenationTest、CertificatePathFixTest、CertificatePathTest、CertificateTest 和 EnvironmentBasedCertificateTest
- 删除了 CouponStatusServiceTest 测试类和 MultiSpecOrderTest 测试类
- 清理历史环境和测试遗留配置,简化项目结构,提高维护性
2026-04-29 10:28:08 +08:00
xm
fb46af7bc3 增加步梯费用设置业务功能 2026-04-29 09:46:03 +08:00
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
1350250847@qq.com
bc3842faee 变更数据库链接 2026-04-20 16:38:04 +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
1350250847@qq.com
8d2a0c46b7 常量注释 2026-04-14 16:57:34 +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
8b83e4862f chore(config): 更新开发环境数据库和Redis配置
- 将application.yml中的active profile由glt2改为dev
- 更新application-dev.yml中的MySQL连接信息,包括url、用户名和密码
- 修改Redis服务器地址以匹配新的环境设置
- 添加新文件expert-history.json和MEMORY.md用于记录扩展历史和内存使用情况
2026-04-12 21:30:49 +08:00
800b4f6f93 chore(config): 更新数据库和Redis连接配置
- 将数据库连接地址更改为1Panel-mysql-XsWW,并调整端口号为3306
- 更新Redis主机地址为1Panel-redis-GmNr,端口改为6379
- 保持其他连接参数和认证信息不变
2026-04-11 14:42:07 +08:00
498a47977e feat(notification): 增加送水订单新单通知配送员功能
- 在订单创建成功后异步通知所有在线配送员有新订单信息
- 查询在线且启用状态的配送员列表,并发送微信订阅消息提醒
- 新增 GltSubscribeMessageService 接口及实现,封装微信小程序订阅消息发送逻辑
- 实现新订单和订单状态变更的微信订阅消息发送方法
- 配置Redis缓存access_token,提升微信接口调用效率
- 日志记录订阅消息发送状态及异常,确保通知稳定性
- ShopDealerUser实体新增分销商等级字段
- ShopGoods实体支持活动方式和配送方式字段增加相关查询条件
- 更新相关Mapper XML文件增加对dealerLevel、activityType及deliveryMode字段的支持
- 修改application-glt2.yml更新Redis host配置
2026-04-10 02:16:59 +08:00
215 changed files with 8791 additions and 3579 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

@@ -227,6 +227,10 @@ public class CmsNavigation implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String text; private String text;
@Schema(description = "栏目文章")
@TableField(exist = false)
private List<CmsArticle> articles;
public String getCategoryName() { public String getCategoryName() {
return this.title; return this.title;
} }

View File

@@ -4,9 +4,11 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.cms.entity.CmsArticle;
import com.gxwebsoft.cms.entity.CmsDesign; import com.gxwebsoft.cms.entity.CmsDesign;
import com.gxwebsoft.cms.entity.CmsModel; import com.gxwebsoft.cms.entity.CmsModel;
import com.gxwebsoft.cms.mapper.CmsNavigationMapper; import com.gxwebsoft.cms.mapper.CmsNavigationMapper;
import com.gxwebsoft.cms.service.CmsArticleService;
import com.gxwebsoft.cms.service.CmsDesignService; import com.gxwebsoft.cms.service.CmsDesignService;
import com.gxwebsoft.cms.service.CmsModelService; import com.gxwebsoft.cms.service.CmsModelService;
import com.gxwebsoft.cms.service.CmsNavigationService; import com.gxwebsoft.cms.service.CmsNavigationService;
@@ -38,6 +40,8 @@ public class CmsNavigationServiceImpl extends ServiceImpl<CmsNavigationMapper, C
private CmsModelService cmsModelService; private CmsModelService cmsModelService;
@Resource @Resource
private UserService userService; private UserService userService;
@Resource
private CmsArticleService cmsArticleService;
@Override @Override
public PageResult<CmsNavigation> pageRel(CmsNavigationParam param) { public PageResult<CmsNavigation> pageRel(CmsNavigationParam param) {
@@ -53,6 +57,11 @@ public class CmsNavigationServiceImpl extends ServiceImpl<CmsNavigationMapper, C
// 排序 // 排序
PageParam<CmsNavigation, CmsNavigationParam> page = new PageParam<>(); PageParam<CmsNavigation, CmsNavigationParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, position asc,navigation_id asc"); page.setDefaultOrder("sort_number asc, position asc,navigation_id asc");
list.forEach(item -> {
// 关联信息
final List<CmsArticle> articleList = cmsArticleService.list(new LambdaQueryWrapper<CmsArticle>().eq(CmsArticle::getCategoryId, item.getNavigationId()).last("limit 5"));
item.setArticles(articleList);
});
return page.sortRecords(list); return page.sortRecords(list);
} }

View File

@@ -32,7 +32,7 @@ public class CertificateProperties {
/** /**
* 开发环境证书路径前缀 * 开发环境证书路径前缀
*/ */
private String devCertPath = "dev"; private String devCertPath = "local";
/** /**
* 微信支付证书配置 * 微信支付证书配置

View File

@@ -60,7 +60,7 @@ public class ConfigProperties {
/** /**
* token过期时间, 单位秒 * token过期时间, 单位秒
*/ */
private Long tokenExpireTime = 60 * 60 * 365 * 24L; private Long tokenExpireTime = 60 * 60 * 30 * 24L;
/** /**
* token快要过期自动刷新时间, 单位分钟 * token快要过期自动刷新时间, 单位分钟

View File

@@ -80,6 +80,7 @@ public class MybatisPlusConfig {
@Override @Override
public boolean ignoreTable(String tableName) { public boolean ignoreTable(String tableName) {
// TenantContext.setIgnoreTenant(Boolean.TRUE);
// 如果当前上下文设置了忽略租户隔离,则忽略所有表的租户隔离 // 如果当前上下文设置了忽略租户隔离,则忽略所有表的租户隔离
if (TenantContext.isIgnoreTenant()) { if (TenantContext.isIgnoreTenant()) {
return true; return true;
@@ -91,18 +92,18 @@ public class MybatisPlusConfig {
"sys_dictionary", "sys_dictionary",
"sys_dictionary_data", "sys_dictionary_data",
"apps_test_data", "apps_test_data",
"cms_lang" "cms_lang",
// "hjm_car", "shop_goods",
// "hjm_fence" "shop_order" ,
// "cms_website" "shop_order_goods",
// "sys_user" "glt_ticket_template",
// "cms_domain" "glt_user_ticket",
// "shop_order_goods", "glt_user_ticket_release",
// "shop_goods" "glt_user_ticket_log",
// "shop_users", "shop_dealer_user",
// "shop_order" // 移除shop_order改为通过注解控制 "shop_dealer_order",
// "shop_order_info", "shop_dealer_referee",
// "booking_user_invoice" "shop_store_rider"
).contains(tableName); ).contains(tableName);
} }
}; };

View File

@@ -87,14 +87,15 @@ public class SwaggerConfig {
} }
/** /**
* OA 模块分组 * Glt 模块分组
*/ */
@Bean @Bean
public GroupedOpenApi oaApi() { public GroupedOpenApi gltApi() {
return GroupedOpenApi.builder() return GroupedOpenApi.builder()
.group("oa") .group("glt")
.pathsToMatch("/api/oa/**") // 订单等用户侧接口在 shop 包内,但路径使用 /api/user/**(前端统一 user 侧 API 前缀)
.packagesToScan("com.gxwebsoft.oa") .pathsToMatch("/api/glt/**", "/api/user/**")
.packagesToScan("com.gxwebsoft.glt")
.build(); .build();
} }

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

@@ -30,7 +30,7 @@ import java.util.Map;
@Tag(name = "数据库修复工具") @Tag(name = "数据库修复工具")
@RestController @RestController
@RequestMapping("/api/database-fix") @RequestMapping("/api/database-fix")
// @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "dev") // @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "local")
public class DatabaseFixController extends BaseController { public class DatabaseFixController extends BaseController {
@Autowired @Autowired

View File

@@ -28,7 +28,7 @@ import java.util.Map;
@Tag(name = "开发环境管理") @Tag(name = "开发环境管理")
@RestController @RestController
@RequestMapping("/api/dev") @RequestMapping("/api/dev")
// @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "dev") // @ConditionalOnProperty(name = "spring.profiles.active", havingValue = "local")
public class DevEnvironmentController extends BaseController { public class DevEnvironmentController extends BaseController {
@Autowired @Autowired

View File

@@ -0,0 +1,41 @@
package com.gxwebsoft.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 用户分销钱包交易业务分类
* @author xm
*/
@AllArgsConstructor
@Getter
public enum ShopDealerCapitalUpdateEnum {
DISTRIBUTION_INCOME(10, "分销收入", true),
MANAGEMENT_INCOME(11, "团队管理津贴收入", true),
DIVIDEND_INCOME(12, "分红收入", true),
PROMOTION_INCOME(13, "现场推广收入", true),
PROMOTION_PARENT_INCOME(14, "现场推广分佣", true),
WITHDRAW_PAYMENT(20, "提现支出", false),
TRANSFER_PAYMENT(30, "转账支出", false),
TRANSFER_INCOME(40, "转账收入", true),
FREEZE_MONEY_THAW(50, "佣金解冻", true),
DELIVERY_REWARD(60, "配送奖励", true),
DELIVERY_INCOME(61, "配送提成", true),
DELIVERY_FLOOR_FEE(62, "配送步梯费", true),
ORDER_REFUND(70, "佣金退回(退单)", false),
;
/**
* 业务分类
*/
private final Integer type;
/**
* 说明
*/
private final String description;
/**
* 是否为增加余额
*/
private final boolean add;
}

View File

@@ -0,0 +1,29 @@
package com.gxwebsoft.common.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单状态枚举
*/
@AllArgsConstructor
@Getter
public enum ShopDealerTypeEnum {
FREEZE_ACCOUNT(1, "操作冻结账户余额"),
WITHDRAW_ACCOUNT(2, "操作提现账户余额【直接结算】"),
DEFROST(3, "解冻"),
ORDER_REFUND(4, "退单");
private final Integer code;
private final String desc;
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}

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;
@@ -66,15 +70,15 @@ public class EnvironmentAwarePaymentService {
* 根据当前环境获取回调地址 * 根据当前环境获取回调地址
*/ */
private String getEnvironmentNotifyUrl() { private String getEnvironmentNotifyUrl() {
if ("dev".equals(activeProfile) || "test".equals(activeProfile)) { if ("local".equals(activeProfile) || "test".equals(activeProfile)) {
// 开发/测试环境使用本地回调地址 // 开发/测试环境使用本地回调地址
return devNotifyUrl; return devNotifyUrl;
} else if ("prod".equals(activeProfile)) { } else if ("prod".equals(activeProfile)) {
// 生产环境使用生产回调地址 // 生产环境使用生产回调地址
return prodNotifyUrl; return prodNotifyUrl;
} else { } else {
// 默认使用配置的服务器地址 // 默认使用 API 网关地址(支付回调需要公网可访问的 API 地址
return serverUrl + "/shop/shop-order/notify"; return (StrUtil.isNotBlank(apiUrl) ? apiUrl : serverUrl) + "/shop/shop-order/notify";
} }
} }
@@ -131,7 +135,7 @@ public class EnvironmentAwarePaymentService {
* 是否为开发环境 * 是否为开发环境
*/ */
public boolean isDevelopmentEnvironment() { public boolean isDevelopmentEnvironment() {
return "dev".equals(activeProfile) || "test".equals(activeProfile); return "local".equals(activeProfile) || "test".equals(activeProfile);
} }
/** /**

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

@@ -84,7 +84,7 @@ public class WechatCertAutoConfig {
String apiV3Key = "0kF5OlPr482EZwtn9zGufUcqa7ovgxRL"; String apiV3Key = "0kF5OlPr482EZwtn9zGufUcqa7ovgxRL";
// 根据环境选择证书路径 // 根据环境选择证书路径
if ("dev".equals(activeProfile)) { if ("local".equals(activeProfile)) {
// 开发环境使用配置文件upload-path拼接证书路径 // 开发环境使用配置文件upload-path拼接证书路径
String uploadPath = configProperties.getUploadPath(); // 配置文件路径 String uploadPath = configProperties.getUploadPath(); // 配置文件路径
String tenantId = "10550"; // 租户ID String tenantId = "10550"; // 租户ID

View File

@@ -105,7 +105,7 @@ public class WechatPayCertificateDiagnostic {
* 检查证书文件 * 检查证书文件
*/ */
private void checkCertificateFiles(Payment payment, Integer tenantId, String environment, DiagnosticResult result) { private void checkCertificateFiles(Payment payment, Integer tenantId, String environment, DiagnosticResult result) {
if ("dev".equals(environment)) { if ("local".equals(environment)) {
// 开发环境证书检查 // 开发环境证书检查
String tenantCertPath = "dev/wechat/" + tenantId; String tenantCertPath = "dev/wechat/" + tenantId;
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile(); String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
@@ -152,7 +152,7 @@ public class WechatPayCertificateDiagnostic {
*/ */
private void validateCertificateContent(Payment payment, Integer tenantId, String environment, DiagnosticResult result) { private void validateCertificateContent(Payment payment, Integer tenantId, String environment, DiagnosticResult result) {
try { try {
if ("dev".equals(environment)) { if ("local".equals(environment)) {
String tenantCertPath = "dev/wechat/" + tenantId; String tenantCertPath = "dev/wechat/" + tenantId;
String apiclientCertPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getApiclientCertFile(); String apiclientCertPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getApiclientCertFile();

View File

@@ -103,7 +103,7 @@ public class WechatPayCertificateFixer {
* 修复证书文件问题 * 修复证书文件问题
*/ */
private void fixCertificateFiles(Payment payment, Integer tenantId, String environment, FixResult result) { private void fixCertificateFiles(Payment payment, Integer tenantId, String environment, FixResult result) {
if ("dev".equals(environment)) { if ("local".equals(environment)) {
fixDevCertificateFiles(tenantId, result); fixDevCertificateFiles(tenantId, result);
} else { } else {
fixProdCertificateFiles(payment, result); fixProdCertificateFiles(payment, result);
@@ -169,7 +169,7 @@ public class WechatPayCertificateFixer {
} }
// 在开发环境中,尝试从证书文件中提取序列号进行验证 // 在开发环境中,尝试从证书文件中提取序列号进行验证
if ("dev".equals(environment)) { if ("local".equals(environment)) {
try { try {
String tenantCertPath = "dev/wechat/" + tenantId; String tenantCertPath = "dev/wechat/" + tenantId;
String apiclientCertPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getApiclientCertFile(); String apiclientCertPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getApiclientCertFile();

View File

@@ -115,7 +115,7 @@ public class WechatPayConfigValidator {
* 验证证书文件 * 验证证书文件
*/ */
private void validateCertificateFiles(Integer tenantId, ValidationResult result) { private void validateCertificateFiles(Integer tenantId, ValidationResult result) {
if ("dev".equals(activeProfile)) { if ("local".equals(activeProfile)) {
// 开发环境证书验证 // 开发环境证书验证
String tenantCertPath = "dev/wechat/" + tenantId; String tenantCertPath = "dev/wechat/" + tenantId;
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile(); String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
@@ -195,7 +195,7 @@ public class WechatPayConfigValidator {
// 证书文件检查 // 证书文件检查
report.append("当前环境: ").append(activeProfile).append("\n"); report.append("当前环境: ").append(activeProfile).append("\n");
if ("dev".equals(activeProfile)) { if ("local".equals(activeProfile)) {
String tenantCertPath = "dev/wechat/" + tenantId; String tenantCertPath = "dev/wechat/" + tenantId;
String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile(); String privateKeyPath = tenantCertPath + "/" + certConfig.getWechatPay().getDev().getPrivateKeyFile();
boolean certExists = certificateLoader.certificateExists(privateKeyPath); boolean certExists = certificateLoader.certificateExists(privateKeyPath);

View File

@@ -78,7 +78,7 @@ public class WechatPayDiagnostic {
} }
// 生产环境检查证书文件 // 生产环境检查证书文件
if (!"dev".equals(environment)) { if (!"local".equals(environment)) {
if (payment.getApiclientCert() != null) { if (payment.getApiclientCert() != null) {
log.info("商户证书文件配置: {}", payment.getApiclientCert()); log.info("商户证书文件配置: {}", payment.getApiclientCert());
} }

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

@@ -262,6 +262,9 @@ public class User implements UserDetails {
@Schema(description = "微信unionid") @Schema(description = "微信unionid")
private String unionid; private String unionid;
@Schema(description = "核销权限是否开启 0-未开启 1-已开启")
private Integer verifyFlag;
@Schema(description = "关联用户ID") @Schema(description = "关联用户ID")
@TableField(exist = false) @TableField(exist = false)
private Integer sysUserId; private Integer sysUserId;

View File

@@ -49,6 +49,9 @@ public interface UserMapper extends BaseMapper<User> {
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
List<User> getOne(@Param("param") UserParam param); List<User> getOne(@Param("param") UserParam param);
@InterceptorIgnore(tenantLine = "true")
User getById(@Param("userId") Integer userId);
List<User> selectListStatisticsRel(@Param("param") UserParam param); List<User> selectListStatisticsRel(@Param("param") UserParam param);
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
@@ -60,11 +63,18 @@ 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);
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
List<User> listByAlert(); List<User> listByAlert();
/**
* 批量查询用户信息
* @param userIdList 用户ID集合
* @return
*/
List<User> selectByUserIdList(@Param("userIdList") List<Integer> userIdList);
} }

View File

@@ -260,5 +260,25 @@
WHERE a.user_id = #{userId} WHERE a.user_id = #{userId}
AND a.deleted = 0 AND a.deleted = 0
</select> </select>
<select id="getById" resultType="com.gxwebsoft.common.system.entity.User">
SELECT * FROM gxwebsoft_core.sys_user WHERE user_id = #{userId} and deleted = 0
</select>
<select id="selectByUserIdList" resultType="com.gxwebsoft.common.system.entity.User">
SELECT
user_id,
username,
nickname,
phone,
real_name,
create_time
FROM
gxwebsoft_core.sys_user
WHERE
deleted = 0
AND user_id IN
<foreach collection="userIdList" item="item" index="index" open="(" close=")" separator=",">
#{item}
</foreach>
</select>
</mapper> </mapper>

View File

@@ -0,0 +1,38 @@
package com.gxwebsoft.common.system.redis;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 支付序号的 Redis DAO
*
* @author 芋道源码
*/
@Repository
public class OrderNoUtils {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 生成序号
* @param prefix 前缀
* @return 序号
*/
public String generate(String prefix) {
// 递增序号
String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN);
String key = noPrefix;
Long no = stringRedisTemplate.opsForValue().increment(key);
// 设置过期时间
stringRedisTemplate.expire(key, Duration.ofMinutes(1L));
return noPrefix + no;
}
}

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

@@ -143,7 +143,7 @@ public class SettingServiceImpl extends ServiceImpl<SettingMapper, Setting> impl
final String apiV3key = jsonObject.getString("wechatApiKey"); final String apiV3key = jsonObject.getString("wechatApiKey");
if(config == null){ if(config == null){
// 根据环境选择不同的证书路径配置 // 根据环境选择不同的证书路径配置
if ("dev".equals(activeProfile)) { if ("local".equals(activeProfile)) {
// 开发环境使用配置文件的upload-path拼接证书路径 - 租户ID 10550 // 开发环境使用配置文件的upload-path拼接证书路径 - 租户ID 10550
System.out.println("=== 开发环境使用配置文件upload-path拼接证书路径 ==="); System.out.println("=== 开发环境使用配置文件upload-path拼接证书路径 ===");
String uploadPath = pathConfig.getUploadPath(); // 获取配置的upload-path String uploadPath = pathConfig.getUploadPath(); // 获取配置的upload-path

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

@@ -1,15 +1,20 @@
package com.gxwebsoft.glt.controller; package com.gxwebsoft.glt.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.mapper.UserMapper;
import com.gxwebsoft.glt.dto.GltTransferOrderDto;
import com.gxwebsoft.glt.dto.NoticeRiderNewOrderDto;
import com.gxwebsoft.glt.entity.GltTicketOrder; import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderDeliveredParam; import com.gxwebsoft.glt.param.GltTicketOrderDeliveredParam;
import com.gxwebsoft.glt.param.GltTicketOrderParam; import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltSubscribeMessageService;
import com.gxwebsoft.glt.service.GltTicketOrderService; import com.gxwebsoft.glt.service.GltTicketOrderService;
import com.gxwebsoft.shop.entity.ShopStoreRider; import com.gxwebsoft.shop.entity.ShopStoreRider;
import com.gxwebsoft.shop.entity.ShopUserAddress; import com.gxwebsoft.shop.entity.ShopUserAddress;
@@ -19,12 +24,14 @@ import com.gxwebsoft.shop.service.ShopUserAddressService;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.extern.slf4j.Slf4j;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* 送水订单控制器 * 送水订单控制器
@@ -32,6 +39,7 @@ import java.util.List;
* @author 科技小王子 * @author 科技小王子
* @since 2026-02-05 18:50:21 * @since 2026-02-05 18:50:21
*/ */
@Slf4j
@Tag(name = "送水订单管理") @Tag(name = "送水订单管理")
@RestController @RestController
@RequestMapping("/api/glt/glt-ticket-order") @RequestMapping("/api/glt/glt-ticket-order")
@@ -44,10 +52,14 @@ public class GltTicketOrderController extends BaseController {
private ShopStoreFenceService shopStoreFenceService; private ShopStoreFenceService shopStoreFenceService;
@Resource @Resource
private ShopStoreRiderService shopStoreRiderService; private ShopStoreRiderService shopStoreRiderService;
@Resource
private GltSubscribeMessageService gltSubscribeMessageService;
@Resource
private UserMapper userMapper;
@Operation(summary = "分页查询送水订单") @Operation(summary = "分页查询送水订单")
@GetMapping("/page") @GetMapping("/page")
public ApiResult<PageResult<GltTicketOrder>> page(GltTicketOrderParam param) { public ApiResult<PageResult<GltTicketOrder>> page(@ParameterObject GltTicketOrderParam param) {
// 使用关联查询 // 使用关联查询
return success(gltTicketOrderService.pageRel(param)); return success(gltTicketOrderService.pageRel(param));
} }
@@ -170,6 +182,71 @@ public class GltTicketOrderController extends BaseController {
return success("下单成功"); return success("下单成功");
} }
/**
* 通知所有在线配送员有新订单
*/
private void notifyRidersOfNewOrder(GltTicketOrder order, Integer tenantId) {
if (order == null || tenantId == null) {
return;
}
// 查询所有启用且在线的配送员
List<ShopStoreRider> onlineRiders = shopStoreRiderService.list(
new LambdaQueryWrapper<ShopStoreRider>()
.eq(ShopStoreRider::getTenantId, tenantId)
.eq(ShopStoreRider::getIsDelete, 0)
.eq(ShopStoreRider::getStatus, 1)
.eq(ShopStoreRider::getWorkStatus, 1) // 在线状态
.or()
.eq(ShopStoreRider::getTenantId, tenantId)
.eq(ShopStoreRider::getIsDelete, 0)
.eq(ShopStoreRider::getStatus, 1)
.isNull(ShopStoreRider::getWorkStatus) // 兼容未设置状态的配送员
);
if (onlineRiders == null || onlineRiders.isEmpty()) {
log.info("当前无在线配送员,无需发送订阅消息");
return;
}
// 获取配送员的 userId 列表
List<Integer> riderUserIds = onlineRiders.stream()
.map(ShopStoreRider::getUserId)
.filter(id -> id != null && id > 0)
.collect(Collectors.toList());
if (riderUserIds.isEmpty()) {
return;
}
// 批量查询配送员的 openId
List<User> riders = userMapper.selectList(
new LambdaQueryWrapper<User>()
.select(User::getUserId, User::getOpenid)
.in(User::getUserId, riderUserIds)
.isNotNull(User::getOpenid)
);
// 发送订阅消息
for (User rider : riders) {
if (StrUtil.isNotBlank(rider.getOpenid())) {
try {
gltSubscribeMessageService.sendNewOrderNotice(order, rider.getOpenid(), tenantId);
} catch (Exception e) {
log.warn("发送订阅消息给配送员失败 - userId={}, error={}", rider.getUserId(), e.getMessage());
}
}
}
}
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员调度单通知")
@PostMapping("/sendRiderNewOrderNotice")
public ApiResult<?> sendRiderNewOrderNotice(@RequestBody NoticeRiderNewOrderDto noticeRiderNewOrderDto) {
gltSubscribeMessageService.sendRiderNewOrderNotice(noticeRiderNewOrderDto);
return success("发送消息成功!");
}
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员接单") @Operation(summary = "配送员接单")
@PostMapping("/{id}/accept") @PostMapping("/{id}/accept")
@@ -184,6 +261,13 @@ public class GltTicketOrderController extends BaseController {
return success("接单成功"); return success("接单成功");
} }
@PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员转单")
@PostMapping("/transferOrder")
public ApiResult<Boolean> transferOrder(@RequestBody GltTransferOrderDto orderDto) {
return success(gltTicketOrderService.transferOrder(orderDto));
}
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@Operation(summary = "配送员开始配送") @Operation(summary = "配送员开始配送")
@PostMapping("/{id}/start") @PostMapping("/{id}/start")
@@ -215,6 +299,13 @@ public class GltTicketOrderController extends BaseController {
return success("确认送达"); return success("确认送达");
} }
@Operation(summary = "订单调度")
@PostMapping("/dispatchOrder")
public ApiResult<?> dispatchOrder(@RequestParam("orderNo") String orderNo, @RequestParam("tenantId") Integer tenantId) {
gltTicketOrderService.dispatchOrder(orderNo, tenantId);
return success("success!");
}
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@Operation(summary = "用户确认收货") @Operation(summary = "用户确认收货")
@PostMapping("/{id}/confirm-receive") @PostMapping("/{id}/confirm-receive")
@@ -253,16 +344,14 @@ public class GltTicketOrderController extends BaseController {
if (addr == null) { if (addr == null) {
return null; return null;
} }
if (StrUtil.isNotBlank(addr.getFullAddress())) {
return addr.getFullAddress();
}
// 兼容旧数据fullAddress 为空时,拼接省市区 + 详细地址 // 兼容旧数据fullAddress 为空时,拼接省市区 + 详细地址
return StrUtil.blankToDefault( return StrUtil.blankToDefault(
StrUtil.join("", StrUtil.join("",
StrUtil.nullToEmpty(addr.getProvince()), StrUtil.nullToEmpty(addr.getProvince()),
StrUtil.nullToEmpty(addr.getCity()), StrUtil.nullToEmpty(addr.getCity()),
StrUtil.nullToEmpty(addr.getRegion()), StrUtil.nullToEmpty(addr.getRegion()),
StrUtil.nullToEmpty(addr.getAddress()) StrUtil.nullToEmpty(addr.getAddress()),
StrUtil.nullToEmpty(addr.getFullAddress())
), ),
addr.getAddress() addr.getAddress()
); );
@@ -300,23 +389,14 @@ public class GltTicketOrderController extends BaseController {
} }
if (gltTicketOrderService.updateById(gltTicketOrder)) { if (gltTicketOrderService.updateById(gltTicketOrder)) {
// 后台指派配送员(直接改 riderId同步商城订单为“已发货”(deliveryStatus=20) gltTicketOrderService.markShopOrderShippedAfterRiderAssigned(gltTicketOrder.getId(), tenantId, gltTicketOrder.getRiderId());
if (gltTicketOrder != null
&& gltTicketOrder.getId() != null
&& gltTicketOrder.getRiderId() != null
&& gltTicketOrder.getRiderId() > 0) {
gltTicketOrderService.markShopOrderShippedAfterRiderAssigned(
gltTicketOrder.getId(),
tenantId,
gltTicketOrder.getRiderId()
);
}
// 后台直接改“已完成”(deliveryStatus=40)时,同步商城订单为“已完成”(orderStatus=1) // 后台直接改“已完成”(deliveryStatus=40)时,同步商城订单为“已完成”(orderStatus=1)
if (gltTicketOrder != null if (gltTicketOrder != null
&& gltTicketOrder.getId() != null && gltTicketOrder.getId() != null
&& gltTicketOrder.getDeliveryStatus() != null && gltTicketOrder.getDeliveryStatus() != null
&& gltTicketOrder.getDeliveryStatus() == GltTicketOrderService.DELIVERY_STATUS_FINISHED) { && gltTicketOrder.getDeliveryStatus() == GltTicketOrderService.DELIVERY_STATUS_FINISHED) {
gltTicketOrderService.markShopOrderCompletedAfterTicketFinished(gltTicketOrder.getId(), tenantId); gltTicketOrderService.markShopOrderCompletedAfterTicketFinished(gltTicketOrder.getId());
} }
return success("修改成功"); return success("修改成功");
} }

View File

@@ -79,6 +79,7 @@ public class GltTicketTemplateController extends BaseController {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
if (loginUser != null) { if (loginUser != null) {
gltTicketTemplate.setUserId(loginUser.getUserId()); gltTicketTemplate.setUserId(loginUser.getUserId());
gltTicketTemplate.setTenantId(loginUser.getTenantId());
} }
if (gltTicketTemplateService.save(gltTicketTemplate)) { if (gltTicketTemplateService.save(gltTicketTemplate)) {
return success("添加成功"); return success("添加成功");

View File

@@ -1,15 +1,15 @@
package com.gxwebsoft.glt.controller; package com.gxwebsoft.glt.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.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.common.system.entity.User;
import com.gxwebsoft.glt.service.GltUserTicketReleaseService;
import com.gxwebsoft.glt.entity.GltUserTicketRelease; import com.gxwebsoft.glt.entity.GltUserTicketRelease;
import com.gxwebsoft.glt.param.GltUserTicketReleaseParam; import com.gxwebsoft.glt.param.GltUserTicketReleaseParam;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.glt.service.GltUserTicketReleaseService;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.glt.service.impl.GltUserTicketAutoReleaseServiceImpl;
import com.gxwebsoft.common.core.web.PageParam;
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.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;
@@ -31,6 +31,9 @@ public class GltUserTicketReleaseController extends BaseController {
@Resource @Resource
private GltUserTicketReleaseService gltUserTicketReleaseService; private GltUserTicketReleaseService gltUserTicketReleaseService;
@Resource
private GltUserTicketAutoReleaseServiceImpl gltUserTicketAutoReleaseService;
@PreAuthorize("hasAuthority('glt:gltUserTicketRelease:list')") @PreAuthorize("hasAuthority('glt:gltUserTicketRelease:list')")
@Operation(summary = "分页查询水票释放") @Operation(summary = "分页查询水票释放")
@GetMapping("/page") @GetMapping("/page")
@@ -126,4 +129,11 @@ public class GltUserTicketReleaseController extends BaseController {
return fail("删除失败"); return fail("删除失败");
} }
@Operation(summary = "水票释放测试")
@PostMapping("/releaseTask")
public ApiResult<?> releaseTask() {
gltUserTicketAutoReleaseService.releaseTask();
return success(true);
}
} }

View File

@@ -0,0 +1,25 @@
package com.gxwebsoft.glt.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* 转单请求类
*
*/
@Data
@Schema(name = "ShopOrderMyVerifyDto", description = "转单请求类")
public class GltTransferOrderDto {
@Schema(description = "订单ID")
@NotEmpty(message = "订单ID不能为空")
private Integer id;
@Schema(description = "转单用户ID")
@NotEmpty(message = "转单用户ID不能为空")
private Integer userId;
}

View File

@@ -0,0 +1,35 @@
package com.gxwebsoft.glt.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 配送员新订单配送通知
*
*/
@Data
@Schema(name = "ShopOrderMyVerifyDto", description = "配送员新订单配送通知")
public class NoticeRiderNewOrderDto {
@Schema(description = "订单号")
private String orderNo;
@Schema(description = "商品名称")
private String goodsName;
@Schema(description = "配送数量")
private Integer productCount;
@Schema(description = "期望送达时间")
private LocalDateTime createTime;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "openID")
private String openId;
}

View File

@@ -1,9 +1,6 @@
package com.gxwebsoft.glt.entity; package com.gxwebsoft.glt.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -22,17 +19,20 @@ import java.time.LocalDateTime;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "GltTicketOrder对象", description = "送水订单") @Schema(name = "GltTicketOrder对象", description = "送水订单")
@TableName("glt_ticket_order")
public class GltTicketOrder implements Serializable { public class GltTicketOrder implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.AUTO)
private Integer id; private Integer id;
@Schema(description = "订单编号")
private String no;
@Schema(description = "用户水票ID") @Schema(description = "用户水票ID")
private Integer userTicketId; private Integer userTicketId;
@Schema(description = "订单编号") @Schema(description = "关联订单编号")
@TableField(exist = false)
private String orderNo; private String orderNo;
@Schema(description = "订单状态") @Schema(description = "订单状态")
@@ -46,6 +46,10 @@ public class GltTicketOrder implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String storeName; private String storeName;
@Schema(description = "品名")
@TableField(exist = false)
private String goodsName;
@Schema(description = "门店地址") @Schema(description = "门店地址")
@TableField(exist = false) @TableField(exist = false)
private String storeAddress; private String storeAddress;
@@ -191,6 +195,19 @@ 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 = "详细地址")
@TableField(exist = false)
private String fullAddress;
@Schema(description = "配送费(步梯+送上楼时计算:数量 × (楼层-1)")
private BigDecimal deliveryFee;
@Schema(description = "排序(数字越小越靠前)") @Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber; private Integer sortNumber;
@@ -200,6 +217,9 @@ public class GltTicketOrder implements Serializable {
@Schema(description = "状态, 0正常, 1冻结") @Schema(description = "状态, 0正常, 1冻结")
private Integer status; private Integer status;
@Schema(description = "水票标识 0-非 1-是")
private Integer waterTicketFlag;
@Schema(description = "是否删除, 0否, 1是") @Schema(description = "是否删除, 0否, 1是")
@TableLogic @TableLogic
private Integer deleted; private Integer deleted;

View File

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable; import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -19,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "GltTicketTemplate对象", description = "水票") @Schema(name = "GltTicketTemplate对象", description = "水票")
@TableName("glt_ticket_template")
public class GltTicketTemplate implements Serializable { public class GltTicketTemplate implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@@ -1,12 +1,9 @@
package com.gxwebsoft.glt.entity; package com.gxwebsoft.glt.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -22,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "GltUserTicket对象", description = "我的水票") @Schema(name = "GltUserTicket对象", description = "我的水票")
@TableName("glt_user_ticket")
public class GltUserTicket implements Serializable { public class GltUserTicket implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@@ -1,10 +1,8 @@
package com.gxwebsoft.glt.entity; package com.gxwebsoft.glt.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -20,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "GltUserTicketLog对象", description = "消费日志") @Schema(name = "GltUserTicketLog对象", description = "消费日志")
@TableName("glt_user_ticket_log")
public class GltUserTicketLog implements Serializable { public class GltUserTicketLog implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@@ -1,10 +1,8 @@
package com.gxwebsoft.glt.entity; package com.gxwebsoft.glt.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -20,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "GltUserTicketRelease对象", description = "水票释放") @Schema(name = "GltUserTicketRelease对象", description = "水票释放")
@TableName("glt_user_ticket_release")
public class GltUserTicketRelease implements Serializable { public class GltUserTicketRelease implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -27,7 +26,7 @@ public class GltUserTicketRelease implements Serializable {
private Long id; private Long id;
@Schema(description = "水票ID") @Schema(description = "水票ID")
private Long userTicketId; private Integer userTicketId;
@Schema(description = "用户ID") @Schema(description = "用户ID")
private Integer userId; private Integer userId;
@@ -57,6 +56,9 @@ public class GltUserTicketRelease implements Serializable {
@Schema(description = "状态") @Schema(description = "状态")
private Integer status; private Integer status;
@Schema(description = "备注")
private String remark;
@Schema(description = "是否删除, 0否, 1是") @Schema(description = "是否删除, 0否, 1是")
@TableLogic @TableLogic
private Integer deleted; private Integer deleted;

View File

@@ -37,6 +37,13 @@ public interface GltUserTicketReleaseMapper extends BaseMapper<GltUserTicketRele
*/ */
List<GltUserTicketRelease> selectListRel(@Param("param") GltUserTicketReleaseParam param); List<GltUserTicketRelease> selectListRel(@Param("param") GltUserTicketReleaseParam param);
/**
* 查询当月待释放水票数据
* @param limitNum 查询数量
* @return List<User>
*/
List<GltUserTicketRelease> getThisMonthReleaseList(@Param("limitNum") Integer limitNum);
/** /**
* 查询待释放且到期的记录(加行锁,防止多实例重复处理) * 查询待释放且到期的记录(加行锁,防止多实例重复处理)
* *

View File

@@ -11,7 +11,7 @@
d.name as receiverName, d.phone as receiverPhone, d.name as receiverName, d.phone as receiverPhone,
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion, d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion,
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng, d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng,
COALESCE(o.order_no, f.order_no) as orderNo, o.order_status as orderStatus COALESCE(o.order_no, a.order_no) as orderNo, o.order_status as orderStatus
FROM glt_ticket_order a FROM glt_ticket_order a
LEFT JOIN shop_store b ON a.store_id = b.id LEFT JOIN shop_store b ON a.store_id = b.id
LEFT JOIN shop_store_warehouse w ON a.warehouse_id = w.id LEFT JOIN shop_store_warehouse w ON a.warehouse_id = w.id
@@ -20,7 +20,6 @@
LEFT JOIN shop_user_address d ON a.address_id = d.id LEFT JOIN shop_user_address d ON a.address_id = d.id
LEFT JOIN glt_user_ticket f ON a.user_ticket_id = f.id LEFT JOIN glt_user_ticket f ON a.user_ticket_id = f.id
LEFT JOIN shop_order o ON f.order_id = o.order_id AND f.tenant_id = o.tenant_id AND o.deleted = 0 LEFT JOIN shop_order o ON f.order_id = o.order_id AND f.tenant_id = o.tenant_id AND o.deleted = 0
<where> <where>
<if test="param.id != null"> <if test="param.id != null">
AND a.id = #{param.id} AND a.id = #{param.id}
@@ -88,7 +87,7 @@
AND a.create_time &lt;= #{param.createTimeEnd} AND a.create_time &lt;= #{param.createTimeEnd}
</if> </if>
<if test="param.orderNo != null"> <if test="param.orderNo != null">
AND (a.id = #{param.orderNo} OR COALESCE(o.order_no, f.order_no) = #{param.orderNo}) AND (a.id = #{param.orderNo} OR COALESCE(o.order_no, a.order_no) = #{param.orderNo})
</if> </if>
<if test="param.phone != null"> <if test="param.phone != null">
AND u.phone = #{param.phone} AND u.phone = #{param.phone}
@@ -103,6 +102,12 @@
OR u.phone LIKE CONCAT('%', #{param.keywords}, '%') OR u.phone LIKE CONCAT('%', #{param.keywords}, '%')
) )
</if> </if>
<if test="param.waterTicketFlag != null">
AND a.water_ticket_flag = #{param.waterTicketFlag}
</if>
<if test="param.deliveryStatus != null and param.deliveryStatus == 10">
AND o.order_status in (0, 1, 5)
</if>
</where> </where>
</sql> </sql>

View File

@@ -58,5 +58,23 @@
<select id="selectListRel" resultType="com.gxwebsoft.glt.entity.GltUserTicketRelease"> <select id="selectListRel" resultType="com.gxwebsoft.glt.entity.GltUserTicketRelease">
<include refid="selectSql"></include> <include refid="selectSql"></include>
</select> </select>
<select id="getThisMonthReleaseList" resultType="com.gxwebsoft.glt.entity.GltUserTicketRelease">
SELECT
id,
user_ticket_id,
user_id,
release_qty,
tenant_id
FROM
glt_user_ticket_release
WHERE
STATUS = 0
AND deleted = 0
AND release_qty > 0
AND DATE_FORMAT(release_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')
ORDER BY
release_time ASC
LIMIT #{limitNum}
</select>
</mapper> </mapper>

View File

@@ -94,4 +94,7 @@ public class GltTicketOrderParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer orderStatus; private Integer orderStatus;
@Schema(description = "水票订单标识 0-否 1-是")
private Integer waterTicketFlag;
} }

View File

@@ -0,0 +1,35 @@
package com.gxwebsoft.glt.service;
import com.gxwebsoft.glt.dto.NoticeRiderNewOrderDto;
import com.gxwebsoft.glt.entity.GltTicketOrder;
/**
* 微信订阅消息服务接口
*/
public interface GltSubscribeMessageService {
/**
* 发送新订单通知给配送员
* @param order 订单信息
* @param riderOpenId 配送员微信openId
* @param tenantId 租户ID
* @return 是否发送成功
*/
boolean sendNewOrderNotice(GltTicketOrder order, String riderOpenId, Integer tenantId);
/**
* 发送订单状态变更通知
* @param order 订单信息
* @param riderOpenId 配送员微信openId
* @param statusText 状态描述
* @param tenantId 租户ID
* @return 是否发送成功
*/
boolean sendOrderStatusNotice(GltTicketOrder order, String riderOpenId, String statusText, Integer tenantId);
/**
* 推送配送师傅接单提醒
* @param noticeRiderNewOrderDto
*/
void sendRiderNewOrderNotice(NoticeRiderNewOrderDto noticeRiderNewOrderDto);
}

View File

@@ -1,28 +1,26 @@
package com.gxwebsoft.glt.service; 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.gxwebsoft.glt.entity.*;
import com.gxwebsoft.glt.entity.GltTicketTemplate; import com.gxwebsoft.glt.task.DealerOrderSettlement10584Task;
import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog;
import com.gxwebsoft.glt.entity.GltUserTicketRelease;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderGoods; import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.mapper.ShopOrderMapper;
import com.gxwebsoft.shop.service.ShopOrderGoodsService; import com.gxwebsoft.shop.service.ShopOrderGoodsService;
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.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 javax.annotation.Resource;
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;
/** /**
* 套票发放(从订单生成用户套票 + 释放计划)的业务逻辑。 * 套票发放(从订单生成用户套票 + 释放计划)的业务逻辑。
@@ -37,6 +35,9 @@ import java.util.Set;
@RequiredArgsConstructor @RequiredArgsConstructor
public class GltTicketIssueService { public class GltTicketIssueService {
@Resource
private GltTicketOrderService gltTicketOrderService;
public static final int CHANGE_TYPE_ISSUE = 10; public static final int CHANGE_TYPE_ISSUE = 10;
private enum IssueOutcome { private enum IssueOutcome {
@@ -46,7 +47,7 @@ public class GltTicketIssueService {
NO_TEMPLATE NO_TEMPLATE
} }
private final ShopOrderService shopOrderService; private final ShopOrderMapper shopOrderMapper;
private final ShopOrderGoodsService shopOrderGoodsService; private final ShopOrderGoodsService shopOrderGoodsService;
private final GltTicketTemplateService gltTicketTemplateService; private final GltTicketTemplateService gltTicketTemplateService;
@@ -54,6 +55,7 @@ public class GltTicketIssueService {
private final GltUserTicketReleaseService gltUserTicketReleaseService; private final GltUserTicketReleaseService gltUserTicketReleaseService;
private final GltUserTicketLogService gltUserTicketLogService; private final GltUserTicketLogService gltUserTicketLogService;
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
private final DealerOrderSettlement10584Task dealerOrderSettlement;
/** /**
* 扫描“今日订单”,执行套票发放。 * 扫描“今日订单”,执行套票发放。
@@ -84,7 +86,7 @@ public class GltTicketIssueService {
LocalDateTime todayStart = LocalDate.now().atStartOfDay(); LocalDateTime todayStart = LocalDate.now().atStartOfDay();
LocalDateTime tomorrowStart = todayStart.plusDays(1); LocalDateTime tomorrowStart = todayStart.plusDays(1);
List<ShopOrder> orders = shopOrderService.list( List<ShopOrder> orders = shopOrderMapper.selectList(
new LambdaQueryWrapper<ShopOrder>() new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId) .eq(ShopOrder::getTenantId, tenantId)
.in(ShopOrder::getFormId, uniqueGoodsIds) .in(ShopOrder::getFormId, uniqueGoodsIds)
@@ -128,6 +130,74 @@ public class GltTicketIssueService {
tenantId, uniqueGoodsIds, orders.size(), success, skipped, failed); tenantId, uniqueGoodsIds, orders.size(), success, skipped, failed);
} }
/**
* 商品订单支付成功后调后需处理业务
* @param orderNo 订单号
* @param tenantId 租户ID
*/
@Async
public void paySuccessTask(String orderNo, Integer tenantId){
//1.发送水票
suerTicketRelease(orderNo, tenantId);
//2.执行分销员分销、统计门店/服务商分销业务
dealerOrderSettlement.orderSettlement(orderNo);
//3.执行平台分红业务 TODO 待开发
//4.普通商品【非水票订单】如果是需要自配送的,则会同步生成派单信息
gltTicketOrderService.dispatchOrder(orderNo, tenantId);
}
/**
* 订单支付成功,直接发送水票【后期优化订单类型,为水票的订单才需要执行此业务】
* @param orderNo 订单号
* @param tenantId 租户ID
*/
@Transactional
public void suerTicketRelease(String orderNo, Integer tenantId){
//1.订单为空跳过执行
ShopOrder shopOrder = shopOrderMapper.selectOne(new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getOrderNo, orderNo)
.eq(ShopOrder::getTenantId, tenantId));
if(shopOrder == null){
return;
}
//2.只有水票订单才需要发送水票
if(!(shopOrder.getWaterTicketFlag() != null && shopOrder.getWaterTicketFlag() == 1)){
return;
}
//3.跳过已完成发放套票订单
if(shopOrder.getOrderStatus() == 1){
return;
}
//4.订单商品为空跳过执行
List<ShopOrderGoods> goodsList = shopOrderGoodsService.getListByOrderIdIgnoreTenant(shopOrder.getOrderId());
if (CollectionUtils.isEmpty(goodsList)) {
return;
}
//5.执行水票发放业务
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);
}
});
//6.更新商品订单为已完成、已收到赠品状态
if (release.get()) {
shopOrder.setHasTakeGift(true);
shopOrder.setUpdateTime(LocalDateTime.now());
shopOrderMapper.updateById(shopOrder);
}
}
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()) {
@@ -155,15 +225,10 @@ public class GltTicketIssueService {
if (shouldCompleteOrder) { if (shouldCompleteOrder) {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
// 任务执行完后将订单置为“已完成”,避免后续扫描重复处理(幂等虽可挡住,但会产生大量无意义查询)。 // 任务执行完后将订单置为“已完成”,避免后续扫描重复处理(幂等虽可挡住,但会产生大量无意义查询)。
shopOrderService.update( order.setOrderStatus(1);
new LambdaUpdateWrapper<ShopOrder>() order.setHasTakeGift(true);
.eq(ShopOrder::getOrderId, order.getOrderId()) order.setUpdateTime(LocalDateTime.now());
.eq(ShopOrder::getTenantId, tenantId) shopOrderMapper.updateById(order);
.eq(ShopOrder::getOrderStatus, 0)
.set(ShopOrder::getOrderStatus, 1)
.set(ShopOrder::getHasTakeGift, true)
.set(ShopOrder::getUpdateTime, now)
);
} }
return issuedCount; return issuedCount;
@@ -184,7 +249,7 @@ public class GltTicketIssueService {
// - 这里先对商城订单行加行锁,保证同一订单在同一时刻只会被一个事务处理。 // - 这里先对商城订单行加行锁,保证同一订单在同一时刻只会被一个事务处理。
// (注意:需数据库支持 SELECT ... FOR UPDATE且 shop_order.order_id 为主键/有索引) // (注意:需数据库支持 SELECT ... FOR UPDATE且 shop_order.order_id 为主键/有索引)
if (order.getOrderId() != null) { if (order.getOrderId() != null) {
shopOrderService.getOne( shopOrderMapper.selectOne(
new LambdaQueryWrapper<ShopOrder>() new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, order.getOrderId()) .eq(ShopOrder::getOrderId, order.getOrderId())
.eq(ShopOrder::getTenantId, tenantId) .eq(ShopOrder::getTenantId, tenantId)
@@ -304,12 +369,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 +443,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 +463,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 +484,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;
@@ -422,7 +500,7 @@ public class GltTicketIssueService {
LocalDateTime releaseTime, LocalDateTime releaseTime,
LocalDateTime now) { LocalDateTime now) {
GltUserTicketRelease r = new GltUserTicketRelease(); GltUserTicketRelease r = new GltUserTicketRelease();
r.setUserTicketId(userTicket.getId() != null ? userTicket.getId().longValue() : null); r.setUserTicketId(userTicket.getId() != null ? userTicket.getId() : null);
r.setUserId(userTicket.getUserId()); r.setUserId(userTicket.getUserId());
r.setPeriodNo(periodNo); r.setPeriodNo(periodNo);
r.setReleaseQty(releaseQty); r.setReleaseQty(releaseQty);

View File

@@ -2,6 +2,7 @@ package com.gxwebsoft.glt.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.glt.dto.GltTransferOrderDto;
import com.gxwebsoft.glt.entity.GltTicketOrder; import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderParam; import com.gxwebsoft.glt.param.GltTicketOrderParam;
@@ -16,10 +17,10 @@ import java.time.LocalDateTime;
*/ */
public interface GltTicketOrderService extends IService<GltTicketOrder> { public interface GltTicketOrderService extends IService<GltTicketOrder> {
int DELIVERY_STATUS_WAITING = 10; int DELIVERY_STATUS_WAITING = 10; //待配送
int DELIVERY_STATUS_DELIVERING = 20; int DELIVERY_STATUS_DELIVERING = 20; //配送中
int DELIVERY_STATUS_WAIT_CONFIRM = 30; int DELIVERY_STATUS_WAIT_CONFIRM = 30; //等待收货
int DELIVERY_STATUS_FINISHED = 40; int DELIVERY_STATUS_FINISHED = 40; //收货完成
int RECEIVE_CONFIRM_TYPE_MANUAL = 10; int RECEIVE_CONFIRM_TYPE_MANUAL = 10;
int RECEIVE_CONFIRM_TYPE_PHOTO = 20; int RECEIVE_CONFIRM_TYPE_PHOTO = 20;
@@ -64,6 +65,13 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
*/ */
void accept(Integer id, Integer riderId, Integer tenantId); void accept(Integer id, Integer riderId, Integer tenantId);
/**
* 配送员转单
* @param orderDto
* @return
*/
Boolean transferOrder(GltTransferOrderDto orderDto);
/** /**
* 指派/接单成功后,同步关联商城订单发货状态为“已发货”(deliveryStatus=20)。 * 指派/接单成功后,同步关联商城订单发货状态为“已发货”(deliveryStatus=20)。
* *
@@ -76,7 +84,7 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
* *
* <p>用于后台直接改 deliveryStatus=40 等不经过 confirmReceive/autoConfirmTimeout 的兜底同步。</p> * <p>用于后台直接改 deliveryStatus=40 等不经过 confirmReceive/autoConfirmTimeout 的兜底同步。</p>
*/ */
void markShopOrderCompletedAfterTicketFinished(Integer ticketOrderId, Integer tenantId); void markShopOrderCompletedAfterTicketFinished(Integer id);
/** /**
* 配送员开始配送10 -> 20并写 sendStartTime。 * 配送员开始配送10 -> 20并写 sendStartTime。
@@ -106,4 +114,10 @@ public interface GltTicketOrderService extends IService<GltTicketOrder> {
*/ */
int autoConfirmTimeout(Integer tenantId, LocalDateTime now, int timeoutHours, int batchSize); int autoConfirmTimeout(Integer tenantId, LocalDateTime now, int timeoutHours, int batchSize);
/**
* 派送订单调度
* @return
*/
Boolean dispatchOrder(String orderNo, Integer tenantId);
} }

View File

@@ -133,7 +133,7 @@ public class GltTicketRevokeService {
LambdaUpdateWrapper<GltUserTicketRelease> uw = new LambdaUpdateWrapper<GltUserTicketRelease>() LambdaUpdateWrapper<GltUserTicketRelease> uw = new LambdaUpdateWrapper<GltUserTicketRelease>()
.eq(GltUserTicketRelease::getTenantId, tenantId) .eq(GltUserTicketRelease::getTenantId, tenantId)
.eq(GltUserTicketRelease::getDeleted, 0) .eq(GltUserTicketRelease::getDeleted, 0)
.eq(GltUserTicketRelease::getUserTicketId, userTicketId.longValue()) .eq(GltUserTicketRelease::getUserTicketId, userTicketId)
// status 为空时也视为“未完成” // status 为空时也视为“未完成”
.and(w -> w.ne(GltUserTicketRelease::getStatus, RELEASE_STATUS_DONE) .and(w -> w.ne(GltUserTicketRelease::getStatus, RELEASE_STATUS_DONE)
.or().isNull(GltUserTicketRelease::getStatus)) .or().isNull(GltUserTicketRelease::getStatus))

View File

@@ -17,10 +17,8 @@ public interface GltUserTicketAutoReleaseService {
int releaseDue(LocalDateTime now, int batchSize); int releaseDue(LocalDateTime now, int batchSize);
/** /**
* 释放到期的冻结水票(使用系统当前时间) * 释放到期的冻结水票【当次执行1000条】
*/ */
default int releaseDue(int batchSize) { void releaseTask();
return releaseDue(LocalDateTime.now(), batchSize);
}
} }

View File

@@ -0,0 +1,266 @@
package com.gxwebsoft.glt.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.utils.DateTimeUtil;
import com.gxwebsoft.glt.dto.NoticeRiderNewOrderDto;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.service.GltSubscribeMessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KEY;
import static com.gxwebsoft.common.core.constants.RedisConstants.MP_WX_KEY;
/**
* 微信订阅消息服务实现
*
* <p>功能:
* <ul>
* <li>新订单通知配送员</li>
* <li>订单状态变更通知</li>
* </ul>
* </p>
*/
@Slf4j
@Service
public class GltSubscribeMessageServiceImpl implements GltSubscribeMessageService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 订阅消息模板ID需在微信公众平台配置
* 模板名称:订单配送通知
* 关键词:订单编号、订单内容、配送地址、订单金额
*/
private static final String ORDER_DELIVERY_ID = "vSMSqGVy3aG1RuzQUZlk282p5hCvuMBcHvix1AFhN90";
public void sendRiderNewOrderNotice(NoticeRiderNewOrderDto entity){
//1.获取微信accessToken
String accessToken = getAccessToken(entity.getTenantId());
if(accessToken == null){
log.error("配送发单消息发生失败获取accessToken失败");
return;
}
//2.品名字段最大能发送20个长度字符大于15的长度需做处理
String goodsName = entity.getGoodsName();
int keepLength = 15;
if (goodsName.length() > keepLength) {
entity.setGoodsName(goodsName.substring(0, keepLength) + "...");
}
//3.组装入参
Map<String, Object> data = new HashMap<>();
data.put("character_string1", Map.of("value", String.valueOf(entity.getOrderNo())));
data.put("thing22", Map.of("value", String.valueOf(entity.getGoodsName())));
data.put("number20", Map.of("value", String.valueOf(entity.getProductCount())));
data.put("time24", Map.of("value", DateTimeUtil.formatDateTime(entity.getCreateTime(), "yyyy-MM-dd HH:mm:ss")));
//推送订阅消息
sendSubscribeMessage(accessToken, entity.getOpenId(), data);
}
/**
* 发送新订单通知给配送员
*/
@Override
public boolean sendNewOrderNotice(GltTicketOrder order, String riderOpenId, Integer tenantId) {
if (order == null || StrUtil.isBlank(riderOpenId) || tenantId == null) {
log.warn("发送订阅消息参数不完整");
return false;
}
try {
String accessToken = getAccessToken(tenantId);
if (StrUtil.isBlank(accessToken)) {
log.warn("获取access_token失败");
return false;
}
// 构建消息内容
Map<String, Object> data = new HashMap<>();
data.put("phrase1", Map.of("value", "待配送")); // 订单状态
data.put("character_string2", Map.of("value", String.valueOf(order.getOrderNo()))); // 订单编号
data.put("thing3", Map.of("value", truncateStr(order.getAddress(), 20))); // 配送地址
data.put("number4", Map.of("value", String.valueOf(order.getTotalNum()))); // 商品数量
data.put("time5", Map.of("value", formatTime(order.getSendTime()))); // 期望送达时间
// 发送订阅消息
return sendSubscribeMessage(accessToken, riderOpenId, data);
} catch (Exception e) {
log.error("发送新订单订阅消息失败 - orderId={}, riderOpenId={}, error={}",
order.getId(), riderOpenId, e.getMessage(), e);
return false;
}
}
/**
* 发送订单状态变更通知
*/
@Override
public boolean sendOrderStatusNotice(GltTicketOrder order, String riderOpenId, String statusText, Integer tenantId) {
if (order == null || StrUtil.isBlank(riderOpenId) || tenantId == null) {
log.warn("发送订阅消息参数不完整");
return false;
}
try {
String accessToken = getAccessToken(tenantId);
if (StrUtil.isBlank(accessToken)) {
log.warn("获取access_token失败");
return false;
}
// 构建消息内容
Map<String, Object> data = new HashMap<>();
data.put("phrase1", Map.of("value", truncateStr(statusText, 5))); // 状态描述
data.put("character_string2", Map.of("value", String.valueOf(order.getId()))); // 订单编号
data.put("time3", Map.of("value", formatTime(null))); // 通知时间
// 发送订阅消息
return sendSubscribeMessage(accessToken, riderOpenId, data);
} catch (Exception e) {
log.error("发送订单状态变更订阅消息失败 - orderId={}, riderOpenId={}, error={}",
order.getId(), riderOpenId, e.getMessage(), e);
return false;
}
}
/**
* 获取小程序的 access_token
*/
private String getAccessToken(Integer tenantId) {
if (tenantId == null) {
throw new BusinessException("tenantId 不能为空");
}
final String tokenCacheKey = ACCESS_TOKEN_KEY + ":" + tenantId;
// 1) 优先从缓存取
String cachedValue = stringRedisTemplate.opsForValue().get(tokenCacheKey);
if (StrUtil.isNotBlank(cachedValue)) {
try {
JSONObject cachedJson = JSON.parseObject(cachedValue);
String accessToken = cachedJson.getString("access_token");
if (StrUtil.isNotBlank(accessToken)) {
return accessToken;
}
} catch (Exception ignore) {
// 旧格式:直接存 token
return cachedValue;
}
}
// 2) 缓存没有则从租户配置获取 appId/appSecret
final String wxConfigKey = MP_WX_KEY + tenantId;
final String wxConfigValue = stringRedisTemplate.opsForValue().get(wxConfigKey);
if (StrUtil.isBlank(wxConfigValue)) {
log.warn("未找到微信小程序配置请检查缓存key: {}", wxConfigKey);
return null;
}
JSONObject wxConfig;
try {
wxConfig = JSON.parseObject(wxConfigValue);
} catch (Exception e) {
log.error("微信小程序配置格式错误: {}", e.getMessage());
return null;
}
final String appId = wxConfig.getString("appId");
final String appSecret = wxConfig.getString("appSecret");
if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
log.error("微信小程序配置不完整(appId/appSecret)");
return null;
}
// 3) 调用微信接口获取 token
final String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
+ "&appid=" + appId + "&secret=" + appSecret;
String result = HttpUtil.get(apiUrl);
JSONObject json = JSON.parseObject(result);
if (json.containsKey("errcode") && json.getIntValue("errcode") != 0) {
log.error("获取小程序access_token失败: {}", json.getString("errmsg"));
return null;
}
String accessToken = json.getString("access_token");
Integer expiresIn = json.getInteger("expires_in");
if (StrUtil.isBlank(accessToken)) {
log.error("获取小程序access_token失败: access_token为空");
return null;
}
// 4) 缓存提前5分钟过期
long ttlSeconds = 7000L;
if (expiresIn != null && expiresIn > 300) {
ttlSeconds = expiresIn - 300L;
}
stringRedisTemplate.opsForValue().set(tokenCacheKey, result, ttlSeconds, java.util.concurrent.TimeUnit.SECONDS);
log.info("获取小程序access_token成功 - tenantId={}, ttlSeconds={}", tenantId, ttlSeconds);
return accessToken;
}
/**
* 发送订阅消息
*/
private boolean sendSubscribeMessage(String accessToken, String openId, Map<String, Object> data) {
String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken;
Map<String, Object> params = new HashMap<>();
params.put("touser", openId); // 用户 openid
params.put("template_id", ORDER_DELIVERY_ID); // 模板ID
// params.put("page", "pages/rider/orders/index"); // 点击后跳转的页面
params.put("data", data);
String response = HttpUtil.createPost(url)
.contentType("application/json")
.body(JSON.toJSONString(params))
.timeout(10000)
.execute()
.body();
JSONObject result = JSON.parseObject(response);
int errCode = result.getIntValue("errcode");
if (errCode == 0) {
log.info("订阅消息发送成功 - openId={}", openId);
return true;
} else {
log.warn("订阅消息发送失败 - openId={}, errcode={}, errmsg={}",
openId, errCode, result.getString("errmsg"));
return false;
}
}
/**
* 截断字符串
*/
private String truncateStr(String str, int maxLen) {
if (str == null) return "";
return str.length() > maxLen ? str.substring(0, maxLen) : str;
}
/**
* 格式化时间
*/
private String formatTime(String timeStr) {
if (StrUtil.isBlank(timeStr)) {
return cn.hutool.core.date.DateUtil.now();
}
return timeStr;
}
}

View File

@@ -1,44 +1,60 @@
package com.gxwebsoft.glt.service.impl; package com.gxwebsoft.glt.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
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;
import com.gxwebsoft.common.core.enums.ShopDealerCapitalUpdateEnum;
import com.gxwebsoft.common.core.enums.ShopDealerTypeEnum;
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.core.utils.LoginUserUtil;
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 com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.mapper.UserMapper; import com.gxwebsoft.common.system.mapper.UserMapper;
import com.gxwebsoft.common.system.redis.OrderNoUtils;
import com.gxwebsoft.glt.dto.GltTransferOrderDto;
import com.gxwebsoft.glt.dto.NoticeRiderNewOrderDto;
import com.gxwebsoft.glt.entity.GltTicketOrder; import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog; import com.gxwebsoft.glt.entity.GltUserTicketLog;
import com.gxwebsoft.glt.mapper.GltTicketOrderMapper; import com.gxwebsoft.glt.mapper.GltTicketOrderMapper;
import com.gxwebsoft.glt.mapper.GltUserTicketMapper; import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
import com.gxwebsoft.glt.param.GltTicketOrderParam; import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltSubscribeMessageService;
import com.gxwebsoft.glt.service.GltTicketOrderService; import com.gxwebsoft.glt.service.GltTicketOrderService;
import com.gxwebsoft.glt.service.GltUserTicketLogService; import com.gxwebsoft.glt.service.GltUserTicketLogService;
import com.gxwebsoft.glt.service.GltUserTicketService; import com.gxwebsoft.glt.service.GltUserTicketService;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.dto.ShopDealerUserReduceDto;
import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.entity.*;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.mapper.ShopGoodsMapper;
import com.gxwebsoft.shop.entity.ShopOrderGoods; import com.gxwebsoft.shop.mapper.ShopStoreRiderMapper;
import com.gxwebsoft.shop.mapper.ShopUserAddressMapper;
import com.gxwebsoft.shop.service.ShopDealerCapitalService; import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopOrderGoodsService; import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.vo.ShopOrderGoodsInfoVO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils; import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.StringUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/** /**
* 送水订单Service实现 * 送水订单Service实现
@@ -83,11 +99,54 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
@Resource @Resource
private ShopOrderGoodsService shopOrderGoodsService; private ShopOrderGoodsService shopOrderGoodsService;
@Resource
private ShopUserAddressMapper shopUserAddressMapper;
@Resource
private OrderNoUtils orderNoUtils;
@Resource
private ShopGoodsMapper shopGoodsMapper;
@Resource
private ShopStoreRiderMapper shopStoreRiderMapper;
@Resource
private GltSubscribeMessageService gltSubscribeMessageService;
// 轮询指针(高并发安全)
private final AtomicInteger index = new AtomicInteger(0);
@Override @Override
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) { public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param); PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc"); page.setDefaultOrder("create_time desc");
List<GltTicketOrder> list = baseMapper.selectPageRel(page, param); List<GltTicketOrder> list = baseMapper.selectPageRel(page, param);
if(CollectionUtils.isNotEmpty(list)){
List<Integer> addressIdList = list.stream().map(GltTicketOrder::getAddressId).distinct().collect(Collectors.toList());
List<String> orderNoList = list.stream().filter(gltTicketOrder -> StrUtil.isNotBlank(gltTicketOrder.getOrderNo())).map(GltTicketOrder::getOrderNo).distinct().collect(Collectors.toList());
List<ShopOrderGoodsInfoVO> orderGoodsInfoVOList = new ArrayList<>();
if(CollectionUtils.isNotEmpty(orderNoList)){
orderGoodsInfoVOList = shopOrderService.getOrderGoodsInfoByOrderNos(orderNoList);
}
List<ShopUserAddress> userAddressList = shopUserAddressMapper.selectBatchIds(addressIdList);
List<ShopOrderGoodsInfoVO> finalOrderGoodsInfoVOList = orderGoodsInfoVOList;
list.forEach(ticketOrder ->{
ShopUserAddress shopUserAddress = userAddressList.stream().filter(address -> ticketOrder.getAddressId().equals(address.getId())).findFirst().orElse(null);
if(shopUserAddress != null){
ticketOrder.setFullAddress(shopUserAddress.getFullAddress());
}
if(StrUtil.isNotBlank(ticketOrder.getOrderNo())){
ShopOrderGoodsInfoVO shopOrderGoodsInfoVO = finalOrderGoodsInfoVOList.stream().filter(orderGoodsInfoVO -> ticketOrder.getOrderNo().equals(orderGoodsInfoVO.getOrderNo())).findFirst().orElse(null);
if(shopOrderGoodsInfoVO != null){
ticketOrder.setGoodsName(shopOrderGoodsInfoVO.getGoodsName());
ticketOrder.setOrderStatus(shopOrderGoodsInfoVO.getOrderStatus());
}
}
});
}
return new PageResult<>(list, page.getTotal()); return new PageResult<>(list, page.getTotal());
} }
@@ -116,6 +175,10 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (userId == null) { if (userId == null) {
throw new BusinessException("请先登录"); throw new BusinessException("请先登录");
} }
String no = orderNoUtils.generate("S");
gltTicketOrder.setNo(no);
Integer userTicketId = gltTicketOrder.getUserTicketId(); Integer userTicketId = gltTicketOrder.getUserTicketId();
if (userTicketId == null) { if (userTicketId == null) {
throw new BusinessException("userTicketId不能为空"); throw new BusinessException("userTicketId不能为空");
@@ -151,6 +214,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
// 4) 插入 glt_ticket_orderstoreId/addressId/totalNum/buyerRemarks… // 4) 插入 glt_ticket_orderstoreId/addressId/totalNum/buyerRemarks…
gltTicketOrder.setOrderNo(userTicket.getOrderNo());
gltTicketOrder.setUserId(userId); gltTicketOrder.setUserId(userId);
// 订单基础字段由后端兜底,避免前端误传/恶意传参 // 订单基础字段由后端兜底,避免前端误传/恶意传参
gltTicketOrder.setStatus(0); gltTicketOrder.setStatus(0);
@@ -168,10 +232,13 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
gltTicketOrder.setCreateTime(now); gltTicketOrder.setCreateTime(now);
} }
// “立刻送水”下单场景不再需要前端选择配送时间;若未传则默认当前时间,便于排序与派单。 // “立刻送水”下单场景不再需要前端选择配送时间;若未传则默认当前时间,便于排序与派单。
if (!StringUtils.hasText(gltTicketOrder.getSendTime())) { if(gltTicketOrder.getSendEndTime() == null){
gltTicketOrder.setSendTime(now.format(SEND_TIME_FMT)); gltTicketOrder.setSendTime(now.format(SEND_TIME_FMT));
} }
gltTicketOrder.setUpdateTime(now); gltTicketOrder.setUpdateTime(now);
//每个骑手平等获取派单
gltTicketOrder.setRiderId(getRiderUserId());
if (!this.save(gltTicketOrder)) { if (!this.save(gltTicketOrder)) {
throw new BusinessException("创建订单失败"); throw new BusinessException("创建订单失败");
} }
@@ -207,6 +274,29 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
throw new BusinessException("写入核销记录失败"); throw new BusinessException("写入核销记录失败");
} }
//发送配送师傅派单配送消息
if(gltTicketOrder.getRiderId() != null){
User user = userMapper.getById(gltTicketOrder.getRiderId());
if(user != null){
NoticeRiderNewOrderDto noticeDto = new NoticeRiderNewOrderDto();
noticeDto.setOrderNo(gltTicketOrder.getOrderNo());
noticeDto.setGoodsName("商品信息见订单详情");
noticeDto.setProductCount(gltTicketOrder.getTotalNum());
noticeDto.setCreateTime(gltTicketOrder.getCreateTime());
noticeDto.setTenantId(tenantId);
noticeDto.setOpenId(user.getOpenid());
ShopOrder shopOrder = shopOrderService.getByOrderNo(gltTicketOrder.getOrderNo(), tenantId);
if(shopOrder != null){
Integer formId = shopOrder.getFormId();
ShopGoods shopGoods = shopGoodsMapper.selectById(formId);
if(shopGoods != null){
noticeDto.setGoodsName(shopGoods.getName());
}
}
gltSubscribeMessageService.sendRiderNewOrderNotice(noticeDto);
}
}
return gltTicketOrder; return gltTicketOrder;
} }
@@ -274,7 +364,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
.update(); .update();
if (ok) { if (ok) {
// 接单成功后同步商城订单发货状态10未发货 -> 20已发货 // 接单成功后同步商城订单发货状态10未发货 -> 20已发货
updateShopOrderDeliveryStatusAfterAccept(id, tenantId, riderId, now); updateShopOrderDeliveryStatusAfterAcceptV2(id, tenantId, riderId, now);
return; return;
} }
@@ -294,13 +384,34 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
@Override @Override
public void markShopOrderShippedAfterRiderAssigned(Integer ticketOrderId, Integer tenantId, Integer riderId) { public Boolean transferOrder(GltTransferOrderDto orderDto) {
updateShopOrderDeliveryStatusAfterAccept(ticketOrderId, tenantId, riderId, LocalDateTime.now()); User loginUser = LoginUserUtil.getLoginUser();
if(loginUser == null){
throw new BusinessException(GlobalErrorCodeConstants.UNAUTHORIZED.getMsg());
}
GltTicketOrder gltTicketOrder = baseMapper.selectById(orderDto.getId());
if(gltTicketOrder != null){
if(!gltTicketOrder.getRiderId().equals(loginUser.getUserId())){
throw new BusinessException("该订单归属非本人,转单操作失败!");
}
gltTicketOrder.setRiderId(orderDto.getUserId());
gltTicketOrder.setUpdateTime(LocalDateTime.now());
return baseMapper.updateById(gltTicketOrder) > 0;
}else {
throw new BusinessException(GlobalErrorCodeConstants.NOT_FOUND.getMsg());
}
} }
@Override @Override
public void markShopOrderCompletedAfterTicketFinished(Integer ticketOrderId, Integer tenantId) { public void markShopOrderShippedAfterRiderAssigned(Integer ticketOrderId, Integer tenantId, Integer riderId) {
updateShopOrderOrderStatusAfterTicketFinished(ticketOrderId, tenantId, LocalDateTime.now()); updateShopOrderDeliveryStatusAfterAcceptV2(ticketOrderId, tenantId, riderId, LocalDateTime.now());
}
@Override
public void markShopOrderCompletedAfterTicketFinished(Integer id) {
updateShopOrderOrderStatusAfterTicketFinishedV2(id);
} }
private void updateShopOrderDeliveryStatusAfterAccept(Integer ticketOrderId, Integer tenantId, Integer riderId, LocalDateTime now) { private void updateShopOrderDeliveryStatusAfterAccept(Integer ticketOrderId, Integer tenantId, Integer riderId, LocalDateTime now) {
@@ -370,22 +481,6 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
shopOrderNo = order.getOrderNo(); shopOrderNo = order.getOrderNo();
} }
} }
LambdaUpdateWrapper<GltUserTicket> backfill = new LambdaUpdateWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, userTicket.getId());
backfill.set(GltUserTicket::getOrderId, shopOrderId);
if (!StringUtils.hasText(userTicket.getOrderNo()) && StringUtils.hasText(shopOrderNo)) {
backfill.set(GltUserTicket::getOrderNo, shopOrderNo);
}
backfill.set(GltUserTicket::getUpdateTime, now);
try {
gltUserTicketService.update(backfill);
} catch (Exception e) {
log.debug("回填水票关联商城订单信息失败(不影响主流程) - tenantId={}, userTicketId={}, orderId={}, orderNo={}",
tenantId, userTicket.getId(), shopOrderId, shopOrderNo, e);
}
} }
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>() LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
@@ -433,6 +528,20 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
} }
private void updateShopOrderDeliveryStatusAfterAcceptV2(Integer ticketOrderId, Integer tenantId, Integer riderId, LocalDateTime now) {
GltTicketOrder gltTicketOrder = baseMapper.selectById(ticketOrderId);
if(gltTicketOrder != null && StrUtil.isNotBlank(gltTicketOrder.getOrderNo())){
String orderNo = gltTicketOrder.getOrderNo();
ShopOrder shopOrder = shopOrderService.getByOrderNo(orderNo, tenantId);
if(shopOrder != null && shopOrder.getOrderStatus() == 0){
shopOrder.setDeliveryStatus(20);
shopOrder.setUpdateTime(now);
shopOrderService.updateById(shopOrder);
}
}
}
@Override @Override
public void start(Integer id, Integer riderId, Integer tenantId) { public void start(Integer id, Integer riderId, Integer tenantId) {
if (id == null) { if (id == null) {
@@ -656,7 +765,80 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return confirmed; return confirmed;
} }
private void updateShopOrderOrderStatusAfterTicketFinished(Integer ticketOrderId, Integer tenantId, LocalDateTime now) { @Override
public Boolean dispatchOrder(String orderNo, Integer tenantId) {
ShopOrder shopOrder = shopOrderService.getByOrderNo(orderNo, tenantId);
if(shopOrder != null){
LocalDateTime now = LocalDateTime.now();
//只有:1-及时自配送、3-预约自配送 方式会生成调度单
if(Arrays.asList(1, 3).contains(shopOrder.getOrderType())){
GltTicketOrder dispatchOrder = BeanUtil.toBean(shopOrder, GltTicketOrder.class);
String no = orderNoUtils.generate("S");
dispatchOrder.setNo(no);
dispatchOrder.setRiderId(getRiderUserId());
dispatchOrder.setSendTime(now.format(SEND_TIME_FMT));
Integer deliveryMethod = shopOrder.getDeliveryMethod();
if(deliveryMethod != null && deliveryMethod == 1){
dispatchOrder.setDeliveryMethod("stairs");
}else {
dispatchOrder.setDeliveryMethod("elevator");
}
dispatchOrder.setComments("系统自动派单");
dispatchOrder.setCreateTime(now);
dispatchOrder.setUpdateTime(now);
baseMapper.insert(dispatchOrder);
//推送配送师傅接单提醒
if(dispatchOrder.getRiderId() != null){
User user = userMapper.getById(dispatchOrder.getRiderId());
if(user != null){
NoticeRiderNewOrderDto noticeDto = new NoticeRiderNewOrderDto();
noticeDto.setOrderNo(dispatchOrder.getOrderNo());
noticeDto.setGoodsName("商品信息见订单详情");
noticeDto.setProductCount(dispatchOrder.getTotalNum());
noticeDto.setCreateTime(dispatchOrder.getCreateTime());
noticeDto.setTenantId(tenantId);
noticeDto.setOpenId(user.getOpenid());
if(shopOrder != null){
Integer formId = shopOrder.getFormId();
ShopGoods shopGoods = shopGoodsMapper.selectById(formId);
if(shopGoods != null){
noticeDto.setGoodsName(shopGoods.getName());
}
}
gltSubscribeMessageService.sendRiderNewOrderNotice(noticeDto);
}
}
}
}
return Boolean.TRUE;
}
/**
* 每个骑手平等获取派单
* @return
*/
private Integer getRiderUserId(){
List<ShopStoreRider> shopStoreRiders = shopStoreRiderMapper.selectList(new LambdaQueryWrapper<ShopStoreRider>().select(ShopStoreRider::getUserId).in(ShopStoreRider::getWorkStatus, Arrays.asList(1, 2)));
if(CollectionUtils.isNotEmpty(shopStoreRiders)){
List<Integer> riderIdList = shopStoreRiders.stream().map(ShopStoreRider::getUserId).distinct().collect(Collectors.toList());
int i = index.getAndIncrement() % riderIdList.size();
return riderIdList.get(i);
}
return null;
}
/**
* 更新送水订单为已完成状态
* @param ticketOrderId
* @param tenantId
* @param now
*/
@Transactional
public void updateShopOrderOrderStatusAfterTicketFinished(Integer ticketOrderId, Integer tenantId, LocalDateTime now) {
if (ticketOrderId == null || tenantId == null) { if (ticketOrderId == null || tenantId == null) {
return; return;
} }
@@ -666,7 +848,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
// 找到关联水票的商城订单glt_user_ticket.orderId / orderNo // 找到关联水票的商城订单glt_user_ticket.orderId / orderNo
GltTicketOrder ticketOrder = this.lambdaQuery() GltTicketOrder ticketOrder = this.lambdaQuery()
.select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId) .select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId, GltTicketOrder::getTotalNum, GltTicketOrder::getRiderId,
GltTicketOrder::getUserId, GltTicketOrder::getNo)
.eq(GltTicketOrder::getId, ticketOrderId) .eq(GltTicketOrder::getId, ticketOrderId)
.eq(GltTicketOrder::getTenantId, tenantId) .eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0) .eq(GltTicketOrder::getDeleted, 0)
@@ -742,6 +925,80 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
} }
ShopOrder order = shopOrderService.getByOrderNo(userTicket.getOrderNo(), userTicket.getTenantId());
//生成配送师傅可提现账户分佣金额数据【配送奖励(按商品设置)、配送提成(每桶0.1)】
int qty = ticketOrder.getTotalNum() == null ? 0 : ticketOrder.getTotalNum();
if (qty > 0) { //配送提成(每桶0.1)
BigDecimal money = RIDER_UNIT_COMMISSION
.multiply(BigDecimal.valueOf(qty))
.setScale(RIDER_COMMISSION_SCALE, RoundingMode.HALF_UP);
if (money.signum() > 0) {
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.WITHDRAW_ACCOUNT);
reduceDto.setUserId(ticketOrder.getRiderId());
reduceDto.setOrderUserId(ticketOrder.getUserId());
if(Arrays.asList(1, 3).contains(order.getOrderType())){
reduceDto.setOrderNo(order.getOrderNo());
}else {
reduceDto.setOrderNo(ticketOrder.getNo());
}
reduceDto.setPrice(money);
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DELIVERY_INCOME);
shopDealerUserService.reduceBalance(reduceDto);
}
}
//配送奖励(按商品设置)
Integer goodsId = userTicket.getGoodsId();
ShopGoods shopGood = shopGoodsMapper.selectById(goodsId);
if(shopGood != null && order != null){
Integer commissionType = shopGood.getCommissionType();
BigDecimal money = BigDecimal.ZERO;
if(commissionType == 10){ //按金额
money = shopGood.getDeliveryMoney();
}else { //按比率
money = order.getPayPrice().multiply(shopGood.getDeliveryMoney()).divide(BigDecimal.valueOf(100), 3, RoundingMode.HALF_UP);
}
if(money.compareTo(BigDecimal.ZERO) > 0){
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.WITHDRAW_ACCOUNT);
reduceDto.setUserId(ticketOrder.getRiderId());
reduceDto.setOrderUserId(ticketOrder.getUserId());
if(Arrays.asList(1, 3).contains(order.getOrderType())){
reduceDto.setOrderNo(order.getOrderNo());
}else {
reduceDto.setOrderNo(ticketOrder.getNo());
}
reduceDto.setPrice(money);
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DELIVERY_REWARD);
shopDealerUserService.reduceBalance(reduceDto);
}
}
//配送费结算
if(Arrays.asList(1, 3).contains(order.getOrderType()) && order.getDeliveryFee().compareTo(BigDecimal.ZERO) > 0){
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.WITHDRAW_ACCOUNT);
reduceDto.setUserId(ticketOrder.getRiderId());
reduceDto.setOrderUserId(ticketOrder.getUserId());
reduceDto.setOrderNo(order.getOrderNo());
reduceDto.setPrice(order.getDeliveryFee());
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DELIVERY_FLOOR_FEE);
shopDealerUserService.reduceBalance(reduceDto);
}
//查询未完成订单,完成资金解冻
if(order != null && order.getOrderStatus() == 0){
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.DEFROST);
reduceDto.setOrderUserId(order.getUserId());
reduceDto.setOrderNo(order.getOrderNo());
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.FREEZE_MONEY_THAW);
//按订单号资金解冻
shopDealerUserService.reduceBalance(reduceDto);
}
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>() LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId) .eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0) .eq(ShopOrder::getDeleted, 0)
@@ -753,7 +1010,6 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} else { } else {
uw.eq(ShopOrder::getOrderNo, shopOrderNo); uw.eq(ShopOrder::getOrderNo, shopOrderNo);
} }
boolean updated = shopOrderService.update(uw); boolean updated = shopOrderService.update(uw);
if (updated) { if (updated) {
return; return;
@@ -775,6 +1031,104 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
} }
} }
/**
* 更新送水订单为已完成状态
* @param id 送水订单ID
*/
@Transactional
public void updateShopOrderOrderStatusAfterTicketFinishedV2(Integer id) {
LocalDateTime now = LocalDateTime.now();
if(id == null){
return;
}
//1.找到关联水票的商城订单
GltTicketOrder ticketOrder = baseMapper.selectById(id);
if (ticketOrder == null) {
return;
}
ShopOrder order = shopOrderService.getByOrderNo(ticketOrder.getOrderNo(), ticketOrder.getTenantId());
//2.配送提成(每桶0.1)
int qty = ticketOrder.getTotalNum() == null ? 0 : ticketOrder.getTotalNum();
if (qty > 0) {
BigDecimal money = RIDER_UNIT_COMMISSION
.multiply(BigDecimal.valueOf(qty))
.setScale(RIDER_COMMISSION_SCALE, RoundingMode.HALF_UP);
if (money.signum() > 0) {
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.WITHDRAW_ACCOUNT);
reduceDto.setUserId(ticketOrder.getRiderId());
reduceDto.setOrderUserId(ticketOrder.getUserId());
if(Arrays.asList(1, 3).contains(order.getOrderType())){
reduceDto.setOrderNo(order.getOrderNo());
}else {
reduceDto.setOrderNo(ticketOrder.getNo());
}
reduceDto.setPrice(money);
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DELIVERY_INCOME);
shopDealerUserService.reduceBalance(reduceDto);
}
}
//3.配送奖励(按商品设置)
Integer goodsId = order.getFormId();
ShopGoods shopGood = shopGoodsMapper.selectById(goodsId);
if(shopGood != null && order != null){
Integer commissionType = shopGood.getCommissionType();
BigDecimal money = BigDecimal.ZERO;
if(commissionType == 10){ //按金额
money = shopGood.getDeliveryMoney();
}else { //按比率
money = order.getPayPrice().multiply(shopGood.getDeliveryMoney()).divide(BigDecimal.valueOf(100), 3, RoundingMode.HALF_UP);
}
if(money.compareTo(BigDecimal.ZERO) > 0){
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.WITHDRAW_ACCOUNT);
reduceDto.setUserId(ticketOrder.getRiderId());
reduceDto.setOrderUserId(ticketOrder.getUserId());
if(Arrays.asList(1, 3).contains(order.getOrderType())){
reduceDto.setOrderNo(order.getOrderNo());
}else {
reduceDto.setOrderNo(ticketOrder.getNo());
}
reduceDto.setPrice(money);
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DELIVERY_REWARD);
shopDealerUserService.reduceBalance(reduceDto);
}
}
//4.配送费结算
if(Arrays.asList(1, 3).contains(order.getOrderType()) && order.getDeliveryFee().compareTo(BigDecimal.ZERO) > 0){
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.WITHDRAW_ACCOUNT);
reduceDto.setUserId(ticketOrder.getRiderId());
reduceDto.setOrderUserId(ticketOrder.getUserId());
reduceDto.setOrderNo(order.getOrderNo());
reduceDto.setPrice(order.getDeliveryFee());
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DELIVERY_FLOOR_FEE);
shopDealerUserService.reduceBalance(reduceDto);
}
//5.查询未完成订单,完成资金解冻
if(order != null && order.getOrderStatus() == 0){
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.DEFROST);
reduceDto.setOrderUserId(order.getUserId());
reduceDto.setOrderNo(order.getOrderNo());
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.FREEZE_MONEY_THAW);
//按订单号资金解冻
shopDealerUserService.reduceBalance(reduceDto);
}
//5.调整订单为已完成、配送完成状态
order.setOrderStatus(1);
order.setDeliveryStatus(40);
order.setUpdateTime(now);
shopOrderService.updateById(order);
}
private void settleRiderCommissionIfEligible(Integer ticketOrderId, Integer tenantId, boolean requirePhoto) { private void settleRiderCommissionIfEligible(Integer ticketOrderId, Integer tenantId, boolean requirePhoto) {
if (ticketOrderId == null || tenantId == null) { if (ticketOrderId == null || tenantId == null) {
return; return;
@@ -845,13 +1199,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 +1232,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 +1250,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

@@ -1,5 +1,6 @@
package com.gxwebsoft.glt.service.impl; package com.gxwebsoft.glt.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog; import com.gxwebsoft.glt.entity.GltUserTicketLog;
import com.gxwebsoft.glt.entity.GltUserTicketRelease; import com.gxwebsoft.glt.entity.GltUserTicketRelease;
@@ -7,13 +8,20 @@ import com.gxwebsoft.glt.mapper.GltUserTicketLogMapper;
import com.gxwebsoft.glt.mapper.GltUserTicketMapper; import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
import com.gxwebsoft.glt.mapper.GltUserTicketReleaseMapper; import com.gxwebsoft.glt.mapper.GltUserTicketReleaseMapper;
import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService; import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService;
import lombok.RequiredArgsConstructor; import com.gxwebsoft.glt.service.GltUserTicketLogService;
import com.gxwebsoft.glt.service.GltUserTicketReleaseService;
import com.gxwebsoft.glt.service.GltUserTicketService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* 冻结水票自动释放实现: * 冻结水票自动释放实现:
@@ -23,7 +31,7 @@ import java.util.List;
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @AllArgsConstructor
public class GltUserTicketAutoReleaseServiceImpl implements GltUserTicketAutoReleaseService { public class GltUserTicketAutoReleaseServiceImpl implements GltUserTicketAutoReleaseService {
/** /**
@@ -44,6 +52,13 @@ public class GltUserTicketAutoReleaseServiceImpl implements GltUserTicketAutoRel
private final GltUserTicketMapper userTicketMapper; private final GltUserTicketMapper userTicketMapper;
private final GltUserTicketLogMapper userTicketLogMapper; private final GltUserTicketLogMapper userTicketLogMapper;
private GltUserTicketReleaseService gltUserTicketReleaseService;
private GltUserTicketService gltUserTicketService;
private GltUserTicketLogService gltUserTicketLogService;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public int releaseDue(LocalDateTime now, int batchSize) { public int releaseDue(LocalDateTime now, int batchSize) {
@@ -71,12 +86,7 @@ public class GltUserTicketAutoReleaseServiceImpl implements GltUserTicketAutoRel
continue; continue;
} }
long userTicketIdLong = rel.getUserTicketId(); Integer userTicketId = rel.getUserTicketId();
if (userTicketIdLong > Integer.MAX_VALUE || userTicketIdLong < 1) {
markFailed(rel.getId(), now, "userTicketId超范围");
continue;
}
Integer userTicketId = (int) userTicketIdLong;
// 先释放冻结数量(条件更新,确保 frozen_qty >= qty // 先释放冻结数量(条件更新,确保 frozen_qty >= qty
int updated = userTicketMapper.releaseFrozenQty( int updated = userTicketMapper.releaseFrozenQty(
@@ -129,4 +139,96 @@ public class GltUserTicketAutoReleaseServiceImpl implements GltUserTicketAutoRel
releaseMapper.updateStatus(releaseId, RELEASE_STATUS_FAILED, now); releaseMapper.updateStatus(releaseId, RELEASE_STATUS_FAILED, now);
log.warn("冻结水票释放标记失败 - releaseId={}, reason={}", releaseId, reason); log.warn("冻结水票释放标记失败 - releaseId={}, reason={}", releaseId, reason);
} }
@Transactional
public void releaseTask() {
LocalDateTime now = LocalDateTime.now();
log.info("***执行水票释放任务***-执行时间:{}", now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
long start = System.currentTimeMillis();
List<GltUserTicketRelease> releaseList = releaseMapper.getThisMonthReleaseList(1000);
if(CollectionUtils.isEmpty(releaseList)){
log.info("***本轮任务无待释放水票数据***");
return;
}
//查询用户水票信息
List<Integer> userTickIdList = releaseList.stream().map(GltUserTicketRelease::getUserTicketId).distinct().collect(Collectors.toList());
LambdaQueryWrapper<GltUserTicket> userTicketLambdaQueryWrapper = new LambdaQueryWrapper<GltUserTicket>().select(GltUserTicket::getId, GltUserTicket::getAvailableQty,
GltUserTicket::getFrozenQty, GltUserTicket::getReleasedQty).in(GltUserTicket::getId, userTickIdList);
List<GltUserTicket> userTicketList = userTicketMapper.selectList(userTicketLambdaQueryWrapper);
//创建修改用户水票集合、水票释放计划记录集合
List<GltUserTicket> updateUserTicketList = new ArrayList<>();
List<GltUserTicketLog> userTicketLogList = new ArrayList<>();
//遍历水票释放数据执行释放任务
releaseList.forEach(release ->{
//1.缺少水票ID、用户ID、租户ID记录错误记录
if(release.getUserTicketId() == null || release.getUserId() == null || release.getTenantId() == null){
release.setRemark("缺少userTicketId/userId/tenantId");
release.setStatus(2);
release.setUpdateTime(now);
}
GltUserTicket userTicket = userTicketList.stream().filter(gltUserTicket -> release.getUserTicketId().equals(gltUserTicket.getId())).findFirst().orElse(null);
if(userTicket != null){
Integer releaseQty = release.getReleaseQty();
if(userTicket.getFrozenQty() > releaseQty){
//更改用户水票可用数量、冻结数量、已释放数量
userTicket.setAvailableQty(userTicket.getAvailableQty() + releaseQty);
userTicket.setFrozenQty(userTicket.getFrozenQty() - releaseQty);
userTicket.setReleasedQty(userTicket.getReleasedQty() + releaseQty);
userTicket.setUpdateTime(now);
updateUserTicketList.add(userTicket);
//记录水票释放计划为成功释放状态
release.setStatus(1);
release.setRemark("success");
release.setUpdateTime(now);
//生成水票释放记录
GltUserTicketLog ticketLog = new GltUserTicketLog();
ticketLog.setUserTicketId(release.getUserTicketId());
ticketLog.setChangeType(CHANGE_TYPE_RELEASE);
ticketLog.setChangeAvailable(releaseQty);
ticketLog.setChangeFrozen(-releaseQty);
ticketLog.setChangeUsed(0);
ticketLog.setAvailableAfter(userTicket.getAvailableQty());
ticketLog.setFrozenAfter(userTicket.getFrozenQty());
ticketLog.setUsedAfter(userTicket.getUsedQty());
ticketLog.setOrderId(Integer.valueOf(String.valueOf(release.getId())));
ticketLog.setUserId(release.getUserId());
ticketLog.setComments("冻结水票到期释放");
ticketLog.setTenantId(release.getTenantId());
ticketLog.setCreateTime(now);
ticketLog.setUpdateTime(now);
userTicketLogList.add(ticketLog);
}else {
release.setRemark("释放数量大于冻结数量,释放:" + releaseQty + ",冻结:" + userTicket.getFrozenQty());
release.setStatus(2);
release.setUpdateTime(LocalDateTime.now());
}
}else {
release.setRemark("查询不到水票数据");
release.setStatus(2);
release.setUpdateTime(LocalDateTime.now());
}
});
if(CollectionUtils.isNotEmpty(releaseList)){
gltUserTicketReleaseService.updateBatchById(releaseList);
}
if(CollectionUtils.isNotEmpty(updateUserTicketList)){
gltUserTicketService.updateBatchById(updateUserTicketList);
}
if(CollectionUtils.isNotEmpty(userTicketLogList)){
gltUserTicketLogService.saveBatch(userTicketLogList);
}
long end = System.currentTimeMillis();
long costMs = end - start;
log.info("***执行水票释放任务完成*** 执行耗时:{} ms", costMs);
}
} }

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

@@ -1,41 +1,38 @@
package com.gxwebsoft.glt.task; package com.gxwebsoft.glt.task;
import com.alibaba.fastjson.JSONObject;
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.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.glt.entity.GltTicketTemplate; import com.gxwebsoft.common.core.enums.ShopDealerCapitalUpdateEnum;
import com.gxwebsoft.glt.service.GltTicketTemplateService; import com.gxwebsoft.common.core.enums.ShopDealerTypeEnum;
import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerReferee;
import com.gxwebsoft.shop.entity.ShopDealerSetting;
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.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.mapper.UserMapper; import com.gxwebsoft.common.system.mapper.UserMapper;
import com.gxwebsoft.shop.service.ShopDealerCapitalService; import com.gxwebsoft.glt.entity.GltTicketTemplate;
import com.gxwebsoft.shop.service.ShopDealerOrderService; import com.gxwebsoft.glt.service.GltTicketTemplateService;
import com.gxwebsoft.shop.service.ShopDealerRefereeService; import com.gxwebsoft.shop.dto.ShopDealerUserReduceDto;
import com.gxwebsoft.shop.service.ShopDealerSettingService; import com.gxwebsoft.shop.entity.*;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.mapper.ShopDealerRefereeMapper;
import com.gxwebsoft.shop.service.ShopGoodsService; import com.gxwebsoft.shop.mapper.ShopOrderMapper;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.*;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.util.UpstreamUserFinder; import com.gxwebsoft.shop.util.UpstreamUserFinder;
import com.alibaba.fastjson.JSONObject; import com.gxwebsoft.shop.vo.ShopDealerRefereeVO;
import com.gxwebsoft.shop.vo.ShopOrderGoodsVO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/** /**
* 租户10584分销订单结算任务 * 租户10584分销订单结算任务
@@ -50,7 +47,6 @@ public class DealerOrderSettlement10584Task {
private static final BigDecimal RATE_0_10 = new BigDecimal("0.10"); private static final BigDecimal RATE_0_10 = new BigDecimal("0.10");
private static final BigDecimal RATE_0_05 = new BigDecimal("0.05"); private static final BigDecimal RATE_0_05 = new BigDecimal("0.05");
private static final BigDecimal RATE_0_03 = new BigDecimal("0.03");
private static final BigDecimal RATE_0_02 = new BigDecimal("0.02"); private static final BigDecimal RATE_0_02 = new BigDecimal("0.02");
private static final BigDecimal RATE_0_01 = new BigDecimal("0.01"); private static final BigDecimal RATE_0_01 = new BigDecimal("0.01");
private static final BigDecimal TOTAL_DEALER_DIVIDEND_RATE = RATE_0_01; private static final BigDecimal TOTAL_DEALER_DIVIDEND_RATE = RATE_0_01;
@@ -92,14 +88,23 @@ public class DealerOrderSettlement10584Task {
@Resource @Resource
private GltTicketTemplateService gltTicketTemplateService; private GltTicketTemplateService gltTicketTemplateService;
@Resource
private ShopOrderMapper shopOrderMapper;
@Resource
private ShopDealerRefereeMapper shopDealerRefereeMapper;
/** /**
* 每10秒执行一次。 * 每10秒执行一次。
*/ */
@Scheduled(cron = "0/10 * * * * ?") // @Scheduled(cron = "0/10 * * * * ?")
@IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常") @IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常")
public void settleTenant10584Orders() { public void settleTenant10584Orders() {
try { try {
//获取水票模板对应的商品信息列表
Set<Integer> waterFormIds = loadWaterFormIds(); Set<Integer> waterFormIds = loadWaterFormIds();
//查询商品列表存在已支付未核销订单数据
List<ShopOrder> orders = findUnsettledPaidOrders(waterFormIds); List<ShopOrder> orders = findUnsettledPaidOrders(waterFormIds);
if (orders.isEmpty()) { if (orders.isEmpty()) {
return; return;
@@ -108,7 +113,11 @@ public class DealerOrderSettlement10584Task {
// Per-run caches to reduce DB chatter across orders. // Per-run caches to reduce DB chatter across orders.
Map<Integer, Integer> level1ParentCache = new HashMap<>(); Map<Integer, Integer> level1ParentCache = new HashMap<>();
Map<Integer, Boolean> shopRoleCache = new HashMap<>(); Map<Integer, Boolean> shopRoleCache = new HashMap<>();
//获取系统设置分销等级
DealerBasicSetting dealerBasicSetting = findDealerBasicSetting(); DealerBasicSetting dealerBasicSetting = findDealerBasicSetting();
//获取分销员type=2第一个分销人作为平台总分红人
ShopDealerUser totalDealerUser = findTotalDealerUser(); ShopDealerUser totalDealerUser = findTotalDealerUser();
if (totalDealerUser == null || totalDealerUser.getUserId() == null) { if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
log.warn("未找到分红账号,订单仍可结算但不会发放分红 - tenantId={}", TENANT_ID); log.warn("未找到分红账号,订单仍可结算但不会发放分红 - tenantId={}", TENANT_ID);
@@ -124,6 +133,7 @@ public class DealerOrderSettlement10584Task {
try { try {
transactionTemplate.executeWithoutResult(status -> { transactionTemplate.executeWithoutResult(status -> {
// 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理) // 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理)
//更新商品订单为已结算状态
if (!claimOrderToSettle(order.getOrderId(), waterFormIds)) { if (!claimOrderToSettle(order.getOrderId(), waterFormIds)) {
return; return;
} }
@@ -138,6 +148,88 @@ public class DealerOrderSettlement10584Task {
} }
} }
/**
* 每10分钟执行一次。
*/
@Scheduled(cron = "0 0/10 * * * ?")
@IgnoreTenant("该定时任务仅处理租户10584但需要显式按tenantId过滤避免定时任务线程无租户上下文导致查询异常")
public void settleTenant10584OrdersV2() {
try {
//获取水票模板对应的商品信息列表
Set<Integer> waterFormIds = loadWaterFormIds();
//查询商品列表存在已支付未核销订单数据【isSettled = 0 payStatus= 1 orderStatus 不在列表2, 3, 4, 5, 6, 7
List<ShopOrder> orders = findUnsettledPaidOrders(waterFormIds);
if (orders.isEmpty()) {
return;
}
// Per-run caches to reduce DB chatter across orders.
Map<Integer, Integer> level1ParentCache = new HashMap<>();
Map<Integer, Boolean> shopRoleCache = new HashMap<>();
//获取系统设置分销等级
DealerBasicSetting dealerBasicSetting = findDealerBasicSetting();
//获取分销员type=2第一个分销人作为平台总分红人
ShopDealerUser totalDealerUser = findTotalDealerUser();
if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
log.warn("未找到分红账号,订单仍可结算但不会发放分红 - tenantId={}", TENANT_ID);
}
log.debug("租户{}分销设置 - level={}", TENANT_ID, dealerBasicSetting.level);
log.info("租户{}待结算订单数: {}, orderNos(sample)={}",
TENANT_ID,
orders.size(),
orders.stream().limit(10).map(ShopOrder::getOrderNo).toList());
for (ShopOrder order : orders) {
try {
transactionTemplate.executeWithoutResult(status -> {
// 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理)
//更新商品订单为已结算状态
if (!claimOrderToSettle(order.getOrderId(), waterFormIds)) {
return;
}
settleOneOrderV2(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level);
});
} catch (Exception e) {
log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e);
}
}
} catch (Exception e) {
log.error("租户{}分销订单结算任务执行失败", TENANT_ID, e);
}
}
/**
* 订单分销金、分润金结算
*/
@Transactional
public void orderSettlement(String orderNo){
LambdaQueryWrapper<ShopOrder> orderLambdaQueryWrapper = new LambdaQueryWrapper<ShopOrder>().eq(ShopOrder::getOrderNo, orderNo).eq(ShopOrder::getIsSettled, 0).eq(ShopOrder::getPayStatus, 1);
ShopOrder order = shopOrderService.getOne(orderLambdaQueryWrapper);
if(order != null){
//获取系统设置分销等级
DealerBasicSetting dealerBasicSetting = findDealerBasicSetting();
//获取分销员type=2第一个分销人作为平台总分红人
ShopDealerUser totalDealerUser = findTotalDealerUser();
Map<Integer, Integer> level1ParentCache = new HashMap<>();
Map<Integer, Boolean> shopRoleCache = new HashMap<>();
transactionTemplate.executeWithoutResult(status -> {
// 先“认领”订单:并发/多实例下避免重复结算update=0 表示被其他线程/实例处理)
//更新商品订单为已结算状态
if (!claimOrderToSettleV2(order.getOrderId())) {
return;
}
settleOneOrderV2(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level);
});
}
}
private List<ShopOrder> findUnsettledPaidOrders(Set<Integer> waterFormIds) { private List<ShopOrder> findUnsettledPaidOrders(Set<Integer> waterFormIds) {
// 租户10584约定 // 租户10584约定
// - 普通订单以发货为准deliveryStatus=20才结算 // - 普通订单以发货为准deliveryStatus=20才结算
@@ -148,7 +240,7 @@ public class DealerOrderSettlement10584Task {
.eq(ShopOrder::getPayStatus, true) .eq(ShopOrder::getPayStatus, true)
.eq(ShopOrder::getIsSettled, 0) .eq(ShopOrder::getIsSettled, 0)
// 退款/取消订单不结算,避免“退款后仍发放分红/分润/佣金” // 退款/取消订单不结算,避免“退款后仍发放分红/分润/佣金”
.and(w -> w.notIn(ShopOrder::getOrderStatus, 2, 4, 5, 6, 7).or().isNull(ShopOrder::getOrderStatus)); .and(w -> w.notIn(ShopOrder::getOrderStatus, 2, 3, 4, 5, 6, 7).or().isNull(ShopOrder::getOrderStatus));
if (waterFormIds != null && !waterFormIds.isEmpty()) { if (waterFormIds != null && !waterFormIds.isEmpty()) {
qw.and(w -> w.eq(ShopOrder::getDeliveryStatus, 20).or().in(ShopOrder::getFormId, waterFormIds)); qw.and(w -> w.eq(ShopOrder::getDeliveryStatus, 20).or().in(ShopOrder::getFormId, waterFormIds));
@@ -178,6 +270,17 @@ public class DealerOrderSettlement10584Task {
return shopOrderService.update(uw); return shopOrderService.update(uw);
} }
private boolean claimOrderToSettleV2(Integer orderId) {
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getOrderId, orderId)
.eq(ShopOrder::getTenantId, TENANT_ID)
.eq(ShopOrder::getIsSettled, 0)
// 二次防御:退款/取消订单不允许被“认领结算”
.and(w -> w.notIn(ShopOrder::getOrderStatus, 2, 3, 4, 5, 6, 7).or().isNull(ShopOrder::getOrderStatus));
uw.set(ShopOrder::getIsSettled, 1);
return shopOrderService.update(uw);
}
private Set<Integer> loadWaterFormIds() { private Set<Integer> loadWaterFormIds() {
try { try {
return gltTicketTemplateService.list( return gltTicketTemplateService.list(
@@ -252,6 +355,45 @@ public class DealerOrderSettlement10584Task {
log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), baseAmount); log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), baseAmount);
} }
private void settleOneOrderV2(ShopOrder order, Map<Integer, Integer> level1ParentCache, Map<Integer, Boolean> shopRoleCache,
ShopDealerUser totalDealerUser, int dealerLevel) {
if (order.getUserId() == null || order.getOrderNo() == null) {
throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId());
}
BigDecimal totalPrice = order.getTotalPrice();
BigDecimal payPrice = order.getPayPrice();
BigDecimal rate = payPrice.divide(totalPrice, 2, RoundingMode.HALF_UP);
if(payPrice.compareTo(BigDecimal.ZERO) <= 0){
log.info("订单号:{}实付金额为0无需执行分销逻辑" + order.getOrderNo());
return;
}
//查询订单号订单所有已开启分销的商品分润信息
List<ShopOrderGoodsVO> orderGoodsVOList = shopOrderMapper.getOrderGoodsInfo(order.getOrderNo());
if(CollectionUtils.isNotEmpty(orderGoodsVOList)){
// 1) 直推/间推(直接增加冻结账户余额)
DealerRefereeCommissionV2 dealerRefereeCommission = settleDealerRefereeCommissionV2(order, rate, orderGoodsVOList, dealerLevel);
// 2) 门店分润上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/间推门店)【只统计数据,不对分销账户进行处理,
// 已日结形式统计分销记录表shop_dealer_order 做对应一级二级管理津贴结算】
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommissionV2(order, rate, orderGoodsVOList, level1ParentCache, shopRoleCache);
// 3) 分红:固定比率,每个订单都分 TODO 总分红未开发,还按原逻辑走
int goodsQty = orderGoodsVOList.stream().mapToInt(ShopOrderGoodsVO::getTotalNum).sum();
TotalDealerCommission totalDealerCommission = settleTotalDealerCommissionV2(order, goodsQty, totalDealerUser);
// 4) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
createDealerOrderRecordV2(order, dealerRefereeCommission, shopRoleCommission, totalDealerCommission);
log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), payPrice);
}else {
log.error("订单号:{},未找到下单分销商品数据!", order.getOrderNo());
return;
}
}
private DealerRefereeCommission settleDealerRefereeCommission( private DealerRefereeCommission settleDealerRefereeCommission(
ShopOrder order, ShopOrder order,
BigDecimal baseAmount, BigDecimal baseAmount,
@@ -329,6 +471,95 @@ public class DealerOrderSettlement10584Task {
return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney); return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney);
} }
/**
* 获取分销员分销霍金数据
* @param order
* @param orderGoodsVOList
* @param dealerLevel
* @return
*/
private DealerRefereeCommissionV2 settleDealerRefereeCommissionV2(ShopOrder order, BigDecimal rate, List<ShopOrderGoodsVO> orderGoodsVOList, int dealerLevel) {
Integer directDealerId = null;
Integer simpleDealerId = null;
AtomicReference<BigDecimal> directMoney = new AtomicReference<>(BigDecimal.ZERO);
AtomicReference<BigDecimal> simpleMoney = new AtomicReference<>(BigDecimal.ZERO);
ShopDealerRefereeVO dealerRefereeVO = shopDealerRefereeMapper.getDealerIdByUserId(order.getUserId());
if(dealerRefereeVO == null){
return null;
}
if (dealerLevel == 1) {
Integer directUserId = dealerRefereeVO.getDirectUserId();
Integer directUserType = dealerRefereeVO.getDirectUserType();
if(directUserId != null && directUserType != null && directUserType == 0){
directDealerId = dealerRefereeVO.getDirectUserId();
}
}else {
Integer directUserId = dealerRefereeVO.getDirectUserId();
Integer directUserType = dealerRefereeVO.getDirectUserType();
Integer simpleUserId = dealerRefereeVO.getSimpleUserId();
Integer simpleUserType = dealerRefereeVO.getSimpleUserType();
if(directUserId != null && directUserType != null && directUserType == 0){
directDealerId = directUserId;
}
if(simpleUserId != null && simpleUserType != null && simpleUserType == 0){
simpleDealerId = simpleUserId;
}
}
if(directDealerId != null || simpleDealerId != null){
Integer finalDirectDealerId = directDealerId;
Integer finalSimpleDealerId = simpleDealerId;
orderGoodsVOList.forEach(orderGoodsVO -> {
//获取商品分润比例/金额
BigDecimal firstMoney = orderGoodsVO.getFirstMoney();
BigDecimal secondMoney = orderGoodsVO.getSecondMoney();
//按实付比例计算单项应参与分润金额
BigDecimal itemRatePrice = orderGoodsVO.getPrice().multiply(BigDecimal.valueOf(orderGoodsVO.getTotalNum())).multiply(rate);
//一级分销员存在(type = 0)且单项实付金额大于0及商品设置了一级分销比例/金额
if(finalDirectDealerId != null && itemRatePrice.compareTo(BigDecimal.ZERO) > 0 && firstMoney.compareTo(BigDecimal.ZERO) > 0){
BigDecimal one = calcMoneyByCommissionType(itemRatePrice, firstMoney, orderGoodsVO.getTotalNum(), 2, orderGoodsVO.getCommissionType());
directMoney.accumulateAndGet(one, BigDecimal::add);
}
//一级分销员存在(type = 0)且单项实付金额大于0及商品设置了一级分销比例/金额
if(finalSimpleDealerId != null && itemRatePrice.compareTo(BigDecimal.ZERO) > 0 && secondMoney.compareTo(BigDecimal.ZERO) > 0 ){
BigDecimal two = calcMoneyByCommissionType(itemRatePrice, secondMoney, orderGoodsVO.getTotalNum(), 2, orderGoodsVO.getCommissionType());
simpleMoney.accumulateAndGet(two, BigDecimal::add);
}
});
//一级分销员账户增加冻结金额
if (directDealerId != null && directMoney.get().compareTo(BigDecimal.ZERO) > 0) {
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.FREEZE_ACCOUNT);
reduceDto.setUserId(directDealerId);
reduceDto.setOrderUserId(order.getUserId());
reduceDto.setOrderNo(order.getOrderNo());
reduceDto.setPrice(directMoney.get());
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DISTRIBUTION_INCOME);
shopDealerUserService.reduceBalance(reduceDto);
}
//二级分销员账户增加冻结金额
if (simpleDealerId != null && simpleMoney.get().compareTo(BigDecimal.ZERO) > 0) {
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.FREEZE_ACCOUNT);
reduceDto.setUserId(simpleDealerId);
reduceDto.setOrderUserId(order.getUserId());
reduceDto.setOrderNo(order.getOrderNo());
reduceDto.setPrice(simpleMoney.get());
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DISTRIBUTION_INCOME);
shopDealerUserService.reduceBalance(reduceDto);
}
return new DealerRefereeCommissionV2(directDealerId, directMoney.get(), simpleDealerId, simpleMoney.get());
}
return null;
}
private Integer getDealerRefereeId(Integer userId) { private Integer getDealerRefereeId(Integer userId) {
return getDealerRefereeId(userId, 1); return getDealerRefereeId(userId, 1);
} }
@@ -412,6 +643,54 @@ public class DealerOrderSettlement10584Task {
return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney); return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
} }
private ShopRoleCommission settleShopRoleRefereeCommissionV2(ShopOrder order, BigDecimal rate, List<ShopOrderGoodsVO> orderGoodsVOList, Map<Integer, Integer> level1ParentCache, Map<Integer, Boolean> shopRoleCache) {
List<Integer> shopRoleReferees = findFirstTwoShopRoleReferees(order.getUserId(), level1ParentCache, shopRoleCache);
log.info("门店分润命中结果(type=1门店角色取前两级) - orderNo={}, buyerUserId={}, shopRoleReferees={}",
order.getOrderNo(), order.getUserId(), shopRoleReferees);
if (shopRoleReferees.isEmpty()) {
return ShopRoleCommission.empty();
}
if(CollectionUtils.isNotEmpty(shopRoleReferees)){
Integer storeDirectUserId;
Integer storeSimpleUserId = null;
AtomicReference<BigDecimal> storeDirectMoney = new AtomicReference<>(BigDecimal.ZERO);
AtomicReference<BigDecimal> storeSimpleMoney = new AtomicReference<>(BigDecimal.ZERO);
if(shopRoleReferees.size() == 1){
storeDirectUserId = shopRoleReferees.get(0);
}else {
storeDirectUserId = shopRoleReferees.get(0);
storeSimpleUserId = shopRoleReferees.get(1);
}
Integer finalStoreDirectUserId = storeDirectUserId;
Integer finalStoreSimpleUserId = storeSimpleUserId;
orderGoodsVOList.forEach(orderGoodsVO ->{
//获取商品对应服务商管理费分润比例/金额
BigDecimal firstMoney = orderGoodsVO.getFirstDividend();
BigDecimal secondMoney = orderGoodsVO.getSecondDividend();
//按实付比例计算单项应参与分润金额
BigDecimal itemRatePrice = orderGoodsVO.getPrice().multiply(BigDecimal.valueOf(orderGoodsVO.getTotalNum())).multiply(rate);
if(finalStoreDirectUserId != null && itemRatePrice.compareTo(BigDecimal.ZERO) > 0){
BigDecimal one = calcMoneyByCommissionType(itemRatePrice, firstMoney, orderGoodsVO.getTotalNum(), 2, orderGoodsVO.getCommissionType());
storeDirectMoney.accumulateAndGet(one, BigDecimal::add);
}
if(finalStoreSimpleUserId != null && itemRatePrice.compareTo(BigDecimal.ZERO) > 0){
BigDecimal two = calcMoneyByCommissionType(itemRatePrice, secondMoney, orderGoodsVO.getTotalNum(), 2, orderGoodsVO.getCommissionType());
storeSimpleMoney.accumulateAndGet(two, BigDecimal::add);
}
});
return new ShopRoleCommission(storeDirectUserId, storeDirectMoney.get(), storeSimpleUserId, storeSimpleMoney.get());
}else {
return null;
}
}
private TotalDealerCommission settleTotalDealerCommission( private TotalDealerCommission settleTotalDealerCommission(
ShopOrder order, ShopOrder order,
BigDecimal baseAmount, BigDecimal baseAmount,
@@ -438,6 +717,32 @@ public class DealerOrderSettlement10584Task {
return new TotalDealerCommission(totalDealerUser.getUserId(), money); return new TotalDealerCommission(totalDealerUser.getUserId(), money);
} }
private TotalDealerCommission settleTotalDealerCommissionV2(ShopOrder order, int goodsQty, ShopDealerUser totalDealerUser) {
if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
return TotalDealerCommission.empty();
}
BigDecimal rate = safePositive(totalDealerUser.getRate());
if (rate.signum() <= 0) {
rate = TOTAL_DEALER_DIVIDEND_RATE;
}
BigDecimal money = calcMoneyByCommissionType(order.getPayPrice(), rate, goodsQty, DIVIDEND_SCALE, 20);
//一级分销员账户增加冻结金额
if (money.compareTo(BigDecimal.ZERO) > 0) {
ShopDealerUserReduceDto reduceDto = new ShopDealerUserReduceDto();
reduceDto.setTypeEnum(ShopDealerTypeEnum.FREEZE_ACCOUNT);
reduceDto.setUserId(totalDealerUser.getUserId());
reduceDto.setOrderUserId(order.getUserId());
reduceDto.setOrderNo(order.getOrderNo());
reduceDto.setPrice(money);
reduceDto.setUpdateEnum(ShopDealerCapitalUpdateEnum.DIVIDEND_INCOME);
shopDealerUserService.reduceBalance(reduceDto);
return new TotalDealerCommission(totalDealerUser.getUserId(), money);
}
return TotalDealerCommission.empty();
}
private ShopDealerUser findTotalDealerUser() { private ShopDealerUser findTotalDealerUser() {
return shopDealerUserService.getOne( return shopDealerUserService.getOne(
new LambdaQueryWrapper<ShopDealerUser>() new LambdaQueryWrapper<ShopDealerUser>()
@@ -620,7 +925,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());
@@ -755,6 +1061,92 @@ public class DealerOrderSettlement10584Task {
order.getOrderNo(), dealerOrder.getFirstUserId(), dealerOrder.getSecondUserId(), dealerOrder.getFirstDividendUser(), dealerOrder.getSecondDividendUser()); order.getOrderNo(), dealerOrder.getFirstUserId(), dealerOrder.getSecondUserId(), dealerOrder.getFirstDividendUser(), dealerOrder.getSecondDividendUser());
} }
/**
* 记录订单分销业务
* @param order 商品订单
* @param dealerRefereeCommission 一级、二级分销员分销数据
* @param shopRoleCommission 门店/服务商一级、二级管理津贴数据
*/
private void createDealerOrderRecordV2(ShopOrder order, DealerRefereeCommissionV2 dealerRefereeCommission, ShopRoleCommission shopRoleCommission, TotalDealerCommission totalDealerCommission) {
// 幂等:同一订单只写一条(依赖 order_no + tenant_id 作为业务唯一)
ShopDealerOrder existed = shopDealerOrderService.getOne(
new LambdaQueryWrapper<ShopDealerOrder>()
.eq(ShopDealerOrder::getTenantId, TENANT_ID)
.eq(ShopDealerOrder::getOrderNo, order.getOrderNo())
.last("limit 1")
);
if (existed != null) {
// 允许“补发”门店分润时回填分润字段,避免订单已结算但分润字段一直为空,影响排查/对账。
LambdaUpdateWrapper<ShopDealerOrder> uw = new LambdaUpdateWrapper<ShopDealerOrder>()
.eq(ShopDealerOrder::getTenantId, TENANT_ID)
.eq(ShopDealerOrder::getOrderNo, order.getOrderNo());
boolean needUpdate = false;
if (shopRoleCommission != null && shopRoleCommission.storeDirectUserId != null) {
Integer existedUser = existed.getFirstDividendUser();
boolean needSetUser = existedUser == null;
boolean needSetMoney = existed.getFirstDividend() == null || existed.getFirstDividend().signum() == 0;
if (needSetUser) {
uw.set(ShopDealerOrder::getFirstDividendUser, shopRoleCommission.storeDirectUserId);
needUpdate = true;
}
boolean sameUser = existedUser == null || Objects.equals(existedUser, shopRoleCommission.storeDirectUserId);
if (sameUser && needSetMoney && shopRoleCommission.storeDirectMoney != null && shopRoleCommission.storeDirectMoney.signum() > 0) {
uw.set(ShopDealerOrder::getFirstDividend, shopRoleCommission.storeDirectMoney);
needUpdate = true;
}
}
if (shopRoleCommission != null && shopRoleCommission.storeSimpleUserId != null) {
Integer existedUser = existed.getSecondDividendUser();
boolean needSetUser = existedUser == null;
boolean needSetMoney = existed.getSecondDividend() == null || existed.getSecondDividend().signum() == 0;
if (needSetUser) {
uw.set(ShopDealerOrder::getSecondDividendUser, shopRoleCommission.storeSimpleUserId);
needUpdate = true;
}
boolean sameUser = existedUser == null || Objects.equals(existedUser, shopRoleCommission.storeSimpleUserId);
if (sameUser && needSetMoney && shopRoleCommission.storeSimpleMoney != null && shopRoleCommission.storeSimpleMoney.signum() > 0) {
uw.set(ShopDealerOrder::getSecondDividend, shopRoleCommission.storeSimpleMoney);
needUpdate = true;
}
}
if (needUpdate) {
shopDealerOrderService.update(uw);
log.info("ShopDealerOrder已存在回填门店分润字段 - orderNo={}, firstDividendUser={}, secondDividendUser={}",
order.getOrderNo(), shopRoleCommission.storeDirectUserId, shopRoleCommission.storeSimpleUserId);
} else {
log.info("ShopDealerOrder已存在跳过写入 - orderNo={}", order.getOrderNo());
}
return;
}else {
ShopDealerOrder dealerOrder = new ShopDealerOrder();
dealerOrder.setUserId(order.getUserId()); // 买家用户ID
dealerOrder.setOrderNo(order.getOrderNo());
dealerOrder.setOrderPrice(order.getTotalPrice());
dealerOrder.setPayPrice(order.getPayPrice());
//一级、二级分销员分销佣金统计
dealerOrder.setFirstUserId(dealerRefereeCommission != null ? dealerRefereeCommission.directDealerId : null);
dealerOrder.setFirstMoney(dealerRefereeCommission != null ? dealerRefereeCommission.directMoney : BigDecimal.ZERO);
dealerOrder.setSecondUserId(dealerRefereeCommission != null ? dealerRefereeCommission.simpleDealerId : null);
dealerOrder.setSecondMoney(dealerRefereeCommission != null ? dealerRefereeCommission.simpleMoney : BigDecimal.ZERO);
//门店(角色shop)两级分润单独落字段(详细以 ShopDealerCapital 为准)
dealerOrder.setFirstDividendUser(shopRoleCommission != null ? shopRoleCommission.storeDirectUserId : null);
dealerOrder.setFirstDividend(shopRoleCommission != null ? shopRoleCommission.storeDirectMoney : BigDecimal.ZERO);
dealerOrder.setSecondDividendUser(shopRoleCommission != null ? shopRoleCommission.storeSimpleUserId : null);
dealerOrder.setSecondDividend(shopRoleCommission != null ? shopRoleCommission.storeSimpleMoney : BigDecimal.ZERO);
dealerOrder.setIsSettled(1);
dealerOrder.setSettleTime(LocalDateTime.now());
dealerOrder.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
dealerOrder.setTenantId(TENANT_ID);
dealerOrder.setComments(buildCommissionTraceCommentV2(dealerRefereeCommission, shopRoleCommission, totalDealerCommission));
shopDealerOrderService.save(dealerOrder);
}
}
private String buildCommissionTraceComment( private String buildCommissionTraceComment(
DealerRefereeCommission dealerRefereeCommission, DealerRefereeCommission dealerRefereeCommission,
ShopRoleCommission shopRoleCommission, ShopRoleCommission shopRoleCommission,
@@ -769,6 +1161,30 @@ public class DealerOrderSettlement10584Task {
+ ",totalDealer=" + totalDealerCommission.userId + ":" + totalDealerCommission.money; + ",totalDealer=" + totalDealerCommission.userId + ":" + totalDealerCommission.money;
} }
private String buildCommissionTraceCommentV2(
DealerRefereeCommissionV2 dealerRefereeCommission,
ShopRoleCommission shopRoleCommission,
TotalDealerCommission totalDealerCommission
) {
// 轻量“过程”留痕,方便排查;详细分佣以 ShopDealerCapital 为准。
Integer direct = dealerRefereeCommission != null ? dealerRefereeCommission.directDealerId : null;
BigDecimal directMoney = dealerRefereeCommission != null ? dealerRefereeCommission.directMoney : BigDecimal.ZERO;
Integer simpleDealerId = dealerRefereeCommission != null ? dealerRefereeCommission.simpleDealerId : null;
BigDecimal simpleMoney = dealerRefereeCommission != null ? dealerRefereeCommission.simpleMoney : BigDecimal.ZERO;
Integer storeDirectUserId = shopRoleCommission != null ? shopRoleCommission.storeDirectUserId : null;
BigDecimal storeDirectMoney = shopRoleCommission != null ? shopRoleCommission.storeDirectMoney : BigDecimal.ZERO;
Integer storeSimpleUserId = shopRoleCommission != null ? shopRoleCommission.storeSimpleUserId : null;
BigDecimal storeSimpleMoney = shopRoleCommission != null ? shopRoleCommission.storeSimpleMoney : BigDecimal.ZERO;
Integer userId = totalDealerCommission != null ? totalDealerCommission.userId : null;
BigDecimal money = totalDealerCommission != null ? totalDealerCommission.money : BigDecimal.ZERO;
return "direct=" + direct + ":" + directMoney
+ ",simple=" + simpleDealerId + ":" + simpleMoney
+ ",dividend1=" + storeDirectUserId + ":" + storeDirectMoney
+ ",dividend2=" + storeSimpleUserId + ":" + storeSimpleMoney
+ ",totalDealer=" + userId + ":" + money;
}
private BigDecimal getOrderBaseAmount(ShopOrder order) { private BigDecimal getOrderBaseAmount(ShopOrder order) {
if (order == null) { if (order == null) {
return null; return null;
@@ -961,6 +1377,25 @@ public class DealerOrderSettlement10584Task {
} }
} }
private static class DealerRefereeCommissionV2 {
private final Integer directDealerId;
private final BigDecimal directMoney;
private final Integer simpleDealerId;
private final BigDecimal simpleMoney;
private DealerRefereeCommissionV2(
Integer directDealerId,
BigDecimal directMoney,
Integer simpleDealerId,
BigDecimal simpleMoney
) {
this.directDealerId = directDealerId;
this.directMoney = directMoney != null ? directMoney : BigDecimal.ZERO;
this.simpleDealerId = simpleDealerId;
this.simpleMoney = simpleMoney != null ? simpleMoney : BigDecimal.ZERO;
}
}
private static class ShopRoleCommission { private static class ShopRoleCommission {
private final Integer storeDirectUserId; private final Integer storeDirectUserId;
private final BigDecimal storeDirectMoney; private final BigDecimal storeDirectMoney;

View File

@@ -33,7 +33,7 @@ public class GltTicketIssue10584Task {
private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${glt.ticket.issue10584.cron:0/15 * * * * ?}") // @Scheduled(cron = "${glt.ticket.issue10584.cron:0/15 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {

View File

@@ -34,7 +34,7 @@ public class GltTicketOrderAutoConfirm10584Task {
private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${glt.ticket-order.auto-confirm10584.cron:0/33 * * * * ?}") // @Scheduled(cron = "${glt.ticket-order.auto-confirm10584.cron:0/33 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {

View File

@@ -35,7 +35,7 @@ public class GltTicketOrderAutoDispatch10584Task {
@Value("${glt.ticket.dispatch10584.batchSize:50}") @Value("${glt.ticket.dispatch10584.batchSize:50}")
private int batchSize; private int batchSize;
@Scheduled(cron = "${glt.ticket.dispatch10584.cron:0/20 * * * * ?}") // @Scheduled(cron = "${glt.ticket.dispatch10584.cron:0/20 * * * * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {

View File

@@ -4,18 +4,19 @@ import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService; import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* 冻结水票自动释放任务: * 冻结水票自动释放任务:每月1日凌晨1-3点每2分钟执行一次任务
* - 扫描 glt_user_ticket_release 中到期且待释放status=0的记录 * - 扫描 glt_user_ticket_release 中本月到期且待释放status=0的记录
* - 释放成功:frozen -> available并将 release.status 置为 1 * - 释放成功:
* 1.修改水票释放任务为已释放状态 glt_user_ticket_release
* 2.修改个人水票中:可用数量、冻结数量、已释放数量 glt_user_ticket
* 3.同步生产水票释放记录数据 glt_user_ticket_log
*/ */
@Slf4j @Slf4j
@Component @Component
@@ -25,25 +26,17 @@ public class GltUserTicketAutoReleaseTask {
private final GltUserTicketAutoReleaseService autoReleaseService; private final GltUserTicketAutoReleaseService autoReleaseService;
@Value("${glt.ticket.auto-release.batch-size:200}")
private int batchSize;
private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(cron = "${glt.ticket.auto-release.cron:0 */10 * * * ?}") @Scheduled(cron = "${glt.ticket.auto-release.cron:0 */2 1-3 1 * ?}")
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;释放记录自带 tenantId更新时会校验 tenantId") @IgnoreTenant("定时任务无登录态,需忽略租户隔离;释放记录自带 tenantId更新时会校验 tenantId")
public void run() { public void run() {
if (!running.compareAndSet(false, true)) { if (!running.compareAndSet(false, true)) {
log.warn("冻结水票自动释放任务仍在执行中,本轮跳过"); log.warn("冻结水票自动释放任务仍在执行中,本轮跳过");
return; return;
} }
try { try {
LocalDateTime now = LocalDateTime.now(); autoReleaseService.releaseTask();
int released = autoReleaseService.releaseDue(now, Math.max(batchSize, 1));
if (released > 0) {
log.info("冻结水票自动释放完成 - released={}, now={}", released, now);
}
} finally { } finally {
running.set(false); running.set(false);
} }

View File

@@ -230,7 +230,7 @@ public class PaymentConstants {
*/ */
public static class Environment { public static class Environment {
/** 开发环境 */ /** 开发环境 */
public static final String DEV = "dev"; public static final String DEV = "local";
/** 测试环境 */ /** 测试环境 */
public static final String TEST = "test"; public static final String TEST = "test";
/** 生产环境 */ /** 生产环境 */

View File

@@ -148,7 +148,7 @@ public class WxPayConfigService {
log.info("从数据库获取支付配置成功租户ID: {},将缓存配置", tenantId); log.info("从数据库获取支付配置成功租户ID: {},将缓存配置", tenantId);
// 开发环境下如果apiclientKey为空设置默认值 // 开发环境下如果apiclientKey为空设置默认值
if ("dev".equals(activeProfile) && if ("local".equals(activeProfile) &&
(payment.getApiclientKey() == null || payment.getApiclientKey().trim().isEmpty())) { (payment.getApiclientKey() == null || payment.getApiclientKey().trim().isEmpty())) {
log.warn("开发环境数据库配置中apiclientKey为空使用默认值租户ID: {}", tenantId); log.warn("开发环境数据库配置中apiclientKey为空使用默认值租户ID: {}", tenantId);
payment.setApiclientKey("apiclient_key.pem"); payment.setApiclientKey("apiclient_key.pem");
@@ -167,7 +167,7 @@ public class WxPayConfigService {
} }
// 数据库也没有配置 // 数据库也没有配置
if (!"dev".equals(activeProfile)) { if (!"local".equals(activeProfile)) {
throw PaymentException.systemError("微信支付配置未找到租户ID: " + tenantId + ",请检查数据库配置", null); throw PaymentException.systemError("微信支付配置未找到租户ID: " + tenantId + ",请检查数据库配置", null);
} }
@@ -180,7 +180,7 @@ public class WxPayConfigService {
* 获取证书文件路径 * 获取证书文件路径
*/ */
private String getCertificatePath(Integer tenantId, Payment payment) throws PaymentException { private String getCertificatePath(Integer tenantId, Payment payment) throws PaymentException {
if ("dev".equals(activeProfile)) { if ("local".equals(activeProfile)) {
return getDevCertificatePath(tenantId); return getDevCertificatePath(tenantId);
} else { } else {
return getProdCertificatePath(payment); return getProdCertificatePath(payment);
@@ -237,7 +237,7 @@ public class WxPayConfigService {
*/ */
private Config createWxPayConfig(Payment payment, String certificatePath) throws PaymentException { private Config createWxPayConfig(Payment payment, String certificatePath) throws PaymentException {
try { try {
if ("dev".equals(activeProfile) && payment == null) { if ("local".equals(activeProfile) && payment == null) {
// 开发环境测试配置 // 开发环境测试配置
return createDevTestConfig(certificatePath); return createDevTestConfig(certificatePath);
} else if (payment != null) { } else if (payment != null) {
@@ -343,7 +343,7 @@ public class WxPayConfigService {
// 开发环境下如果apiclientKey为空给一个警告但不抛异常 // 开发环境下如果apiclientKey为空给一个警告但不抛异常
// 生产环境必须有apiclientKey // 生产环境必须有apiclientKey
if (payment.getApiclientKey() == null || payment.getApiclientKey().trim().isEmpty()) { if (payment.getApiclientKey() == null || payment.getApiclientKey().trim().isEmpty()) {
if ("dev".equals(activeProfile)) { if ("local".equals(activeProfile)) {
log.warn("开发环境:证书文件名(apiclientKey)未配置,将使用默认值"); log.warn("开发环境:证书文件名(apiclientKey)未配置,将使用默认值");
} else { } else {
throw PaymentException.systemError("证书文件名(apiclientKey)未配置", null); throw PaymentException.systemError("证书文件名(apiclientKey)未配置", null);

View File

@@ -78,7 +78,7 @@ public class WxPayConstants {
public static final int AMOUNT_MULTIPLIER = 100; public static final int AMOUNT_MULTIPLIER = 100;
/** 开发环境标识 */ /** 开发环境标识 */
public static final String PROFILE_DEV = "dev"; public static final String PROFILE_DEV = "local";
/** 生产环境标识 */ /** 生产环境标识 */
public static final String PROFILE_PROD = "prod"; public static final String PROFILE_PROD = "prod";
} }

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

@@ -1,17 +1,19 @@
package com.gxwebsoft.shop.controller; 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.BaseController;
import com.gxwebsoft.shop.service.ShopDealerCapitalService; 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.dto.ShopDealerCapitalWaterDto;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.param.ShopDealerCapitalParam; import com.gxwebsoft.shop.param.ShopDealerCapitalParam;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.shop.vo.ShopDealerCapitalWaterVO;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.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.springdoc.api.annotations.ParameterObject;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -34,11 +36,19 @@ public class ShopDealerCapitalController extends BaseController {
@PreAuthorize("hasAuthority('shop:shopDealerCapital:list')") @PreAuthorize("hasAuthority('shop:shopDealerCapital:list')")
@Operation(summary = "分页查询分销商资金明细表") @Operation(summary = "分页查询分销商资金明细表")
@GetMapping("/page") @GetMapping("/page")
public ApiResult<PageResult<ShopDealerCapital>> page(ShopDealerCapitalParam param) { public ApiResult<PageResult<ShopDealerCapital>> page(@ParameterObject ShopDealerCapitalParam param) {
// 使用关联查询 // 使用关联查询
return success(shopDealerCapitalService.pageRel(param)); return success(shopDealerCapitalService.pageRel(param));
} }
@PreAuthorize("hasAuthority('shop:shopDealerCapital:list')")
@Operation(summary = "分页查询分销商个人流水")
@GetMapping("/myCapitalWater")
public ApiResult<PageResult<ShopDealerCapitalWaterVO>> myCapitalWater(ShopDealerCapitalWaterDto waterDto) {
// 使用关联查询
return success(shopDealerCapitalService.myCapitalWater(waterDto));
}
@PreAuthorize("hasAuthority('shop:shopDealerCapital:list')") @PreAuthorize("hasAuthority('shop:shopDealerCapital:list')")
@Operation(summary = "查询全部分销商资金明细表") @Operation(summary = "查询全部分销商资金明细表")
@GetMapping() @GetMapping()

View File

@@ -3,19 +3,18 @@ package com.gxwebsoft.shop.controller;
import cn.afterturn.easypoi.excel.ExcelImportUtil; import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.gxwebsoft.common.core.annotation.OperationLog;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.param.ShopDealerOrderParam;
import com.gxwebsoft.shop.param.ShopDealerOrderImportParam;
import com.gxwebsoft.common.core.utils.JSONUtil; import com.gxwebsoft.common.core.utils.JSONUtil;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.param.ShopDealerOrderImportParam;
import com.gxwebsoft.shop.param.ShopDealerOrderParam;
import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.task.OrderSettlementTask;
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;
@@ -24,7 +23,9 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 分销商订单记录表控制器 * 分销商订单记录表控制器
@@ -39,6 +40,9 @@ public class ShopDealerOrderController extends BaseController {
@Resource @Resource
private ShopDealerOrderService shopDealerOrderService; private ShopDealerOrderService shopDealerOrderService;
@Resource
private OrderSettlementTask orderSettlementTask;
@PreAuthorize("hasAuthority('shop:shopDealerOrder:list')") @PreAuthorize("hasAuthority('shop:shopDealerOrder:list')")
@Operation(summary = "分页查询分销商订单记录表") @Operation(summary = "分页查询分销商订单记录表")
@GetMapping("/page") @GetMapping("/page")
@@ -63,6 +67,13 @@ public class ShopDealerOrderController extends BaseController {
return success(shopDealerOrderService.getByIdRel(id)); return success(shopDealerOrderService.getByIdRel(id));
} }
@PreAuthorize("hasAuthority('shop:shopDealerOrder:list')")
@Operation(summary = "查询个人今日收益")
@GetMapping("/todayRevenue")
public ApiResult<BigDecimal> todayRevenue() {
return success(shopDealerOrderService.todayRevenue());
}
@PreAuthorize("hasAuthority('shop:shopDealerOrder:save')") @PreAuthorize("hasAuthority('shop:shopDealerOrder:save')")
@OperationLog @OperationLog
@Operation(summary = "添加分销商订单记录表") @Operation(summary = "添加分销商订单记录表")
@@ -168,4 +179,29 @@ 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);
}
}
@Operation(summary = "店、服务商结算任务每天一点每10分钟执行一次结算任务")
@PutMapping("/teamSettlement")
public ApiResult<?> teamSettlement() {
orderSettlementTask.teamSettlement();
return success("success");
}
} }

View File

@@ -1,19 +1,19 @@
package com.gxwebsoft.shop.controller; package com.gxwebsoft.shop.controller;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.Constants; import com.gxwebsoft.common.core.Constants;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.exception.BusinessException; import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.shop.service.ShopDealerRefereeService; 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.ShopDealerReferee; import com.gxwebsoft.shop.entity.ShopDealerReferee;
import com.gxwebsoft.shop.param.ShopDealerRefereeParam; import com.gxwebsoft.shop.param.ShopDealerRefereeParam;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.shop.service.ShopDealerRefereeService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.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.springdoc.api.annotations.ParameterObject;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -41,6 +41,13 @@ public class ShopDealerRefereeController extends BaseController {
return success(shopDealerRefereeService.pageRel(param)); return success(shopDealerRefereeService.pageRel(param));
} }
@PreAuthorize("hasAuthority('shop:shopDealerReferee:list')")
@Operation(summary = "分页查询分销商推荐关系表")
@GetMapping("/appPage")
public ApiResult<PageResult<ShopDealerReferee>> appPage(@ParameterObject ShopDealerRefereeParam param) {
return success(shopDealerRefereeService.appPage(param));
}
@PreAuthorize("hasAuthority('shop:shopDealerReferee:list')") @PreAuthorize("hasAuthority('shop:shopDealerReferee:list')")
@Operation(summary = "查询全部分销商推荐关系表") @Operation(summary = "查询全部分销商推荐关系表")
@GetMapping() @GetMapping()

View File

@@ -4,18 +4,19 @@ import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.core.utils.JSONUtil;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.param.ShopDealerUserParam;
import com.gxwebsoft.shop.param.ShopDealerUserImportParam;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.utils.JSONUtil;
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.common.system.entity.User;
import com.gxwebsoft.shop.dto.ShopDealerRefundDto;
import com.gxwebsoft.shop.dto.ShopDealerUserReduceDto;
import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.param.ShopDealerUserImportParam;
import com.gxwebsoft.shop.param.ShopDealerUserParam;
import com.gxwebsoft.shop.service.ShopDealerUserService;
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;
@@ -78,6 +79,13 @@ public class ShopDealerUserController extends BaseController {
return fail("添加失败"); return fail("添加失败");
} }
@PreAuthorize("hasAuthority('shop:shopDealerUser:update')")
@Operation(summary = "开启/关闭分销商用户核销权限")
@PutMapping("/verifyEnable")
public ApiResult<Boolean> verifyEnable(@RequestParam("id") Integer id) {
return success(shopDealerUserService.verifyEnable(id));
}
@PreAuthorize("hasAuthority('shop:shopDealerUser:update')") @PreAuthorize("hasAuthority('shop:shopDealerUser:update')")
@Operation(summary = "修改分销商用户记录表") @Operation(summary = "修改分销商用户记录表")
@PutMapping() @PutMapping()
@@ -178,4 +186,18 @@ public class ShopDealerUserController extends BaseController {
return fail("导入失败", null); return fail("导入失败", null);
} }
@Operation(summary = "分销结算")
@PostMapping("/dealerCapital")
@Deprecated
public ApiResult<?> dealerCapital(@RequestBody ShopDealerUserReduceDto reduceDto) {
return success(shopDealerUserService.reduceBalance(reduceDto));
}
@Operation(summary = "分销退单")
@PostMapping("/refundOrder")
@Deprecated
public ApiResult<?> refundOrder(@RequestBody ShopDealerRefundDto refundDto) {
return success(shopDealerUserService.refundOrder(refundDto));
}
} }

View File

@@ -2,6 +2,7 @@ package com.gxwebsoft.shop.controller;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.redis.OrderNoUtils;
import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopDealerWithdrawService; import com.gxwebsoft.shop.service.ShopDealerWithdrawService;
@@ -43,6 +44,8 @@ public class ShopDealerWithdrawController extends BaseController {
private ShopDealerUserService shopDealerUserService; private ShopDealerUserService shopDealerUserService;
@Resource @Resource
private WxTransferService wxTransferService; private WxTransferService wxTransferService;
@Resource
private OrderNoUtils orderNoUtils;
@PreAuthorize("hasAuthority('shop:shopDealerWithdraw:list')") @PreAuthorize("hasAuthority('shop:shopDealerWithdraw:list')")
@Operation(summary = "分页查询分销商提现明细表") @Operation(summary = "分页查询分销商提现明细表")
@@ -89,6 +92,8 @@ public class ShopDealerWithdrawController extends BaseController {
return fail("tenantId为空无法发起提现"); return fail("tenantId为空无法发起提现");
} }
String orderNo = orderNoUtils.generate("WD");
shopDealerWithdraw.setOrderNo(orderNo);
shopDealerWithdraw.setTenantId(tenantId); shopDealerWithdraw.setTenantId(tenantId);
shopDealerWithdraw.setUserId(loginUser.getUserId()); shopDealerWithdraw.setUserId(loginUser.getUserId());
@@ -251,7 +256,8 @@ public class ShopDealerWithdrawController extends BaseController {
} }
// 使用提现记录ID构造单号保持幂等微信要求 5-32 且仅字母/数字 // 使用提现记录ID构造单号保持幂等微信要求 5-32 且仅字母/数字
String outBillNo = String.format("WD%03d", db.getId()); String outBillNo = db.getOrderNo();
String remark = "分销商提现"; String remark = "分销商提现";
String userName = db.getRealName(); String userName = db.getRealName();

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, Integer popFlag) {
// 使用关联查询
return success(shopFlashSaleActivityService.getMyActive(tenantId, popFlag));
}
// @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<ShopFlashSaleActivityVO> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopFlashSaleActivityService.getInfoById(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

@@ -6,7 +6,6 @@ import com.gxwebsoft.shop.entity.ShopGoodsCategory;
import com.gxwebsoft.shop.param.ShopGoodsCategoryParam; import com.gxwebsoft.shop.param.ShopGoodsCategoryParam;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;

View File

@@ -0,0 +1,93 @@
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.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopGoodsProfit;
import com.gxwebsoft.shop.param.ShopGoodsProfitParam;
import com.gxwebsoft.shop.service.ShopGoodsProfitService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 商品分润设定控制器
*
* @author xm
* @since 2026-05-20 14:33:00
*/
@Tag(name = "商品分润设定管理")
@RestController
@RequestMapping("/api/shop/shop-goods-profit")
public class ShopGoodsProfitController extends BaseController {
@Resource
private ShopGoodsProfitService shopGoodsProfitService;
// @PreAuthorize("hasAuthority('shop:shopGoodsProfit:list')")
@Operation(summary = "分页查询商品分润设定")
@GetMapping("/page")
public ApiResult<PageResult<ShopGoodsProfit>> page(ShopGoodsProfitParam param) {
// 使用关联查询
return success(shopGoodsProfitService.pageRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopGoodsProfit:list')")
@Operation(summary = "查询全部商品分润设定")
@GetMapping()
public ApiResult<List<ShopGoodsProfit>> list(ShopGoodsProfitParam param) {
// 使用关联查询
return success(shopGoodsProfitService.listRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopGoodsProfit:list')")
@Operation(summary = "根据id查询商品分润设定")
@GetMapping("/{id}")
public ApiResult<ShopGoodsProfit> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopGoodsProfitService.getByIdRel(id));
}
// @PreAuthorize("hasAuthority('shop:shopGoodsProfit:save')")
@OperationLog
@Operation(summary = "添加商品分润设定")
@PostMapping()
public ApiResult<?> save(@RequestBody ShopGoodsProfit shopGoodsProfit) {
// 记录当前登录用户id
User loginUser = getLoginUser();
if (loginUser != null) {
shopGoodsProfit.setUserId(loginUser.getUserId());
}
if (shopGoodsProfitService.save(shopGoodsProfit)) {
return success("添加成功");
}
return fail("添加失败");
}
// @PreAuthorize("hasAuthority('shop:shopGoodsProfit:update')")
@OperationLog
@Operation(summary = "修改商品分润设定")
@PutMapping()
public ApiResult<?> update(@RequestBody ShopGoodsProfit shopGoodsProfit) {
if (shopGoodsProfitService.updateById(shopGoodsProfit)) {
return success("修改成功");
}
return fail("修改失败");
}
// @PreAuthorize("hasAuthority('shop:shopGoodsProfit:remove')")
@OperationLog
@Operation(summary = "删除商品分润设定")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (shopGoodsProfitService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -5,42 +5,45 @@ import cn.hutool.core.util.NumberUtil;
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 com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.config.CertificateProperties; import com.gxwebsoft.common.core.config.CertificateProperties;
import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.utils.CertificateLoader; import com.gxwebsoft.common.core.utils.CertificateLoader;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.core.utils.WechatCertAutoConfig; 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.ApiResult;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.Payment; import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.shop.entity.ShopOrderDelivery; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.shop.entity.ShopUserAddress; import com.gxwebsoft.common.system.redis.OrderNoUtils;
import com.gxwebsoft.shop.service.*; import com.gxwebsoft.glt.service.GltTicketIssueService;
import com.gxwebsoft.glt.service.GltTicketRevokeService; import com.gxwebsoft.glt.service.GltTicketRevokeService;
import com.gxwebsoft.shop.service.impl.KuaiDi100Impl; import com.gxwebsoft.glt.task.DealerOrderSettlement10584Task;
import com.gxwebsoft.shop.task.OrderAutoCancelTask;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.param.ShopOrderParam;
import com.gxwebsoft.shop.dto.OrderCreateRequest;
import com.gxwebsoft.shop.dto.OrderPrepayRequest;
import com.gxwebsoft.shop.dto.UpdatePaymentStatusRequest;
import com.gxwebsoft.payment.service.PaymentService;
import com.gxwebsoft.payment.dto.PaymentResponse; import com.gxwebsoft.payment.dto.PaymentResponse;
import com.gxwebsoft.payment.enums.PaymentType; import com.gxwebsoft.payment.enums.PaymentType;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.payment.service.PaymentService;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.shop.dto.*;
import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.shop.entity.ShopOrderDelivery;
import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.param.ShopOrderParam;
import com.gxwebsoft.shop.service.*;
import com.gxwebsoft.shop.service.impl.KuaiDi100Impl;
import com.gxwebsoft.shop.task.OrderAutoCancelTask;
import com.gxwebsoft.shop.vo.ShopOrderMyVerifyVO;
import com.wechat.pay.java.core.RSAAutoCertificateConfig;
import com.wechat.pay.java.core.notification.NotificationConfig; 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.service.payments.model.Transaction;
import com.wechat.pay.java.service.partnerpayments.jsapi.model.Transaction;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springdoc.api.annotations.ParameterObject;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -53,6 +56,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 +69,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 +113,12 @@ public class ShopOrderController extends BaseController {
private GltTicketRevokeService gltTicketRevokeService; private GltTicketRevokeService gltTicketRevokeService;
@Resource @Resource
private ShopDealerCommissionRollbackService shopDealerCommissionRollbackService; private ShopDealerCommissionRollbackService shopDealerCommissionRollbackService;
@Resource
private GltTicketIssueService gltTicketIssueService;
@Resource
private OrderNoUtils orderNoUtils;
@Resource
private DealerOrderSettlement10584Task dealerOrderSettlement;
@Operation(summary = "分页查询订单") @Operation(summary = "分页查询订单")
@GetMapping("/page") @GetMapping("/page")
@@ -130,14 +142,20 @@ public class ShopOrderController extends BaseController {
return success(shopOrderService.getByIdRel(id)); return success(shopOrderService.getByIdRel(id));
} }
@Operation(summary = "添加订单") // @PreAuthorize("hasAuthority('shop:shopOrder:list')")
@Operation(summary = "我已核销订单")
@GetMapping("/myVerifyOrder")
public ApiResult<ShopOrderMyVerifyVO> myVerifyOrder(@ParameterObject ShopOrderMyVerifyDto myVerifyDto) {
return success(shopOrderService.myVerifyOrder(myVerifyDto));
}
@Operation(summary = "添加订单【单商品添加】")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody OrderCreateRequest request) { public ApiResult<?> save(@RequestBody OrderCreateRequest request) {
User loginUser = getLoginUser(); User loginUser = getLoginUser();
if (loginUser == null) { if (loginUser == null) {
return fail("用户未登录"); return fail("用户未登录");
} }
try { try {
Map<String, String> wxOrderInfo = orderBusinessService.createOrder(request, loginUser); Map<String, String> wxOrderInfo = orderBusinessService.createOrder(request, loginUser);
return success("下单成功", wxOrderInfo); return success("下单成功", wxOrderInfo);
@@ -438,15 +456,16 @@ public class ShopOrderController extends BaseController {
} }
@PreAuthorize("hasAuthority('shop:shopOrder:refund')") @PreAuthorize("hasAuthority('shop:shopOrder:refund')")
@Operation(summary = "订单退款操作(申请退款/同意退款)", description = "orderStatus=4 申请退款orderStatus=6 同意退款并发起原路退款") @Operation(summary = "订单退款操作(申请退款/同意退款/拒绝退款)", description = "orderStatus=4 申请退款orderStatus=5 拒绝退款orderStatus=6 同意退款并发起原路退款orderStatus=7 客户端申请退款")
@PutMapping("/refund") @PutMapping("/refund")
public ApiResult<?> refund(@RequestBody ShopOrder req) { public ApiResult<?> refund(@RequestBody ShopOrder req) {
if (req == null || req.getOrderId() == null || req.getOrderStatus() == null) { if (req == null || req.getOrderId() == null || req.getOrderStatus() == null) {
return fail("orderId 和 orderStatus 不能为空"); return fail("orderId 和 orderStatus 不能为空");
} }
if (!Objects.equals(req.getOrderStatus(), 4) && !Objects.equals(req.getOrderStatus(), 6) && !Objects.equals(req.getOrderStatus(), 7)) { if (!Objects.equals(req.getOrderStatus(), 4) && !Objects.equals(req.getOrderStatus(), 5)
return fail("orderStatus 仅支持 4(申请退款) 或 6(同意退款) 或 7(客户端申请退款)"); && !Objects.equals(req.getOrderStatus(), 6) && !Objects.equals(req.getOrderStatus(), 7)) {
} return fail("orderStatus 仅支持 4(申请退款)、5(拒绝退款)、6(同意退款)、7(客户端申请退款)");
}
ShopOrder current = shopOrderService.getById(req.getOrderId()); ShopOrder current = shopOrderService.getById(req.getOrderId());
if (current == null) { if (current == null) {
@@ -490,12 +509,12 @@ public class ShopOrderController extends BaseController {
if (!Boolean.TRUE.equals(current.getPayStatus())) { if (!Boolean.TRUE.equals(current.getPayStatus())) {
return fail("订单未支付,无法退款"); return fail("订单未支付,无法退款");
} }
if (StrUtil.isNotBlank(current.getRefundOrder())) { if (StrUtil.isNotBlank(current.getRefundOrder()) || current.getOrderStatus() == 6) {
logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", current.getOrderNo(), current.getRefundOrder()); logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", current.getOrderNo(), current.getRefundOrder() != null ? current.getRefundOrder() : "");
return fail("订单已退款,请勿重复操作"); return fail("订单已退款,请勿重复操作");
} }
String refundNo = "RF" + IdUtil.getSnowflakeNextId(); String refundNo = orderNoUtils.generate("RF");
BigDecimal refundAmount = req.getRefundMoney(); BigDecimal refundAmount = req.getRefundMoney();
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) { if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
@@ -569,7 +588,7 @@ public class ShopOrderController extends BaseController {
rollbackOrder.setOrderNo(current.getOrderNo()); rollbackOrder.setOrderNo(current.getOrderNo());
rollbackOrder.setPayPrice(current.getPayPrice()); rollbackOrder.setPayPrice(current.getPayPrice());
rollbackOrder.setTotalPrice(current.getTotalPrice()); rollbackOrder.setTotalPrice(current.getTotalPrice());
boolean rollbackOk = shopDealerCommissionRollbackService.rollbackOnOrderRefund(rollbackOrder, refundAmount); boolean rollbackOk = shopDealerCommissionRollbackService.rollbackOnOrderRefundV2(rollbackOrder, refundAmount);
if (!rollbackOk) { if (!rollbackOk) {
logger.error("退款成功但回退分红/分润/佣金失败 - tenantId={}, orderId={}, orderNo={}", logger.error("退款成功但回退分红/分润/佣金失败 - tenantId={}, orderId={}, orderNo={}",
tenantId, current.getOrderId(), current.getOrderNo()); tenantId, current.getOrderId(), current.getOrderNo());
@@ -760,7 +779,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 +810,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("local")) {
// 开发环境 - 构建包含租户号的私钥路径
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 +915,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 +950,10 @@ public class ShopOrderController extends BaseController {
System.out.println("实际付款金额 = " + order.getPayPrice()); System.out.println("实际付款金额 = " + order.getPayPrice());
// 更新订单状态并处理支付成功后的业务逻辑(包括累加商品销量) // 更新订单状态并处理支付成功后的业务逻辑(包括累加商品销量)
shopOrderService.updateByOutTradeNo(order); shopOrderService.updateByOutTradeNo(order);
//支付成功执行异步任务
gltTicketIssueService.paySuccessTask(order.getOrderNo(), tenantId);
return "SUCCESS"; return "SUCCESS";
} }
} }
@@ -940,6 +974,33 @@ public class ShopOrderController extends BaseController {
return "fail"; return "fail";
} }
@Operation(summary = "核销订单", description = "核销员扫码核销用户订单")
@PutMapping("/verifyOrder")
public ApiResult<Boolean> verifyOrder(@RequestBody VerifyShopOrderDto verifyDto){
return success(shopOrderService.verifyOrder(verifyDto));
}
@Operation(summary = "支付成功任务", description = "用户支付成功后执行任务")
@PutMapping("/paySuccessTask")
public ApiResult<Boolean> paySuccessTask(@RequestBody PaySuccessTaskDto taskDto){
gltTicketIssueService.paySuccessTask(taskDto.getOrderNo(), taskDto.getTenantId());
return success(Boolean.TRUE);
}
@Operation(summary = "支付成功发送水票", description = "支付成功发送水票")
@PutMapping("/suerTicketRelease")
public ApiResult<Boolean> suerTicketRelease(@RequestBody PaySuccessTaskDto taskDto){
gltTicketIssueService.suerTicketRelease(taskDto.getOrderNo(), taskDto.getTenantId());
return success(Boolean.TRUE);
}
@Operation(summary = "支付成功结算", description = "支付成功结算")
@PutMapping("/orderSettlement")
public ApiResult<Boolean> orderSettlement(@RequestBody PaySuccessTaskDto taskDto){
dealerOrderSettlement.orderSettlement(taskDto.getOrderNo());
return success(Boolean.TRUE);
}
@Operation(summary = "更新订单支付状态", description = "用户支付成功后主动同步订单状态") @Operation(summary = "更新订单支付状态", description = "用户支付成功后主动同步订单状态")
@PutMapping("/payment-status") @PutMapping("/payment-status")
public ApiResult<?> updateOrderPaymentStatus(@RequestBody UpdatePaymentStatusRequest request) { public ApiResult<?> updateOrderPaymentStatus(@RequestBody UpdatePaymentStatusRequest request) {

View File

@@ -0,0 +1,88 @@
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.PageResult;
import com.gxwebsoft.shop.entity.ShopSurchargeConfig;
import com.gxwebsoft.shop.param.ShopSurchargeConfigParam;
import com.gxwebsoft.shop.service.ShopSurchargeConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 步梯费用设置控制器
*
* @author xm
* @since 2026-04-28 16:30:00
*/
@Tag(name = "步梯费用设置管理")
@RestController
@RequestMapping("/api/shop/shop-surcharge-config")
public class ShopSurchargeConfigController extends BaseController {
@Resource
private ShopSurchargeConfigService shopSurchargeConfigService;
// @PreAuthorize("hasAuthority('shop:shopSurchargeConfig:list')")
@Operation(summary = "分页查询步梯费用设置")
@GetMapping("/page")
public ApiResult<PageResult<ShopSurchargeConfig>> page(ShopSurchargeConfigParam param) {
// 使用关联查询
return success(shopSurchargeConfigService.pageRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopSurchargeConfig:list')")
@Operation(summary = "根据id查询步梯费用设置")
@GetMapping("/{id}")
public ApiResult<ShopSurchargeConfig> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopSurchargeConfigService.getByIdRel(id));
}
// @PreAuthorize("hasAuthority('shop:shopSurchargeConfig:list')")
@Operation(summary = "根据类型查询步梯费用设置")
@GetMapping("/getInfoByType")
public ApiResult<ShopSurchargeConfig> getInfoByType(@RequestParam Integer type) {
// 使用关联查询
return success(shopSurchargeConfigService.getInfoByType(type));
}
// @PreAuthorize("hasAuthority('shop:shopSurchargeConfig:save')")
@OperationLog
@Operation(summary = "添加步梯费用设置")
@PostMapping()
public ApiResult<Integer> save(@RequestBody ShopSurchargeConfig shopSurchargeConfig) {
return success(shopSurchargeConfigService.saveInfo(shopSurchargeConfig));
}
// @PreAuthorize("hasAuthority('shop:shopSurchargeConfig:update')")
@OperationLog
@Operation(summary = "修改步梯费用设置")
@PutMapping()
public ApiResult<Boolean> update(@RequestBody ShopSurchargeConfig shopSurchargeConfig) {
return success(shopSurchargeConfigService.updateInfo(shopSurchargeConfig));
}
// @PreAuthorize("hasAuthority('shop:shopSurchargeConfig:update')")
@OperationLog
@Operation(summary = "修改步梯费用设置")
@PutMapping("/updateStatus")
public ApiResult<Boolean> updateStatus(@RequestParam("id") Integer id) {
return success(shopSurchargeConfigService.updateStatus(id));
}
// @PreAuthorize("hasAuthority('shop:shopSurchargeConfig:remove')")
@OperationLog
@Operation(summary = "删除步梯费用设置")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (shopSurchargeConfigService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -79,11 +79,8 @@
@OperationLog @OperationLog
@Operation(summary = "修改收货地址") @Operation(summary = "修改收货地址")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody ShopUserAddress shopUserAddress) { public ApiResult<Boolean> update(@RequestBody ShopUserAddress shopUserAddress) {
if (shopUserAddressService.updateById(shopUserAddress)) { return success(shopUserAddressService.updateInfo(shopUserAddress));
return success("修改成功");
}
return fail("修改失败");
} }
@PreAuthorize("hasAuthority('shop:shopUserAddress:remove')") @PreAuthorize("hasAuthority('shop:shopUserAddress:remove')")

View File

@@ -0,0 +1,103 @@
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.PageResult;
import com.gxwebsoft.shop.entity.ShopVerifyUser;
import com.gxwebsoft.shop.param.ShopVerifyUserParam;
import com.gxwebsoft.shop.service.ShopVerifyUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 订单核销人管理控制器
*
* @author xm
* @since 2026-05-25 17:13:01
*/
@Tag(name = "订单核销人管理管理")
@RestController
@RequestMapping("/api/shop/shop-verify-user")
public class ShopVerifyUserController extends BaseController {
@Resource
private ShopVerifyUserService shopVerifyUserService;
// @PreAuthorize("hasAuthority('shop:shopVerifyUser:list')")
@Operation(summary = "分页查询订单核销人管理")
@GetMapping("/page")
public ApiResult<PageResult<ShopVerifyUser>> page(ShopVerifyUserParam param) {
// 使用关联查询
return success(shopVerifyUserService.pageRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopVerifyUser:list')")
@Operation(summary = "查询全部订单核销人管理")
@GetMapping()
public ApiResult<List<ShopVerifyUser>> list(ShopVerifyUserParam param) {
// 使用关联查询
return success(shopVerifyUserService.listRel(param));
}
// @PreAuthorize("hasAuthority('shop:shopVerifyUser:list')")
@Operation(summary = "根据id查询订单核销人管理")
@GetMapping("/{id}")
public ApiResult<ShopVerifyUser> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(shopVerifyUserService.getByIdRel(id));
}
// @PreAuthorize("hasAuthority('shop:shopVerifyUser:list')")
@Operation(summary = "根据用户ID查询订单核销人管理")
@GetMapping("/getByUserId")
public ApiResult<ShopVerifyUser> getByUserId(@RequestParam("userId") Integer userId) {
return success(shopVerifyUserService.getInfo(userId));
}
// @PreAuthorize("hasAuthority('shop:shopVerifyUser:save')")
@OperationLog
@Operation(summary = "添加订单核销人管理")
@PostMapping()
public ApiResult<Boolean> save(@RequestBody ShopVerifyUser shopVerifyUser) {
return success(shopVerifyUserService.saveInfo(shopVerifyUser));
}
// @PreAuthorize("hasAuthority('shop:shopVerifyUser:update')")
@Operation(summary = "修改订单核销人管理")
@PutMapping()
public ApiResult<?> update(@RequestBody ShopVerifyUser shopVerifyUser) {
if (shopVerifyUserService.updateById(shopVerifyUser)) {
return success("修改成功");
}
return fail("修改失败");
}
@Operation(summary = "修改订单核销人开启状态")
@PutMapping("/updateStatus")
public ApiResult<Boolean> updateStatus(@RequestParam("id") Integer id) {
return success(shopVerifyUserService.updateStatus(id));
}
@Operation(summary = "修改订单核销人核销权限")
@PutMapping("/updateVerifyFlag")
public ApiResult<Boolean> updateVerifyFlag(@RequestParam("id") Integer id) {
return success(shopVerifyUserService.updateVerifyFlag(id));
}
// @PreAuthorize("hasAuthority('shop:shopVerifyUser:remove')")
@OperationLog
@Operation(summary = "删除订单核销人管理")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (shopVerifyUserService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -27,6 +27,9 @@ public class OrderCreateRequest {
@Max(value = 2, message = "订单类型值无效") @Max(value = 2, message = "订单类型值无效")
private Integer type; private Integer type;
@Schema(description = "订单类型 1-及时自配送 2-自提 3-预约自配送 4-发快递 5-配送【系统自动识别电子围栏内转及时配送,电子围栏外发快递】")
private Integer orderType;
@Size(max = 60, message = "备注长度不能超过60个字符") @Size(max = 60, message = "备注长度不能超过60个字符")
@Schema(description = "订单标题") @Schema(description = "订单标题")
private String title; private String title;
@@ -88,6 +91,12 @@ public class OrderCreateRequest {
@Schema(description = "发货店铺") @Schema(description = "发货店铺")
private String expressMerchantName; private String expressMerchantName;
@Schema(description = "配送方式 0-电梯 1-步梯")
private Integer deliveryMethod;
@Schema(description = "楼层")
private Integer deliveryFloor;
@Schema(description = "订单总额") @Schema(description = "订单总额")
@NotNull(message = "订单总额不能为空") @NotNull(message = "订单总额不能为空")
@DecimalMin(value = "0.01", message = "订单总额必须大于0") @DecimalMin(value = "0.01", message = "订单总额必须大于0")
@@ -147,6 +156,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 +170,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,25 @@
package com.gxwebsoft.shop.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 支付成功执行请求类
*
* @author xm
* @since 2026-05-04
*/
@Data
@Schema(name = "PaySuccessTaskDto", description = "支付成功执行请求类")
public class PaySuccessTaskDto {
@Schema(description = "订单号")
@NotBlank(message = "订单号不能为空")
private String orderNo;
@Schema(description = "租户ID", example = "10584")
private Integer tenantId;
}

View File

@@ -0,0 +1,28 @@
package com.gxwebsoft.shop.dto;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* 查询个人流水
*
*/
@Data
@Schema(name = "ShopDealerCapitalWaterDto", description = "查询个人流水")
public class ShopDealerCapitalWaterDto extends BaseParam {
@Schema(description = "用户ID")
@NotEmpty(message = "用户ID不能为空")
private Integer userId;
@Schema(description = "起始时间", example = "2026-05-01")
private String dateStart;
@Schema(description = "结束时间", example = "2026-05-31")
private String dateEnd;
}

View File

@@ -0,0 +1,28 @@
package com.gxwebsoft.shop.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 分销订单退款
* @author xm
* @since 2026-05-13
*/
@Data
@Schema(name = "ShopDealerUserReduceDto", description = "分销订单退款")
public class ShopDealerRefundDto {
@Schema(description = "订单号")
@NotNull(message = "变订单号不可为空!")
private String orderNo;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "退款金额")
private BigDecimal refundAmount;
}

View File

@@ -0,0 +1,31 @@
package com.gxwebsoft.shop.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 分销订单结算
* @author xm
* @since 2026-05-13
*/
@Data
@Schema(name = "ShopDealerSettlementDto", description = "分销订单结算")
public class ShopDealerSettlementDto {
@Schema(description = "变动类型 1-操作冻结账户余额 2-操作提现账户余额【直接结算】 3-解冻 4-退款")
private Integer type;
@Schema(description = "资金流动类型 (10分销收入 11团队管理津贴收入 12分红收入 13现场推广收入 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励 70佣金退回【退单】)")
private Integer flowType;
@Schema(description = "描述")
private String comments;
@Schema(description = "商城ID")
private Integer tenantId;
@Schema(description = "明细列表")
private List<ShopDealerSettlementItemDto> itemList;
}

View File

@@ -0,0 +1,26 @@
package com.gxwebsoft.shop.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 分销订单退款
* @author xm
* @since 2026-05-13
*/
@Data
@Schema(name = "ShopDealerSettlementItemDto", description = "分销订单退款")
public class ShopDealerSettlementItemDto {
@Schema(description = "订单号")
private String no;
@Schema(description = "分销商用户ID")
private Integer userId;
@Schema(description = "变更金额")
private BigDecimal money;
}

View File

@@ -0,0 +1,42 @@
package com.gxwebsoft.shop.dto;
import com.gxwebsoft.common.core.enums.ShopDealerCapitalUpdateEnum;
import com.gxwebsoft.common.core.enums.ShopDealerTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 更新分销用户资金
* @author xm
* @since 2026-05-06
*/
@Data
@Schema(name = "ShopDealerUserReduceDto", description = "更新分销用户资金请求")
public class ShopDealerUserReduceDto {
@Schema(description = "变动类型 1-操作冻结账户余额 2-操作提现账户余额【直接结算】 3-解冻")
@NotNull(message = "变更类型不可为空!")
private ShopDealerTypeEnum typeEnum;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "订单主体用户ID")
private Integer orderUserId;
@Schema(description = "订单号")
@NotEmpty(message = "订单号不能为空!")
private String orderNo;
@Schema(description = "变更金额")
private BigDecimal price;
@Schema(description = "业务类型")
@NotEmpty(message = "业务类型不能为空!")
private ShopDealerCapitalUpdateEnum updateEnum;
}

View File

@@ -0,0 +1,27 @@
package com.gxwebsoft.shop.dto;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 查询个人核销记录
*
*/
@Data
@Schema(name = "ShopOrderMyVerifyDto", description = "查询个人核销记录")
public class ShopOrderMyVerifyDto extends BaseParam {
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "订单号")
private String orderNo;
@Schema(description = "开始时间【核销时间】", example = "2026-05-20")
private String dateStart;
@Schema(description = "结束时间【核销时间】", example = "2026-05-25")
private String dateEnd;
}

View File

@@ -26,7 +26,7 @@ public class UserOrderStats implements Serializable {
@Schema(description = "待核销statusFilter=2") @Schema(description = "待核销statusFilter=2")
private Long waitVerify; private Long waitVerify;
@Schema(description = "待收货statusFilter=3") @Schema(description = "待收货/使用statusFilter=3")
private Long waitReceive; private Long waitReceive;
@Schema(description = "待评价statusFilter=4") @Schema(description = "待评价statusFilter=4")

View File

@@ -0,0 +1,25 @@
package com.gxwebsoft.shop.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 更新订单支付状态请求DTO
*
* @author xm
* @since 2026-05-04
*/
@Data
@Schema(name = "VerifyShopOrderDto", description = "更新订单支付状态请求")
public class VerifyShopOrderDto {
@Schema(description = "核销码")
@NotBlank(message = "核销码不能为空")
private String verifyCode;
@Schema(description = "核销码类型 1-普通核销【无推广佣金】 2-推广结算【有订单佣金】", example = "2")
private Integer verifyType;
}

View File

@@ -0,0 +1,81 @@
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 = "分享底图")
private String shareImg;
@Schema(description = "图片地址集合")
@TableField(exist = false)
private List<String> imgUrlList;
@Schema(description = "启用状态 0-启用 1-禁用")
private Integer status;
@Schema(description = "排序")
private Integer sortNumber;
@Schema(description = "跳转商品ID")
private Integer goodsId;
@Schema(description = "跳转商品名称")
@TableField(exist = false)
private String goodsName;
@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

@@ -1,10 +1,11 @@
package com.gxwebsoft.shop.entity; package com.gxwebsoft.shop.entity;
import java.math.BigDecimal; import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -20,6 +21,7 @@ import lombok.EqualsAndHashCode;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "ShopDealerCapital对象", description = "分销商资金明细表") @Schema(name = "ShopDealerCapital对象", description = "分销商资金明细表")
@TableName("shop_dealer_capital")
public class ShopDealerCapital implements Serializable { public class ShopDealerCapital implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -27,26 +29,41 @@ public class ShopDealerCapital implements Serializable {
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.AUTO)
private Integer id; private Integer id;
@Schema(description = "订单编号")
private String no;
@Schema(description = "分销商用户ID") @Schema(description = "分销商用户ID")
private Integer userId; private Integer userId;
@Schema(description = "变动类型 1-操作冻结账户余额 2-操作提现账户余额【直接结算】 3-解冻 4-退款")
private Integer type;
@Schema(description = "分销商昵称") @Schema(description = "分销商昵称")
@TableField(exist = false) @TableField(exist = false)
private String nickName; private String nickName;
@Schema(description = "订单编号") @Schema(description = "关联订单编号")
private String orderNo; private String orderNo;
@Schema(description = "订单状态") @Schema(description = "订单状态")
@TableField(exist = false) @TableField(exist = false)
private Integer orderStatus; private Integer orderStatus;
@Schema(description = "资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励)") @Schema(description = "资金流动类型 (10分销收入 11团队管理津贴收入 12分红收入 13现场推广收入 14现场推广分佣 20提现支出 30转账支出 40转账收入 50佣金解冻 60配送奖励 61配送提成 62配送步梯费 70佣金退回【退单】)")
private Integer flowType; private Integer flowType;
@Schema(description = "金额") @Schema(description = "变更金额")
private BigDecimal money; private BigDecimal money;
@Schema(description = "变更后金额")
private BigDecimal moneyAfter;
@Schema(description = "变更冻结金额")
private BigDecimal freezeMoney;
@Schema(description = "变更冻结后金额")
private BigDecimal freezeMoneyAfter;
@Schema(description = "描述") @Schema(description = "描述")
private String comments; private String comments;
@@ -60,9 +77,15 @@ public class ShopDealerCapital implements Serializable {
@Schema(description = "结算月份") @Schema(description = "结算月份")
private String month; private String month;
@Schema(description = "结算标识 0-未结算 1-已结算")
private Integer paymentFlag;
@Schema(description = "商城ID") @Schema(description = "商城ID")
private Integer tenantId; private Integer tenantId;
@Schema(description = "创建人")
private Integer creator;
@Schema(description = "创建时间") @Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private LocalDateTime createTime;
@@ -71,4 +94,8 @@ public class ShopDealerCapital implements Serializable {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private LocalDateTime updateTime;
@Schema(description = "是否删除 0-未删 1-已删")
@TableLogic
private Integer deleted;
} }

View File

@@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable; import java.io.Serializable;
@@ -23,6 +25,7 @@ import lombok.EqualsAndHashCode;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "ShopDealerOrder对象", description = "分销商订单记录表") @Schema(name = "ShopDealerOrder对象", description = "分销商订单记录表")
@TableName("shop_dealer_order")
public class ShopDealerOrder implements Serializable { public class ShopDealerOrder implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -66,6 +69,9 @@ public class ShopDealerOrder implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String firstNickname; private String firstNickname;
@Schema(description = "分销佣金(一级)")
private BigDecimal firstMoney;
@Schema(description = "分销商用户id(二级)") @Schema(description = "分销商用户id(二级)")
private Integer secondUserId; private Integer secondUserId;
@@ -73,42 +79,57 @@ public class ShopDealerOrder implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String secondNickname; private String secondNickname;
@Schema(description = "分销商用户id(三级)")
private Integer thirdUserId;
@Schema(description = "分销商用户昵称(三级)")
@TableField(exist = false)
private String thirdNickname;
@Schema(description = "分销佣金(一级)")
private BigDecimal firstMoney;
@Schema(description = "分销佣金(二级)") @Schema(description = "分销佣金(二级)")
private BigDecimal secondMoney; private BigDecimal secondMoney;
@Schema(description = "分销佣金(三级)") @Schema(description = "分销商用户id(弃用)")
private Integer thirdUserId;
@Schema(description = "分销商用户昵称(弃用)")
@TableField(exist = false)
private String thirdNickname;
@Schema(description = "分销佣金(弃用)")
private BigDecimal thirdMoney; private BigDecimal thirdMoney;
@Schema(description = "门店(一级)") @Schema(description = "一级服务商/门店")
private Integer firstDividendUser; private Integer firstDividendUser;
@Schema(description = "门店名称(一级)") @Schema(description = "一级服务商/门店名称")
@TableField(exist = false) @TableField(exist = false)
private String firstDividendUserName; private String firstDividendUserName;
@Schema(description = "分红(一级)") @Schema(description = "一级服务商/门店管理津贴")
private BigDecimal firstDividend; private BigDecimal firstDividend;
@Schema(description = "门店(二级)") @Schema(description = "一级服务商/门店结算标识 0-否 1-是")
private Integer firstDividendFlag;
@Schema(description = "一级服务商/门店结算单号")
private String firstDividendNo;
@Schema(description = "一级服务商/门店结算时间")
private LocalDateTime firstDividendTime;
@Schema(description = "二级服务商/门店")
private Integer secondDividendUser; private Integer secondDividendUser;
@Schema(description = "门店名称(二级)") @Schema(description = "二级服务商/门店名称")
@TableField(exist = false) @TableField(exist = false)
private String secondDividendUserName; private String secondDividendUserName;
@Schema(description = "分红(二级)") @Schema(description = "二级服务商/门店管理津贴")
private BigDecimal secondDividend; private BigDecimal secondDividend;
@Schema(description = "二级服务商/门店结算标识 0-否 1-是")
private Integer secondDividendFlag;
@Schema(description = "二级服务商/门店结算单号")
private String secondDividendNo;
@Schema(description = "二级服务商/门店结算时间")
private LocalDateTime secondDividendTime;
@Schema(description = "佣金比例") @Schema(description = "佣金比例")
private BigDecimal rate; private BigDecimal rate;

View File

@@ -3,6 +3,8 @@ package com.gxwebsoft.shop.entity;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable; import java.io.Serializable;
@@ -87,4 +89,16 @@ public class ShopDealerReferee implements Serializable {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private LocalDateTime updateTime;
@Schema(description = "团队成员")
@TableField(exist = false)
private Integer teamNum;
@Schema(description = "订单数")
@TableField(exist = false)
private Integer orderNum;
@Schema(description = "订单金额")
@TableField(exist = false)
private BigDecimal orderAmount;
} }

View File

@@ -1,10 +1,11 @@
package com.gxwebsoft.shop.entity; package com.gxwebsoft.shop.entity;
import java.math.BigDecimal; import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable; import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -20,6 +21,7 @@ import lombok.EqualsAndHashCode;
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = false)
@Schema(name = "ShopDealerUser对象", description = "分销商用户记录表") @Schema(name = "ShopDealerUser对象", description = "分销商用户记录表")
@TableName("shop_dealer_user")
public class ShopDealerUser implements Serializable { public class ShopDealerUser implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -102,6 +104,9 @@ public class ShopDealerUser implements Serializable {
@Schema(description = "排序号") @Schema(description = "排序号")
private Integer sortNumber; private Integer sortNumber;
@Schema(description = "核销权限是否开启 0-未开启 1-已开启")
private Boolean verifyFlag;
@Schema(description = "是否删除") @Schema(description = "是否删除")
private Integer isDelete; private Integer isDelete;
@@ -112,8 +117,18 @@ public class ShopDealerUser implements Serializable {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private LocalDateTime createTime;
@Schema(description = "更新人")
private Integer updater;
@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;
@Schema(description = "分销商等级0-普通用户 1-超级管理员 2-合伙人(总店) 3-合伙人(分店)")
private Integer dealerLevel;
@Schema(description = "删除 0-未删 1-已删")
@TableLogic
private Integer deleted;
} }

View File

@@ -27,6 +27,9 @@ public class ShopDealerWithdraw implements Serializable {
@TableId(value = "id", type = IdType.AUTO) @TableId(value = "id", type = IdType.AUTO)
private Integer id; private Integer id;
@Schema(description = "订单号")
private String orderNo;
@Schema(description = "分销商用户ID") @Schema(description = "分销商用户ID")
private Integer userId; private Integer userId;

Some files were not shown because too many files have changed in this diff Show More