|
|
@@ -1,5 +1,7 @@
|
|
|
package com.fs.his.service.impl;
|
|
|
|
|
|
+import cn.binarywang.wx.miniapp.api.WxMaService;
|
|
|
+import cn.binarywang.wx.miniapp.bean.shop.request.shipping.*;
|
|
|
import cn.hutool.core.date.DateTime;
|
|
|
import cn.hutool.core.lang.TypeReference;
|
|
|
import cn.hutool.core.util.IdUtil;
|
|
|
@@ -22,7 +24,9 @@ import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.common.utils.poi.ExcelUtil;
|
|
|
import com.fs.company.domain.CompanyUser;
|
|
|
import com.fs.company.mapper.CompanyUserMapper;
|
|
|
+import com.fs.core.config.WxMaConfiguration;
|
|
|
import com.fs.core.utils.OrderCodeUtils;
|
|
|
+import com.fs.erp.constant.ErpQueryOrderStatusEnum;
|
|
|
import com.fs.erp.constant.OrderStatusEnum;
|
|
|
import com.fs.erp.dto.*;
|
|
|
import com.fs.erp.http.JstErpHttpService;
|
|
|
@@ -34,6 +38,7 @@ import com.fs.his.enums.FsUserIntegralLogTypeEnum;
|
|
|
import com.fs.his.enums.PaymentMethodEnum;
|
|
|
import com.fs.his.mapper.*;
|
|
|
import com.fs.his.param.*;
|
|
|
+import com.fs.his.service.IFsExpressService;
|
|
|
import com.fs.his.service.IFsIntegralCartService;
|
|
|
import com.fs.his.service.IFsIntegralOrderService;
|
|
|
import com.fs.his.service.IFsStorePaymentService;
|
|
|
@@ -48,6 +53,7 @@ import com.fs.his.vo.FsStoreProductDeliverExcelVO;
|
|
|
import com.fs.hisStore.domain.FsShippingTemplatesFreeScrm;
|
|
|
import com.fs.hisStore.domain.FsShippingTemplatesRegionScrm;
|
|
|
import com.fs.hisStore.domain.FsShippingTemplatesScrm;
|
|
|
+import com.fs.hisStore.enums.ShipperCodeEnum;
|
|
|
import com.fs.hisStore.enums.ShippingTempEnum;
|
|
|
import com.fs.hisStore.mapper.FsShippingTemplatesFreeScrmMapper;
|
|
|
import com.fs.hisStore.mapper.FsShippingTemplatesRegionScrmMapper;
|
|
|
@@ -74,9 +80,14 @@ import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.time.ZoneId;
|
|
|
+import java.time.ZonedDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
import java.util.stream.Collectors;
|
|
|
+import javax.annotation.Resource;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
|
|
/**
|
|
|
* 积分商品订单Service业务层处理
|
|
|
@@ -89,14 +100,13 @@ import java.util.stream.Collectors;
|
|
|
public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
{
|
|
|
/**
|
|
|
- * 限购缓存 Redis Key 前缀
|
|
|
- * <p>完整格式:integral:purchase:count:{userId}:{goodsId},缓存用户已购某商品的有效订单数</p>
|
|
|
+ * 限购计数器 Redis Hash Key 前缀
|
|
|
+ * <p>完整格式:integral:purchase:count:{userId}(Hash 结构)
|
|
|
+ * HashKey 为 goodsId,Value 为已购有效订单数(仅排除 status=5已取消、-2已退款)。
|
|
|
+ * 通过 HINCRBY 原子增减保证一致性:下单成功 +N,取消/退款 -N。</p>
|
|
|
*/
|
|
|
private static final String REDIS_KEY_INTEGRAL_PURCHASE_COUNT = "integral:purchase:count:";
|
|
|
|
|
|
- /** 限购缓存过期时间(1小时),过期后回源数据库重建 */
|
|
|
- private static final Integer PURCHASE_COUNT_CACHE_TTL = 1;
|
|
|
-
|
|
|
@Autowired
|
|
|
private FsCouponMapper fsCouponMapper;
|
|
|
|
|
|
@@ -106,6 +116,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
@Autowired
|
|
|
private FsIntegralOrderMapper fsIntegralOrderMapper;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private FsIntegralOrderItemMapper fsIntegralOrderItemMapper;
|
|
|
+
|
|
|
@Autowired
|
|
|
private FsUserAddressMapper fsUserAddressMapper;
|
|
|
|
|
|
@@ -138,6 +151,13 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
private IFsUserIntegralLogsService userIntegralLogsService;
|
|
|
@Autowired
|
|
|
private RedisCache redisCache;
|
|
|
+ /**
|
|
|
+ * 限购计数器专用 RedisTemplate(Hash 结构)
|
|
|
+ * <p>Key: integral:purchase:count:{userId},HashKey: {goodsId},Value: 已购有效订单数
|
|
|
+ * 使用 GenericToStringSerializer 序列化 Integer,支持 HINCRBY 原子增减操作</p>
|
|
|
+ */
|
|
|
+ @Resource(name = "redisTemplateForInteger")
|
|
|
+ private RedisTemplate<String, Integer> redisTemplateForInteger;
|
|
|
@Autowired
|
|
|
private IPayService ybPayService;
|
|
|
/**
|
|
|
@@ -179,6 +199,12 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
@Autowired
|
|
|
private JstErpHttpService jstErpHttpService;
|
|
|
|
|
|
+ /**
|
|
|
+ * 快递公司服务(物流回调订阅、本地快递编码映射)
|
|
|
+ */
|
|
|
+ @Autowired
|
|
|
+ private IFsExpressService expressService;
|
|
|
+
|
|
|
/**
|
|
|
* 查询积分商品订单
|
|
|
*
|
|
|
@@ -310,6 +336,14 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
if (Objects.isNull(integralGoods)) {
|
|
|
return R.error("商品不存在");
|
|
|
}
|
|
|
+ // 有效期校验:isLongTerm=1 非长期有效时,校验当前时间是否在有效期区间内
|
|
|
+ if (integralGoods.getIsLongTerm() != null && integralGoods.getIsLongTerm() == 1) {
|
|
|
+ Date now = new Date();
|
|
|
+ if (integralGoods.getValidStartTime() == null || integralGoods.getValidEndTime() == null
|
|
|
+ || now.before(integralGoods.getValidStartTime()) || now.after(integralGoods.getValidEndTime())) {
|
|
|
+ return R.error("积分商品已过兑换有效期限");
|
|
|
+ }
|
|
|
+ }
|
|
|
if(integralGoods.getStock()<=0L){
|
|
|
return R.error("库存不足");
|
|
|
}
|
|
|
@@ -317,9 +351,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
return R.error("积分不足");
|
|
|
}
|
|
|
|
|
|
- // 限购校验:limitNum > 0 表示开启限购,校验用户已购数量是否已达上限
|
|
|
+ // 限购校验:limitNum > 0 表示开启限购,读 Redis 计数器(懒加载+降级查库)
|
|
|
if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
|
|
|
- int purchasedCount = getUserPurchasedCount(param.getUserId(), param.getGoodsId());
|
|
|
+ int purchasedCount = getPurchasedCount(param.getUserId(), param.getGoodsId());
|
|
|
if (purchasedCount + 1 > integralGoods.getLimitNum()) {
|
|
|
return R.error("超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
|
|
|
}
|
|
|
@@ -399,6 +433,10 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
}
|
|
|
|
|
|
if(fsIntegralOrderMapper.insertFsIntegralOrder(order)>0){
|
|
|
+ // 批量写入订单商品明细表(与主订单同事务,承接聚水潭回写的溯源码/批次号)
|
|
|
+ List<FsIntegralOrderItem> orderItems = buildOrderItems(order, goodsItem);
|
|
|
+ fsIntegralOrderItemMapper.batchInsert(orderItems);
|
|
|
+
|
|
|
if (order.getPayType() != 2) {
|
|
|
// 原子扣减用户积分(SQL 层 integral = integral - ? where integral >= ?,并发安全)
|
|
|
// 参考 ylrz 商城订单 FsStoreOrderScrmServiceImpl.decIntegral 规范实现
|
|
|
@@ -416,8 +454,10 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
userIntegralLogsService.insertLog(logs);
|
|
|
}
|
|
|
|
|
|
- // 失效限购缓存,下次查询时从数据库重建(保证数据一致性)
|
|
|
- invalidatePurchaseCountCache(order.getUserId(), goodsItem.get(0).getGoodsId());
|
|
|
+ // 限购计数器 +N(遍历所有商品,购物车下单时 goodsItem 可能为多个)
|
|
|
+ for (FsIntegralGoods g : goodsItem) {
|
|
|
+ incrPurchasedCount(order.getUserId(), g.getGoodsId(), g.getNum() == null ? 1 : g.getNum());
|
|
|
+ }
|
|
|
|
|
|
// 积分支付
|
|
|
if (order.getPayType() == 1) {
|
|
|
@@ -443,44 +483,143 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取用户已购买指定积分商品的有效订单数(限购校验用)
|
|
|
- * <p>缓存策略:优先查 Redis,未命中则回源数据库并回填缓存(1小时过期)。
|
|
|
- * 缓存过期后会重新从数据库查询,保证用户取消订单后限购数量能及时更新。</p>
|
|
|
+ * 构建积分订单商品明细列表
|
|
|
+ * <p>从下单时的 goodsItem(List<FsIntegralGoods>)映射为明细实体,
|
|
|
+ * 冗余 isDrug 字段,避免后续商品被修改影响历史订单的药品属性;
|
|
|
+ * 商品快照 jsonInfo 存入图/原价/分类等只读展示字段。</p>
|
|
|
+ *
|
|
|
+ * @param order 积分订单(已含 orderId/orderCode)
|
|
|
+ * @param goodsItem 下单商品集合
|
|
|
+ * @return 明细实体集合
|
|
|
+ */
|
|
|
+ private List<FsIntegralOrderItem> buildOrderItems(FsIntegralOrder order, List<FsIntegralGoods> goodsItem) {
|
|
|
+ List<FsIntegralOrderItem> items = new ArrayList<>();
|
|
|
+ Date now = new Date();
|
|
|
+ for (FsIntegralGoods g : goodsItem) {
|
|
|
+ FsIntegralOrderItem item = new FsIntegralOrderItem();
|
|
|
+ item.setOrderId(order.getOrderId());
|
|
|
+ item.setOrderCode(order.getOrderCode());
|
|
|
+ item.setGoodsId(g.getGoodsId());
|
|
|
+ item.setGoodsName(g.getGoodsName());
|
|
|
+ item.setBarCode(g.getBarCode());
|
|
|
+ item.setNum(g.getNum() != null ? g.getNum() : 1);
|
|
|
+ item.setCash(g.getCash());
|
|
|
+ item.setIsDrug(g.getIsDrug() != null ? g.getIsDrug() : 0);
|
|
|
+ item.setJsonInfo(JSONUtil.toJsonStr(g));
|
|
|
+ item.setCreateTime(now);
|
|
|
+ items.add(item);
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取用户已购买指定积分商品的有效订单数(限购校验用,Redis 计数器方案)
|
|
|
+ * <p>读取 Redis Hash(key=integral:purchase:count:{userId}, hashKey={goodsId}),
|
|
|
+ * 未命中时从数据库懒加载并回填,Redis 不可用时降级查库。</p>
|
|
|
*
|
|
|
* @param userId 用户ID
|
|
|
* @param goodsId 积分商品ID
|
|
|
* @return 已购买有效订单数(非负)
|
|
|
*/
|
|
|
- private int getUserPurchasedCount(Long userId, Long goodsId) {
|
|
|
- String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
|
|
|
- Integer count = redisCache.getCacheObject(redisKey);
|
|
|
- if (count != null) {
|
|
|
- return count;
|
|
|
- }
|
|
|
- // 回源数据库查询有效订单数(排除已取消和已删除)
|
|
|
- count = fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
|
|
|
- if (count < 0) {
|
|
|
- count = 0;
|
|
|
- }
|
|
|
- // 回填 Redis 缓存,1小时过期
|
|
|
- redisCache.setCacheObject(redisKey, count, PURCHASE_COUNT_CACHE_TTL, TimeUnit.HOURS);
|
|
|
- return count;
|
|
|
+ private int getPurchasedCount(Long userId, Long goodsId) {
|
|
|
+ if (userId == null || goodsId == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
|
|
|
+ String hashKey = goodsId.toString();
|
|
|
+ try {
|
|
|
+ Integer count = (Integer) redisTemplateForInteger.opsForHash().get(redisKey, hashKey);
|
|
|
+ if (count != null) {
|
|
|
+ return count;
|
|
|
+ }
|
|
|
+ // 懒加载:Redis 无记录时从 DB 初始化(并发下多次 put 只是覆盖为相同的 DB 值,结果一致)
|
|
|
+ int dbCount = fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
|
|
|
+ redisTemplateForInteger.opsForHash().put(redisKey, hashKey, dbCount);
|
|
|
+ return dbCount;
|
|
|
+ } catch (Exception e) {
|
|
|
+ // Redis 异常降级查库,保证限购功能可用
|
|
|
+ log.warn("限购计数器读取Redis失败,降级查DB userId={}, goodsId={}", userId, goodsId, e);
|
|
|
+ return fsIntegralOrderMapper.selectUserPurchasedCount(userId, goodsId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 增加用户已购商品计数(订单创建成功后调用)
|
|
|
+ * <p>通过 HINCRBY 原子增加,保证高并发下计数准确。</p>
|
|
|
+ *
|
|
|
+ * @param userId 用户ID
|
|
|
+ * @param goodsId 积分商品ID
|
|
|
+ * @param num 本次购买数量
|
|
|
+ */
|
|
|
+ private void incrPurchasedCount(Long userId, Long goodsId, int num) {
|
|
|
+ if (userId == null || goodsId == null || num <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
|
|
|
+ redisTemplateForInteger.opsForHash().increment(redisKey, goodsId.toString(), num);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // Redis 异常不影响主流程,下次限购校验时懒加载会从 DB 重建最新值
|
|
|
+ log.warn("限购计数器incr失败 userId={}, goodsId={}, num={}", userId, goodsId, num, e);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 失效用户购买指定商品的限购缓存
|
|
|
- * <p>订单创建成功后调用,确保下次限购校验时从数据库重新加载最新数据。</p>
|
|
|
+ * 减少用户已购商品计数(取消订单/退款成功后调用)
|
|
|
+ * <p>通过 HINCRBY 原子减少,保证计数与订单终态一致。
|
|
|
+ * 仅在订单变为终态无效(5已取消、-2已退款)时调用。</p>
|
|
|
*
|
|
|
* @param userId 用户ID
|
|
|
* @param goodsId 积分商品ID
|
|
|
+ * @param num 本次退还数量
|
|
|
*/
|
|
|
- private void invalidatePurchaseCountCache(Long userId, Long goodsId) {
|
|
|
+ private void decrPurchasedCount(Long userId, Long goodsId, int num) {
|
|
|
+ if (userId == null || goodsId == null || num <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
try {
|
|
|
- String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId + ":" + goodsId;
|
|
|
- redisCache.deleteObject(redisKey);
|
|
|
+ String redisKey = REDIS_KEY_INTEGRAL_PURCHASE_COUNT + userId;
|
|
|
+ // 使用 increment 传负数实现递减;递减后若计数为负,说明 Redis 与 DB 不一致(如懒加载前旧数据),重置为 0
|
|
|
+ Long newVal = redisTemplateForInteger.opsForHash().increment(redisKey, goodsId.toString(), -num);
|
|
|
+ if (newVal != null && newVal < 0) {
|
|
|
+ redisTemplateForInteger.opsForHash().put(redisKey, goodsId.toString(), 0);
|
|
|
+ }
|
|
|
} catch (Exception e) {
|
|
|
- // 缓存失效失败不影响主流程,下次限购校验时缓存过期后会自动重建
|
|
|
- log.warn("失效限购缓存失败, userId={}, goodsId={}", userId, goodsId, e);
|
|
|
+ // Redis 异常不影响主流程,下次限购校验时懒加载会从 DB 重建最新值
|
|
|
+ log.warn("限购计数器decr失败 userId={}, goodsId={}, num={}", userId, goodsId, num, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 减少订单中所有商品对应的限购计数(订单变为终态无效时调用)
|
|
|
+ * <p>解析 item_json 获取 goodsId 和购买数量,逐一调用 decr。
|
|
|
+ * 兼容 item_json 单对象(旧数据)和数组(新数据)两种格式。</p>
|
|
|
+ *
|
|
|
+ * @param order 积分订单
|
|
|
+ */
|
|
|
+ private void decrPurchasedCountByOrder(FsIntegralOrder order) {
|
|
|
+ if (order == null || order.getUserId() == null || StringUtils.isEmpty(order.getItemJson())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String itemJson = order.getItemJson();
|
|
|
+ try {
|
|
|
+ if (itemJson.startsWith("[") && itemJson.endsWith("]")) {
|
|
|
+ List<FsIntegralGoods> goodsItem = JSONUtil.toBean(itemJson, new TypeReference<List<FsIntegralGoods>>(){}, true);
|
|
|
+ for (FsIntegralGoods goods : goodsItem) {
|
|
|
+ if (goods != null && goods.getGoodsId() != null) {
|
|
|
+ decrPurchasedCount(order.getUserId(), goods.getGoodsId(),
|
|
|
+ goods.getNum() == null ? 1 : goods.getNum());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ FsIntegralGoods goods = JSONUtil.toBean(itemJson, FsIntegralGoods.class);
|
|
|
+ if (goods != null && goods.getGoodsId() != null) {
|
|
|
+ decrPurchasedCount(order.getUserId(), goods.getGoodsId(),
|
|
|
+ goods.getNum() == null ? 1 : goods.getNum());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("订单限购计数decr失败, orderId={}, itemJson={}", order.getOrderId(), itemJson, e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -518,10 +657,27 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
if (Objects.isNull(integralGoods)) {
|
|
|
throw new CustomException("商品不存在");
|
|
|
}
|
|
|
+ // 有效期校验:非长期有效时,校验当前时间是否在有效期区间内
|
|
|
+ if (integralGoods.getIsLongTerm() != null && integralGoods.getIsLongTerm() == 1) {
|
|
|
+ Date now = new Date();
|
|
|
+ if (integralGoods.getValidStartTime() == null || integralGoods.getValidEndTime() == null
|
|
|
+ || now.before(integralGoods.getValidStartTime()) || now.after(integralGoods.getValidEndTime())) {
|
|
|
+ throw new CustomException("积分商品「" + integralGoods.getGoodsName() + "」已过兑换有效期限");
|
|
|
+ }
|
|
|
+ }
|
|
|
if(integralGoods.getStock() < cart.getCartNum()){
|
|
|
throw new CustomException("库存不足");
|
|
|
}
|
|
|
|
|
|
+ // 限购校验:limitNum > 0 表示开启限购,读 Redis 计数器(懒加载+降级查库)
|
|
|
+ if (integralGoods.getLimitNum() != null && integralGoods.getLimitNum() > 0) {
|
|
|
+ int purchasedCount = getPurchasedCount(param.getUserId(), cart.getGoodsId());
|
|
|
+ if (purchasedCount + cart.getCartNum() > integralGoods.getLimitNum()) {
|
|
|
+ throw new CustomException("商品「" + integralGoods.getGoodsName()
|
|
|
+ + "」超过限购数量,每人限购" + integralGoods.getLimitNum() + "件");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// 减库存
|
|
|
if (fsIntegralGoodsMapper.subStock(integralGoods.getGoodsId(), cart.getCartNum()) <= 0) {
|
|
|
throw new CustomException("库存不足");
|
|
|
@@ -534,7 +690,15 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
goodsItem.add(integralGoods);
|
|
|
}
|
|
|
|
|
|
- return createOrder(user, address, totalIntegral, totalCash, goodsItem, null);
|
|
|
+ R result = createOrder(user, address, totalIntegral, totalCash, goodsItem, null);
|
|
|
+ // 订单创建成功后,清除购物车中已下单的商品(限购计数器 +N 已在 createOrder 内部完成)
|
|
|
+ Object codeObj = result == null ? null : result.get("code");
|
|
|
+ if (codeObj != null && Integer.valueOf(200).equals(codeObj)) {
|
|
|
+ // 清除购物车中已下单的商品(按购物车ID删除,避免误删同商品的其他购物车记录)
|
|
|
+ List<Long> cartIds = carts.stream().map(FsIntegralCart::getId).collect(Collectors.toList());
|
|
|
+ cartService.delCartByIds(user.getUserId(), cartIds);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -607,7 +771,12 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
|
|
|
@Override
|
|
|
public FsIntegralOrderPVO selectFsIntegralOrderPVO(Long orderId) {
|
|
|
- return fsIntegralOrderMapper.selectFsIntegralOrderPVO(orderId);
|
|
|
+ FsIntegralOrderPVO pvo = fsIntegralOrderMapper.selectFsIntegralOrderPVO(orderId);
|
|
|
+ if (pvo != null) {
|
|
|
+ // 填充订单商品明细(含溯源码/批次号/是否药品)
|
|
|
+ pvo.setItemList(fsIntegralOrderItemMapper.selectListByOrderId(orderId));
|
|
|
+ }
|
|
|
+ return pvo;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -649,6 +818,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
logs.setCreateTime(new Date());
|
|
|
userIntegralLogsService.insertLog(logs);
|
|
|
|
|
|
+ // 订单取消后,限购计数器 -N(status=5不计入限购),保证计数与订单终态一致
|
|
|
+ decrPurchasedCountByOrder(order);
|
|
|
+
|
|
|
return R.ok();
|
|
|
}
|
|
|
|
|
|
@@ -939,6 +1111,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
// 5. 无需等待回调的场景(纯积分订单无支付记录、或易宝退款立即成功),直接标记为已退款(-2)
|
|
|
if (!needCallback) {
|
|
|
fsIntegralOrderMapper.refundOrder(order.getOrderId());
|
|
|
+ // 订单已变为已退款(-2)终态,限购计数器 -N(status=-2不计入限购),保证计数与订单终态一致
|
|
|
+ decrPurchasedCountByOrder(order);
|
|
|
}
|
|
|
|
|
|
return 1;
|
|
|
@@ -1201,6 +1375,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
int rows = fsIntegralOrderMapper.refundOrder(orderId);
|
|
|
if (rows > 0) {
|
|
|
log.info("积分订单退款回调(Yop) 更新订单状态完成 orderId: {}, status: -1->-2", orderId);
|
|
|
+ // 订单已变为已退款(-2)终态,限购计数器 -N(status=-2不计入限购),保证计数与订单终态一致
|
|
|
+ decrPurchasedCountByOrder(order);
|
|
|
} else {
|
|
|
log.warn("积分订单退款回调(Yop) 更新订单状态失败(可能已被其他流程处理) orderId: {}", orderId);
|
|
|
}
|
|
|
@@ -1764,4 +1940,316 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
|
|
|
default: return "积分兑换";
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 聚水潭同步发货(单订单回写物流信息)
|
|
|
+ * <p>流程:更新订单状态为待收货(2) → 订阅物流回调 → 微信小程序发货同步。</p>
|
|
|
+ * <p>与普通订单({@link com.fs.hisStore.service.impl.FsStoreOrderScrmServiceImpl#deliveryOrder})逻辑对齐,
|
|
|
+ * 区别:积分订单 deliveryCode=快递公司编码、deliverySn=快递单号、无订单状态日志表、无模板消息。</p>
|
|
|
+ *
|
|
|
+ * @param order 积分订单
|
|
|
+ * @param trackingNo 快递单号(聚水潭 lId / mail_no)
|
|
|
+ * @param expressCode 快递公司OMS编码(聚水潭 lcId)
|
|
|
+ * @param expressName 快递公司名称(聚水潭 logisticsCompany)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void deliveryOrderBySync(FsIntegralOrder order, String trackingNo, String expressCode, String expressName) {
|
|
|
+ try {
|
|
|
+ // 1. 通过OMS编码映射本地快递公司编码
|
|
|
+ String localExpressCode = expressCode;
|
|
|
+ String localExpressName = expressName;
|
|
|
+ FsExpress express = expressService.selectFsExpressByOmsCode(expressCode);
|
|
|
+ if (express != null) {
|
|
|
+ localExpressCode = express.getCode();
|
|
|
+ if (StringUtils.isNotEmpty(express.getName())) {
|
|
|
+ localExpressName = express.getName();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 回写订单物流信息,状态 → 2 待收货
|
|
|
+ FsIntegralOrder updateOrder = new FsIntegralOrder();
|
|
|
+ updateOrder.setOrderId(order.getOrderId());
|
|
|
+ updateOrder.setStatus(2);
|
|
|
+ updateOrder.setDeliveryCode(localExpressCode);
|
|
|
+ updateOrder.setDeliveryName(localExpressName);
|
|
|
+ updateOrder.setDeliverySn(trackingNo);
|
|
|
+ updateOrder.setDeliveryTime(DateUtils.getNowDate());
|
|
|
+ fsIntegralOrderMapper.updateFsIntegralOrder(updateOrder);
|
|
|
+ log.info("聚水潭同步发货回写成功 orderId={}, orderCode={}, trackingNo={}, expressCode={}",
|
|
|
+ order.getOrderId(), order.getOrderCode(), trackingNo, localExpressCode);
|
|
|
+
|
|
|
+ // 3. 订阅物流回调(顺丰需要校验手机号后四位)
|
|
|
+ String lastFourNumber = "";
|
|
|
+ if (ShipperCodeEnum.SF.getValue().equals(localExpressCode)) {
|
|
|
+ lastFourNumber = order.getUserPhone();
|
|
|
+ if (StringUtils.isNotEmpty(lastFourNumber) && lastFourNumber.length() == 11) {
|
|
|
+ lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ expressService.subscribeEspress(order.getOrderCode(), localExpressCode, trackingNo, lastFourNumber);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("订阅物流回调异常 orderCode={}, trackingNo={}", order.getOrderCode(), trackingNo, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 微信小程序发货同步(线上发货)
|
|
|
+ syncWeChatShippingOnline(order, trackingNo, localExpressCode);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("聚水潭同步发货异常 orderId={}, orderCode={}", order.getOrderId(), order.getOrderCode(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 同步微信发货(线上发货,logisticsType=1)
|
|
|
+ * <p>复用 fs_store_payment 中的 appId/openId/bankTransactionId 调用微信小程序发货信息同步接口。</p>
|
|
|
+ *
|
|
|
+ * @param order 积分订单
|
|
|
+ * @param trackingNo 快递单号
|
|
|
+ * @param expressCode 本地快递公司编码
|
|
|
+ */
|
|
|
+ private void syncWeChatShippingOnline(FsIntegralOrder order, String trackingNo, String expressCode) {
|
|
|
+ try {
|
|
|
+ List<FsStorePayment> payments = fsStorePaymentMapper.selectFsStorePaymentByIntegralOrderId(order.getOrderId());
|
|
|
+ FsStorePayment payment = null;
|
|
|
+ if (payments != null && !payments.isEmpty()) {
|
|
|
+ for (FsStorePayment p : payments) {
|
|
|
+ if (p.getStatus() != null && p.getStatus() == 1
|
|
|
+ && StringUtils.isNotEmpty(p.getBankTransactionId())
|
|
|
+ && StringUtils.isNotEmpty(p.getOpenId())
|
|
|
+ && StringUtils.isNotEmpty(p.getAppId())) {
|
|
|
+ payment = p;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (payment == null) {
|
|
|
+ log.info("微信发货同步跳过,未找到有效支付记录 orderCode={}", order.getOrderCode());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 取首个商品名称作为物品描述
|
|
|
+ String itemDesc = "积分商品";
|
|
|
+ if (StringUtils.isNotEmpty(order.getItemJson())) {
|
|
|
+ try {
|
|
|
+ cn.hutool.json.JSONArray itemArray = JSONUtil.parseArray(order.getItemJson());
|
|
|
+ if (!itemArray.isEmpty()) {
|
|
|
+ String goodsName = itemArray.getJSONObject(0).getStr("goodsName");
|
|
|
+ if (StringUtils.isNotEmpty(goodsName)) {
|
|
|
+ itemDesc = goodsName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignore) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String uploadTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"))
|
|
|
+ .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
|
+ WxMaService wxService = WxMaConfiguration.getMaService(payment.getAppId());
|
|
|
+ uploadSingleShippingInfoOnline(wxService, payment, order, trackingNo, expressCode, itemDesc, uploadTime);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("微信发货同步异常 orderCode={}", order.getOrderCode(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传单个订单物流信息到微信(线上发货,logisticsType=1)
|
|
|
+ */
|
|
|
+ private boolean uploadSingleShippingInfoOnline(WxMaService wxService,
|
|
|
+ FsStorePayment payment,
|
|
|
+ FsIntegralOrder order,
|
|
|
+ String trackingNo,
|
|
|
+ String expressCode,
|
|
|
+ String itemDesc,
|
|
|
+ String uploadTime) {
|
|
|
+ try {
|
|
|
+ WxMaOrderShippingInfoUploadRequest request = new WxMaOrderShippingInfoUploadRequest();
|
|
|
+ OrderKeyBean orderKeyBean = new OrderKeyBean();
|
|
|
+ orderKeyBean.setOrderNumberType(2);
|
|
|
+ orderKeyBean.setTransactionId(payment.getBankTransactionId());
|
|
|
+ request.setOrderKey(orderKeyBean);
|
|
|
+ request.setDeliveryMode(1);
|
|
|
+ request.setLogisticsType(1);
|
|
|
+
|
|
|
+ List<ShippingListBean> shippingList = new ArrayList<>();
|
|
|
+ ShippingListBean shippingListBean = new ShippingListBean();
|
|
|
+ shippingListBean.setTrackingNo(trackingNo);
|
|
|
+ shippingListBean.setExpressCompany(expressCode);
|
|
|
+ shippingListBean.setItemDesc(StringUtils.isEmpty(itemDesc) ? "积分商品" : itemDesc);
|
|
|
+ ContactBean contactBean = new ContactBean();
|
|
|
+ contactBean.setReceiverContact(order.getUserPhone());
|
|
|
+ shippingListBean.setContact(contactBean);
|
|
|
+ shippingList.add(shippingListBean);
|
|
|
+ request.setShippingList(shippingList);
|
|
|
+ request.setUploadTime(uploadTime);
|
|
|
+
|
|
|
+ PayerBean payerBean = new PayerBean();
|
|
|
+ payerBean.setOpenid(payment.getOpenId());
|
|
|
+ request.setPayer(payerBean);
|
|
|
+
|
|
|
+ boolean success = wxService.getWxMaOrderShippingService().upload(request).getErrCode() == 0;
|
|
|
+ if (!success) {
|
|
|
+ log.warn("上传物流信息到微信返回非0 orderCode={}", order.getOrderCode());
|
|
|
+ }
|
|
|
+ return success;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("上传物流信息到微信失败 orderCode={}", order.getOrderCode(), e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量同步聚水潭溯源码和批次号
|
|
|
+ * <p>查询聚水潭销售出库单,按 sku_id(即 barCode)匹配积分订单明细,回写 verifyCode 和 batchNumber。</p>
|
|
|
+ * <p>与普通订单({@link com.fs.hisStore.service.impl.FsStoreOrderScrmServiceImpl#syncSkuSnFromJstBatch})逻辑对齐。</p>
|
|
|
+ *
|
|
|
+ * @param orders 已发货的积分订单集合
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void syncSkuSnFromJstBatch(List<FsIntegralOrder> orders) {
|
|
|
+ try {
|
|
|
+ FsSysConfig erpConfig = configUtil.getSysConfig();
|
|
|
+ if (erpConfig.getErpOpen() == null || erpConfig.getErpOpen() != 1
|
|
|
+ || erpConfig.getErpType() == null || erpConfig.getErpType() != 5) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 过滤有效订单(必须有extendOrderId)
|
|
|
+ List<FsIntegralOrder> validOrders = new ArrayList<>();
|
|
|
+ for (FsIntegralOrder order : orders) {
|
|
|
+ if (StringUtils.isNotEmpty(order.getExtendOrderId())) {
|
|
|
+ validOrders.add(order);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (validOrders.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量查询所有订单明细
|
|
|
+ List<Long> orderIds = new ArrayList<>();
|
|
|
+ for (FsIntegralOrder order : validOrders) {
|
|
|
+ orderIds.add(order.getOrderId());
|
|
|
+ }
|
|
|
+ List<FsIntegralOrderItem> allItems = fsIntegralOrderItemMapper.selectListByOrderIds(orderIds);
|
|
|
+ Map<Long, List<FsIntegralOrderItem>> orderItemsMap = new HashMap<>();
|
|
|
+ if (allItems != null) {
|
|
|
+ for (FsIntegralOrderItem item : allItems) {
|
|
|
+ orderItemsMap.computeIfAbsent(item.getOrderId(), k -> new ArrayList<>()).add(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 收集所有extendOrderId,按最多50个一组分批查询销售出库单
|
|
|
+ List<String> allOIds = new ArrayList<>();
|
|
|
+ Map<String, FsIntegralOrder> extendOrderMap = new HashMap<>();
|
|
|
+ for (FsIntegralOrder order : validOrders) {
|
|
|
+ allOIds.add(order.getExtendOrderId());
|
|
|
+ extendOrderMap.put(order.getExtendOrderId(), order);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 分批查询出库单(o_ids最多50条)
|
|
|
+ int batchSize = 50;
|
|
|
+ List<OutboundOrderQueryResponseDTO.OutboundOrder> allOutboundOrders = new ArrayList<>();
|
|
|
+ for (int i = 0; i < allOIds.size(); i += batchSize) {
|
|
|
+ List<String> batchOIds = allOIds.subList(i, Math.min(i + batchSize, allOIds.size()));
|
|
|
+
|
|
|
+ int pageIndex = 1;
|
|
|
+ while (true) {
|
|
|
+ OutboundOrderQueryRequestDTO requestDTO = OutboundOrderQueryRequestDTO.builder()
|
|
|
+ .oIds(batchOIds)
|
|
|
+ .status("Confirmed")
|
|
|
+ .pageIndex(pageIndex)
|
|
|
+ .pageSize(50)
|
|
|
+ .build();
|
|
|
+ OutboundOrderQueryResponseDTO response = jstErpHttpService.queryOutboundOrder(requestDTO);
|
|
|
+ if (response == null || response.getData() == null
|
|
|
+ || response.getData().getDatas() == null || response.getData().getDatas().isEmpty()) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ allOutboundOrders.addAll(response.getData().getDatas());
|
|
|
+ if (!Boolean.TRUE.equals(response.getData().getHasNext())) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ pageIndex++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (allOutboundOrders.isEmpty()) {
|
|
|
+ log.info("聚水潭销售出库批量查询无数据,积分订单数: {}", validOrders.size());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按o_id分组出库单
|
|
|
+ Map<String, List<OutboundOrderQueryResponseDTO.OutboundOrder>> outboundByOId = new HashMap<>();
|
|
|
+ for (OutboundOrderQueryResponseDTO.OutboundOrder outOrder : allOutboundOrders) {
|
|
|
+ if (outOrder.getOId() != null) {
|
|
|
+ String oIdStr = String.valueOf(outOrder.getOId());
|
|
|
+ outboundByOId.computeIfAbsent(oIdStr, k -> new ArrayList<>()).add(outOrder);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 遍历每个订单,从出库单中提取溯源码和批次号
|
|
|
+ for (FsIntegralOrder order : validOrders) {
|
|
|
+ List<OutboundOrderQueryResponseDTO.OutboundOrder> matchedOutOrders = outboundByOId.get(order.getExtendOrderId());
|
|
|
+ if (matchedOutOrders == null || matchedOutOrders.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<FsIntegralOrderItem> items = orderItemsMap.get(order.getOrderId());
|
|
|
+ if (items == null || items.isEmpty()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从出库单中提取: sns[]→溯源码, batchs[]→批次号,按sku_id分组
|
|
|
+ Map<String, StringBuilder> skuSnMap = new HashMap<>();
|
|
|
+ Map<String, StringBuilder> batchMap = new HashMap<>();
|
|
|
+
|
|
|
+ for (OutboundOrderQueryResponseDTO.OutboundOrder outOrder : matchedOutOrders) {
|
|
|
+ // 提取唯一码(sns)
|
|
|
+ if (outOrder.getSns() != null) {
|
|
|
+ for (OutboundOrderQueryResponseDTO.SnInfo snInfo : outOrder.getSns()) {
|
|
|
+ String skuId = snInfo.getSkuId();
|
|
|
+ if (StringUtils.isNotEmpty(skuId) && StringUtils.isNotEmpty(snInfo.getSn())) {
|
|
|
+ skuSnMap.computeIfAbsent(skuId, k -> new StringBuilder());
|
|
|
+ if (skuSnMap.get(skuId).length() > 0) {
|
|
|
+ skuSnMap.get(skuId).append(",");
|
|
|
+ }
|
|
|
+ skuSnMap.get(skuId).append(snInfo.getSn());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 提取批次号(batchs)
|
|
|
+ if (outOrder.getBatchs() != null) {
|
|
|
+ for (OutboundOrderQueryResponseDTO.BatchInfo batchInfo : outOrder.getBatchs()) {
|
|
|
+ String skuId = batchInfo.getSkuId();
|
|
|
+ String batchNo = batchInfo.getBatchNo();
|
|
|
+ if (StringUtils.isNotEmpty(skuId) && StringUtils.isNotEmpty(batchNo)) {
|
|
|
+ batchMap.computeIfAbsent(skuId, k -> new StringBuilder());
|
|
|
+ if (batchMap.get(skuId).length() > 0 && !batchMap.get(skuId).toString().contains(batchNo)) {
|
|
|
+ batchMap.get(skuId).append(",");
|
|
|
+ batchMap.get(skuId).append(batchNo);
|
|
|
+ } else if (batchMap.get(skuId).length() == 0) {
|
|
|
+ batchMap.get(skuId).append(batchNo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通过barCode匹配sku_id,回写溯源码和批次号
|
|
|
+ for (FsIntegralOrderItem item : items) {
|
|
|
+ String barCode = item.getBarCode();
|
|
|
+ if (StringUtils.isNotEmpty(barCode)) {
|
|
|
+ String verifyCode = skuSnMap.containsKey(barCode) ? skuSnMap.get(barCode).toString() : null;
|
|
|
+ String batchNumber = batchMap.containsKey(barCode) ? batchMap.get(barCode).toString() : null;
|
|
|
+ if (StringUtils.isNotEmpty(verifyCode) || StringUtils.isNotEmpty(batchNumber)) {
|
|
|
+ fsIntegralOrderItemMapper.updateVerifyCodeAndBatch(item.getItemId(), verifyCode, batchNumber);
|
|
|
+ log.info("积分订单明细溯源码同步成功 orderCode={}, barCode={}, verifyCode={}, batchNumber={}",
|
|
|
+ order.getOrderCode(), barCode, verifyCode, batchNumber);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("批量同步聚水潭溯源码失败(积分订单)", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|