Compare commits

..

23 Commits

Author SHA1 Message Date
fe15c7120f feat(order): 实现订单地址快照功能
- 移除 GltTicketOrder 实体中 address 字段的 @TableField(exist = false) 注解
- 添加 ShopUserAddress 和 ShopUserAddressService 的依赖注入
- 在订单创建时实现地址快照逻辑,将用户地址信息保存到订单表
- 添加地址验证和权限检查功能
- 实现默认地址获取和地址拼接功能
- 添加订单参数空值校验
2026-02-06 13:43:08 +08:00
6285429753 feat(order): 添加订单实体扩展字段和用户关联查询
- 在GltTicketOrder实体中添加address、province、city、region等地址相关字段
- 在GltTicketOrder实体中添加nickname、phone、avatar等用户信息字段
- 所有新增字段均使用@TableField(exist = false)注解标记为非数据库字段
- 更新GltTicketOrderMapper.xml中的关联
2026-02-06 13:36:19 +08:00
46b5ce3971 1 2026-02-06 12:40:30 +08:00
4832929a11 1 2026-02-06 12:39:58 +08:00
1536f1780b feat(order): 添加订单快递单号同步到发货单功能
- 在订单更新时检查并同步快递单号到发货单表
- 使用LambdaQueryWrapper查询相关发货单记录
- 支持新增发货单或更新现有发货单的快递单号
- 添加异常处理避免同步失败影响主流程
- 实现手动录入和无需物流两种配送方式的处理
- 添加日志记录同步失败的情况便于排查问题
2026-02-06 02:27:56 +08:00
804a5a7bef feat(shop): 添加微信小程序发货信息自动同步功能
- 新增 ShopWechatShippingSyncService 接口及实现类
- 在订单发货时自动同步实物快递和无需物流的发货信息到微信后台
- 添加微信小程序 access_token 获取服务及缓存机制
- 优化订单发货逻辑,支持无需物流/自提订单的自动同步处理
- 添加详细的日志记录和异常处理机制
- 实现发货信息同步失败时的容错处理
2026-02-06 01:09:41 +08:00
60279fca4c fix(cache): 解决缓存中JSON null值导致的空指针问题
- 添加对历史缓存中JSON "null" 字符串的兼容处理
- 当缓存解析出null值时清理缓存并回源数据库
- 在CmsWebsiteServiceImpl中增加缓存清理逻辑
- 在ShopWebsiteServiceImpl中统一缓存异常处理机制
- 添加单元测试验证JSON null值场景的正确回退行为
2026-02-06 00:49:00 +08:00
2c076e2b0f feat(order): 添加送水订单配送时间和完整下单流程
- 在GltTicketOrder实体中新增sendTime字段用于记录配送时间
- 移除送水订单查询接口的权限验证要求,开放查询功能
- 实现完整的下单流程:验证登录用户、扣减水票、写入核销记录、创建订单
- 新增createWithWriteOff方法处理事务性下单操作,确保数据一致性
- 添加数据库行锁机制防止并发扣减问题
- 优化水票相关接口描述,明确为可用水票总数
- 移除水票日志添加接口的权限验证和操作日志注解
2026-02-06 00:15:31 +08:00
48cd2e1f7b feat(order): 添加送水订单配送时间和完整下单流程
- 在GltTicketOrder实体中新增sendTime字段用于记录配送时间
- 移除送水订单查询接口的权限验证要求,开放查询功能
- 实现完整的下单流程:验证登录用户、扣减水票、写入核销记录、创建订单
- 新增createWithWriteOff方法处理事务性下单操作,确保数据一致性
- 添加数据库行锁机制防止并发扣减问题
- 优化水票相关接口描述,明确为可用水票总数
- 移除水票日志添加接口的权限验证和操作日志注解
2026-02-06 00:15:20 +08:00
88afd149c3 feat(glt): 添加送水订单模块并优化经销商结算功能
- 新增送水订单实体类 GltTicketOrder 及其相关控制器、服务、映射器
- 添加送水订单参数类 GltTicketOrderParam 和 XML 映射配置
- 实现送水订单的增删改查、分页查询等完整 CRUD 功能
- 在经销商结算任务中引入分销设置功能,支持按级别控制分佣
- 更新总经销商分润计算逻辑,使用动态费率替代固定值
- 删除不再使用的中文字体修复脚本文件
- 重构经销商推荐佣金结算逻辑,支持最多三级分佣
- 优化订单状态检查逻辑,在退款流程中排除已完成订单
2026-02-05 18:51:53 +08:00
9672be2252 feat(settlement): 添加总经销商分润功能
- 引入 TOTAL_DEALER_DIVIDEND_RATE 常量用于总经销商分润计算
- 添加 findTotalDealerUserId 方法查找总经销商用户ID
- 新增 settleTotalDealerCommission 方法实现总经销商分润逻辑
- 修改 settleOneOrder 方法传入总经销商用户ID参数
- 更新 createDealerOrderRecord 方法支持总经销商分润记录
- 扩展 buildCommissionTraceComment 方法包含总经销商分润信息
- 添加 TotalDealerCommission 内部类封装总经销商分润数据
- 实现总经销商分润的幂等处理和日志记录功能
2026-02-05 15:47:40 +08:00
1107b9144f feat(settlement): 添加总经销商分润功能
- 引入 TOTAL_DEALER_DIVIDEND_RATE 常量用于总经销商分润计算
- 添加 findTotalDealerUserId 方法查找总经销商用户ID
- 新增 settleTotalDealerCommission 方法实现总经销商分润逻辑
- 修改 settleOneOrder 方法传入总经销商用户ID参数
- 更新 createDealerOrderRecord 方法支持总经销商分润记录
- 扩展 buildCommissionTraceComment 方法包含总经销商分润信息
- 添加 TotalDealerCommission 内部类封装总经销商分润数据
- 实现总经销商分润的幂等处理和日志记录功能
2026-02-05 15:43:02 +08:00
acc543b50a refactor(task): 将经销商订单结算任务从shop模块迁移到glt模块
- 修改包路径从com.gxwebsoft.shop.task到com.gxwebsoft.glt.task
- 调整模块间的依赖关系
- 更新相关的导入引用
2026-02-05 15:26:34 +08:00
b9c70bb4a3 perf(shop): 优化经销商设置列表排序逻辑
- 将默认排序字段从 create_time 改为 update_time
- 移除 PageParam 的排序方法,改用 Java Stream 的 sort 进行排序
- 添加空值检查,避免对空列表进行排序操作
- 使用 Comparator.nullsLast 处理空值情况
- 提升列表排序性能,减少不必要的对象创建
2026-02-05 15:15:51 +08:00
ee9ea88ce9 fix(entity): 修复ShopDealerSetting实体映射问题
- 添加TableField注解以正确映射数据库字段
- 将关键字字段名用反引号包围避免SQL语法冲突
- 更新XML映射文件中的字段引用为带反引号的形式
- 确保数据库查询与实体字段映射一致
2026-02-05 15:08:55 +08:00
093826435e feat(shop): 修改经销商设置实体ID生成策略并优化保存更新逻辑
- 将ShopDealerSetting实体的@TableId注解type从AUTO改为INPUT
- 新增saveOrUpdateByKey方法统一处理保存和更新操作
- 移除LambdaQueryWrapper手动构建的更新逻辑
- 简化控制器中的保存和更新接口实现
- 优化多租户场景下的数据操作逻辑
2026-02-05 15:06:50 +08:00
85a8d17194 feat(shop): 更新分销商设置表的保存和修改功能
- 添加 ShopDealerSettingSaveParam 参数类用于保存和修改操作
- 修改 save 方法使用新的参数类并实现实体构建逻辑
- 更新 update 方法使用 LambdaQueryWrapper 进行精确更新
- 添加 buildEntity 方法用于将参数转换为实体对象
- 实现 normalizeUpdateTime 方法处理时间戳溢出问题
- 添加租户ID默认值获取逻辑
- 增强更新操作的数据验证和错误处理机制
2026-02-05 14:33:17 +08:00
bbd41da1d3 fix(shop): 修复商城信息缓存解析逻辑
- 优化缓存数据解析流程,添加空值检查
- 当缓存解析失败时清理无效缓存键
- 改进异常处理机制,避免返回空数据
- 移除调试代码并完善日志记录
2026-02-05 10:46:28 +08:00
e4e10d46cc fix(mapper): 修复用户票券关联查询中的数据放大问题
- 修改 shop_order 表关联条件,从 order_no 改为 order_id + tenant_id 组合
- 添加 tenant_id 筛选避免跨租户数据污染
- 添加 deleted 字段过滤确保只关联未删除订单
- 将 pay_price 字段别名规范化为 camelCase 格式
2026-02-05 10:15:03 +08:00
195e90df5e fix(settlement): 修复经销商订单结算任务中的分润收入计算问题
- 注释掉三级经销商佣金计算逻辑以解决结算异常
- 保留直接推荐奖和二级分润收入的正常计算流程
- 防止因三级佣金计算导致的订单结算失败问题
2026-02-04 17:49:00 +08:00
c5da6f371b feat(order): 分离订单退款功能到独立接口并优化水票统计
- 将订单退款逻辑从update方法中分离到独立的refund接口
- 添加退款相关操作权限控制和参数验证
- 实现申请退款和同意退款两种状态的分别处理
- 新增水票总数统计功能,包括service、mapper和controller层实现
- 修改佣金注释文本从"第3级佣金"为"分润收入"
- 优化订单更新逻辑,禁止通过普通更新接口进行退款操作
2026-02-04 17:38:00 +08:00
51d3a029cc feat(shop): 添加订单支付功能支持
- 新增 OrderPrepayRequest DTO 用于处理支付请求参数
- 实现 prepay 接口支持 /pay、/prepay、/repay 多路径兼容
- 添加用户登录验证和租户权限校验机制
- 集成微信支付创建订单功能并返回支付信息
- 实现订单状态验证包括已支付、已删除、已过期等状态检查
- 支持通过订单ID或订单号查询并处理支付请求
- 添加支付类型参数处理和默认值设置逻辑
2026-02-04 15:57:34 +08:00
30c7e72a80 fix(order): 修复订单处理中的空指针异常和状态比较问题
- 添加 Objects 工具类导入用于安全的对象比较
- 修复 shopOrderNow 为空时的空指针异常
- 使用 Objects.equals 替换直接的 equals 比较避免 NPE
- 为发货状态变更逻辑添加清晰的注释说明
- 修复支付状态检查中的布尔值比较逻辑
2026-02-04 15:47:23 +08:00
34 changed files with 1921 additions and 350 deletions

View File

@@ -1,125 +0,0 @@
#!/bin/bash
###############################################################################
# 捐款证书中文乱码修复脚本
# 用途在运行中的Docker容器内安装中文字体
# 适用于:无法重新构建镜像的紧急情况
###############################################################################
set -e # 遇到错误立即退出
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 容器名称(可根据实际情况修改)
CONTAINER_NAME="websoft-api-container"
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}中文字体修复脚本${NC}"
echo -e "${GREEN}================================${NC}"
echo ""
# 检查容器是否存在
echo -e "${YELLOW}步骤 1/6: 检查容器状态...${NC}"
if ! docker ps | grep -q "$CONTAINER_NAME"; then
echo -e "${RED}错误:容器 $CONTAINER_NAME 未运行!${NC}"
echo "当前运行的容器:"
docker ps --format "table {{.Names}}\t{{.Status}}"
echo ""
read -p "请输入正确的容器名称: " CONTAINER_NAME
if [ -z "$CONTAINER_NAME" ]; then
echo -e "${RED}容器名称不能为空,退出。${NC}"
exit 1
fi
fi
echo -e "${GREEN}✓ 容器正在运行${NC}"
echo ""
# 检查是否已安装字体
echo -e "${YELLOW}步骤 2/6: 检查是否已安装中文字体...${NC}"
if docker exec "$CONTAINER_NAME" fc-list :lang=zh 2>/dev/null | grep -q "WenQuanYi"; then
echo -e "${GREEN}✓ 中文字体已安装${NC}"
docker exec "$CONTAINER_NAME" fc-list :lang=zh
echo ""
read -p "是否重新安装?(y/N): " REINSTALL
if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then
echo "跳过安装,退出。"
exit 0
fi
else
echo -e "${YELLOW}未检测到中文字体,开始安装...${NC}"
fi
echo ""
# 安装字体工具
echo -e "${YELLOW}步骤 3/6: 安装字体工具...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "apk add --no-cache fontconfig ttf-dejavu wget" || {
echo -e "${RED}✗ 字体工具安装失败${NC}"
exit 1
}
echo -e "${GREEN}✓ 字体工具安装成功${NC}"
echo ""
# 下载中文字体
echo -e "${YELLOW}步骤 4/6: 下载文泉驿微米黑字体...${NC}"
echo "正在从GitHub下载约10MB请稍候..."
docker exec -u root "$CONTAINER_NAME" sh -c "
wget -O /tmp/wqy-microhei.ttc https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc 2>&1 | grep -E 'Connecting|Length|saved' || true
" || {
echo -e "${YELLOW}GitHub下载失败尝试使用代理...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "
wget -O /tmp/wqy-microhei.ttc https://ghproxy.com/https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc
" || {
echo -e "${RED}✗ 字体下载失败${NC}"
echo "请检查网络连接或手动下载字体文件。"
exit 1
}
}
echo -e "${GREEN}✓ 字体下载成功${NC}"
echo ""
# 安装字体
echo -e "${YELLOW}步骤 5/6: 安装字体文件...${NC}"
docker exec -u root "$CONTAINER_NAME" sh -c "
mkdir -p /usr/share/fonts/truetype/wqy && \
mv /tmp/wqy-microhei.ttc /usr/share/fonts/truetype/wqy/ && \
fc-cache -fv
" || {
echo -e "${RED}✗ 字体安装失败${NC}"
exit 1
}
echo -e "${GREEN}✓ 字体安装成功${NC}"
echo ""
# 验证安装
echo -e "${YELLOW}步骤 6/6: 验证字体安装...${NC}"
FONT_COUNT=$(docker exec "$CONTAINER_NAME" fc-list :lang=zh | wc -l)
if [ "$FONT_COUNT" -gt 0 ]; then
echo -e "${GREEN}✓ 中文字体验证成功!${NC}"
echo "已安装的中文字体:"
docker exec "$CONTAINER_NAME" fc-list :lang=zh
else
echo -e "${RED}✗ 字体验证失败${NC}"
exit 1
fi
echo ""
# 完成提示
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}修复完成!${NC}"
echo -e "${GREEN}================================${NC}"
echo ""
echo "后续步骤:"
echo "1. 不需要重启容器,字体已生效"
echo "2. 重新生成捐款证书即可看到效果"
echo "3. 如果仍有问题,请查看容器日志:"
echo " docker logs -f $CONTAINER_NAME"
echo ""
echo -e "${YELLOW}注意:${NC}"
echo "- 此修复方法在容器重启后会失效"
echo "- 建议后续使用更新后的Dockerfile重新构建镜像"
echo "- 详细文档请参考docs/chinese-font-fix-guide.md"
echo ""

View File

@@ -337,9 +337,16 @@ public class CmsWebsiteServiceImpl extends ServiceImpl<CmsWebsiteMapper, CmsWebs
if (StrUtil.isNotBlank(siteInfo)) { if (StrUtil.isNotBlank(siteInfo)) {
log.info("从缓存获取网站信息租户ID: {}", tenantId); log.info("从缓存获取网站信息租户ID: {}", tenantId);
try { try {
return JSONUtil.parseObject(siteInfo, ShopVo.class); // 兼容历史缓存JSON "null" 会被解析为 null此时应视为未命中并回源数据库。
ShopVo cacheVo = JSONUtil.parseObject(siteInfo, ShopVo.class);
if (cacheVo != null) {
return cacheVo;
}
log.warn("网站信息缓存命中但内容为空(null)清理缓存后回源数据库租户ID: {}", tenantId);
redisUtil.delete(cacheKey);
} catch (Exception e) { } catch (Exception e) {
log.warn("缓存解析失败,从数据库重新获取: {}", e.getMessage()); log.warn("缓存解析失败,清理缓存后从数据库重新获取: {}", e.getMessage());
redisUtil.delete(cacheKey);
} }
} }

View File

@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package cn.afterturn.easypoi.excel.entity; package com.gxwebsoft.common.core.web;
import cn.afterturn.easypoi.excel.entity.ExcelBaseParams;
import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler; import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler;
import lombok.Data; import lombok.Data;

View File

@@ -0,0 +1,18 @@
package com.gxwebsoft.common.system.service;
/**
* 微信小程序 access_token 获取服务(按租户)。
*
* <p>用于调用微信小程序开放接口(例如:上传发货信息)。</p>
*/
public interface WxMiniappAccessTokenService {
/**
* 获取指定租户的小程序 access_token内部带缓存
*
* @param tenantId 租户ID
* @return access_token
*/
String getAccessToken(Integer tenantId);
}

View File

@@ -0,0 +1,108 @@
package com.gxwebsoft.common.system.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.RedisUtil;
import com.gxwebsoft.common.system.service.WxMiniappAccessTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KEY;
import static com.gxwebsoft.common.core.constants.RedisConstants.MP_WX_KEY;
/**
* 微信小程序 access_token 获取实现(按租户)。
*
* <p>复用现有缓存结构:
* <ul>
* <li>小程序配置Redis key = {@code mp-weixin:{tenantId}}value 为 JSON包含 appId/appSecret</li>
* <li>access_tokenRedis key = {@code access-token:{tenantId}}value 为微信返回的 JSON 字符串</li>
* </ul>
* </p>
*/
@Slf4j
@Service
public class WxMiniappAccessTokenServiceImpl implements WxMiniappAccessTokenService {
@Resource
private RedisUtil redisUtil;
@Override
public String getAccessToken(Integer tenantId) {
if (tenantId == null) {
throw new BusinessException("tenantId 不能为空");
}
final String tokenCacheKey = ACCESS_TOKEN_KEY + ":" + tenantId;
// 1) 优先从缓存取(兼容 JSON 或纯字符串 token 的历史格式)
String cachedValue = redisUtil.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 = redisUtil.get(wxConfigKey);
if (StrUtil.isBlank(wxConfigValue)) {
throw new BusinessException("未找到微信小程序配置请检查缓存key: " + wxConfigKey);
}
JSONObject wxConfig;
try {
wxConfig = JSON.parseObject(wxConfigValue);
} catch (Exception e) {
throw new BusinessException("微信小程序配置格式错误: " + e.getMessage());
}
final String appId = wxConfig.getString("appId");
final String appSecret = wxConfig.getString("appSecret");
if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
throw new BusinessException("微信小程序配置不完整(appId/appSecret)");
}
// 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) {
Integer errcode = json.getInteger("errcode");
String errmsg = json.getString("errmsg");
throw new BusinessException("获取小程序access_token失败: " + errmsg + " (errcode: " + errcode + ")");
}
String accessToken = json.getString("access_token");
Integer expiresIn = json.getInteger("expires_in");
if (StrUtil.isBlank(accessToken)) {
throw new BusinessException("获取小程序access_token失败: access_token为空");
}
// 4) 缓存微信原始 JSON与现有实现保持一致提前5分钟过期
long ttlSeconds = 7000L;
if (expiresIn != null && expiresIn > 300) {
ttlSeconds = expiresIn - 300L;
}
redisUtil.set(tokenCacheKey, result, ttlSeconds, TimeUnit.SECONDS);
log.info("获取小程序access_token成功 - tenantId={}, ttlSeconds={}", tenantId, ttlSeconds);
return accessToken;
}
}

View File

@@ -322,16 +322,41 @@ public class CreditJudgmentDebtorController extends BaseController {
return headerText.equals(value.trim()); return headerText.equals(value.trim());
} }
private static boolean hasMeaningfulPartyValue(String value) {
if (ImportHelper.isBlank(value)) {
return false;
}
return !"-".equals(value.trim());
}
private CreditJudgmentDebtor convertImportParamToEntity(CreditJudgmentDebtorImportParam param) { private CreditJudgmentDebtor convertImportParamToEntity(CreditJudgmentDebtorImportParam param) {
CreditJudgmentDebtor entity = new CreditJudgmentDebtor(); CreditJudgmentDebtor entity = new CreditJudgmentDebtor();
entity.setCaseNumber(param.getCaseNumber()); entity.setCaseNumber(param.getCaseNumber());
entity.setName1(param.getName1()); entity.setName1(param.getName1());
String debtorName = ImportHelper.isBlank(param.getName()) ? param.getName1() : param.getName(); String debtorName = ImportHelper.isBlank(param.getName()) ? param.getName1() : param.getName();
if (debtorName != null) {
debtorName = debtorName.trim();
}
// Some upstream XLS templates store party/company name in "原告/上诉人/被告/第三人" columns.
// When present, use them to populate the debtor "name" for compatibility.
if (hasMeaningfulPartyValue(param.getPlaintiffAppellant())) {
debtorName = param.getPlaintiffAppellant().trim();
} else if (hasMeaningfulPartyValue(param.getAppellee())) {
debtorName = param.getAppellee().trim();
} else if (hasMeaningfulPartyValue(param.getOtherPartiesThirdParty())) {
debtorName = param.getOtherPartiesThirdParty().trim();
}
entity.setName(debtorName); entity.setName(debtorName);
entity.setCode(param.getCode()); entity.setCode(param.getCode());
entity.setOccurrenceTime(param.getOccurrenceTime()); String occurrenceTime = !ImportHelper.isBlank(param.getOccurrenceTime2())
entity.setAmount(param.getAmount()); ? param.getOccurrenceTime2().trim()
: (param.getOccurrenceTime() != null ? param.getOccurrenceTime().trim() : null);
entity.setOccurrenceTime(occurrenceTime);
String amount = !ImportHelper.isBlank(param.getInvolvedAmount2())
? param.getInvolvedAmount2().trim()
: (param.getAmount() != null ? param.getAmount().trim() : null);
entity.setAmount(amount);
entity.setCourtName(param.getCourtName()); entity.setCourtName(param.getCourtName());
entity.setDataStatus(param.getDataStatus()); entity.setDataStatus(param.getDataStatus());
entity.setComments(param.getComments()); entity.setComments(param.getComments());

View File

@@ -1,6 +1,7 @@
package com.gxwebsoft.credit.param; package com.gxwebsoft.credit.param;
import cn.afterturn.easypoi.excel.annotation.Excel; import cn.afterturn.easypoi.excel.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.io.Serializable; import java.io.Serializable;
@@ -38,4 +39,21 @@ public class CreditJudgmentDebtorImportParam implements Serializable {
@Excel(name = "备注") @Excel(name = "备注")
private String comments; private String comments;
@Excel(name = "原告/上诉人")
private String plaintiffAppellant;
@Excel(name = "被告/被上诉人")
private String appellee;
@Excel(name = "其他当事人/第三人")
private String otherPartiesThirdParty;
@Schema(description = "发生时间")
@Excel(name = "发生时间")
private String occurrenceTime2;
@Schema(description = "涉案金额")
@Excel(name = "涉案金额")
private String involvedAmount2;
} }

View File

@@ -0,0 +1,169 @@
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.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltTicketOrderService;
import com.gxwebsoft.shop.entity.ShopUserAddress;
import com.gxwebsoft.shop.service.ShopUserAddressService;
import cn.hutool.core.util.StrUtil;
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.util.List;
/**
* 送水订单控制器
*
* @author 科技小王子
* @since 2026-02-05 18:50:21
*/
@Tag(name = "送水订单管理")
@RestController
@RequestMapping("/api/glt/glt-ticket-order")
public class GltTicketOrderController extends BaseController {
@Resource
private GltTicketOrderService gltTicketOrderService;
@Resource
private ShopUserAddressService shopUserAddressService;
@Operation(summary = "分页查询送水订单")
@GetMapping("/page")
public ApiResult<PageResult<GltTicketOrder>> page(GltTicketOrderParam param) {
// 使用关联查询
return success(gltTicketOrderService.pageRel(param));
}
@Operation(summary = "查询全部送水订单")
@GetMapping()
public ApiResult<List<GltTicketOrder>> list(GltTicketOrderParam param) {
// 使用关联查询
return success(gltTicketOrderService.listRel(param));
}
@Operation(summary = "根据id查询送水订单")
@GetMapping("/{id}")
public ApiResult<GltTicketOrder> get(@PathVariable("id") Integer id) {
// 使用关联查询
return success(gltTicketOrderService.getByIdRel(id));
}
@Operation(summary = "添加送水订单")
@PostMapping()
public ApiResult<?> save(@RequestBody GltTicketOrder gltTicketOrder) {
// 下单:后端原子完成(扣水票 + 写核销记录 + 生成订单)
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
if (gltTicketOrder == null) {
return fail("订单参数不能为空");
}
// 地址快照:把用户关联的“详细地址”落到 glt_ticket_order.address避免用户后续修改地址导致历史订单丢失
ShopUserAddress userAddress = null;
if (gltTicketOrder.getAddressId() != null) {
userAddress = shopUserAddressService.getByIdRel(gltTicketOrder.getAddressId());
} else {
userAddress = shopUserAddressService.getDefaultAddress(loginUser.getUserId());
}
if (userAddress == null) {
return fail("请先添加收货地址");
}
if (!loginUser.getUserId().equals(userAddress.getUserId())) {
return fail("收货地址不存在或无权限");
}
if (loginUser.getTenantId() != null && userAddress.getTenantId() != null
&& !loginUser.getTenantId().equals(userAddress.getTenantId())) {
return fail("收货地址不存在或无权限");
}
gltTicketOrder.setAddressId(userAddress.getId());
gltTicketOrder.setAddress(buildAddressSnapshot(userAddress));
gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId());
return success("下单成功");
}
private String buildAddressSnapshot(ShopUserAddress addr) {
if (addr == null) {
return null;
}
if (StrUtil.isNotBlank(addr.getFullAddress())) {
return addr.getFullAddress();
}
// 兼容旧数据fullAddress 为空时,拼接省市区 + 详细地址
return StrUtil.blankToDefault(
StrUtil.join("",
StrUtil.nullToEmpty(addr.getProvince()),
StrUtil.nullToEmpty(addr.getCity()),
StrUtil.nullToEmpty(addr.getRegion()),
StrUtil.nullToEmpty(addr.getAddress())
),
addr.getAddress()
);
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
@OperationLog
@Operation(summary = "修改送水订单")
@PutMapping()
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
if (gltTicketOrderService.updateById(gltTicketOrder)) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:remove')")
@OperationLog
@Operation(summary = "删除送水订单")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (gltTicketOrderService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:save')")
@OperationLog
@Operation(summary = "批量添加送水订单")
@PostMapping("/batch")
public ApiResult<?> saveBatch(@RequestBody List<GltTicketOrder> list) {
if (gltTicketOrderService.saveBatch(list)) {
return success("添加成功");
}
return fail("添加失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
@OperationLog
@Operation(summary = "批量修改送水订单")
@PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<GltTicketOrder> batchParam) {
if (batchParam.update(gltTicketOrderService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@PreAuthorize("hasAuthority('glt:gltTicketOrder:remove')")
@OperationLog
@Operation(summary = "批量删除送水订单")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (gltTicketOrderService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
}

View File

@@ -1,13 +1,11 @@
package com.gxwebsoft.glt.controller; package com.gxwebsoft.glt.controller;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.glt.service.GltUserTicketService; import com.gxwebsoft.glt.service.GltUserTicketService;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.param.GltUserTicketParam; import com.gxwebsoft.glt.param.GltUserTicketParam;
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 io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -16,7 +14,9 @@ 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 我的水票控制器 * 我的水票控制器
@@ -38,6 +38,26 @@ public class GltUserTicketController extends BaseController {
return success(gltUserTicketService.pageRel(param)); return success(gltUserTicketService.pageRel(param));
} }
@Operation(summary = "可用水票总数")
@GetMapping("/my-total")
public ApiResult<?> myTotal() {
Integer userId = getLoginUserId();
if (userId == null) {
return fail("未登录");
}
Integer tenantId = getTenantId();
if (tenantId == null) {
return fail("租户信息缺失");
}
Integer availableQty = gltUserTicketService.sumAvailableQtyByUserId(userId, tenantId);
Map<String, Object> data = new HashMap<>();
data.put("userId", userId);
// 兼容旧字段totalQty 表示“可用水票总数”
data.put("totalQty", availableQty);
data.put("availableQty", availableQty);
return success(data);
}
@PreAuthorize("hasAuthority('glt:gltUserTicket:list')") @PreAuthorize("hasAuthority('glt:gltUserTicket:list')")
@Operation(summary = "查询全部我的水票") @Operation(summary = "查询全部我的水票")
@GetMapping() @GetMapping()

View File

@@ -55,8 +55,6 @@ public class GltUserTicketLogController extends BaseController {
return success(gltUserTicketLogService.getByIdRel(id)); return success(gltUserTicketLogService.getByIdRel(id));
} }
@PreAuthorize("hasAuthority('glt:gltUserTicketLog:save')")
@OperationLog
@Operation(summary = "添加消费日志") @Operation(summary = "添加消费日志")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody GltUserTicketLog gltUserTicketLog) { public ApiResult<?> save(@RequestBody GltUserTicketLog gltUserTicketLog) {

View File

@@ -0,0 +1,113 @@
package com.gxwebsoft.glt.entity;
import com.baomidou.mybatisplus.annotation.IdType;
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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 送水订单
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(name = "GltTicketOrder对象", description = "送水订单")
public class GltTicketOrder implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@Schema(description = "用户水票ID")
private Integer userTicketId;
@Schema(description = "门店ID")
private Integer storeId;
@Schema(description = "配送员")
private Integer riderId;
@Schema(description = "仓库ID")
private Integer warehouseId;
@Schema(description = "关联收货地址")
private Integer addressId;
@Schema(description = "收货地址")
private String address;
@Schema(description = "")
@TableField(exist = false)
private String province;
@Schema(description = "")
@TableField(exist = false)
private String city;
@Schema(description = "")
@TableField(exist = false)
private String region;
@Schema(description = "买家留言")
private String buyerRemarks;
@Schema(description = "用于统计")
private BigDecimal price;
@Schema(description = "购买数量")
private Integer totalNum;
@Schema(description = "配送时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private String sendTime;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "昵称")
@TableField(exist = false)
private String nickname;
@Schema(description = "手机号")
@TableField(exist = false)
private String phone;
@Schema(description = "头像")
@TableField(exist = false)
private String avatar;
@Schema(description = "排序(数字越小越靠前)")
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "状态, 0正常, 1冻结")
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@Schema(description = "租户id")
private Integer tenantId;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "修改时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,37 @@
package com.gxwebsoft.glt.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 送水订单Mapper
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
public interface GltTicketOrderMapper extends BaseMapper<GltTicketOrder> {
/**
* 分页查询
*
* @param page 分页对象
* @param param 查询参数
* @return List<GltTicketOrder>
*/
List<GltTicketOrder> selectPageRel(@Param("page") IPage<GltTicketOrder> page,
@Param("param") GltTicketOrderParam param);
/**
* 查询全部
*
* @param param 查询参数
* @return List<User>
*/
List<GltTicketOrder> selectListRel(@Param("param") GltTicketOrderParam param);
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.glt.entity.GltUserTicket; import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.param.GltUserTicketParam; import com.gxwebsoft.glt.param.GltUserTicketParam;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
@@ -34,4 +35,32 @@ public interface GltUserTicketMapper extends BaseMapper<GltUserTicket> {
*/ */
List<GltUserTicket> selectListRel(@Param("param") GltUserTicketParam param); List<GltUserTicket> selectListRel(@Param("param") GltUserTicketParam param);
/**
* 统计用户可用水票总数sum(available_qty)
*
* @param userId 用户ID
* @param tenantId 租户ID
* @return 可用总数
*/
Integer sumAvailableQtyByUserId(@Param("userId") Integer userId,
@Param("tenantId") Integer tenantId);
/**
* 按当前用户锁定水票记录(用于扣减/核销的事务场景)
*/
@Select("""
SELECT *
FROM glt_user_ticket
WHERE id = #{id}
AND user_id = #{userId}
AND tenant_id = #{tenantId}
AND status = 0
AND deleted = 0
LIMIT 1
FOR UPDATE
""")
GltUserTicket selectByIdForUpdate(@Param("id") Integer id,
@Param("userId") Integer userId,
@Param("tenantId") Integer tenantId);
} }

View File

@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gxwebsoft.glt.mapper.GltTicketOrderMapper">
<!-- 关联查询sql -->
<sql id="selectSql">
SELECT a.*, u.nickname, u.phone, u.avatar
FROM glt_ticket_order a
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.userTicketId != null">
AND a.user_ticket_id = #{param.userTicketId}
</if>
<if test="param.storeId != null">
AND a.store_id = #{param.storeId}
</if>
<if test="param.riderId != null">
AND a.rider_id = #{param.riderId}
</if>
<if test="param.warehouseId != null">
AND a.warehouse_id = #{param.warehouseId}
</if>
<if test="param.addressId != null">
AND a.address_id = #{param.addressId}
</if>
<if test="param.address != null">
AND a.address LIKE CONCAT('%', #{param.address}, '%')
</if>
<if test="param.buyerRemarks != null">
AND a.buyer_remarks LIKE CONCAT('%', #{param.buyerRemarks}, '%')
</if>
<if test="param.price != null">
AND a.price = #{param.price}
</if>
<if test="param.totalNum != null">
AND a.total_num = #{param.totalNum}
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.sortNumber != null">
AND a.sort_number = #{param.sortNumber}
</if>
<if test="param.comments != null">
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<!-- 分页查询 -->
<select id="selectPageRel" resultType="com.gxwebsoft.glt.entity.GltTicketOrder">
<include refid="selectSql"></include>
</select>
<!-- 查询全部 -->
<select id="selectListRel" resultType="com.gxwebsoft.glt.entity.GltTicketOrder">
<include refid="selectSql"></include>
</select>
</mapper>

View File

@@ -4,11 +4,12 @@
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price AS payPrice
FROM glt_user_ticket a FROM glt_user_ticket a
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
LEFT JOIN glt_ticket_template m ON a.template_id = m.id LEFT JOIN glt_ticket_template m ON a.template_id = m.id
LEFT JOIN shop_order o ON a.order_no = o.order_no <!-- 使用 order_id + tenant_id 关联,避免 order_no 跨租户/重复导致 a.id 数据被 JOIN 放大 -->
LEFT JOIN shop_order o ON a.order_id = o.order_id AND a.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}
@@ -86,4 +87,14 @@
<include refid="selectSql"></include> <include refid="selectSql"></include>
</select> </select>
<!-- 我的可用水票总数sum(available_qty) -->
<select id="sumAvailableQtyByUserId" resultType="java.lang.Integer">
SELECT IFNULL(SUM(available_qty), 0)
FROM glt_user_ticket
WHERE user_id = #{userId}
AND tenant_id = #{tenantId}
AND status = 0
AND deleted = 0
</select>
</mapper> </mapper>

View File

@@ -0,0 +1,82 @@
package com.gxwebsoft.glt.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 送水订单查询参数
*
* @author 科技小王子
* @since 2026-02-05 18:50:19
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "GltTicketOrderParam对象", description = "送水订单查询参数")
public class GltTicketOrderParam extends BaseParam {
private static final long serialVersionUID = 1L;
@QueryField(type = QueryType.EQ)
private Integer id;
@Schema(description = "用户水票ID")
@QueryField(type = QueryType.EQ)
private Integer userTicketId;
@Schema(description = "门店ID")
@QueryField(type = QueryType.EQ)
private Integer storeId;
@Schema(description = "配送员")
@QueryField(type = QueryType.EQ)
private Integer riderId;
@Schema(description = "仓库ID")
@QueryField(type = QueryType.EQ)
private Integer warehouseId;
@Schema(description = "关联收货地址")
@QueryField(type = QueryType.EQ)
private Integer addressId;
@Schema(description = "收货地址")
private String address;
@Schema(description = "买家留言")
private String buyerRemarks;
@Schema(description = "用于统计")
@QueryField(type = QueryType.EQ)
private BigDecimal price;
@Schema(description = "购买数量")
@QueryField(type = QueryType.EQ)
private Integer totalNum;
@Schema(description = "用户ID")
@QueryField(type = QueryType.EQ)
private Integer userId;
@Schema(description = "排序(数字越小越靠前)")
@QueryField(type = QueryType.EQ)
private Integer sortNumber;
@Schema(description = "备注")
private String comments;
@Schema(description = "状态, 0正常, 1冻结")
@QueryField(type = QueryType.EQ)
private Integer status;
@Schema(description = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
}

View File

@@ -0,0 +1,52 @@
package com.gxwebsoft.glt.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import java.util.List;
/**
* 送水订单Service
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
public interface GltTicketOrderService extends IService<GltTicketOrder> {
/**
* 分页关联查询
*
* @param param 查询参数
* @return PageResult<GltTicketOrder>
*/
PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param);
/**
* 关联查询全部
*
* @param param 查询参数
* @return List<GltTicketOrder>
*/
List<GltTicketOrder> listRel(GltTicketOrderParam param);
/**
* 根据id查询
*
* @param id
* @return GltTicketOrder
*/
GltTicketOrder getByIdRel(Integer id);
/**
* 下单(事务):校验水票 -> 扣减水票 -> 写核销记录 -> 创建送水订单。
*
* @param gltTicketOrder 订单请求体
* @param userId 当前登录用户ID
* @param tenantId 当前租户ID
* @return 创建后的订单含id
*/
GltTicketOrder createWithWriteOff(GltTicketOrder gltTicketOrder, Integer userId, Integer tenantId);
}

View File

@@ -39,4 +39,13 @@ public interface GltUserTicketService extends IService<GltUserTicket> {
*/ */
GltUserTicket getByIdRel(Integer id); GltUserTicket getByIdRel(Integer id);
/**
* 统计指定用户可用水票总数sum(available_qty)
*
* @param userId 用户ID
* @param tenantId 租户ID
* @return 可用总数无记录返回0
*/
Integer sumAvailableQtyByUserId(Integer userId, Integer tenantId);
} }

View File

@@ -0,0 +1,162 @@
package com.gxwebsoft.glt.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.exception.BusinessException;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.glt.entity.GltTicketOrder;
import com.gxwebsoft.glt.entity.GltUserTicket;
import com.gxwebsoft.glt.entity.GltUserTicketLog;
import com.gxwebsoft.glt.mapper.GltTicketOrderMapper;
import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
import com.gxwebsoft.glt.param.GltTicketOrderParam;
import com.gxwebsoft.glt.service.GltTicketOrderService;
import com.gxwebsoft.glt.service.GltUserTicketLogService;
import com.gxwebsoft.glt.service.GltUserTicketService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* 送水订单Service实现
*
* @author 科技小王子
* @since 2026-02-05 18:50:20
*/
@Service
public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper, GltTicketOrder> implements GltTicketOrderService {
public static final int CHANGE_TYPE_WRITE_OFF = 20;
@Resource
private GltUserTicketMapper gltUserTicketMapper;
@Resource
private GltUserTicketService gltUserTicketService;
@Resource
private GltUserTicketLogService gltUserTicketLogService;
@Override
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<GltTicketOrder> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal());
}
@Override
public List<GltTicketOrder> listRel(GltTicketOrderParam param) {
List<GltTicketOrder> list = baseMapper.selectListRel(param);
// 排序
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public GltTicketOrder getByIdRel(Integer id) {
GltTicketOrderParam param = new GltTicketOrderParam();
param.setId(id);
return param.getOne(baseMapper.selectListRel(param));
}
@Override
@Transactional(rollbackFor = Exception.class)
public GltTicketOrder createWithWriteOff(GltTicketOrder gltTicketOrder, Integer userId, Integer tenantId) {
if (gltTicketOrder == null) {
throw new BusinessException("订单参数不能为空");
}
if (userId == null) {
throw new BusinessException("请先登录");
}
Integer userTicketId = gltTicketOrder.getUserTicketId();
if (userTicketId == null) {
throw new BusinessException("userTicketId不能为空");
}
int totalNum = gltTicketOrder.getTotalNum() == null ? 0 : gltTicketOrder.getTotalNum();
if (totalNum <= 0) {
throw new BusinessException("totalNum必须大于0");
}
if (tenantId == null) {
throw new BusinessException("租户信息缺失");
}
// 1) 校验水票归属当前用户 + 正常状态,并锁定记录,避免并发扣减导致日志不准确
GltUserTicket userTicket = gltUserTicketMapper.selectByIdForUpdate(userTicketId, userId, tenantId);
if (userTicket == null) {
throw new BusinessException("水票不存在、已失效或无权限");
}
int availableQty = userTicket.getAvailableQty() == null ? 0 : userTicket.getAvailableQty();
int usedQty = userTicket.getUsedQty() == null ? 0 : userTicket.getUsedQty();
if (availableQty < totalNum) {
throw new BusinessException("水票数量不足");
}
// 2) 更新 glt_user_ticket: availableQty -= totalNum, usedQty += totalNum
LocalDateTime now = LocalDateTime.now();
int availableAfter = availableQty - totalNum;
int usedAfter = usedQty + totalNum;
userTicket.setAvailableQty(availableAfter);
userTicket.setUsedQty(usedAfter);
userTicket.setUpdateTime(now);
if (!gltUserTicketService.updateById(userTicket)) {
throw new BusinessException("扣减水票失败");
}
// 4) 插入 glt_ticket_orderstoreId/addressId/totalNum/buyerRemarks…
gltTicketOrder.setUserId(userId);
// 订单基础字段由后端兜底,避免前端误传/恶意传参
gltTicketOrder.setStatus(0);
gltTicketOrder.setDeleted(0);
gltTicketOrder.setTenantId(tenantId);
if (gltTicketOrder.getSortNumber() == null) {
gltTicketOrder.setSortNumber(0);
}
if (gltTicketOrder.getCreateTime() == null) {
gltTicketOrder.setCreateTime(now);
}
gltTicketOrder.setUpdateTime(now);
if (!this.save(gltTicketOrder)) {
throw new BusinessException("创建订单失败");
}
// 3) 插入 glt_user_ticket_log核销记录
GltUserTicketLog log = new GltUserTicketLog();
log.setUserTicketId(userTicketId);
log.setChangeType(CHANGE_TYPE_WRITE_OFF);
log.setChangeAvailable(-totalNum);
log.setChangeFrozen(0);
log.setChangeUsed(totalNum);
log.setAvailableAfter(availableAfter);
log.setFrozenAfter(userTicket.getFrozenQty() == null ? 0 : userTicket.getFrozenQty());
log.setUsedAfter(usedAfter);
log.setOrderId(gltTicketOrder.getId());
log.setOrderNo(gltTicketOrder.getId() == null ? null : String.valueOf(gltTicketOrder.getId()));
log.setUserId(userId);
log.setSortNumber(0);
String comments = gltTicketOrder.getComments();
if (StrUtil.isBlank(comments)) {
comments = gltTicketOrder.getBuyerRemarks();
}
if (StrUtil.isBlank(comments)) {
comments = "水票下单核销";
}
log.setComments(comments);
log.setStatus(0);
log.setDeleted(0);
log.setTenantId(tenantId);
log.setCreateTime(now);
log.setUpdateTime(now);
if (!gltUserTicketLogService.save(log)) {
throw new BusinessException("写入核销记录失败");
}
return gltTicketOrder;
}
}

View File

@@ -44,4 +44,10 @@ public class GltUserTicketServiceImpl extends ServiceImpl<GltUserTicketMapper, G
return param.getOne(baseMapper.selectListRel(param)); return param.getOne(baseMapper.selectListRel(param));
} }
@Override
public Integer sumAvailableQtyByUserId(Integer userId, Integer tenantId) {
Integer availableQty = baseMapper.sumAvailableQtyByUserId(userId, tenantId);
return availableQty == null ? 0 : availableQty;
}
} }

View File

@@ -1,4 +1,4 @@
package com.gxwebsoft.shop.task; package com.gxwebsoft.glt.task;
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;
@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.annotation.IgnoreTenant;
import com.gxwebsoft.shop.entity.ShopDealerCapital; import com.gxwebsoft.shop.entity.ShopDealerCapital;
import com.gxwebsoft.shop.entity.ShopDealerOrder; import com.gxwebsoft.shop.entity.ShopDealerOrder;
import com.gxwebsoft.shop.entity.ShopDealerReferee; import com.gxwebsoft.shop.entity.ShopDealerReferee;
import com.gxwebsoft.shop.entity.ShopDealerSetting;
import com.gxwebsoft.shop.entity.ShopDealerUser; import com.gxwebsoft.shop.entity.ShopDealerUser;
import com.gxwebsoft.shop.entity.ShopGoods; import com.gxwebsoft.shop.entity.ShopGoods;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
@@ -15,11 +16,13 @@ import com.gxwebsoft.common.system.mapper.UserMapper;
import com.gxwebsoft.shop.service.ShopDealerCapitalService; import com.gxwebsoft.shop.service.ShopDealerCapitalService;
import com.gxwebsoft.shop.service.ShopDealerOrderService; import com.gxwebsoft.shop.service.ShopDealerOrderService;
import com.gxwebsoft.shop.service.ShopDealerRefereeService; import com.gxwebsoft.shop.service.ShopDealerRefereeService;
import com.gxwebsoft.shop.service.ShopDealerSettingService;
import com.gxwebsoft.shop.service.ShopDealerUserService; import com.gxwebsoft.shop.service.ShopDealerUserService;
import com.gxwebsoft.shop.service.ShopGoodsService; import com.gxwebsoft.shop.service.ShopGoodsService;
import com.gxwebsoft.shop.service.ShopOrderService; import com.gxwebsoft.shop.service.ShopOrderService;
import com.gxwebsoft.shop.service.ShopOrderGoodsService; import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.util.UpstreamUserFinder; import com.gxwebsoft.shop.util.UpstreamUserFinder;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -48,6 +51,7 @@ public class DealerOrderSettlement10584Task {
private static final BigDecimal RATE_0_03 = new BigDecimal("0.03"); 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 int MAX_ORDERS_PER_RUN = 50; private static final int MAX_ORDERS_PER_RUN = 50;
private static final int MAX_REFEREE_CHAIN_DEPTH = 20; private static final int MAX_REFEREE_CHAIN_DEPTH = 20;
@@ -71,6 +75,9 @@ public class DealerOrderSettlement10584Task {
@Resource @Resource
private ShopDealerOrderService shopDealerOrderService; private ShopDealerOrderService shopDealerOrderService;
@Resource
private ShopDealerSettingService shopDealerSettingService;
@Resource @Resource
private ShopGoodsService shopGoodsService; private ShopGoodsService shopGoodsService;
@@ -95,6 +102,12 @@ 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();
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)={}", log.info("租户{}待结算订单数: {}, orderNos(sample)={}",
TENANT_ID, TENANT_ID,
@@ -108,7 +121,7 @@ public class DealerOrderSettlement10584Task {
if (!claimOrderToSettle(order.getOrderId())) { if (!claimOrderToSettle(order.getOrderId())) {
return; return;
} }
settleOneOrder(order, level1ParentCache, shopRoleCache); settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level);
}); });
} catch (Exception e) { } catch (Exception e) {
log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e); log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e);
@@ -141,7 +154,13 @@ public class DealerOrderSettlement10584Task {
); );
} }
private void settleOneOrder(ShopOrder order, Map<Integer, Integer> level1ParentCache, Map<Integer, Boolean> shopRoleCache) { private void settleOneOrder(
ShopOrder order,
Map<Integer, Integer> level1ParentCache,
Map<Integer, Boolean> shopRoleCache,
ShopDealerUser totalDealerUser,
int dealerLevel
) {
if (order.getUserId() == null || order.getOrderNo() == null) { if (order.getUserId() == null || order.getOrderNo() == null) {
throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId()); throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId());
} }
@@ -178,29 +197,52 @@ public class DealerOrderSettlement10584Task {
commissionConfig.storeSimpleValue); commissionConfig.storeSimpleValue);
// 1) 直推/间推shop_dealer_referee // 1) 直推/间推shop_dealer_referee
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig); DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig, dealerLevel);
// 2) 门店分红上级从下单用户开始逐级向上找命中 ShopDealerUser.type=1 的最近两级直推门店/间推门店 // 2) 门店分红上级从下单用户开始逐级向上找命中 ShopDealerUser.type=1 的最近两级直推门店/间推门店
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache); ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache);
// 3) 写入分销订单记录用于排查/统计详细分佣以 ShopDealerCapital 为准 // 3) 总经销商分润固定比率每个订单都分
createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission); TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUser);
// 4) 写入分销订单记录用于排查/统计详细分佣以 ShopDealerCapital 为准
createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission, totalDealerCommission);
log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), baseAmount); log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), baseAmount);
} }
private DealerRefereeCommission settleDealerRefereeCommission(ShopOrder order, BigDecimal baseAmount, int goodsQty, CommissionConfig commissionConfig) { private DealerRefereeCommission settleDealerRefereeCommission(
ShopOrder order,
BigDecimal baseAmount,
int goodsQty,
CommissionConfig commissionConfig,
int dealerLevel
) {
// 兼容两种数据形态 // 兼容两种数据形态
// 1) 同一 userId 下有 level=1/2 的多级关系直接按 level // 1) 同一 userId 下有 level=1/2/3 的多级关系直接按 level
// 2) 仅维护 level=1查两次回退获取上级 // 2) 仅维护 level=1查两次回退获取上级
Integer directDealerId = getDealerRefereeId(order.getUserId(), 1); //
Integer simpleDealerId = getDealerRefereeId(order.getUserId(), 2); // 严格按分销设置 level决定发放到第几级避免 level=2 时仍触发第3级发放逻辑
if (simpleDealerId == null && directDealerId != null) { int normalizedLevel = normalizeDealerLevel(dealerLevel);
simpleDealerId = getDealerRefereeId(directDealerId, 1);
Integer directDealerId = null;
Integer simpleDealerId = null;
Integer thirdDealerId = null;
if (normalizedLevel >= 1) {
directDealerId = getDealerRefereeId(order.getUserId(), 1);
} }
Integer thirdDealerId = getDealerRefereeId(order.getUserId(), 3); if (normalizedLevel >= 2) {
if (thirdDealerId == null && simpleDealerId != null) { simpleDealerId = getDealerRefereeId(order.getUserId(), 2);
thirdDealerId = getDealerRefereeId(simpleDealerId, 1); if (simpleDealerId == null && directDealerId != null) {
simpleDealerId = getDealerRefereeId(directDealerId, 1);
}
}
if (normalizedLevel >= 3) {
thirdDealerId = getDealerRefereeId(order.getUserId(), 3);
if (thirdDealerId == null && simpleDealerId != null) {
thirdDealerId = getDealerRefereeId(simpleDealerId, 1);
}
} }
BigDecimal directMoney = BigDecimal directMoney =
@@ -215,27 +257,33 @@ public class DealerOrderSettlement10584Task {
order.getOrderNo(), order.getUserId(), directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney); order.getOrderNo(), order.getUserId(), directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney);
// 直推对方=买家推荐奖(5%)对方=直推分销商便于在资金明细中看出来自哪个下级分销商/团队订单 // 直推对方=买家推荐奖(5%)对方=直推分销商便于在资金明细中看出来自哪个下级分销商/团队订单
creditDealerCommission( if (normalizedLevel >= 1) {
directDealerId, creditDealerCommission(
directMoney, directDealerId,
order, directMoney,
order.getUserId(), order,
buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty) order.getUserId(),
); buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty)
creditDealerCommission( );
simpleDealerId, }
simpleMoney, if (normalizedLevel >= 2) {
order, creditDealerCommission(
directDealerId, simpleDealerId,
buildCommissionComment("推荐奖", commissionConfig.commissionType, commissionConfig.dealerSimpleValue, goodsQty) simpleMoney,
); order,
creditDealerCommission( directDealerId,
thirdDealerId, buildCommissionComment("推荐奖", commissionConfig.commissionType, commissionConfig.dealerSimpleValue, goodsQty)
thirdMoney, );
order, }
simpleDealerId, if (normalizedLevel >= 3) {
buildCommissionComment("第3级佣金", commissionConfig.commissionType, commissionConfig.dealerThirdValue, goodsQty) creditDealerCommission(
); thirdDealerId,
thirdMoney,
order,
simpleDealerId,
buildCommissionComment("分润收入", commissionConfig.commissionType, commissionConfig.dealerThirdValue, goodsQty)
);
}
return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney); return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney);
} }
@@ -323,6 +371,83 @@ 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 TotalDealerCommission settleTotalDealerCommission(
ShopOrder order,
BigDecimal baseAmount,
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(baseAmount, rate, goodsQty, DIVIDEND_SCALE, 20);
log.info("总经销商分润发放 - orderNo={}, totalDealerUserId={}, rate={}, money={}",
order.getOrderNo(), totalDealerUser.getUserId(), rate, money);
creditDealerCommission(
totalDealerUser.getUserId(),
money,
order,
order.getUserId(),
buildCommissionComment("总经销商分润", 20, rate, goodsQty)
);
return new TotalDealerCommission(totalDealerUser.getUserId(), money);
}
private ShopDealerUser findTotalDealerUser() {
return shopDealerUserService.getOne(
new LambdaQueryWrapper<ShopDealerUser>()
.eq(ShopDealerUser::getTenantId, TENANT_ID)
.eq(ShopDealerUser::getType, 2)
.and(w -> w.eq(ShopDealerUser::getIsDelete, 0).or().isNull(ShopDealerUser::getIsDelete))
.orderByAsc(ShopDealerUser::getId)
.last("limit 1")
);
}
private DealerBasicSetting findDealerBasicSetting() {
int level = 2;
ShopDealerSetting setting = shopDealerSettingService.getOne(
new LambdaQueryWrapper<ShopDealerSetting>()
.eq(ShopDealerSetting::getTenantId, TENANT_ID)
.eq(ShopDealerSetting::getKey, "basic")
.last("limit 1")
);
if (setting != null && setting.getValues() != null && !setting.getValues().isBlank()) {
try {
JSONObject json = JSONObject.parseObject(setting.getValues());
Integer levelVal = json.getInteger("level");
if (levelVal != null && levelVal > 0) {
level = Math.min(levelVal, 3);
}
} catch (Exception e) {
log.warn("解析分销设置失败,将使用默认等级 - tenantId={}, values={}", TENANT_ID, setting.getValues(), e);
}
}
return new DealerBasicSetting(level);
}
private int normalizeDealerLevel(int dealerLevel) {
if (dealerLevel <= 0) {
return 2;
}
return Math.min(dealerLevel, 3);
}
/**
* shop_dealer_setting(key=basic) 的关键配置仅取结算任务需要的字段
*/
private static class DealerBasicSetting {
private final int level;
private DealerBasicSetting(int level) {
this.level = level;
}
}
/** /**
* 门店分红规则 * 门店分红规则
* - 门店角色为 ShopDealerUser.type=1 * - 门店角色为 ShopDealerUser.type=1
@@ -502,7 +627,8 @@ public class DealerOrderSettlement10584Task {
ShopOrder order, ShopOrder order,
BigDecimal baseAmount, BigDecimal baseAmount,
DealerRefereeCommission dealerRefereeCommission, DealerRefereeCommission dealerRefereeCommission,
ShopRoleCommission shopRoleCommission ShopRoleCommission shopRoleCommission,
TotalDealerCommission totalDealerCommission
) { ) {
// 幂等同一订单只写一条依赖 order_no + tenant_id 作为业务唯一 // 幂等同一订单只写一条依赖 order_no + tenant_id 作为业务唯一
ShopDealerOrder existed = shopDealerOrderService.getOne( ShopDealerOrder existed = shopDealerOrderService.getOne(
@@ -580,20 +706,25 @@ public class DealerOrderSettlement10584Task {
dealerOrder.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"))); dealerOrder.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
dealerOrder.setTenantId(TENANT_ID); dealerOrder.setTenantId(TENANT_ID);
dealerOrder.setComments(buildCommissionTraceComment(dealerRefereeCommission, shopRoleCommission)); dealerOrder.setComments(buildCommissionTraceComment(dealerRefereeCommission, shopRoleCommission, totalDealerCommission));
shopDealerOrderService.save(dealerOrder); shopDealerOrderService.save(dealerOrder);
log.info("写入ShopDealerOrder完成 - orderNo={}, firstUserId={}, secondUserId={}, firstDividendUser={}, secondDividendUser={}", log.info("写入ShopDealerOrder完成 - orderNo={}, firstUserId={}, secondUserId={}, firstDividendUser={}, secondDividendUser={}",
order.getOrderNo(), dealerOrder.getFirstUserId(), dealerOrder.getSecondUserId(), dealerOrder.getFirstDividendUser(), dealerOrder.getSecondDividendUser()); order.getOrderNo(), dealerOrder.getFirstUserId(), dealerOrder.getSecondUserId(), dealerOrder.getFirstDividendUser(), dealerOrder.getSecondDividendUser());
} }
private String buildCommissionTraceComment(DealerRefereeCommission dealerRefereeCommission, ShopRoleCommission shopRoleCommission) { private String buildCommissionTraceComment(
DealerRefereeCommission dealerRefereeCommission,
ShopRoleCommission shopRoleCommission,
TotalDealerCommission totalDealerCommission
) {
// 轻量过程留痕方便排查详细分佣以 ShopDealerCapital 为准 // 轻量过程留痕方便排查详细分佣以 ShopDealerCapital 为准
return "direct=" + dealerRefereeCommission.directDealerId + ":" + dealerRefereeCommission.directMoney return "direct=" + dealerRefereeCommission.directDealerId + ":" + dealerRefereeCommission.directMoney
+ ",simple=" + dealerRefereeCommission.simpleDealerId + ":" + dealerRefereeCommission.simpleMoney + ",simple=" + dealerRefereeCommission.simpleDealerId + ":" + dealerRefereeCommission.simpleMoney
+ ",third=" + dealerRefereeCommission.thirdDealerId + ":" + dealerRefereeCommission.thirdMoney + ",third=" + dealerRefereeCommission.thirdDealerId + ":" + dealerRefereeCommission.thirdMoney
+ ",dividend1=" + shopRoleCommission.storeDirectUserId + ":" + shopRoleCommission.storeDirectMoney + ",dividend1=" + shopRoleCommission.storeDirectUserId + ":" + shopRoleCommission.storeDirectMoney
+ ",dividend2=" + shopRoleCommission.storeSimpleUserId + ":" + shopRoleCommission.storeSimpleMoney; + ",dividend2=" + shopRoleCommission.storeSimpleUserId + ":" + shopRoleCommission.storeSimpleMoney
+ ",totalDealer=" + totalDealerCommission.userId + ":" + totalDealerCommission.money;
} }
private BigDecimal getOrderBaseAmount(ShopOrder order) { private BigDecimal getOrderBaseAmount(ShopOrder order) {
@@ -805,4 +936,18 @@ public class DealerOrderSettlement10584Task {
this.storeSimpleMoney = storeSimpleMoney != null ? storeSimpleMoney : BigDecimal.ZERO; this.storeSimpleMoney = storeSimpleMoney != null ? storeSimpleMoney : BigDecimal.ZERO;
} }
} }
private static class TotalDealerCommission {
private final Integer userId;
private final BigDecimal money;
private static TotalDealerCommission empty() {
return new TotalDealerCommission(null, BigDecimal.ZERO);
}
private TotalDealerCommission(Integer userId, BigDecimal money) {
this.userId = userId;
this.money = money != null ? money : BigDecimal.ZERO;
}
}
} }

View File

@@ -4,6 +4,7 @@ import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.shop.service.ShopDealerSettingService; import com.gxwebsoft.shop.service.ShopDealerSettingService;
import com.gxwebsoft.shop.entity.ShopDealerSetting; import com.gxwebsoft.shop.entity.ShopDealerSetting;
import com.gxwebsoft.shop.param.ShopDealerSettingParam; import com.gxwebsoft.shop.param.ShopDealerSettingParam;
import com.gxwebsoft.shop.param.ShopDealerSettingSaveParam;
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.PageParam;
@@ -59,24 +60,54 @@ public class ShopDealerSettingController extends BaseController {
@OperationLog @OperationLog
@Operation(summary = "添加分销商设置表") @Operation(summary = "添加分销商设置表")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody ShopDealerSetting shopDealerSetting) { public ApiResult<?> save(@RequestBody ShopDealerSettingSaveParam param) {
if (shopDealerSettingService.save(shopDealerSetting)) { ShopDealerSetting shopDealerSetting = buildEntity(param);
return success("添加成功"); if (shopDealerSettingService.saveOrUpdateByKey(shopDealerSetting)) {
return success("保存成功");
} }
return fail("添加失败"); return fail("保存失败");
} }
@PreAuthorize("hasAuthority('shop:shopDealerSetting:update')") @PreAuthorize("hasAuthority('shop:shopDealerSetting:update')")
@OperationLog @OperationLog
@Operation(summary = "修改分销商设置表") @Operation(summary = "修改分销商设置表")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody ShopDealerSetting shopDealerSetting) { public ApiResult<?> update(@RequestBody ShopDealerSettingSaveParam param) {
if (shopDealerSettingService.updateById(shopDealerSetting)) { ShopDealerSetting shopDealerSetting = buildEntity(param);
if (shopDealerSetting.getKey() == null) {
return fail("修改失败");
}
if (shopDealerSettingService.saveOrUpdateByKey(shopDealerSetting)) {
return success("修改成功"); return success("修改成功");
} }
return fail("修改失败"); return fail("修改失败");
} }
private ShopDealerSetting buildEntity(ShopDealerSettingSaveParam param) {
ShopDealerSetting shopDealerSetting = new ShopDealerSetting();
shopDealerSetting.setKey(param.getKey());
shopDealerSetting.setDescribe(param.getDescribe());
shopDealerSetting.setValues(param.getValues());
Integer tenantId = param.getTenantId();
if (tenantId == null) {
tenantId = getTenantId();
}
shopDealerSetting.setTenantId(tenantId);
shopDealerSetting.setUpdateTime(normalizeUpdateTime(param.getUpdateTime()));
return shopDealerSetting;
}
private Integer normalizeUpdateTime(Long updateTime) {
long value = updateTime != null ? updateTime : System.currentTimeMillis();
if (value > Integer.MAX_VALUE) {
value = value / 1000;
}
if (value > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
return (int) value;
}
@PreAuthorize("hasAuthority('shop:shopDealerSetting:remove')") @PreAuthorize("hasAuthority('shop:shopDealerSetting:remove')")
@OperationLog @OperationLog
@Operation(summary = "删除分销商设置表") @Operation(summary = "删除分销商设置表")

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.NumberUtil; 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.gxwebsoft.common.core.config.ConfigProperties; 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.utils.RedisUtil;
@@ -19,6 +20,7 @@ import com.gxwebsoft.shop.task.OrderAutoCancelTask;
import com.gxwebsoft.shop.entity.ShopOrder; import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.param.ShopOrderParam; import com.gxwebsoft.shop.param.ShopOrderParam;
import com.gxwebsoft.shop.dto.OrderCreateRequest; import com.gxwebsoft.shop.dto.OrderCreateRequest;
import com.gxwebsoft.shop.dto.OrderPrepayRequest;
import com.gxwebsoft.shop.dto.UpdatePaymentStatusRequest; import com.gxwebsoft.shop.dto.UpdatePaymentStatusRequest;
import com.gxwebsoft.payment.service.PaymentService; import com.gxwebsoft.payment.service.PaymentService;
import com.gxwebsoft.payment.dto.PaymentResponse; import com.gxwebsoft.payment.dto.PaymentResponse;
@@ -48,6 +50,7 @@ import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
/** /**
* 订单控制器 * 订单控制器
@@ -93,6 +96,8 @@ public class ShopOrderController extends BaseController {
@Resource @Resource
private ShopOrderDeliveryService shopOrderDeliveryService; private ShopOrderDeliveryService shopOrderDeliveryService;
@Resource @Resource
private ShopWechatShippingSyncService shopWechatShippingSyncService;
@Resource
private PaymentService paymentService; private PaymentService paymentService;
@Operation(summary = "分页查询订单") @Operation(summary = "分页查询订单")
@@ -169,139 +174,303 @@ public class ShopOrderController extends BaseController {
return fail("添加失败"); return fail("添加失败");
} }
@Operation(summary = "发起支付/重新支付(兼容 pay/prepay/repay")
@PostMapping({"/pay", "/prepay", "/repay"})
public ApiResult<?> prepay(@RequestBody OrderPrepayRequest request) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("用户未登录");
}
// 允许从请求显式传 tenantId兼容一些历史调用否则优先从请求头/登录态推断
Integer tenantId = request != null ? request.getTenantId() : null;
if (tenantId == null) {
tenantId = ObjectUtil.defaultIfNull(getTenantId(), loginUser.getTenantId());
}
if (request == null || (request.getOrderId() == null && StrUtil.isBlank(request.getOrderNo()))) {
return fail("orderId 或 orderNo 不能为空");
}
ShopOrder order;
if (request.getOrderId() != null) {
order = shopOrderService.getById(request.getOrderId());
} else {
if (tenantId == null) {
return fail("tenantId 不能为空");
}
order = shopOrderService.getByOrderNo(request.getOrderNo(), tenantId);
}
if (order == null) {
return fail("订单不存在");
}
// 校验租户(避免用别的租户订单号撞库)
if (tenantId != null && order.getTenantId() != null && !tenantId.equals(order.getTenantId())) {
return fail("订单不存在");
}
// 普通用户只能操作自己的订单;管理员可越权(复用取消权限判断即可)
if (!loginUser.getUserId().equals(order.getUserId()) && !hasOrderCancelAuthority()) {
return fail("无权限操作此订单");
}
// 业务状态校验这些错误需要明确返回code!=0避免前端误判为接口不支持而降级走创建订单
if (Boolean.TRUE.equals(order.getPayStatus())) {
return fail("订单已支付");
}
if (order.getDeleted() != null && order.getDeleted() == 1) {
return fail("订单已删除");
}
if (order.getOrderStatus() != null) {
// 2=已取消6=退款成功7=客户端申请退款其他非0状态也视为不可再次发起支付
if (!Objects.equals(order.getOrderStatus(), 0)) {
return fail("订单状态不允许发起支付");
}
}
if (order.getExpirationTime() != null && order.getExpirationTime().isBefore(LocalDateTime.now())) {
return fail("订单已过期");
}
// 补齐 createWxOrder 所需字段注意openid 在 ShopOrder 上是非持久化字段,查询出来为空)
Integer payType = request.getPayType() != null ? request.getPayType() : order.getPayType();
if (payType == null) {
payType = 1;
}
if (!Objects.equals(payType, 1) && !Objects.equals(payType, 102)) {
return fail("该订单不支持发起微信支付");
}
order.setPayType(payType);
order.setPayUserId(loginUser.getUserId());
order.setOpenid(loginUser.getOpenid());
if (order.getPayPrice() == null) {
order.setPayPrice(ObjectUtil.defaultIfNull(order.getTotalPrice(), BigDecimal.ZERO));
}
if (StrUtil.isBlank(order.getComments())) {
order.setComments("订单支付");
}
try {
Map<String, String> wxOrderInfo = shopOrderService.createWxOrder(order);
return success(wxOrderInfo);
} catch (Exception e) {
logger.error("发起支付失败 - userId={}, orderId={}, orderNo={}",
loginUser.getUserId(), order.getOrderId(), order.getOrderNo(), e);
return fail("发起支付失败:" + e.getMessage());
}
}
@PreAuthorize("hasAuthority('shop:shopOrder:update')") @PreAuthorize("hasAuthority('shop:shopOrder:update')")
@Operation(summary = "修改订单") @Operation(summary = "修改订单")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody ShopOrder shopOrder) throws Exception { public ApiResult<?> update(@RequestBody ShopOrder shopOrder) throws Exception {
// 1. 验证订单是否可以退款
if (shopOrder == null) { if (shopOrder == null) {
return fail("订单不存在"); return fail("订单不存在");
}
// 退款相关操作单独走退款接口,便于做财务权限隔离
if (Objects.equals(shopOrder.getOrderStatus(), 6)) {
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
} }
ShopOrder shopOrderNow = shopOrderService.getById(shopOrder.getOrderId()); ShopOrder shopOrderNow = shopOrderService.getById(shopOrder.getOrderId());
// 申请退款 if (shopOrderNow == null) {
if (shopOrder.getOrderStatus().equals(4)) { return fail("订单不存在");
shopOrder.setRefundApplyTime(LocalDateTime.now());
} }
if (shopOrderNow.getDeliveryStatus().equals(10) && shopOrder.getDeliveryStatus().equals(20)) { // 发货状态从“未发货(10)”变更为“已发货(20)”时,记录发货信息
ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery(); if (Objects.equals(shopOrderNow.getDeliveryStatus(), 10) && Objects.equals(shopOrder.getDeliveryStatus(), 20)) {
shopOrderDelivery.setOrderId(shopOrder.getOrderId()); // 1) 无需物流/自提不走快递100下单直接置为已发货并同步到微信后台
shopOrderDelivery.setDeliveryMethod(30); if (shopOrder.getExpressId() == null || shopOrder.getExpressId() == 0) {
shopOrderDelivery.setExpressId(shopOrder.getExpressId()); ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
shopOrderDelivery.setSendName(shopOrder.getSendName()); shopOrderDelivery.setOrderId(shopOrder.getOrderId());
shopOrderDelivery.setSendPhone(shopOrder.getSendPhone()); shopOrderDelivery.setDeliveryMethod(20);
shopOrderDelivery.setSendAddress(shopOrder.getSendAddress()); shopOrderDelivery.setSendName(shopOrder.getSendName());
shopOrderDeliveryService.save(shopOrderDelivery); shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
shopOrderDeliveryService.save(shopOrderDelivery);
shopOrderDeliveryService.setExpress(getLoginUser(), shopOrderDelivery, shopOrder); ShopOrder patch = new ShopOrder();
patch.setOrderId(shopOrder.getOrderId());
patch.setDeliveryStatus(20);
patch.setDeliveryTime(LocalDateTime.now());
shopOrderService.updateById(patch);
// 同步到微信后台(发货信息录入)
ShopOrder syncOrder = shopOrderNow;
syncOrder.setDeliveryStatus(20);
syncOrder.setDeliveryTime(patch.getDeliveryTime());
try {
shopWechatShippingSyncService.uploadNoLogisticsShippingInfo(syncOrder);
} catch (Exception e) {
logger.warn("同步微信发货信息失败(无需物流,不影响发货成功) - orderId={}", shopOrder.getOrderId(), e);
}
} else {
// 2) 实物快递创建发货单并走快递100电子面单发货成功后同步微信后台
ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
shopOrderDelivery.setOrderId(shopOrder.getOrderId());
shopOrderDelivery.setDeliveryMethod(30);
shopOrderDelivery.setExpressId(shopOrder.getExpressId());
shopOrderDelivery.setSendName(shopOrder.getSendName());
shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
shopOrderDeliveryService.save(shopOrderDelivery);
// 需要用订单的持久化字段(例如 addressId/tenantId/userId/transactionId同时补齐临时的发货地址
ShopOrder orderForDelivery = shopOrderNow;
orderForDelivery.setSendAddress(shopOrder.getSendAddress());
shopOrderDeliveryService.setExpress(getLoginUser(), shopOrderDelivery, orderForDelivery);
} }
// 退款操作
if(shopOrder.getOrderStatus().equals(6)){
// 当订单状态更改为6已退款执行退款操作
try {
// 检查订单是否已支付
if (!shopOrder.getPayStatus()) {
return fail("订单未支付,无法退款");
}
// 检查是否已经退款过了
if (StrUtil.isNotBlank(shopOrderNow.getRefundOrder())) {
logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), shopOrderNow.getRefundOrder());
return fail("订单已退款,请勿重复操作");
}
// 2. 生成退款单号
String refundNo = "RF" + IdUtil.getSnowflakeNextId();
// 3. 确定退款金额(默认全额退款)
BigDecimal refundAmount = shopOrder.getRefundMoney();
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
// 如果没有指定退款金额,使用订单实付金额
refundAmount = shopOrderNow.getTotalPrice();
}
// 验证退款金额不能大于订单金额
if (refundAmount.compareTo(shopOrderNow.getTotalPrice()) > 0) {
return fail("退款金额不能大于订单金额");
}
// 4. 确定支付类型默认为微信Native支付
PaymentType paymentType = PaymentType.WECHAT_NATIVE;
if (shopOrderNow.getPayType() != null) {
// 根据订单的支付类型确定
// 支付方式0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付
paymentType = PaymentType.getByCode(shopOrderNow.getPayType());
// 如果是微信支付,需要根据微信支付子类型确定具体的支付方式
if (paymentType == PaymentType.WECHAT) {
// 目前统一使用WECHAT_NATIVE进行退款
paymentType = PaymentType.WECHAT_NATIVE;
}
}
// 5. 调用统一支付服务的退款接口
logger.info("开始处理订单退款 - 订单号: {}, 退款单号: {}, 退款金额: {}, 支付方式: {}",
shopOrderNow.getOrderNo(), refundNo, refundAmount, paymentType);
PaymentResponse refundResponse = paymentService.refund(
shopOrderNow.getOrderNo(), // 原订单号
refundNo, // 退款单号
paymentType, // 支付方式
shopOrderNow.getTotalPrice(), // 订单总金额
refundAmount, // 退款金额
shopOrder.getRefundReason() != null ? shopOrder.getRefundReason() : "用户申请退款", // 退款原因
shopOrderNow.getTenantId() // 租户ID
);
// 6. 处理退款结果
if (refundResponse.getSuccess()) {
// 退款成功,更新订单信息
shopOrder.setRefundOrder(refundNo);
shopOrder.setRefundMoney(refundAmount);
shopOrder.setRefundTime(LocalDateTime.now());
shopOrder.setOrderStatus(6); // 退款成功
// 根据退款状态决定订单状态
// 如果微信返回退款处理中则设置订单状态为5退款处理中
// 如果微信返回退款成功则保持状态为6退款成功
// if (refundResponse.getPaymentStatus() != null) {
// switch (refundResponse.getPaymentStatus()) {
// case REFUNDING:
// shopOrder.setOrderStatus(5); // 退款处理中
// logger.info("订单退款处理中,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
// break;
// case REFUNDED:
// shopOrder.setOrderStatus(6); // 退款成功
// logger.info("订单退款成功,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
// break;
// case REFUND_FAILED:
// logger.error("订单退款失败,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
// return fail("退款失败,请联系管理员");
// default:
// shopOrder.setOrderStatus(5); // 默认为退款处理中
// }
// }
logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}",
shopOrderNow.getOrderNo(), refundNo, refundResponse.getTransactionId());
} else {
// 退款失败
logger.error("订单退款失败 - 订单号: {}, 错误: {}", shopOrderNow.getOrderNo(), refundResponse.getErrorMessage());
return fail("退款失败: " + refundResponse.getErrorMessage());
}
} catch (Exception e) {
logger.error("处理订单退款异常 - 订单号: {}, 错误: {}", shopOrderNow.getOrderNo(), e.getMessage(), e);
return fail("退款处理异常: " + e.getMessage());
}
} }
if (shopOrderService.updateById(shopOrder)) { if (shopOrderService.updateById(shopOrder)) {
// 如果订单上带了快递单号(常见于后台手工修正/补录),同步到发货单表,避免发货单还是旧单号
if (StrUtil.isNotBlank(shopOrder.getExpressNo()) && shopOrder.getOrderId() != null) {
try {
List<ShopOrderDelivery> deliveryList = shopOrderDeliveryService.list(
new LambdaQueryWrapper<ShopOrderDelivery>()
.eq(ShopOrderDelivery::getOrderId, shopOrder.getOrderId())
);
if (deliveryList == null || deliveryList.isEmpty()) {
ShopOrderDelivery delivery = new ShopOrderDelivery();
delivery.setOrderId(shopOrder.getOrderId());
delivery.setTenantId(ObjectUtil.defaultIfNull(shopOrder.getTenantId(), shopOrderNow.getTenantId()));
delivery.setExpressId(ObjectUtil.defaultIfNull(shopOrder.getExpressId(), shopOrderNow.getExpressId()));
delivery.setExpressNo(shopOrder.getExpressNo());
// 10手动录入 / 20无需物流
if (delivery.getExpressId() == null || delivery.getExpressId() == 0) {
delivery.setDeliveryMethod(20);
} else {
delivery.setDeliveryMethod(10);
}
delivery.setSendName(ObjectUtil.defaultIfNull(shopOrder.getSendName(), shopOrderNow.getSendName()));
delivery.setSendPhone(ObjectUtil.defaultIfNull(shopOrder.getSendPhone(), shopOrderNow.getSendPhone()));
delivery.setSendAddress(ObjectUtil.defaultIfNull(shopOrder.getSendAddress(), shopOrderNow.getSendAddress()));
shopOrderDeliveryService.save(delivery);
} else {
for (ShopOrderDelivery d : deliveryList) {
if (d == null || d.getDeliveryId() == null) {
continue;
}
ShopOrderDelivery patch = new ShopOrderDelivery();
patch.setDeliveryId(d.getDeliveryId());
patch.setExpressNo(shopOrder.getExpressNo());
shopOrderDeliveryService.updateById(patch);
}
}
} catch (Exception e) {
logger.warn("同步更新发货单运单号失败 - orderId={}, expressNo={}",
shopOrder.getOrderId(), shopOrder.getExpressNo(), e);
}
}
return success("修改成功"); return success("修改成功");
} }
return fail("修改失败"); return fail("修改失败");
} }
@PreAuthorize("hasAuthority('shop:shopOrder:refund')")
@Operation(summary = "订单退款操作(申请退款/同意退款)", description = "orderStatus=4 申请退款orderStatus=6 同意退款并发起原路退款")
@PutMapping("/refund")
public ApiResult<?> refund(@RequestBody ShopOrder req) {
if (req == null || req.getOrderId() == null || req.getOrderStatus() == null) {
return fail("orderId 和 orderStatus 不能为空");
}
if (!Objects.equals(req.getOrderStatus(), 4) && !Objects.equals(req.getOrderStatus(), 6)) {
return fail("orderStatus 仅支持 4(申请退款) 或 6(同意退款)");
}
ShopOrder current = shopOrderService.getById(req.getOrderId());
if (current == null) {
return fail("订单不存在");
}
// 申请退款:只记录申请时间/原因/金额(如有)
if (Objects.equals(req.getOrderStatus(), 4)) {
ShopOrder patch = new ShopOrder();
patch.setOrderId(req.getOrderId());
patch.setOrderStatus(4);
patch.setRefundApplyTime(LocalDateTime.now());
if (StrUtil.isNotBlank(req.getRefundReason())) {
patch.setRefundReason(req.getRefundReason());
}
if (req.getRefundMoney() != null) {
patch.setRefundMoney(req.getRefundMoney());
}
if (shopOrderService.updateById(patch)) {
return success("申请退款成功");
}
return fail("申请退款失败");
}
// 同意退款:发起原路退款并更新退款信息
try {
if (!Boolean.TRUE.equals(current.getPayStatus())) {
return fail("订单未支付,无法退款");
}
if (StrUtil.isNotBlank(current.getRefundOrder())) {
logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", current.getOrderNo(), current.getRefundOrder());
return fail("订单已退款,请勿重复操作");
}
String refundNo = "RF" + IdUtil.getSnowflakeNextId();
BigDecimal refundAmount = req.getRefundMoney();
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
refundAmount = current.getTotalPrice();
}
if (refundAmount.compareTo(current.getTotalPrice()) > 0) {
return fail("退款金额不能大于订单金额");
}
PaymentType paymentType = PaymentType.WECHAT_NATIVE;
if (current.getPayType() != null) {
// 支付方式0余额支付1微信支付2支付宝支付3银联支付4现金支付5POS机支付6免费7积分支付
paymentType = PaymentType.getByCode(current.getPayType());
if (paymentType == PaymentType.WECHAT) {
paymentType = PaymentType.WECHAT_NATIVE;
}
}
logger.info("开始处理订单退款 - 订单号: {}, 退款单号: {}, 退款金额: {}, 支付方式: {}",
current.getOrderNo(), refundNo, refundAmount, paymentType);
PaymentResponse refundResponse = paymentService.refund(
current.getOrderNo(),
refundNo,
paymentType,
current.getTotalPrice(),
refundAmount,
StrUtil.isNotBlank(req.getRefundReason()) ? req.getRefundReason() : "用户申请退款",
current.getTenantId()
);
if (!Boolean.TRUE.equals(refundResponse.getSuccess())) {
logger.error("订单退款失败 - 订单号: {}, 错误: {}", current.getOrderNo(), refundResponse.getErrorMessage());
return fail("退款失败: " + refundResponse.getErrorMessage());
}
ShopOrder patch = new ShopOrder();
patch.setOrderId(req.getOrderId());
patch.setRefundOrder(refundNo);
patch.setRefundMoney(refundAmount);
patch.setRefundTime(LocalDateTime.now());
patch.setOrderStatus(6);
if (StrUtil.isNotBlank(req.getRefundReason())) {
patch.setRefundReason(req.getRefundReason());
}
if (!shopOrderService.updateById(patch)) {
logger.error("退款已成功但订单更新失败 - orderId={}, orderNo={}, refundNo={}", current.getOrderId(), current.getOrderNo(), refundNo);
return fail("退款成功,但订单状态更新失败,请联系管理员");
}
logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}",
current.getOrderNo(), refundNo, refundResponse.getTransactionId());
return success("退款成功");
} catch (Exception e) {
logger.error("处理订单退款异常 - 订单号: {}, 错误: {}", current.getOrderNo(), e.getMessage(), e);
return fail("退款处理异常: " + e.getMessage());
}
}
@Operation(summary = "删除订单") @Operation(summary = "删除订单")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
@@ -323,6 +492,13 @@ public class ShopOrderController extends BaseController {
@Operation(summary = "批量修改订单") @Operation(summary = "批量修改订单")
@PutMapping("/batch") @PutMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopOrder> batchParam) { public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopOrder> batchParam) {
if (batchParam != null && batchParam.getData() != null) {
Integer status = batchParam.getData().getOrderStatus();
// 退款相关操作单独走退款接口,避免绕过财务权限
if (Objects.equals(status, 4) || Objects.equals(status, 6)) {
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
}
}
if (batchParam.update(shopOrderService, "order_id")) { if (batchParam.update(shopOrderService, "order_id")) {
return success("修改成功"); return success("修改成功");
} }

View File

@@ -0,0 +1,32 @@
package com.gxwebsoft.shop.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.Positive;
/**
* 订单重新发起支付请求DTO
*
* 前端会按 /shop/shop-order/pay -> /prepay -> /repay 依次尝试。
* 后端可统一实现为同一套逻辑多个URL别名避免出现 404 导致前端误判为不支持接口。
*/
@Data
@Schema(name = "OrderPrepayRequest", description = "订单重新发起支付请求")
public class OrderPrepayRequest {
@Schema(description = "订单ID二选一orderId 或 orderNo")
@Positive(message = "订单ID必须为正数")
private Integer orderId;
@Schema(description = "订单号二选一orderId 或 orderNo")
private String orderNo;
@Schema(description = "支付方式1=微信支付102=微信Native兼容旧类型。不传则使用订单原支付方式/默认微信支付")
private Integer payType;
@Schema(description = "租户ID可选不传则从当前登录用户/请求头推断)")
@Positive(message = "租户ID必须为正数")
private Integer tenantId;
}

View File

@@ -1,6 +1,7 @@
package com.gxwebsoft.shop.entity; 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.TableId; import com.baomidou.mybatisplus.annotation.TableId;
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,13 +21,15 @@ public class ShopDealerSetting implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "设置项标示") @Schema(description = "设置项标示")
@TableId(value = "key", type = IdType.AUTO) @TableId(value = "`key`", type = IdType.INPUT)
private String key; private String key;
@Schema(description = "设置项描述") @Schema(description = "设置项描述")
@TableField(value = "`describe`")
private String describe; private String describe;
@Schema(description = "设置内容(json格式)") @Schema(description = "设置内容(json格式)")
@TableField(value = "`values`")
private String values; private String values;
@Schema(description = "商城ID") @Schema(description = "商城ID")

View File

@@ -8,13 +8,13 @@
FROM shop_dealer_setting a FROM shop_dealer_setting a
<where> <where>
<if test="param.key != null"> <if test="param.key != null">
AND a.key = #{param.key} AND a.`key` = #{param.key}
</if> </if>
<if test="param.describe != null"> <if test="param.describe != null">
AND a.describe LIKE CONCAT('%', #{param.describe}, '%') AND a.`describe` LIKE CONCAT('%', #{param.describe}, '%')
</if> </if>
<if test="param.values != null"> <if test="param.values != null">
AND a.values LIKE CONCAT('%', #{param.values}, '%') AND a.`values` LIKE CONCAT('%', #{param.values}, '%')
</if> </if>
<if test="param.keywords != null"> <if test="param.keywords != null">
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')

View File

@@ -0,0 +1,35 @@
package com.gxwebsoft.shop.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* Shop dealer setting save parameters.
*
* @author WebSoft
* @since 2025-08-11 23:51:41
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "ShopDealerSettingSaveParam", description = "Shop dealer setting save parameters")
public class ShopDealerSettingSaveParam implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "Setting key")
private String key;
@Schema(description = "Setting description")
private String describe;
@Schema(description = "Settings JSON content")
private String values;
@Schema(description = "Tenant ID")
private Integer tenantId;
@Schema(description = "Update time")
private Long updateTime;
}

View File

@@ -39,4 +39,12 @@ public interface ShopDealerSettingService extends IService<ShopDealerSetting> {
*/ */
ShopDealerSetting getByIdRel(String key); ShopDealerSetting getByIdRel(String key);
/**
* 根据 key + tenantId 保存或更新
*
* @param setting 设置项
* @return boolean
*/
boolean saveOrUpdateByKey(ShopDealerSetting setting);
} }

View File

@@ -0,0 +1,24 @@
package com.gxwebsoft.shop.service;
import com.gxwebsoft.shop.entity.ShopExpress;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderDelivery;
/**
* 微信小程序“发货信息管理”同步服务。
*
* <p>用于将系统内发货/无需物流状态同步到微信小程序后台,避免人工在后台录入。</p>
*/
public interface ShopWechatShippingSyncService {
/**
* 实物快递发货同步到微信后台(上传运单号/快递公司)。
*/
boolean uploadExpressShippingInfo(ShopOrder order, ShopOrderDelivery orderDelivery, ShopExpress express);
/**
* 无需物流/自提发货同步到微信后台(上传无需物流)。
*/
boolean uploadNoLogisticsShippingInfo(ShopOrder order);
}

View File

@@ -7,8 +7,10 @@ import com.gxwebsoft.shop.entity.ShopDealerSetting;
import com.gxwebsoft.shop.param.ShopDealerSettingParam; import com.gxwebsoft.shop.param.ShopDealerSettingParam;
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.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List; import java.util.List;
/** /**
@@ -23,7 +25,7 @@ public class ShopDealerSettingServiceImpl extends ServiceImpl<ShopDealerSettingM
@Override @Override
public PageResult<ShopDealerSetting> pageRel(ShopDealerSettingParam param) { public PageResult<ShopDealerSetting> pageRel(ShopDealerSettingParam param) {
PageParam<ShopDealerSetting, ShopDealerSettingParam> page = new PageParam<>(param); PageParam<ShopDealerSetting, ShopDealerSettingParam> page = new PageParam<>(param);
page.setDefaultOrder("create_time desc"); page.setDefaultOrder("update_time desc");
List<ShopDealerSetting> list = baseMapper.selectPageRel(page, param); List<ShopDealerSetting> list = baseMapper.selectPageRel(page, param);
return new PageResult<>(list, page.getTotal()); return new PageResult<>(list, page.getTotal());
} }
@@ -31,10 +33,14 @@ public class ShopDealerSettingServiceImpl extends ServiceImpl<ShopDealerSettingM
@Override @Override
public List<ShopDealerSetting> listRel(ShopDealerSettingParam param) { public List<ShopDealerSetting> listRel(ShopDealerSettingParam param) {
List<ShopDealerSetting> list = baseMapper.selectListRel(param); List<ShopDealerSetting> list = baseMapper.selectListRel(param);
// 排序 if (list == null || list.size() < 2) {
PageParam<ShopDealerSetting, ShopDealerSettingParam> page = new PageParam<>(); return list;
page.setDefaultOrder("create_time desc"); }
return page.sortRecords(list); list.sort(Comparator.comparing(
ShopDealerSetting::getUpdateTime,
Comparator.nullsLast(Integer::compareTo)
).reversed());
return list;
} }
@Override @Override
@@ -44,4 +50,17 @@ public class ShopDealerSettingServiceImpl extends ServiceImpl<ShopDealerSettingM
return param.getOne(baseMapper.selectListRel(param)); return param.getOne(baseMapper.selectListRel(param));
} }
@Override
public boolean saveOrUpdateByKey(ShopDealerSetting setting) {
if (setting == null || setting.getKey() == null) {
return false;
}
LambdaQueryWrapper<ShopDealerSetting> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ShopDealerSetting::getKey, setting.getKey());
if (setting.getTenantId() != null) {
wrapper.eq(ShopDealerSetting::getTenantId, setting.getTenantId());
}
return this.saveOrUpdate(setting, wrapper);
}
} }

View File

@@ -12,6 +12,7 @@ import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.kuaidi100.sdk.pojo.HttpResult; import com.kuaidi100.sdk.pojo.HttpResult;
import com.kuaidi100.sdk.request.BOrderReq; import com.kuaidi100.sdk.request.BOrderReq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@@ -27,6 +28,7 @@ import java.util.Map;
* @since 2025-01-11 10:45:12 * @since 2025-01-11 10:45:12
*/ */
@Service @Service
@Slf4j
public class ShopOrderDeliveryServiceImpl extends ServiceImpl<ShopOrderDeliveryMapper, ShopOrderDelivery> implements ShopOrderDeliveryService { public class ShopOrderDeliveryServiceImpl extends ServiceImpl<ShopOrderDeliveryMapper, ShopOrderDelivery> implements ShopOrderDeliveryService {
@Resource @Resource
private ShopExpressService expressService; private ShopExpressService expressService;
@@ -40,6 +42,8 @@ public class ShopOrderDeliveryServiceImpl extends ServiceImpl<ShopOrderDeliveryM
private ShopGoodsService shopGoodsService; private ShopGoodsService shopGoodsService;
@Resource @Resource
private ShopUserAddressService shopUserAddressService; private ShopUserAddressService shopUserAddressService;
@Resource
private ShopWechatShippingSyncService shopWechatShippingSyncService;
@Override @Override
public PageResult<ShopOrderDelivery> pageRel(ShopOrderDeliveryParam param) { public PageResult<ShopOrderDelivery> pageRel(ShopOrderDeliveryParam param) {
@@ -111,45 +115,12 @@ public class ShopOrderDeliveryServiceImpl extends ServiceImpl<ShopOrderDeliveryM
order.setDeliveryTime(LocalDateTime.now()); order.setDeliveryTime(LocalDateTime.now());
shopOrderService.updateById(order); shopOrderService.updateById(order);
if (order.getPayType().equals(1)) { // 同步发货信息到微信小程序后台(发货信息录入),避免人工录入
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.getListByOrderId(order.getOrderId()); try {
// 上传小程序发货信息 shopWechatShippingSyncService.uploadExpressShippingInfo(order, orderDelivery, express);
// WxMaOrderShippingInfoUploadRequest uploadRequest = new WxMaOrderShippingInfoUploadRequest(); } catch (Exception e) {
// uploadRequest.setLogisticsType(1); // 不影响本地发货流程,记录日志即可(可配合定时任务后补偿重试)
// uploadRequest.setDeliveryMode(1); log.warn("同步微信发货信息失败(不影响发货成功) - orderId={}", order.getOrderId(), e);
//
// OrderKeyBean orderKeyBean = new OrderKeyBean();
// orderKeyBean.setOrderNumberType(2);
// orderKeyBean.setTransactionId(order.getTransactionId());
// uploadRequest.setOrderKey(orderKeyBean);
//
// List<ShippingListBean> shippingList = new ArrayList<>();
// ShippingListBean shippingListBean = new ShippingListBean();
// shippingListBean.setTrackingNo((String) bOrderData.get("kuaidinum"));
// shippingListBean.setExpressCompany(express.getWxCode());
// ContactBean contactBean = new ContactBean();
// contactBean.setReceiverContact(user.getMobile());
// shippingListBean.setContact(contactBean);
//
// ShopGoods shopGoods = shopGoodsService.getById(orderGoodsList.get(0).getGoodsId());
//
// String itemDesc = shopGoods.getName();
// if (orderGoodsList.size() > 1) itemDesc += "等" + orderGoodsList.size() + "件商品";
// shippingListBean.setItemDesc(itemDesc);
// shippingList.add(shippingListBean);
// uploadRequest.setShippingList(shippingList);
//
// uploadRequest.setUploadTime(new DateTime().toString(DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN));
//
// PayerBean payerBean = new PayerBean();
//
// payerBean.setOpenid(user.getOpenid());
// uploadRequest.setPayer(payerBean);
//
// WxMaService wxMaService = weChatController.wxMaService();
// WxMaOrderShippingService wxMaOrderShippingService = new WxMaOrderShippingServiceImpl(wxMaService);
// WxMaOrderShippingInfoBaseResponse response = wxMaOrderShippingService.upload(uploadRequest);
// System.out.println("response" + response);
} }
return new HashMap<>() {{ return new HashMap<>() {{
put("res", true); put("res", true);

View File

@@ -45,12 +45,19 @@ public class ShopWebsiteServiceImpl implements ShopWebsiteService {
String cacheKey = SHOP_INFO_KEY_PREFIX + tenantId; String cacheKey = SHOP_INFO_KEY_PREFIX + tenantId;
String shopInfo = redisUtil.get(cacheKey); String shopInfo = redisUtil.get(cacheKey);
if (StrUtil.isNotBlank(shopInfo)) { if (StrUtil.isNotBlank(shopInfo)) {
log.info("从缓存获取商城信息租户ID: {}", tenantId); try {
// try { ShopVo cacheVo = JSONUtil.parseObject(shopInfo, ShopVo.class);
// return JSONUtil.parseObject(shopInfo, ShopVo.class); if (cacheVo != null) {
// } catch (Exception e) { log.info("从缓存获取商城信息租户ID: {}", tenantId);
// log.warn("商城缓存解析失败,从数据库重新获取: {}", e.getMessage()); return cacheVo;
// } }
// 兼容历史缓存JSON "null" 会被解析为 null此时清理缓存并回源。
log.warn("商城信息缓存命中但内容为空(null)清理缓存后回源数据库租户ID: {}", tenantId);
redisUtil.delete(cacheKey);
} catch (Exception e) {
log.warn("商城缓存解析失败,清理缓存后重新获取: {}", e.getMessage());
redisUtil.delete(cacheKey);
}
} }
// 直接调用 CMS 服务获取站点信息,然后使用商城专用缓存 // 直接调用 CMS 服务获取站点信息,然后使用商城专用缓存

View File

@@ -0,0 +1,231 @@
package com.gxwebsoft.shop.service.impl;
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.OrderKeyBean;
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.PayerBean;
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.ShippingListBean;
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.WxMaOrderShippingInfoUploadRequest;
import cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.system.entity.Payment;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import com.gxwebsoft.common.system.service.WxMiniappAccessTokenService;
import com.gxwebsoft.shop.entity.ShopExpress;
import com.gxwebsoft.shop.entity.ShopGoods;
import com.gxwebsoft.shop.entity.ShopOrder;
import com.gxwebsoft.shop.entity.ShopOrderDelivery;
import com.gxwebsoft.shop.entity.ShopOrderGoods;
import com.gxwebsoft.shop.service.ShopGoodsService;
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
import com.gxwebsoft.shop.service.ShopWechatShippingSyncService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 同步系统发货信息到微信小程序后台(发货信息录入)。
*/
@Slf4j
@Service
public class ShopWechatShippingSyncServiceImpl implements ShopWechatShippingSyncService {
private static final int ORDER_NUMBER_TYPE_OUT_TRADE_NO = 1;
private static final int ORDER_NUMBER_TYPE_TRANSACTION_ID = 2;
// 这两个值在项目原有注释代码中已经使用过(实物快递)。
private static final int LOGISTICS_TYPE_PHYSICAL = 1;
private static final int DELIVERY_MODE_EXPRESS = 1;
// 无需物流/自提:微信侧会在“发货信息录入”里变为已发货(具体枚举以微信接口为准)。
private static final int LOGISTICS_TYPE_NO_LOGISTICS = 3;
private static final int DELIVERY_MODE_NO_LOGISTICS = 3;
private static final Gson GSON = new Gson();
@Resource
private WxMiniappAccessTokenService wxMiniappAccessTokenService;
@Resource
private UserService userService;
@Resource
private ShopOrderGoodsService shopOrderGoodsService;
@Resource
private ShopGoodsService shopGoodsService;
@Resource
private RedisUtil redisUtil;
@Override
public boolean uploadExpressShippingInfo(ShopOrder order, ShopOrderDelivery orderDelivery, ShopExpress express) {
if (order == null || order.getOrderId() == null) {
return false;
}
if (orderDelivery == null || StrUtil.isBlank(orderDelivery.getExpressNo())) {
log.warn("上传微信发货信息跳过:缺少运单号 - orderId={}", order.getOrderId());
return false;
}
if (express == null || StrUtil.isBlank(express.getWxCode())) {
log.warn("上传微信发货信息跳过:缺少微信快递公司编码(wxCode) - orderId={}", order.getOrderId());
return false;
}
List<ShippingListBean> shippingList = new ArrayList<>();
ShippingListBean item = new ShippingListBean();
item.setTrackingNo(orderDelivery.getExpressNo());
item.setExpressCompany(express.getWxCode());
item.setItemDesc(buildItemDesc(order.getOrderId()));
shippingList.add(item);
return doUpload(order, LOGISTICS_TYPE_PHYSICAL, DELIVERY_MODE_EXPRESS, shippingList);
}
@Override
public boolean uploadNoLogisticsShippingInfo(ShopOrder order) {
if (order == null || order.getOrderId() == null) {
return false;
}
// 无需物流情况下通常不需要 shipping_list
return doUpload(order, LOGISTICS_TYPE_NO_LOGISTICS, DELIVERY_MODE_NO_LOGISTICS, Collections.emptyList());
}
private boolean doUpload(ShopOrder order, int logisticsType, int deliveryMode, List<ShippingListBean> shippingList) {
// 仅对微信支付订单尝试同步(微信后台“待发货”来自微信支付交易)
if (!ObjectUtil.equals(order.getPayType(), 1) && !ObjectUtil.equals(order.getPayType(), 102)) {
return false;
}
if (!Boolean.TRUE.equals(order.getPayStatus())) {
return false;
}
if (order.getTenantId() == null) {
return false;
}
// payer openid必须是下单用户不是后台操作员
User buyer = userService.getByIdIgnoreTenant(order.getUserId());
if (buyer == null || StrUtil.isBlank(buyer.getOpenid())) {
log.warn("上传微信发货信息失败买家openid为空 - orderId={}, userId={}", order.getOrderId(), order.getUserId());
return false;
}
String accessToken;
try {
accessToken = wxMiniappAccessTokenService.getAccessToken(order.getTenantId());
} catch (Exception e) {
log.error("获取小程序access_token失败 - orderId={}, tenantId={}", order.getOrderId(), order.getTenantId(), e);
return false;
}
OrderKeyBean orderKey = buildOrderKey(order);
if (orderKey == null) {
log.warn("上传微信发货信息跳过无法构建order_key - orderId={}", order.getOrderId());
return false;
}
WxMaOrderShippingInfoUploadRequest uploadRequest = new WxMaOrderShippingInfoUploadRequest();
uploadRequest.setOrderKey(orderKey);
uploadRequest.setLogisticsType(logisticsType);
uploadRequest.setDeliveryMode(deliveryMode);
uploadRequest.setIsAllDelivered(true);
if (shippingList != null && !shippingList.isEmpty()) {
uploadRequest.setShippingList(shippingList);
}
uploadRequest.setUploadTime(new DateTime().toString(DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN));
PayerBean payerBean = new PayerBean();
payerBean.setOpenid(buyer.getOpenid());
uploadRequest.setPayer(payerBean);
String url = WxMaApiUrlConstants.OrderShipping.UPLOAD_SHIPPING_INFO + "?access_token=" + accessToken;
String body = GSON.toJson(uploadRequest);
try {
String resp = HttpRequest.post(url)
.header("Content-Type", "application/json")
.body(body)
.timeout(10000)
.execute()
.body();
JsonObject json = JsonParser.parseString(resp).getAsJsonObject();
int errcode = json.has("errcode") ? json.get("errcode").getAsInt() : -1;
String errmsg = json.has("errmsg") ? json.get("errmsg").getAsString() : resp;
if (errcode == 0) {
log.info("✅ 微信发货信息同步成功 - orderId={}, logisticsType={}, deliveryMode={}",
order.getOrderId(), logisticsType, deliveryMode);
return true;
}
log.error("❌ 微信发货信息同步失败 - orderId={}, errcode={}, errmsg={}, req={}",
order.getOrderId(), errcode, errmsg, body);
return false;
} catch (Exception e) {
log.error("❌ 微信发货信息同步异常 - orderId={}, req={}", order.getOrderId(), body, e);
return false;
}
}
private OrderKeyBean buildOrderKey(ShopOrder order) {
if (StrUtil.isNotBlank(order.getTransactionId())) {
OrderKeyBean key = new OrderKeyBean();
key.setOrderNumberType(ORDER_NUMBER_TYPE_TRANSACTION_ID);
key.setTransactionId(order.getTransactionId());
return key;
}
// transactionId 为空时,尝试使用 out_trade_no + mchid
if (StrUtil.isBlank(order.getOrderNo())) {
return null;
}
Payment payment = loadWechatPaymentConfig(order.getTenantId());
if (payment == null || StrUtil.isBlank(payment.getMchId())) {
return null;
}
OrderKeyBean key = new OrderKeyBean();
key.setOrderNumberType(ORDER_NUMBER_TYPE_OUT_TRADE_NO);
key.setOutTradeNo(order.getOrderNo());
key.setMchId(payment.getMchId());
return key;
}
private Payment loadWechatPaymentConfig(Integer tenantId) {
if (tenantId == null) {
return null;
}
// 与微信支付回调一致Payment:1:{tenantId}
String key = "Payment:1:" + tenantId;
try {
return redisUtil.get(key, Payment.class);
} catch (Exception e) {
log.warn("读取支付配置失败 - key={}", key, e);
return null;
}
}
private String buildItemDesc(Integer orderId) {
try {
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.getListByOrderId(orderId);
if (orderGoodsList == null || orderGoodsList.isEmpty()) {
return "订单商品";
}
ShopGoods shopGoods = shopGoodsService.getById(orderGoodsList.get(0).getGoodsId());
String itemDesc = shopGoods != null && StrUtil.isNotBlank(shopGoods.getName()) ? shopGoods.getName() : "订单商品";
if (orderGoodsList.size() > 1) {
itemDesc += "" + orderGoodsList.size() + "件商品";
}
return itemDesc;
} catch (Exception e) {
log.warn("构建微信发货 item_desc 失败 - orderId={}", orderId, e);
return "订单商品";
}
}
}

View File

@@ -0,0 +1,66 @@
package com.gxwebsoft.cms.service.impl;
import com.gxwebsoft.cms.entity.CmsWebsite;
import com.gxwebsoft.cms.mapper.CmsWebsiteMapper;
import com.gxwebsoft.cms.service.CmsNavigationService;
import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.shop.vo.ShopVo;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class CmsWebsiteServiceImplCacheTest {
@Test
void getSiteInfo_cacheValueIsJsonNull_shouldFallbackToDb() throws Exception {
Integer tenantId = 1;
RedisUtil redisUtil = mock(RedisUtil.class);
CmsNavigationService cmsNavigationService = mock(CmsNavigationService.class);
CmsWebsiteMapper cmsWebsiteMapper = mock(CmsWebsiteMapper.class);
// 历史缓存可能存在字符串 "null"Jackson 解析后会得到 null需回源 DB。
when(redisUtil.get("SiteInfo:" + tenantId)).thenReturn("null");
when(cmsNavigationService.listRel(any())).thenReturn(Collections.emptyList());
CmsWebsite website = new CmsWebsite();
website.setTenantId(tenantId);
website.setTenantName("tenant");
website.setWebsiteName("site");
when(cmsWebsiteMapper.getByTenantId(tenantId)).thenReturn(website);
CmsWebsiteServiceImpl service = new CmsWebsiteServiceImpl();
setField(service, "redisUtil", redisUtil);
setField(service, "cmsNavigationService", cmsNavigationService);
setField(service, "baseMapper", cmsWebsiteMapper);
ShopVo vo = service.getSiteInfo(tenantId);
assertNotNull(vo);
verify(redisUtil).delete("SiteInfo:" + tenantId);
verify(cmsWebsiteMapper).getByTenantId(tenantId);
}
private static void setField(Object target, String fieldName, Object value) throws Exception {
Class<?> c = target.getClass();
while (c != null) {
try {
Field f = c.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(target, value);
return;
} catch (NoSuchFieldException ignored) {
c = c.getSuperclass();
}
}
throw new NoSuchFieldException(fieldName);
}
}