Explorar o código

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin hai 2 días
pai
achega
5e9692dff2
Modificáronse 37 ficheiros con 592 adicións e 189 borrados
  1. 13 2
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  2. 1 1
      fs-admin/src/main/java/com/fs/third/controller/WeizouController.java
  3. 3 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  4. 8 15
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  5. 6 11
      fs-live-ws/src/main/java/com/fs/live/ws/service/impl/LiveWsAdminMessageHandler.java
  6. 5 10
      fs-live-ws/src/main/java/com/fs/live/ws/service/impl/LiveWsAutoTaskHandler.java
  7. 1 1
      fs-service/src/main/java/com/fs/enums/WeizouOrderExpressEnum.java
  8. 18 9
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  9. 21 10
      fs-service/src/main/java/com/fs/hisStore/mapper/FsShippingTemplatesRegionScrmMapper.java
  10. 2 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  11. 4 3
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  12. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java
  13. 3 0
      fs-service/src/main/java/com/fs/live/constant/LiveOrderOptConstants.java
  14. 10 1
      fs-service/src/main/java/com/fs/live/domain/LiveCoupon.java
  15. 4 4
      fs-service/src/main/java/com/fs/live/domain/LiveCouponIssue.java
  16. 4 4
      fs-service/src/main/java/com/fs/live/domain/LiveCouponUser.java
  17. 2 0
      fs-service/src/main/java/com/fs/live/dto/LiveOrderComputeDTO.java
  18. 5 3
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponMapper.java
  19. 19 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java
  20. 17 0
      fs-service/src/main/java/com/fs/live/mq/LiveGoodsRocketMQTemplate.java
  21. 2 2
      fs-service/src/main/java/com/fs/live/param/LiveCouponPublishParam.java
  22. 27 0
      fs-service/src/main/java/com/fs/live/service/ILiveCouponService.java
  23. 10 0
      fs-service/src/main/java/com/fs/live/service/ILiveCouponUserService.java
  24. 150 3
      fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java
  25. 16 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveCouponUserServiceImpl.java
  26. 71 61
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderOptServiceImpl.java
  27. 52 20
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  28. 2 2
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  29. 2 16
      fs-service/src/main/java/com/fs/store/service/impl/FsUserCourseCountServiceImpl.java
  30. 8 0
      fs-service/src/main/resources/application-druid-bjzm-test.yml
  31. 8 0
      fs-service/src/main/resources/application-druid-bjzm.yml
  32. 1 0
      fs-service/src/main/resources/mapper/live/LiveCouponUserMapper.xml
  33. 9 4
      fs-service/src/main/resources/mapper/store/FsUserCourseCountMapper.xml
  34. 7 3
      fs-user-app/src/main/java/com/fs/app/controller/CommonController.java
  35. 21 1
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveController.java
  36. 43 0
      fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveCouponController.java
  37. 14 1
      fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

+ 13 - 2
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -371,9 +371,20 @@ public class LiveTask {
 
     }
 
+    /**
+     * 直播优惠券过期处理:仅批量查询「未核销且已过有效期」的数据并标记过期,
+     * 已核销/已过期数据不参与查询,降低 IO。
+     * 每小时整点执行;也可在调度中心配置调用 liveTask.couponOp
+     */
     public void couponOp() {
-        // 直播优惠券过期处理,如果有对应方法则调用
-        // liveCouponService.updateFsCouponByExpire();
+        try {
+            int count = liveCouponService.expireLiveCouponUsersBatch(500);
+            if (count > 0) {
+                log.info("直播优惠券过期处理完成,本次失效 {} 条", count);
+            }
+        } catch (Exception e) {
+            log.error("直播优惠券过期处理异常", e);
+        }
     }
 
     //退款自动处理 24小时未审核自动审核通过 每小时执行一次

+ 1 - 1
fs-admin/src/main/java/com/fs/third/controller/WeizouController.java

@@ -69,7 +69,7 @@ public class WeizouController extends BaseController {
         if (map.get("orderCode") == null) {
             return error("参数异常请核验参数");
         }
-        fsStoreOrder.setOrderId(Long.valueOf((String) map.get("orderCode")));
+        fsStoreOrder.setOrderCode(((String) map.get("orderCode")));
         //取消订单
         if (map.get("operation") == null)return error("参数异常请核验参数:operator不能为空");
         fsStoreOrder.setOperator((String) map.get("operation"));

+ 3 - 0
fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java

@@ -16,6 +16,9 @@ public class LiveKeysConstant {
     public static final Integer LIVE_HOME_PAGE_LIST_EXPIRE = 300; //首页缓存过期时间
     public static final String LIVE_WATCH_USERS = "live:watch:users:%s"; //在线人数
     public static final String LIVE_COUPON_NUM = "live:coupon:num:%s"; //直播间优惠券数量
+    /** 当前直播间正在展示的优惠券活动(WS 开启且剩余数量>0 时写入) */
+    public static final String LIVE_CURRENT_COUPON = "live:current:coupon:%s";
+    public static final Integer LIVE_CURRENT_COUPON_EXPIRE = 30; // 分钟
 
     public static final String LIVE_HOME_PAGE_DETAIL = "live:detail:%s"; //直播间详情
     public static final Integer LIVE_HOME_PAGE_DETAIL_EXPIRE = 300; //直播间详情过期时间

+ 8 - 15
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -109,6 +109,7 @@ public class WebSocketServer {
     private final ILiveGoodsService liveGoodsService =  SpringUtils.getBean(ILiveGoodsService.class);
     private final ILiveUserFirstEntryService liveUserFirstEntryService =  SpringUtils.getBean(ILiveUserFirstEntryService.class);
     private final ILiveCouponIssueService liveCouponIssueService =  SpringUtils.getBean(ILiveCouponIssueService.class);
+    private final ILiveCouponService liveCouponService = SpringUtils.getBean(ILiveCouponService.class);
     private final LiveCouponMapper liveCouponMapper = SpringUtils.getBean(LiveCouponMapper.class);
     private final ILiveWatchLogService liveWatchLogService = SpringUtils.getBean(ILiveWatchLogService.class);
     private final ILiveVideoService liveVideoService = SpringUtils.getBean(ILiveVideoService.class);
@@ -772,17 +773,12 @@ public class WebSocketServer {
         Integer status = jsonObject.getInteger("status");
         msg.setStatus(status);
         Long couponIssueId = jsonObject.getLong("couponIssueId");
-        // ①  检查  缓存是否存在  ② 如果是发布 放入缓存 ③ 删除缓存
-        if (status == 1) {
-            Object cacheObject = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
-            if (cacheObject == null) {
-                LiveCouponIssue liveCoupon = liveCouponIssueService.selectLiveCouponIssueById(couponIssueId);
-                if (liveCoupon != null) {
-                    redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId), liveCoupon.getRemainCount().intValue(), 30, TimeUnit.MINUTES);
-                }
+        // 开启且剩余数量>0 时写入当前展示券缓存,供 currentActivities 补偿查询
+        liveCouponService.syncCurrentCouponCache(liveId, couponIssueId, status, jsonObject);
+        if (status == null || status != 1) {
+            if (couponIssueId != null) {
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, couponIssueId));
             }
-        } else {
-            redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , couponIssueId));
         }
         // 管理员消息插队
         enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
@@ -1352,20 +1348,17 @@ public class WebSocketServer {
                 }
                 LiveCouponIssue liveCouponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(liveCoupon.getCouponId());
                 LiveCouponIssueRelation relation = liveCouponMapper.selectCouponRelation(task.getLiveId(), liveCouponIssue.getId());
-                if (liveCoupon != null) {
-                    redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , liveCouponIssue.getId()), liveCouponIssue.getRemainCount().intValue(), 30, TimeUnit.MINUTES);
-                }
                 HashMap<String, Object> data = new HashMap<>();
                 data.put("liveId", task.getLiveId());
                 data.put("couponIssueId", liveCouponIssue.getId());
                 data.put("status", 1);
-                data.put("goodsId", relation.getGoodsId());
+                data.put("goodsId", relation != null ? relation.getGoodsId() : null);
                 data.put("couponName", liveCoupon.getTitle());
                 data.put("couponPrice", liveCoupon.getCouponPrice());
                 data.put("useMinPrice", liveCoupon.getUseMinPrice());
                 data.put("couponTime", liveCoupon.getCouponTime());
+                liveCouponService.syncCurrentCouponCache(task.getLiveId(), liveCouponIssue.getId(), 1, data);
                 msg.setData(JSON.toJSONString(data));
-                liveCouponMapper.updateChangeShow(task.getLiveId(), liveCouponIssue.getId());
             } else if (task.getTaskType() == 6L) {
                 // 上架/下架商品
                 msg.setCmd("goods");

+ 6 - 11
fs-live-ws/src/main/java/com/fs/live/ws/service/impl/LiveWsAdminMessageHandler.java

@@ -52,7 +52,7 @@ public class LiveWsAdminMessageHandler {
     @Autowired
     private ILiveLotteryConfService liveLotteryConfService;
     @Autowired
-    private ILiveCouponIssueService liveCouponIssueService;
+    private ILiveCouponService liveCouponService;
     @Autowired
     private RedisCache redisCache;
     @Autowired(required = false)
@@ -304,17 +304,12 @@ public class LiveWsAdminMessageHandler {
         Integer status = jsonObject.getInteger("status");
         msg.setStatus(status);
         Long couponIssueId = jsonObject.getLong("couponIssueId");
-        if (status != null && status == 1) {
-            Object cacheObject = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, couponIssueId));
-            if (cacheObject == null && couponIssueId != null) {
-                com.fs.live.domain.LiveCouponIssue liveCoupon = liveCouponIssueService.selectLiveCouponIssueById(couponIssueId);
-                if (liveCoupon != null) {
-                    redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, couponIssueId),
-                            liveCoupon.getRemainCount().intValue(), 30, TimeUnit.MINUTES);
-                }
+        // 开启且剩余数量>0 时写入当前展示券缓存,供 currentActivities 补偿查询
+        liveCouponService.syncCurrentCouponCache(liveId, couponIssueId, status, jsonObject);
+        if (status == null || status != 1) {
+            if (couponIssueId != null) {
+                redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, couponIssueId));
             }
-        } else if (couponIssueId != null) {
-            redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, couponIssueId));
         }
         broadcastAdmin(liveId, msg);
     }

+ 5 - 10
fs-live-ws/src/main/java/com/fs/live/ws/service/impl/LiveWsAutoTaskHandler.java

@@ -2,9 +2,7 @@ package com.fs.live.ws.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
-import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.domain.R;
-import com.fs.common.core.redis.RedisCache;
 import com.fs.live.domain.*;
 import com.fs.live.mapper.LiveCouponMapper;
 import com.fs.live.service.*;
@@ -18,7 +16,6 @@ import org.springframework.stereotype.Service;
 
 import java.util.Date;
 import java.util.HashMap;
-import java.util.concurrent.TimeUnit;
 
 /**
  * 直播自动任务(商品/红包/抽奖/消息/优惠券)处理,迁移自 fs-live-app WebSocketServer#handleAutoTask。
@@ -43,9 +40,9 @@ public class LiveWsAutoTaskHandler {
     @Autowired
     private ILiveCouponIssueService liveCouponIssueService;
     @Autowired
-    private LiveCouponMapper liveCouponMapper;
+    private ILiveCouponService liveCouponService;
     @Autowired
-    private RedisCache redisCache;
+    private LiveCouponMapper liveCouponMapper;
 
     public void handle(LiveAutoTask task) {
         WsSendMsgVo msg = new WsSendMsgVo();
@@ -104,20 +101,18 @@ public class LiveWsAutoTaskHandler {
                 }
                 LiveCouponIssue liveCouponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(liveCoupon.getCouponId());
                 LiveCouponIssueRelation relation = liveCouponMapper.selectCouponRelation(task.getLiveId(), liveCouponIssue.getId());
-                redisCache.setCacheObject(
-                        String.format(LiveKeysConstant.LIVE_COUPON_NUM, liveCouponIssue.getId()),
-                        liveCouponIssue.getRemainCount().intValue(), 30, TimeUnit.MINUTES);
                 HashMap<String, Object> data = new HashMap<>();
                 data.put("liveId", task.getLiveId());
                 data.put("couponIssueId", liveCouponIssue.getId());
                 data.put("status", 1);
-                data.put("goodsId", relation.getGoodsId());
+                data.put("goodsId", relation != null ? relation.getGoodsId() : null);
                 data.put("couponName", liveCoupon.getTitle());
                 data.put("couponPrice", liveCoupon.getCouponPrice());
                 data.put("useMinPrice", liveCoupon.getUseMinPrice());
                 data.put("couponTime", liveCoupon.getCouponTime());
+                // 剩余数量>0 时写入当前展示券缓存,供进房 currentActivities 补偿
+                liveCouponService.syncCurrentCouponCache(task.getLiveId(), liveCouponIssue.getId(), 1, data);
                 msg.setData(JSON.toJSONString(data));
-                liveCouponMapper.updateChangeShow(task.getLiveId(), liveCouponIssue.getId());
             } else if (task.getTaskType() == 6L) {
                 msg.setCmd("goods");
                 JSONObject jsonObject = JSON.parseObject(task.getContent());

+ 1 - 1
fs-service/src/main/java/com/fs/enums/WeizouOrderExpressEnum.java

@@ -24,7 +24,7 @@ public enum WeizouOrderExpressEnum {
 
     SIGNED("10","已签收"),
 
-    CANCELED("11","已取消");
+    CANCELED("11","已签收");//2026年7月21日变更
 
     private final String code;
     private final String desc;

+ 18 - 9
fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java

@@ -4839,7 +4839,7 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             FsStoreOrder o = (FsStoreOrder) order;
             if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
                 fsStoreOrder.setOrderId(o.getOrderId());
-                updateOrderToCancelled(fsStoreOrder);
+                updateOrderToCancelled(o);
             }
             return this::saveOrderLog;
         }
@@ -4847,14 +4847,14 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             FsStoreOrderScrm o = (FsStoreOrderScrm) order;
             if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
                 fsStoreOrder.setOrderId(o.getId());
-                updateOrderToCancelledScrm(fsStoreOrder);
+                updateOrderToCancelledScrm(o);
             }
             return this::saveOrderLogScrm;
         }
         FsIntegralOrder o = (FsIntegralOrder) order;
         if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
             fsStoreOrder.setOrderId(o.getOrderId());
-            updateOrderToCancelledIntegral(fsStoreOrder);
+            updateOrderToCancelledIntegral(o);
         }
         return this::saveOrderLogIntegral;
     }
@@ -4865,7 +4865,10 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             if (o.getStatus() == STATUS_SHIPPED) {
                 fsStoreOrder.setOrderId(o.getOrderId());
                 if (fsStoreOrder.getOperator().equals(WeizouOrderExpressEnum.CANCELED.getCode())){
-                    updateOrderToEvaluated(fsStoreOrder,STATUS_TO_EVALUATED);
+                    o.setStatus(STATUS_TO_EVALUATED);
+                    o.setUpdateTime(new DateTime());
+                    o.setDeliveryTime(getCurrentDateTimeString());
+                    fsStoreOrderMapper.updateFsStoreOrder(o);
                 }
             }
             return this::saveOrderLog;
@@ -4875,7 +4878,10 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
                 fsStoreOrder.setOrderId(o.getId());
                 if (fsStoreOrder.getOperator().equals(WeizouOrderExpressEnum.CANCELED.getCode())){
-                    updateOrderToEvaluated(fsStoreOrder,STATUS_SHIPPED);
+                    o.setStatus(STATUS_SHIPPED);
+                    o.setUpdateTime(new DateTime());
+                    o.setDeliveryTime(getCurrentDateTimeString());
+                    fsStoreOrderScrmMapper.updateFsStoreOrder(o);
                 }
             }
             return this::saveOrderLogScrm;
@@ -4884,7 +4890,10 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
         if (o.getStatus() == STATUS_PENDING_SHIPMENT) {
             fsStoreOrder.setOrderId(o.getOrderId());
             if (fsStoreOrder.getOperator().equals(WeizouOrderExpressEnum.CANCELED.getCode())){
-                updateOrderToEvaluated(fsStoreOrder,STATUS_SHIPPED);
+                o.setStatus(STATUS_SHIPPED);
+                o.setUpdateTime(new DateTime());
+                o.setDeliveryTime(new Date());
+                fsIntegralOrderMapper.updateFsIntegralOrder(o);
             }
         }
         return this::saveOrderLogIntegral;
@@ -5014,16 +5023,16 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
 
         fsStoreOrderMapper.updateFsStoreOrder(updateOrder);
     }
-    private void updateOrderToCancelledScrm(FsStoreOrder fsStoreOrder) {
+    private void updateOrderToCancelledScrm(FsStoreOrderScrm fsStoreOrder) {
         FsStoreOrderScrm updateOrder = new FsStoreOrderScrm();
-        updateOrder.setId(fsStoreOrder.getOrderId());
+        updateOrder.setId(fsStoreOrder.getId());
         updateOrder.setStatus(STATUS_PENDING_SHIPMENT);//待发货
         updateOrder.setUpdateTime(new DateTime());
         updateOrder.setDeliveryTime(getCurrentDateTimeString());
 
         fsStoreOrderScrmMapper.updateFsStoreOrder(updateOrder);
     }
-    private void updateOrderToCancelledIntegral(FsStoreOrder fsStoreOrder) {
+    private void updateOrderToCancelledIntegral(FsIntegralOrder fsStoreOrder) {
         FsIntegralOrder updateOrder = new FsIntegralOrder();
         updateOrder.setOrderId(fsStoreOrder.getOrderId());
         updateOrder.setStatus(STATUS_PENDING_SHIPMENT);//待发货

+ 21 - 10
fs-service/src/main/java/com/fs/hisStore/mapper/FsShippingTemplatesRegionScrmMapper.java

@@ -11,15 +11,15 @@ import org.apache.ibatis.annotations.Select;
 
 /**
  * 邮费区域Mapper接口
- * 
+ *
  * @author fs
  * @date 2022-03-15
  */
-public interface FsShippingTemplatesRegionScrmMapper 
+public interface FsShippingTemplatesRegionScrmMapper
 {
     /**
      * 查询邮费区域
-     * 
+     *
      * @param id 邮费区域ID
      * @return 邮费区域
      */
@@ -27,7 +27,7 @@ public interface FsShippingTemplatesRegionScrmMapper
 
     /**
      * 查询邮费区域列表
-     * 
+     *
      * @param fsShippingTemplatesRegion 邮费区域
      * @return 邮费区域集合
      */
@@ -35,7 +35,7 @@ public interface FsShippingTemplatesRegionScrmMapper
 
     /**
      * 新增邮费区域
-     * 
+     *
      * @param fsShippingTemplatesRegion 邮费区域
      * @return 结果
      */
@@ -43,7 +43,7 @@ public interface FsShippingTemplatesRegionScrmMapper
 
     /**
      * 修改邮费区域
-     * 
+     *
      * @param fsShippingTemplatesRegion 邮费区域
      * @return 结果
      */
@@ -51,7 +51,7 @@ public interface FsShippingTemplatesRegionScrmMapper
 
     /**
      * 删除邮费区域
-     * 
+     *
      * @param id 邮费区域ID
      * @return 结果
      */
@@ -59,7 +59,7 @@ public interface FsShippingTemplatesRegionScrmMapper
 
     /**
      * 批量删除邮费区域
-     * 
+     *
      * @param ids 需要删除的数据ID
      * @return 结果
      */
@@ -67,7 +67,18 @@ public interface FsShippingTemplatesRegionScrmMapper
 
     @Delete("delete from fs_shipping_templates_region where temp_id=#{tempId}")
     int deleteFsShippingTemplatesRegionByTempId(Long tempId);
-    @Select("select * from fs_shipping_templates_region where find_in_set(temp_id,#{tempIds}) and find_in_set(city_id,#{cityIds})")
-    @DataSource(DataSourceType.SLAVE)
+
+    /**
+     * 按模板 + 城市链匹配运费区域。
+     * 支持:市级精确匹配;省级整省配置(city_id 为空,用 province_id 匹配)。
+     * 走主库,避免模板刚保存后从库延迟导致算不出邮费。
+     */
+    @Select("select * from fs_shipping_templates_region where find_in_set(temp_id, #{tempIds}) "
+            + "and ("
+            + "  (city_id is not null and find_in_set(cast(city_id as char), #{cityIds})) "
+            + "  or (city_id is null and find_in_set(cast(province_id as char), #{cityIds})) "
+            + ") "
+            + "order by (city_id is null), id")
+    @DataSource(DataSourceType.MASTER)
     List<FsShippingTemplatesRegionScrm> selectFsShippingTemplatesRegionListByTempIdsAndCityIds(@Param("tempIds") String tempIds, @Param("cityIds") String cityIds);
 }

+ 2 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java

@@ -90,7 +90,7 @@ public interface FsStoreAfterSalesScrmMapper
     @Select({"<script> " +
             "select s.*,o.delivery_status,o.delivery_id,u.phone as user_phone,c.company_name ,cu.nick_name as company_user_nick_name ," +
             "cu.phonenumber as company_usere_phonenumber,o.pay_money,o.id as orderId,o.create_time as orderCreateTime,o.user_phone," +
-            "o.real_name as userName,o.item_json,o.user_address,o.pay_time as orderPayTime,o.pay_price,o.total_postage," +
+            "o.real_name as userName,o.item_json,o.user_address,o.pay_time as orderPayTime,o.pay_price,o.total_postage,o.coupon_price as couponPrice," +
             "fsps.bank_serial_no,fsps.bank_transaction_id,o.delivery_id as orderDeliveryId,o.delivery_name as orderDeliveryName,o.delivery_sn as orderDeliverySn," +
             "o.status as orderStatus,fsps.pay_code as payCode, s.reason_level1_text as reasonValue1, s.reason_level2_text as reasonValue2 " +
             " from fs_store_after_sales_scrm s " +
@@ -255,7 +255,7 @@ public interface FsStoreAfterSalesScrmMapper
     @Select({"<script> " +
             "select s.*,o.delivery_status,o.delivery_id,u.phone as user_phone,c.company_name ,cu.nick_name as company_user_nick_name ," +
             "cu.phonenumber as company_usere_phonenumber,o.pay_money,o.id as orderId,o.create_time as orderCreateTime,o.user_phone," +
-            "o.real_name as userName,o.item_json,o.user_address,o.pay_time as orderPayTime,o.pay_price,o.total_postage," +
+            "o.real_name as userName,o.item_json,o.user_address,o.pay_time as orderPayTime,o.pay_price,o.total_postage,o.coupon_price as couponPrice," +
             "fsps.bank_serial_no,fsps.bank_transaction_id,o.delivery_id as orderDeliveryId,o.delivery_name as orderDeliveryName,o.delivery_sn as orderDeliverySn," +
             "o.status as orderStatus,fsps.pay_code as payCode, s.reason_level1_text as reasonValue1, s.reason_level2_text as reasonValue2 " +
             " from fs_store_after_sales_scrm s " +

+ 4 - 3
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -4781,7 +4781,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             // 有运费模板,但当前城市没有匹配的区域
             if (shippingTemplatesList != null && !shippingTemplatesList.isEmpty()
                     && (shippingTemplatesRegionList == null || shippingTemplatesRegionList.isEmpty())&&!isQg) {
-                logger.error("运费模板存在,但城市不在运费模板区域内,cityId: {}", cityId);
+                logger.error("运费模板存在,但城市不在运费模板区域内,cityId: {}, matchCityIds: {}, tempIds: {}",
+                        cityId, citys, tempIds);
                 return badCode;
             }
 
@@ -4794,8 +4795,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             Map<Long, FsShippingTemplatesRegionScrm> shippingTemplatesRegionMap =
                     shippingTemplatesRegionList.stream()
                             .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId,
-                                    YxShippingTemplatesRegion -> YxShippingTemplatesRegion,
-                                    (key1, key2) -> key2));
+                                    r -> r,
+                                    (a, b) -> a.getCityId() != null ? a : b));
 
 
             Map<Long, TemplateDTO> templateDTOMap = new HashMap<>();

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java

@@ -138,6 +138,9 @@ public class FsStoreAfterSalesVO implements Serializable
 
     private BigDecimal payMoney;
 
+    /** 直播优惠券金额(来自订单 coupon_price) */
+    private BigDecimal couponPrice;
+
     private String cateName;
     private Date orderCreateTime;
 

+ 3 - 0
fs-service/src/main/java/com/fs/live/constant/LiveOrderOptConstants.java

@@ -19,4 +19,7 @@ public final class LiveOrderOptConstants {
     public static final String CACHE_SHIPPING_REGION = "fs:shipping:region:";
     public static final String CACHE_LIVE_COUPON_USER = "live:coupon:user:";
     public static final String LOCK_ORDER_KEY_CREATING = "orderKey:creating:";
+
+    /** 直播库存扣减上报 Topic(对应 consumer group: common-group) */
+    public static final String TOPIC_LIVE_GOODS_UPLOAD = "live-goods-upload";
 }

+ 10 - 1
fs-service/src/main/java/com/fs/live/domain/LiveCoupon.java

@@ -3,6 +3,7 @@ package com.fs.live.domain;
 import java.math.BigDecimal;
 import java.util.Date;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
@@ -40,7 +41,7 @@ public class LiveCoupon extends BaseEntity
     private BigDecimal useMinPrice;
 
     /** 优惠券有效期限(单位:天) */
-    @Excel(name = "优惠券有效期限", readConverterExp = "单=位:天")
+    @Excel(name = "优惠券有效期限(天)")
     private Long couponTime;
 
     /** 排序 */
@@ -77,4 +78,12 @@ public class LiveCoupon extends BaseEntity
     private String productName;
     private String image;
 
+    /** 领取开始时间(来自 live_coupon_issue) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 领取结束时间(来自 live_coupon_issue) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date limitTime;
+
 }

+ 4 - 4
fs-service/src/main/java/com/fs/live/domain/LiveCouponIssue.java

@@ -36,13 +36,13 @@ public class LiveCouponIssue extends BaseEntity
     private Integer couponType;
 
     /** 优惠券领取开启时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd")
-    @Excel(name = "优惠券领取开启时间", width = 30, dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "优惠券领取开启时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
     private Date startTime;
 
     /** 优惠券领取结束时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd")
-    @Excel(name = "优惠券领取结束时间", width = 30, dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "优惠券领取结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
     private Date limitTime;
 
     /** 优惠券领取数量 */

+ 4 - 4
fs-service/src/main/java/com/fs/live/domain/LiveCouponUser.java

@@ -44,13 +44,13 @@ public class LiveCouponUser extends BaseEntity
     private BigDecimal useMinPrice;
 
     /** 优惠券结束时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd")
-    @Excel(name = "优惠券结束时间", width = 30, dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "优惠券结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
     private Date limitTime;
 
     /** 使用时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd")
-    @Excel(name = "使用时间", width = 30, dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "使用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
     private Date useTime;
 
     /** 获取方式 */

+ 2 - 0
fs-service/src/main/java/com/fs/live/dto/LiveOrderComputeDTO.java

@@ -30,5 +30,7 @@ public class LiveOrderComputeDTO implements Serializable
 
     private BigDecimal deductionPrice;
 
+    /** 优惠券抵扣金额 */
+    private BigDecimal couponPrice;
 
 }

+ 5 - 3
fs-service/src/main/java/com/fs/live/mapper/LiveCouponMapper.java

@@ -78,10 +78,12 @@ public interface LiveCouponMapper
     List<LiveCoupon> selectLiveCouponByLiveId(@Param("liveId") Long liveId);
 
     @Select("<script>" +
-            "select lc.*,lci.id from live_coupon_issue lci left join live_coupon lc on lc.coupon_id=lci.coupon_id " +
+            "select lc.*,lci.id,lci.start_time as startTime,lci.limit_time as limitTime " +
+            "from live_coupon_issue lci left join live_coupon lc on lc.coupon_id=lci.coupon_id " +
             "where lci.status=1 and lc.status=1 " +
-            " and lc.title like concat('%', #{couponName}, '%')" +
-            " and id not in (select coupon_issue_id as id from live_coupon_issue_relation where live_id = #{liveId})" +
+            " and (lci.limit_time is null or lci.limit_time &gt; now()) " +
+            " <if test='couponName != null and couponName != \"\"'> and lc.title like concat('%', #{couponName}, '%') </if>" +
+            " and lci.id not in (select coupon_issue_id from live_coupon_issue_relation where live_id = #{liveId})" +
             "</script>"
     )
     List<LiveCoupon> listCoupon(@Param("liveId") Long liveId, @Param("couponName") String couponName);

+ 19 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java

@@ -9,6 +9,7 @@ import com.fs.live.param.CouponPO;
 import com.fs.live.vo.LiveCouponUserDetailVo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 
 /**
  * 优惠券发放记录Mapper接口
@@ -76,6 +77,7 @@ public interface LiveCouponUserMapper
     @Select("<script>" +
             "select lcu.* from live_coupon_user lcu where lcu.user_id = #{coupon.userId} and lcu.is_del = 0" +
             " <if test='coupon.status != null'> and lcu.status = #{coupon.status}</if>" +
+            " <if test='coupon.liveId != null'> and lcu.type like concat('%-live-', #{coupon.liveId})</if>" +
             " order by lcu.create_time desc" +
             "</script>")
     List<LiveCouponUser> selectLiveCouponUserByCouponPO(@Param("coupon") CouponPO coupon);
@@ -93,6 +95,7 @@ public interface LiveCouponUserMapper
     @Select("<script>" +
             "select lcu.* from live_coupon_user lcu where lcu.user_id= #{coupon.userId} and lcu.is_del = 0" +
             " <if test='coupon.status != null'> and lcu.status = #{coupon.status}</if>" +
+            " <if test='coupon.liveId != null'> and lcu.type like concat('%-live-', #{coupon.liveId})</if>" +
             " <if test='coupon.goodsId != null'>" +
             " and (lcu.goods_id= #{coupon.goodsId} or lcu.goods_id is null or lcu.goods_id = 0)" +
             " </if>" +
@@ -110,4 +113,20 @@ public interface LiveCouponUserMapper
 
     LiveCouponUserDetailVo selectLiveCouponUserDetailByVerifyCode(@Param("verifyCode") String verifyCode);
 
+    /**
+     * 仅查询「未核销且已过有效期」的记录 id,避免扫描已核销/已过期数据
+     */
+    @Select("select id from live_coupon_user " +
+            "where status = 0 and is_del = 0 and limit_time is not null and limit_time < now() " +
+            "order by id asc limit #{limit}")
+    @DataSource(DataSourceType.SLAVE)
+    List<Long> selectExpiredCouponUserIds(@Param("limit") int limit);
+
+    @Update("<script>" +
+            "update live_coupon_user set status = 2, is_fail = 0, update_time = now() " +
+            "where status = 0 and id in " +
+            "<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>" +
+            "</script>")
+    int batchExpireByIds(@Param("ids") List<Long> ids);
+
 }

+ 17 - 0
fs-service/src/main/java/com/fs/live/mq/LiveGoodsRocketMQTemplate.java

@@ -0,0 +1,17 @@
+package com.fs.live.mq;
+
+import org.apache.rocketmq.spring.annotation.ExtRocketMQTemplateConfiguration;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+
+/**
+ * 直播库存上报专用 Producer:group=common-group,topic=live-goods-upload。
+ * 与默认 course-finish-group 的 RocketMQTemplate 隔离。
+ */
+@ExtRocketMQTemplateConfiguration(
+        nameServer = "${rocketmq.live.name-server:${rocketmq.name-server}}",
+        group = "${rocketmq.live.producer.group:common-group}",
+        accessKey = "${rocketmq.live.producer.access-key:${rocketmq.producer.access-key:}}",
+        secretKey = "${rocketmq.live.producer.secret-key:${rocketmq.producer.secret-key:}}"
+)
+public class LiveGoodsRocketMQTemplate extends RocketMQTemplate {
+}

+ 2 - 2
fs-service/src/main/java/com/fs/live/param/LiveCouponPublishParam.java

@@ -12,10 +12,10 @@ public class LiveCouponPublishParam implements Serializable
 
     private Long couponId;
 
-    @JsonFormat(pattern = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date startTime;
 
-    @JsonFormat(pattern = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date limitTime;
 
     private Integer totalCount;

+ 27 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCouponService.java

@@ -111,4 +111,31 @@ public interface ILiveCouponService
      * 销售扫码核销优惠券
      */
     R verifyCoupon(LiveCouponVerifyParam param);
+
+    /**
+     * WS 开启/关闭优惠券时同步「当前展示优惠券」缓存。
+     * status=1 且剩余数量&gt;0 时写入;否则清除。
+     *
+     * @param liveId         直播间ID
+     * @param couponIssueId  发放批次ID
+     * @param status         1=开启展示,其他=关闭
+     * @param payload        WS 原始 data(可含 goodsId/couponName 等,可为 null)
+     */
+    void syncCurrentCouponCache(Long liveId, Long couponIssueId, Integer status, Map<String, Object> payload);
+
+    /**
+     * 清除直播间当前展示优惠券缓存
+     */
+    void clearCurrentCouponCache(Long liveId);
+
+    /**
+     * 读取直播间当前展示优惠券缓存(无则返回 null)
+     */
+    Map<String, Object> getCurrentCouponCache(Long liveId);
+
+    /**
+     * 批量将已过期且未核销的用户优惠券标记为过期
+     * @return 本次处理条数
+     */
+    int expireLiveCouponUsersBatch(int batchSize);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCouponUserService.java

@@ -74,4 +74,14 @@ public interface ILiveCouponUserService
     LiveCouponUserDetailVo selectLiveCouponUserDetailByVerifyCode(String verifyCode);
 
     LiveCouponUser selectLiveCouponUserByVerifyCode(String verifyCode);
+
+    /**
+     * 查询已过期但未核销的用户券 ID(仅有效待失效数据)
+     */
+    List<Long> selectExpiredCouponUserIds(int limit);
+
+    /**
+     * 按 ID 批量标记过期
+     */
+    int batchExpireByIds(List<Long> ids);
 }

+ 150 - 3
fs-service/src/main/java/com/fs/live/service/impl/LiveCouponServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.live.service.impl;
 
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import cn.hutool.core.util.ObjectUtil;
@@ -22,7 +23,8 @@ import com.fs.live.vo.LiveCouponListVo;
 import com.fs.live.vo.LiveCouponUserDetailVo;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
-import org.checkerframework.checker.units.qual.A;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.live.mapper.LiveCouponMapper;
@@ -37,10 +39,14 @@ import org.springframework.transaction.annotation.Transactional;
 @Service
 public class LiveCouponServiceImpl implements ILiveCouponService
 {
+    private static final Logger log = LoggerFactory.getLogger(LiveCouponServiceImpl.class);
 
     /** 获取方式:1-直播优惠卷 */
     private static final String COMPLETION_COUPON_TYPE = "1";
 
+    /** 过期任务单批最大处理条数 */
+    private static final int EXPIRE_BATCH_SIZE = 500;
+
     @Autowired
     private LiveCouponMapper liveCouponMapper;
 
@@ -296,11 +302,23 @@ public class LiveCouponServiceImpl implements ILiveCouponService
     @Override
     @Transactional
     public R claimCoupon(CouponPO coupon) {
+        if (coupon == null || coupon.getLiveId() == null || coupon.getCouponIssueId() == null) {
+            return R.error("参数不能为空");
+        }
         LiveCouponIssue issue = liveCouponMapper.selectLiveCouponIssueByLiveIdAndCouponId(coupon.getLiveId(), coupon.getCouponIssueId());
-        if (coupon == null || issue.getStatus() != 1) {
+        if (issue == null || issue.getStatus() == null || issue.getStatus() != 1) {
             return R.error("优惠券不存在或者已下架!");
         }
 
+        Date now = DateUtils.getNowDate();
+        // 校验领取时间范围(开始时间 <= 当前 <= 结束时间)
+        if (issue.getStartTime() != null && now.before(issue.getStartTime())) {
+            return R.error("优惠券不在领取时间范围内,请联系管理员处理");
+        }
+        if (issue.getLimitTime() != null && now.after(issue.getLimitTime())) {
+            return R.error("优惠券不在领取时间范围内,请联系管理员处理");
+        }
+
         LiveCoupon liveCoupon = liveCouponMapper.selectLiveCouponById(issue.getCouponId());
         if (liveCoupon == null) {
             return R.error("优惠券配置不存在!");
@@ -330,10 +348,11 @@ public class LiveCouponServiceImpl implements ILiveCouponService
             issue.setStatus(-1);
             issue.setRemainCount(0L);
             redisCache.deleteObject(String.valueOf(issue.getId()));
+            redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM, coupon.getCouponIssueId()));
+            clearCurrentCouponCache(coupon.getLiveId());
             liveCouponIssueService.updateLiveCouponIssue(issue);
             return R.error("此优惠券已领完");
         }
-        Date now = DateUtils.getNowDate();
         LiveCouponIssueUser record = new LiveCouponIssueUser();
         record.setUserId(coupon.getUserId());
         record.setIssueId(issue.getId());
@@ -371,6 +390,22 @@ public class LiveCouponServiceImpl implements ILiveCouponService
             redisCache.hashPut(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_COUPON, coupon.getLiveId(), coupon.getCouponIssueId()), String.valueOf(coupon.getUserId()), JSONUtil.toJsonStr(record));
         }
 
+        // 领完后清除当前展示优惠券缓存,避免进房补偿仍弹出
+        if (issue.getRemainCount() != null && issue.getRemainCount() <= 0) {
+            clearCurrentCouponCache(coupon.getLiveId());
+        } else {
+            Map<String, Object> current = getCurrentCouponCache(coupon.getLiveId());
+            if (current != null && current.get("couponIssueId") != null
+                    && String.valueOf(current.get("couponIssueId")).equals(String.valueOf(coupon.getCouponIssueId()))) {
+                current.put("remainCount", issue.getRemainCount());
+                redisCache.setCacheObject(
+                        String.format(LiveKeysConstant.LIVE_CURRENT_COUPON, coupon.getLiveId()),
+                        current,
+                        LiveKeysConstant.LIVE_CURRENT_COUPON_EXPIRE,
+                        TimeUnit.MINUTES);
+            }
+        }
+
         return R.ok("恭喜您抢到优惠券");
     }
 
@@ -545,6 +580,118 @@ public class LiveCouponServiceImpl implements ILiveCouponService
         return null;
     }
 
+    @Override
+    public void syncCurrentCouponCache(Long liveId, Long couponIssueId, Integer status, Map<String, Object> payload) {
+        if (liveId == null) {
+            return;
+        }
+        if (status == null || status != 1 || couponIssueId == null) {
+            clearCurrentCouponCache(liveId);
+            return;
+        }
+
+        LiveCouponIssue issue = liveCouponIssueService.selectLiveCouponIssueById(couponIssueId);
+        if (issue == null || issue.getRemainCount() == null || issue.getRemainCount() <= 0) {
+            clearCurrentCouponCache(liveId);
+            return;
+        }
+
+        LiveCoupon liveCoupon = issue.getCouponId() != null
+                ? liveCouponMapper.selectLiveCouponById(issue.getCouponId()) : null;
+        LiveCouponIssueRelation relation = liveCouponMapper.selectCouponRelation(liveId, couponIssueId);
+
+        Map<String, Object> data = new HashMap<>();
+        if (payload != null) {
+            data.putAll(payload);
+        }
+        data.put("liveId", liveId);
+        data.put("couponIssueId", couponIssueId);
+        data.put("status", 1);
+        data.put("remainCount", issue.getRemainCount());
+        if (data.get("goodsId") == null && relation != null) {
+            data.put("goodsId", relation.getGoodsId());
+        }
+        if (data.get("couponName") == null) {
+            data.put("couponName", issue.getCouponName() != null ? issue.getCouponName()
+                    : (liveCoupon != null ? liveCoupon.getTitle() : null));
+        }
+        if (data.get("couponPrice") == null) {
+            data.put("couponPrice", issue.getCouponPrice() != null ? issue.getCouponPrice()
+                    : (liveCoupon != null ? liveCoupon.getCouponPrice() : null));
+        }
+        if (data.get("useMinPrice") == null) {
+            data.put("useMinPrice", issue.getUseMinPrice() != null ? issue.getUseMinPrice()
+                    : (liveCoupon != null ? liveCoupon.getUseMinPrice() : null));
+        }
+        if (data.get("couponTime") == null) {
+            data.put("couponTime", issue.getCouponTime() != null ? issue.getCouponTime()
+                    : (liveCoupon != null ? liveCoupon.getCouponTime() : null));
+        }
+
+        redisCache.setCacheObject(
+                String.format(LiveKeysConstant.LIVE_CURRENT_COUPON, liveId),
+                data,
+                LiveKeysConstant.LIVE_CURRENT_COUPON_EXPIRE,
+                TimeUnit.MINUTES);
+        redisCache.setCacheObject(
+                String.format(LiveKeysConstant.LIVE_COUPON_NUM, couponIssueId),
+                issue.getRemainCount().intValue(),
+                LiveKeysConstant.LIVE_CURRENT_COUPON_EXPIRE,
+                TimeUnit.MINUTES);
+        liveCouponMapper.updateChangeShow(liveId, couponIssueId);
+    }
+
+    @Override
+    public void clearCurrentCouponCache(Long liveId) {
+        if (liveId == null) {
+            return;
+        }
+        redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_CURRENT_COUPON, liveId));
+    }
 
+    @Override
+    @SuppressWarnings("unchecked")
+    public Map<String, Object> getCurrentCouponCache(Long liveId) {
+        if (liveId == null) {
+            return null;
+        }
+        Object cached = redisCache.getCacheObject(String.format(LiveKeysConstant.LIVE_CURRENT_COUPON, liveId));
+        if (cached == null) {
+            return null;
+        }
+        if (cached instanceof Map) {
+            return (Map<String, Object>) cached;
+        }
+        if (cached instanceof String) {
+            String json = (String) cached;
+            if (StringUtils.isNotEmpty(json) && json.trim().startsWith("{")) {
+                return JSONUtil.toBean(json, Map.class);
+            }
+        }
+        try {
+            return JSONUtil.parseObj(cached).toBean(Map.class);
+        } catch (Exception e) {
+            log.warn("解析当前优惠券缓存失败 liveId={}", liveId, e);
+            return null;
+        }
+    }
+
+    @Override
+    public int expireLiveCouponUsersBatch(int batchSize) {
+        int size = batchSize > 0 ? batchSize : EXPIRE_BATCH_SIZE;
+        int total = 0;
+        while (true) {
+            List<Long> ids = liveCouponUserService.selectExpiredCouponUserIds(size);
+            if (ids == null || ids.isEmpty()) {
+                break;
+            }
+            int updated = liveCouponUserService.batchExpireByIds(ids);
+            total += updated;
+            if (ids.size() < size) {
+                break;
+            }
+        }
+        return total;
+    }
 
 }

+ 16 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCouponUserServiceImpl.java

@@ -133,4 +133,20 @@ public class LiveCouponUserServiceImpl implements ILiveCouponUserService
     public LiveCouponUser selectLiveCouponUserByVerifyCode(String verifyCode) {
         return liveCouponUserMapper.selectLiveCouponUserByVerifyCode(verifyCode);
     }
+
+    @Override
+    public List<Long> selectExpiredCouponUserIds(int limit) {
+        if (limit <= 0) {
+            return Collections.emptyList();
+        }
+        return liveCouponUserMapper.selectExpiredCouponUserIds(limit);
+    }
+
+    @Override
+    public int batchExpireByIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return 0;
+        }
+        return liveCouponUserMapper.batchExpireByIds(ids);
+    }
 }

+ 71 - 61
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderOptServiceImpl.java

@@ -61,7 +61,8 @@ import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import com.fs.live.mq.LiveGoodsRocketMQTemplate;
+import org.apache.commons.lang.ObjectUtils;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.springframework.aop.framework.AopContext;
@@ -109,7 +110,7 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
     @Autowired
     private CloudHostProper cloudHostProper;
     @Autowired
-    private RocketMQTemplate rocketMQTemplate;
+    private LiveGoodsRocketMQTemplate liveGoodsRocketMQTemplate;
     @Autowired
     private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
     @Autowired
@@ -146,20 +147,20 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         try {
             String orderKey = redisCache.getCacheObject("orderKey:" + liveOrder.getOrderKey());
             if (StringUtils.isEmpty(orderKey)) {
-                timer.finish("fail:orderKeyExpired");
+              //  timer.finish("fail:orderKeyExpired");
                 return R.error("订单已过期");
             }
             String creatingKey = LiveOrderOptConstants.LOCK_ORDER_KEY_CREATING + liveOrder.getOrderKey();
             if (!redisCache.setIfAbsent(creatingKey, "1", 30, TimeUnit.SECONDS)) {
-                timer.finish("fail:orderCreating");
+              //  timer.finish("fail:orderCreating");
                 return R.error("订单正在创建中,请勿重复提交");
             }
-            timer.step("orderKeyCheck");
+          //  timer.step("orderKeyCheck");
 
             R validateResult = validateCreateParam(liveOrder);
-            timer.step("validateParam");
+          //  timer.step("validateParam");
             if (validateResult != null) {
-                timer.finish("fail:validate");
+              //  timer.finish("fail:validate");
                 return validateResult;
             }
 
@@ -167,37 +168,37 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
             Integer purchaseNum = Integer.parseInt(liveOrder.getTotalNum());
 
             LiveGoods goods = getLiveGoodsCached(liveOrder.getLiveId(), liveOrder.getProductId());
-            timer.step("loadLiveGoods");
+          //  timer.step("loadLiveGoods");
             if (goods == null) {
-                timer.finish("fail:goodsNotFound");
+              //  timer.finish("fail:goodsNotFound");
                 return R.error("当前商品不存在");
             }
 
             R stockResult = deductStockIfNeeded(liveOrder, goods);
-            timer.step("deductStock");
+          //  timer.step("deductStock");
             if (stockResult != null) {
-                timer.finish("fail:stock");
+              //  timer.finish("fail:stock");
                 return stockResult;
             }
 
             if (liveService.selectLiveByLiveId(liveOrder.getLiveId()) == null) {
-                timer.finish("fail:liveNotFound");
+              //  timer.finish("fail:liveNotFound");
                 return R.error("当前直播不存在");
             }
-            timer.step("loadLive");
+          //  timer.step("loadLive");
 
             FsStoreProductScrm fsStoreProduct = fsStoreProductService.selectFsStoreRedisProductById(liveOrder.getProductId());
-            timer.step("loadProduct");
+          //  timer.step("loadProduct");
             if (fsStoreProduct == null) {
-                timer.finish("fail:productNotFound");
+              //  timer.finish("fail:productNotFound");
                 return R.error("商品不存在,购买失败");
             }
             if (fsStoreProduct.getIsShow() == 0 || goods.getStatus() == 0) {
-                timer.finish("fail:productOffShelf");
+              //  timer.finish("fail:productOffShelf");
                 return R.error("商品已下架,购买失败");
             }
             if (!"1".equals(fsStoreProduct.getIsAudit())) {
-                timer.finish("fail:productNotAudit");
+              //  timer.finish("fail:productNotAudit");
                 return R.error("商品已下架,购买失败");
             }
 
@@ -205,23 +206,23 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
             if (liveOrder.getAttrValueId() != null) {
                 attrValue = getProductAttrValueCached(liveOrder.getAttrValueId());
             }
-            timer.step("loadAttrValue");
+          //  timer.step("loadAttrValue");
 
             checkPurchaseLimit(userId, fsStoreProduct, purchaseNum);
-            timer.step("checkPurchaseLimit");
+          //  timer.step("checkPurchaseLimit");
 
             publishStockUpdate(goods.getGoodsId(), purchaseNum);
-            timer.step("publishStockUpdate");
+          //  timer.step("publishStockUpdate");
 
             FsStoreOrderScrm storeOrder = buildStoreOrder(liveOrder, fsStoreProduct, attrValue, goods);
-            timer.step("buildStoreOrder");
+          //  timer.step("buildStoreOrder");
             LiveCouponUser couponToUse = resolveCoupon(liveOrder, userId, storeOrder);
-            timer.step("resolveCoupon");
+          //  timer.step("resolveCoupon");
 
             Integer flag = fsStoreOrderScrmMapper.insertFsStoreOrder(storeOrder);
-            timer.step("insertOrder");
+          //  timer.step("insertOrder");
             if (flag == null || flag == 0) {
-                timer.finish("fail:insertOrder");
+              //  timer.finish("fail:insertOrder");
                 return R.error("订单创建失败");
             }
 
@@ -231,27 +232,27 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
                 liveCouponUserService.updateLiveCouponUser(couponToUse);
                 redisCache.deleteObject(LiveOrderOptConstants.CACHE_LIVE_COUPON_USER + couponToUse.getId());
             }
-            timer.step("updateCoupon");
+          //  timer.step("updateCoupon");
 
             insertOrderItem(storeOrder, fsStoreProduct, attrValue, liveOrder.getTotalNum());
-            timer.step("insertOrderItem");
+          //  timer.step("insertOrderItem");
             orderStatusService.create(storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(),
                     OrderLogEnum.CREATE_ORDER.getDesc());
-            timer.step("orderStatusLog");
+          //  timer.step("orderStatusLog");
 
             StoreConfig config = getStoreConfigCached();
-            timer.step("loadStoreConfig");
+          //  timer.step("loadStoreConfig");
             String redisKey = StoreConstants.REDIS_ORDER_OUTTIME_UNPAY + storeOrder.getId();
             if (config != null && config.getUnPayTime() != null && config.getUnPayTime() > 0) {
                 redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
             } else {
                 redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
             }
-            timer.step("redisUnpayKey");
+          //  timer.step("redisUnpayKey");
 
             redisCache.deleteObject("orderKey:" + liveOrder.getOrderKey());
             redisCache.deleteObject(LiveOrderOptConstants.LOCK_ORDER_KEY_CREATING + liveOrder.getOrderKey());
-            timer.step("cleanupOrderKey");
+          //  timer.step("cleanupOrderKey");
 
             Calendar calendar = Calendar.getInstance();
             calendar.setTime(storeOrder.getCreateTime());
@@ -260,13 +261,13 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
             }
             String payLimitTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(calendar.getTime());
 
-            timer.finish("ok orderId=" + storeOrder.getId());
+          //  timer.finish("ok orderId=" + storeOrder.getId());
             return R.ok("下单成功").put("order", storeOrder).put("payLimitTime", payLimitTime);
         } catch (CustomException e) {
-            timer.finish("fail:" + e.getMessage());
+          //  timer.finish("fail:" + e.getMessage());
             throw e;
         } catch (Exception e) {
-            timer.finish("fail:" + e.getMessage());
+          //  timer.finish("fail:" + e.getMessage());
             log.error("[createOpt] 订单创建失败", e);
             return R.error("订单创建失败:" + e.getMessage());
         }
@@ -279,62 +280,62 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         RLock lock = redissonClient.getLock(String.format(LOCK_KEY_PAY, orderId));
         try {
             boolean locked = lock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
-            timer.step("tryLock");
+          //  timer.step("tryLock");
             if (!locked) {
-                timer.finish("fail:lockBusy");
+              //  timer.finish("fail:lockBusy");
                 return R.error("订单正在处理中,请勿重复提交");
             }
 
             FsStoreOrderScrm order = fsStoreOrderScrmService.selectFsStoreOrderById(orderId);
-            timer.step("loadOrder");
+          //  timer.step("loadOrder");
             if (order == null) {
-                timer.finish("fail:orderNotFound");
+              //  timer.finish("fail:orderNotFound");
                 return R.error("订单不存在");
             }
             if (order.getStatus() != null && order.getStatus() != OrderInfoEnum.STATUS_0.getValue()) {
-                timer.finish("fail:orderPaid");
+              //  timer.finish("fail:orderPaid");
                 return R.error("当前订单已支付");
             }
 
             String payingFlag = redisCache.getCacheObject("isPaying:" + orderId);
-            timer.step("checkPayingFlag");
+          //  timer.step("checkPayingFlag");
             if (StringUtils.isNotEmpty(payingFlag) && payingFlag.equals(order.getId().toString())) {
-                timer.finish("fail:paying");
+              //  timer.finish("fail:paying");
                 return R.error("正在支付中...");
             }
 
             R preCheck = checkExistingPayments(orderId);
-            timer.step("checkExistingPayments");
+          //  timer.step("checkExistingPayments");
             if (preCheck != null) {
-                timer.finish("fail:existingPayment");
+              //  timer.finish("fail:existingPayment");
                 return preCheck;
             }
 
             FsUserScrm user = getUserCached(order.getUserId());
-            timer.step("loadUser");
+          //  timer.step("loadUser");
             if (user == null) {
-                timer.finish("fail:userNotFound");
+              //  timer.finish("fail:userNotFound");
                 return R.error("用户OPENID不存在");
             }
 
             applyPayTypeAmount(order, param);
             fsStoreOrderScrmService.updateFsStoreOrder(order);
-            timer.step("applyPayTypeAndUpdateOrder");
+          //  timer.step("applyPayTypeAndUpdateOrder");
 
             R payResult = invokeThirdPartyPay(order, user, param, timer);
-            timer.finish("pay");
+          //  timer.finish("pay");
             return payResult;
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
-            timer.finish("fail:interrupted");
+          //  timer.finish("fail:interrupted");
             return R.error("支付处理被中断,请稍后重试");
         } catch (CustomException e) {
             redisCache.deleteObject("isPaying:" + orderId);
-            timer.finish("fail:" + e.getMessage());
+          //  timer.finish("fail:" + e.getMessage());
             throw e;
         } catch (Exception e) {
             redisCache.deleteObject("isPaying:" + orderId);
-            timer.finish("fail:" + e.getMessage());
+          //  timer.finish("fail:" + e.getMessage());
             log.error("[payOpt] 支付异常 orderId={}", orderId, e);
             return R.error("支付失败:" + e.getMessage());
         } finally {
@@ -382,7 +383,7 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         LiveGoodsUploadMqVo vo = LiveGoodsUploadMqVo.builder().goodsId(goodsId).goodsNum(purchaseNum).build();
         try {
             if ("北京卓美".equals(cloudHostProper.getCompanyName())) {
-                rocketMQTemplate.syncSend("live-goods-upload", JSON.toJSONString(vo));
+                liveGoodsRocketMQTemplate.syncSend(LiveOrderOptConstants.TOPIC_LIVE_GOODS_UPLOAD, JSON.toJSONString(vo));
             } else {
                 liveGoodsMapper.updateStock(goodsId, purchaseNum);
             }
@@ -477,6 +478,11 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         }
 
         BigDecimal finalPay = payPrice.subtract(discountMoney);
+        // 券全覆盖时兜底 0.1 元,保证可走支付回调
+        if (discountMoney != null && discountMoney.compareTo(BigDecimal.ZERO) > 0
+                && (discountMoney.compareTo(payPrice) >= 0 || finalPay.compareTo(BigDecimal.ZERO) <= 0)) {
+            finalPay = new BigDecimal("0.1");
+        }
         storeOrder.setPayPrice(finalPay);
         storeOrder.setPayMoney(finalPay);
         return storeOrder;
@@ -490,8 +496,11 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         if (couponUser == null || couponUser.getStatus() != 0) {
             return null;
         }
-        if (!couponUser.getUserId().equals(userId)) {
-            throw new CustomException("非法操作");
+        if (couponUser.getUserId() == null) {
+            throw new CustomException("优惠券无归属,请联系管理员进行处理!");
+        }
+        if (!Long.valueOf(couponUser.getUserId()).equals(userId)) {
+            throw new CustomException("优惠券归属异常,请联系管理员进行处理!");
         }
         if (storeOrder.getCouponId() == null) {
             return null;
@@ -589,7 +598,7 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
             if ("3".equals(order.getPayType())) {
                 IFsStoreOrderScrmService proxy = (IFsStoreOrderScrmService) AopContext.currentProxy();
                 proxy.payConfirm(2, order.getId(), null, null, null, null);
-                timer.step("payConfirmOffline");
+              //  timer.step("payConfirmOffline");
                 return R.ok().put("payType", param.getPayType());
             }
             return R.error("支付金额异常");
@@ -597,10 +606,10 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         if (order.getPayMoney().compareTo(BigDecimal.ZERO) <= 0) {
             return R.error("支付金额异常");
         }
-        timer.step("payAmountValidate");
+      //  timer.step("payAmountValidate");
 
         MiniAppPayBundle bundle = getMiniAppPayBundleCached(param.getAppId());
-        timer.step("loadMiniAppPayBundle");
+      //  timer.step("loadMiniAppPayBundle");
 
         String payCode = SnowflakeUtil.nextIdStr();
         FsStorePaymentScrm storePayment = buildBasePayment(order, user, param, payCode);
@@ -609,7 +618,7 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         storePayment.setMerConfigId(bundle.getMerchantAppConfig().getId());
         storePayment.setAppId(bundle.getPlayConfig().getAppid() == null ? "" : bundle.getPlayConfig().getAppid());
         fsStorePaymentScrmMapper.insertFsStorePayment(storePayment);
-        timer.step("insertPayment");
+      //  timer.step("insertPayment");
 
         MerchantAppConfig merchantAppConfig = bundle.getMerchantAppConfig();
         if ("hf".equals(merchantAppConfig.getMerchantType())) {
@@ -636,9 +645,9 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         } catch (Exception e) {
             log.error("分账出错 orderId={}", order.getId(), e);
         }
-        timer.step("huiFuPrepareDiv");
+      //  timer.step("huiFuPrepareDiv");
         HuifuCreateOrderResult result = huiFuService.createOrder(o);
-        timer.step("huiFuRemoteCreateOrder");
+      //  timer.step("huiFuRemoteCreateOrder");
         if (result.getResp_code() != null
                 && ("00000000".equals(result.getResp_code()) || "00000100".equals(result.getResp_code()))) {
             FsStorePaymentScrm mt = new FsStorePaymentScrm();
@@ -667,7 +676,7 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         payConfig.setKeyPath(fsPayConfig.getKeyPath());
         payConfig.setNotifyUrl(fsPayConfig.getNotifyUrlScrm());
         wxPayService.setConfig(payConfig);
-        timer.step("wxPrepareConfig");
+      //  timer.step("wxPrepareConfig");
         WxPayUnifiedOrderRequest orderRequest = new WxPayUnifiedOrderRequest();
         orderRequest.setOpenid(user.getMaOpenId());
         orderRequest.setBody("直播订单支付");
@@ -677,7 +686,7 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         orderRequest.setSpbillCreateIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
         try {
             WxPayMpOrderResult orderResult = wxPayService.createOrder(orderRequest);
-            timer.step("wxRemoteCreateOrder");
+          //  timer.step("wxRemoteCreateOrder");
             redisCache.setCacheObject("isPaying:" + order.getId(), order.getId().toString(), 1, TimeUnit.MINUTES);
             return R.ok().put("result", orderResult).put("type", "wx").put("isPay", 0).put("payType", param.getPayType());
         } catch (WxPayException e) {
@@ -891,7 +900,8 @@ public class LiveOrderOptServiceImpl implements ILiveOrderOptService {
         Map<Long, Integer> shippingTemplatesMap = shippingTemplatesList.stream()
                 .collect(Collectors.toMap(FsShippingTemplatesScrm::getId, FsShippingTemplatesScrm::getType));
         Map<Long, FsShippingTemplatesRegionScrm> shippingTemplatesRegionMap = shippingTemplatesRegionList.stream()
-                .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId, r -> r, (a, b) -> b));
+                .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId, r -> r,
+                        (a, b) -> a.getCityId() != null ? a : b));
         Long tempId = Long.valueOf(fsStoreProduct.getTempId());
         Integer templateType = shippingTemplatesMap.get(tempId);
         FsStoreProductAttrValueScrm productAttrValue = knownAttrValue != null

+ 52 - 20
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -131,7 +131,7 @@ import me.chanjar.weixin.common.error.WxErrorException;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang.ObjectUtils;
 import org.apache.http.util.Asserts;
-import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import com.fs.live.mq.LiveGoodsRocketMQTemplate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
@@ -166,7 +166,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Autowired
     private FsUserWxMapper fsUserWxMapper;
     @Autowired
-    private RocketMQTemplate rocketMQTemplate;
+    private LiveGoodsRocketMQTemplate liveGoodsRocketMQTemplate;
 
     @Autowired
     private FsCoursePlaySourceConfigMapper fsCoursePlaySourceConfigMapper;
@@ -2376,10 +2376,12 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
 
         if (param.getCouponUserId() != null) {
             LiveCouponUser couponUser = liveCouponUserService.selectLiveCouponUserById(param.getCouponUserId());
-            if (couponUser != null && couponUser.getStatus() == 0) {
-                if (couponUser.getUseMinPrice().compareTo(payPrice) < 1) {
-                    payPrice = payPrice.subtract(couponUser.getCouponPrice());
+            if (couponUser != null && couponUser.getStatus() != null && couponUser.getStatus() == 0
+                    && couponUser.getCouponPrice() != null) {
+                BigDecimal useMin = couponUser.getUseMinPrice() != null ? couponUser.getUseMinPrice() : BigDecimal.ZERO;
+                if (useMin.compareTo(payPrice) < 1) {
                     deductionPrice = couponUser.getCouponPrice();
+                    payPrice = calcPayAfterCoupon(payPrice, deductionPrice);
                 }
             }
         }
@@ -2388,6 +2390,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         return LiveOrderComputeDTO.builder().payPrice(payPrice)
                 .payDelivery(payDelivery)
                 .deductionPrice(deductionPrice)
+                .couponPrice(deductionPrice)
                 .totalPrice(totalPrice)
                 .build();
     }
@@ -2399,6 +2402,28 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     private static final String CACHE_SHIPPING_REGION = "fs:shipping:region:";
     private static final String CACHE_LIVE_COUPON_USER = "live:coupon:user:";
     private static final String CACHE_LIVE_COMPUTED = "live:computed:";
+    /** 优惠券全覆盖时的最低支付金额,保证支付流程可进入回调 */
+    private static final BigDecimal MIN_PAY_AMOUNT = new BigDecimal("0.1");
+
+    /**
+     * 扣券后应付:若优惠券金额 >= 原应付(全覆盖),则强制 0.1 元。
+     */
+    private BigDecimal calcPayAfterCoupon(BigDecimal payPrice, BigDecimal couponPrice) {
+        if (payPrice == null) {
+            return MIN_PAY_AMOUNT;
+        }
+        if (couponPrice == null || couponPrice.compareTo(BigDecimal.ZERO) <= 0) {
+            return payPrice;
+        }
+        if (couponPrice.compareTo(payPrice) >= 0) {
+            return MIN_PAY_AMOUNT;
+        }
+        BigDecimal after = payPrice.subtract(couponPrice);
+        if (after.compareTo(BigDecimal.ZERO) <= 0) {
+            return MIN_PAY_AMOUNT;
+        }
+        return after;
+    }
 
     @Override
     public LiveOrderComputeDTO computedOrderCache(long userId, LiveOrderComputedParam param) {
@@ -2453,11 +2478,13 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
 
         if (param.getCouponUserId() != null) {
             LiveCouponUser couponUser = getCouponUserCached(param.getCouponUserId());
-            if (couponUser != null && couponUser.getStatus() == 0
-                    && couponUser.getUserId() != null && couponUser.getUserId().longValue() == userId) {
-                if (couponUser.getUseMinPrice().compareTo(payPrice) < 1) {
-                    payPrice = payPrice.subtract(couponUser.getCouponPrice());
+            if (couponUser != null && couponUser.getStatus() != null && couponUser.getStatus() == 0
+                    && couponUser.getUserId() != null && couponUser.getUserId().longValue() == userId
+                    && couponUser.getCouponPrice() != null) {
+                BigDecimal useMin = couponUser.getUseMinPrice() != null ? couponUser.getUseMinPrice() : BigDecimal.ZERO;
+                if (useMin.compareTo(payPrice) < 1) {
                     deductionPrice = couponUser.getCouponPrice();
+                    payPrice = calcPayAfterCoupon(payPrice, deductionPrice);
                 }
             }
         }
@@ -2466,6 +2493,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 .payPrice(payPrice)
                 .payDelivery(payDelivery)
                 .deductionPrice(deductionPrice)
+                .couponPrice(deductionPrice)
                 .totalPrice(totalPrice)
                 .build();
         redisCache.setCacheObject(resultCacheKey, result, 10, TimeUnit.SECONDS);
@@ -2596,7 +2624,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         Map<Long, FsShippingTemplatesRegionScrm> shippingTemplatesRegionMap = shippingTemplatesRegionList.stream()
                 .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId,
                         region -> region,
-                        (key1, key2) -> key2));
+                        (a, b) -> a.getCityId() != null ? a : b));
         Long tempId = Long.valueOf(fsStoreProduct.getTempId());
         Integer templateType = shippingTemplatesMap.get(tempId);
         FsStoreProductAttrValueScrm productAttrValue = knownAttrValue != null
@@ -3091,7 +3119,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 shippingTemplatesRegionList.stream()
                         .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId,
                                 YxShippingTemplatesRegion -> YxShippingTemplatesRegion,
-                                (key1, key2) -> key2));
+                                (a, b) -> a.getCityId() != null ? a : b));
         Long tempId = Long.valueOf(fsStoreProduct.getTempId());
         double num = 0d;
         Integer templateType = shippingTemplatesMap.get(tempId);
@@ -4387,7 +4415,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             log.info("订单提交MQ:{}", vo);
 
             if (("北京卓美").equals(cloudHostProper.getCompanyName())){
-                rocketMQTemplate.syncSend("live-goods-upload", JSON.toJSONString(vo));
+                liveGoodsRocketMQTemplate.syncSend("live-goods-upload", JSON.toJSONString(vo));
             }else {
                 //其他客户没有mq,暂时直接更新
                 updateStockAndSales(goods.getGoodsId(), purchaseNum);
@@ -4489,9 +4517,10 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             storeOrder.setServiceFee(config.getServiceFee());
         }
 
-        // 设置支付金额
-        storeOrder.setPayPrice(payPrice.subtract(discountMoney));
-        storeOrder.setPayMoney(payPrice.subtract(discountMoney));
+        // 设置支付金额(券全覆盖时兜底 0.1 元,保证可走支付回调)
+        BigDecimal finalPay = calcPayAfterCoupon(payPrice, discountMoney);
+        storeOrder.setPayPrice(finalPay);
+        storeOrder.setPayMoney(finalPay);
 
         // 设置订单状态
         storeOrder.setStatus(0); // 待支付
@@ -4658,7 +4687,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         try {
             log.info("订单提交MQ:{}", vo);
            if (("北京卓美").equals(cloudHostProper.getCompanyName())){
-                            rocketMQTemplate.syncSend("live-goods-upload", JSON.toJSONString(vo));
+                            liveGoodsRocketMQTemplate.syncSend("live-goods-upload", JSON.toJSONString(vo));
             }else {
                //其他客户没有mq,暂时直接更新
                updateStockAndSales(goods.getGoodsId(), purchaseNum);
@@ -4759,9 +4788,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         liveOrder.setStatus(OrderInfoEnum.STATUS_0.getValue());
         liveOrder.setPayType("1");
         liveOrder.setTotalPrice(payPrice);
-        liveOrder.setPayPrice(payPrice.subtract(liveOrder.getDiscountMoney()));
+        // 券全覆盖时兜底 0.1 元,保证可走支付回调
+        BigDecimal finalPay = calcPayAfterCoupon(payPrice, liveOrder.getDiscountMoney());
+        liveOrder.setPayPrice(finalPay);
         if(CloudHostUtils.hasCloudHostName("济世百康","蒙牛")) {
-            liveOrder.setPayMoney(payPrice.subtract(liveOrder.getDiscountMoney()));
+            liveOrder.setPayMoney(finalPay);
         }
         try {
             if (baseMapper.insertLiveOrder(liveOrder) > 0) {
@@ -4947,7 +4978,8 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         liveOrder.setStatus(OrderInfoEnum.STATUS_0.getValue());
         liveOrder.setPayType("1");
         liveOrder.setTotalPrice(payPrice);
-        liveOrder.setPayPrice(payPrice.subtract(liveOrder.getDiscountMoney()));
+        // 券全覆盖时兜底 0.1 元,保证可走支付回调
+        liveOrder.setPayPrice(calcPayAfterCoupon(payPrice, liveOrder.getDiscountMoney()));
         try {
             if (baseMapper.insertLiveOrder(liveOrder) > 0) {
                 LiveOrderItemDTO dto = new LiveOrderItemDTO();
@@ -5027,7 +5059,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 shippingTemplatesRegionList.stream()
                         .collect(Collectors.toMap(FsShippingTemplatesRegionScrm::getTempId,
                                 YxShippingTemplatesRegion -> YxShippingTemplatesRegion,
-                                (key1, key2) -> key2));
+                                (a, b) -> a.getCityId() != null ? a : b));
 
 
         Map<Long, TemplateDTO> templateDTOMap = new HashMap<>();

+ 2 - 2
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -1684,11 +1684,11 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
 
                             break;
                         case "21":
-                            boolean check = !Long.valueOf(0L).equals(sopLogs.getFsUserId());
+                            boolean check = sopLogs.getFsUserId() != null && !Long.valueOf(0L).equals(sopLogs.getFsUserId());
                             if ("济南联志健康".equals(signProjectName)) {
                                 check = true;
                             }
-                            if (sopLogs.getFsUserId() != null && check) {
+                            if (check) {
                                 addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(), item.getFsUserId(), qwUserId, companyUserId, companyId,
                                         item.getExternalId(), item.getStartTime(), createTime,2);
                                 String link = createSmsShortLink(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),

+ 2 - 16
fs-service/src/main/java/com/fs/store/service/impl/FsUserCourseCountServiceImpl.java

@@ -18,8 +18,6 @@ import org.springframework.util.CollectionUtils;
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
 
 /**
  * 用户看课统计Service业务层处理
@@ -159,20 +157,8 @@ public class FsUserCourseCountServiceImpl implements IFsUserCourseCountService
         // 获取统计结果
 //        List<FsUserCourseCount> countResult = fsUserCourseCountMapper.getCountResult();
 
-        // 查询用户-每天的最新的看课状态,和最后的心跳时间
-//        List<FsUserCourseCount> userStatusAndLastWatchDate = fsUserCourseCountMapper.getUserStatusAndLastWatchDate();
-//        Map<String, FsUserCourseCount> map = userStatusAndLastWatchDate.stream()
-//                .collect(Collectors.toMap(k -> String.format("%s-%s-%s", k.getUserId(),k.getProjectId(), k.getLastDate()), v -> v));
-
-//        for (FsUserCourseCount data : countResult) {
-//            String key = String.format("%s-%s-%s", data.getUserId(),data.getProjectId(), data.getLastDate());
-//            FsUserCourseCount fsUserCourseCount = map.get(key);
-//            if(fsUserCourseCount != null){
-//                data.setLastWatchDate(fsUserCourseCount.getLastWatchDate());
-//                data.setStatus(fsUserCourseCount.getStatus());
-//                data.setStopWatchDays(fsUserCourseCount.getStopWatchDays());
-//            }
-//        }
+        // stop_watch_days 已在 getCountResult 中按 Max(last_heartbeat_time) 计算:
+        // DATEDIFF(今天, 最近心跳日期),并通过 batchInsertOrUpdate 写入/更新
     }
 
     /**

+ 8 - 0
fs-service/src/main/resources/application-druid-bjzm-test.yml

@@ -150,6 +150,14 @@ rocketmq:
         TOPIC: course-finish-note
         access-key: ak1gawpw9p33db21b4aaaf2 # 替换为实际的 accessKey
         secret-key: sk90be66c23f39ad85 # 替换为实际的 secretKey
+    # 直播库存上报专用(LiveGoodsRocketMQTemplate)
+    live:
+        name-server: rmq-1gawpw9p3.rocketmq.gz.public.tencenttdmq.com:8080
+        producer:
+            group: common-group
+            access-key: ak1gawpw9p33db21b4aaaf2
+            secret-key: sk90be66c23f39ad85
+        topic: live-goods-upload
 openIM:
     secret: openIM123
     userID: imAdmin

+ 8 - 0
fs-service/src/main/resources/application-druid-bjzm.yml

@@ -227,6 +227,14 @@ rocketmq:
         group: common-group #这里有两个group 打标签的mq是course-finish-group 直播的是common-group
         access-key: ak1gawpw9p33db21b4aaaf2 # 替换为实际的 accessKey
         secret-key: sk90be66c23f39ad85 # 替换为实际的 secretKey
+    # 直播库存上报专用(LiveGoodsRocketMQTemplate)
+    live:
+        name-server: rmq-1gawpw9p3.rocketmq.gz.qcloud.tencenttdmq.com:8080
+        producer:
+            group: common-group
+            access-key: ak1gawpw9p33db21b4aaaf2
+            secret-key: sk90be66c23f39ad85
+        topic: live-goods-upload
 openIM:
     secret: openIM123
     userID: imAdmin

+ 1 - 0
fs-service/src/main/resources/mapper/live/LiveCouponUserMapper.xml

@@ -115,6 +115,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             lcu.is_del = 0
             <if test="userId != null"> and lcu.user_id = #{userId}</if>
             <if test="status != null"> and lcu.status = #{status}</if>
+            <if test="liveId != null"> and lcu.type like concat('%-live-', #{liveId})</if>
             <if test="goodsId != null">
                 and (lcu.goods_id = #{goodsId} or lcu.goods_id is null or lcu.goods_id = 0)
             </if>

+ 9 - 4
fs-service/src/main/resources/mapper/store/FsUserCourseCountMapper.xml

@@ -166,6 +166,7 @@
             DATE_FORMAT(fwl.create_time,'%Y-%m-%d') AS create_date,
             DATE (fwl.create_time ) AS lastDate
             ,Max( fwl.last_heartbeat_time ) AS lastWatchDate,
+            DATEDIFF(CURDATE(), DATE(MAX(fwl.last_heartbeat_time))) AS stopWatchDays,
             CASE
             WHEN fwl.log_type = 1
             OR fwl.log_type = 2 THEN
@@ -192,6 +193,7 @@
             ucu.project_id,
             Max( fs_course_watch_log.last_heartbeat_time ) AS lastWatchDate,
             Max( DATE ( fs_course_watch_log.create_time ) ) AS lastDate,
+            DATEDIFF(CURDATE(), DATE(MAX(fs_course_watch_log.last_heartbeat_time))) AS stopWatchDays,
             CASE
 
                 WHEN fs_course_watch_log.log_type = 1
@@ -202,9 +204,6 @@
                 WHEN fs_course_watch_log.log_type = 3 THEN
                     3
                 END AS STATUS
-        --             DATEDIFF(
---                     DATE_FORMAT( NOW(), '%Y-%m-%d' ),
---                     DATE_FORMAT( fs_course_watch_log.last_heartbeat_time, '%Y-%m-%d' )) AS stop_watch_days
         FROM fs_course_watch_log
         left join fs_user_company_user ucu on ucu.user_id = fs_course_watch_log.user_id
         WHERE fs_course_watch_log.send_type = 1
@@ -235,6 +234,7 @@
             <if test="partCourseCount != null">part_course_count,</if>
             <if test="lastWatchDate != null">last_watch_date,</if>
             <if test="status != null">status,</if>
+            <if test="stopWatchDays != null">stop_watch_days,</if>
             <if test="createTime != null">create_time,</if>
             <if test="updateTime != null">update_time,</if>
             <if test="createBy != null">create_by,</if>
@@ -254,6 +254,7 @@
             <if test="partCourseCount != null">#{partCourseCount},</if>
             <if test="lastWatchDate != null">#{lastWatchDate},</if>
             <if test="status != null">#{status},</if>
+            <if test="stopWatchDays != null">#{stopWatchDays},</if>
             <if test="createTime != null">#{createTime},</if>
             <if test="updateTime != null">#{updateTime},</if>
             <if test="createBy != null">#{createBy},</if>
@@ -273,6 +274,7 @@
             <if test="partCourseCount != null">part_course_count = #{partCourseCount},</if>
             <if test="lastWatchDate != null">last_watch_date = #{lastWatchDate},</if>
             <if test="status != null">status = #{status},</if>
+            <if test="stopWatchDays != null">stop_watch_days = #{stopWatchDays},</if>
             <if test="completeWatchDate != null">complete_watch_date = #{completeWatchDate},</if>
             <if test="completeWatchCount != null">complete_watch_count = #{completeWatchCount},</if>
             <if test="watchTimes != null">watch_times = #{watchTimes},</if>
@@ -291,6 +293,7 @@
             part_course_count,
             last_watch_date,
             status,
+            stop_watch_days,
             create_time,
             update_time,
             complete_watch_date,
@@ -301,7 +304,8 @@
         VALUES
         <foreach collection="list" item="item" separator=",">
             (#{item.userId}, #{item.watchCourseCount}, #{item.missCourseCount}, #{item.missCourseStatus}, #{item.courseIds},
-             #{item.partCourseCount}, #{item.lastWatchDate}, #{item.status},#{item.createTime},#{item.updateTime},#{item.completeWatchDate}
+             #{item.partCourseCount}, #{item.lastWatchDate}, #{item.status}, #{item.stopWatchDays},
+             #{item.createTime},#{item.updateTime},#{item.completeWatchDate}
             ,#{item.completeWatchCount},#{item.watchTimes},#{item.createDate},#{item.projectId})
         </foreach>
         on duplicate key update
@@ -312,6 +316,7 @@
         part_course_count = VALUES(part_course_count),
         last_watch_date = VALUES(last_watch_date),
         status = VALUES(status),
+        stop_watch_days = VALUES(stop_watch_days),
         complete_watch_date = VALUES(complete_watch_date),
         complete_watch_count = VALUES(complete_watch_count),
         watch_times = VALUES(watch_times),

+ 7 - 3
fs-user-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -813,7 +813,8 @@ public class CommonController extends AppBaseController {
 		if (denied != null) {
 			return denied;
 		}
-		return userService.getWallet(userId);
+//		return userService.getWallet(userId); todo yhq  不要提交
+		return null;
 	}
 
 	/**
@@ -826,7 +827,9 @@ public class CommonController extends AppBaseController {
 		if (denied != null) {
 			return denied;
 		}
-		return userService.integralExchange(userId);
+//		return userService.integralExchange(userId); todo yhq  不要提交
+
+		return null;
 	}
 
 	/**
@@ -840,7 +843,8 @@ public class CommonController extends AppBaseController {
 		if (denied != null) {
 			return denied;
 		}
-		return userService.exchangDetail(params);
+//		return userService.exchangDetail(params); todo yhq  不要提交
+		return null;
 	}
 
 }

+ 21 - 1
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveController.java

@@ -25,6 +25,7 @@ import com.fs.live.domain.LiveCompletionPointsRecord;
 import com.fs.live.domain.LiveMsg;
 import com.fs.live.domain.LiveRedConf;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveCouponService;
 import com.fs.live.service.ILiveMsgService;
 import com.fs.live.service.ILiveService;
 import com.fs.live.vo.LiveConfigVo;
@@ -66,6 +67,8 @@ public class CacheLiveController extends CacheLiveBaseController {
     @Autowired
     private ILiveCompletionPointsRecordService completionPointsRecordService;
     @Autowired
+    private ILiveCouponService liveCouponService;
+    @Autowired
     private IFsUserService userService;
     @Autowired
     private ICompanyUserService companyUserService;
@@ -235,11 +238,28 @@ public class CacheLiveController extends CacheLiveBaseController {
                 String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, LiveKeysConstant.TOP_MSG));
         LiveMsgVo liveMsg = parseTopMsgCache(cacheObject, liveId);
 
+        // 当前展示中的优惠券(WS 开启且剩余数量>0 时写入缓存)
+        Object coupon = liveCouponService.getCurrentCouponCache(liveId);
+        if (coupon != null) {
+            Object remain = coupon instanceof java.util.Map
+                    ? ((java.util.Map<?, ?>) coupon).get("remainCount") : null;
+            if (remain != null) {
+                try {
+                    if (Long.parseLong(String.valueOf(remain)) <= 0) {
+                        coupon = null;
+                    }
+                } catch (NumberFormatException ignored) {
+                    // remainCount 异常时仍返回缓存内容,由前端自行判断
+                }
+            }
+        }
+
         return R.ok()
                 .put("red", redConfs)
                 .put("lottery", lotteryConfs)
                 .put("goods", liveConfigVo.getLiveGoodsVo())
-                .put("topMsg", liveMsg);
+                .put("topMsg", liveMsg)
+                .put("coupon", coupon);
     }
 
     @ApiOperation("微信直播间urlScheme")

+ 43 - 0
fs-user-app/src/main/java/com/fs/app/controller/cacheLive/CacheLiveCouponController.java

@@ -11,6 +11,9 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+/**
+ * 直播间优惠券(缓存版)
+ */
 @RestController
 @RequestMapping("/app/cacheLive/coupon")
 public class CacheLiveCouponController extends CacheLiveBaseController {
@@ -28,6 +31,23 @@ public class CacheLiveCouponController extends CacheLiveBaseController {
                 () -> liveCouponService.userCouponList(coupon));
     }
 
+    /**
+     * 查询当前用户在当前直播间领取到的优惠券
+     * liveId 必传
+     */
+    @Login
+    @PostMapping("/liveList")
+    public R liveList(@RequestBody CouponPO coupon) {
+        if (coupon == null || coupon.getLiveId() == null) {
+            return R.error("直播间ID不能为空");
+        }
+        coupon.setUserId(Long.parseLong(getUserId()));
+        String cacheKey = buildCacheKey("live:cache:coupon:list:%s:%s",
+                coupon.getUserId(), coupon.getLiveId());
+        return cacheAsideR(cacheKey, LiveKeysConstant.TTL_LIVE_DATA,
+                () -> liveCouponService.userCouponList(coupon));
+    }
+
     @Login
     @PostMapping("/curCoupon")
     public R curCoupon(@RequestBody CouponPO coupon) {
@@ -37,4 +57,27 @@ public class CacheLiveCouponController extends CacheLiveBaseController {
         return cacheAsideR(cacheKey, LiveKeysConstant.TTL_LIVE_GOODS,
                 () -> liveCouponService.curCoupon(coupon));
     }
+
+    /**
+     * 查询当前用户在当前直播间、当前商品下的优惠券
+     * liveId、goodsId 必传
+     */
+    @Login
+    @PostMapping("/curLiveCoupon")
+    public R curLiveCoupon(@RequestBody CouponPO coupon) {
+        if (coupon == null) {
+            return R.error("参数不能为空");
+        }
+        if (coupon.getLiveId() == null) {
+            return R.error("直播间ID不能为空");
+        }
+        if (coupon.getGoodsId() == null) {
+            return R.error("商品ID不能为空");
+        }
+        coupon.setUserId(Long.parseLong(getUserId()));
+        String cacheKey = buildCacheKey("live:cache:coupon:cur:%s:%s:%s",
+                coupon.getUserId(), coupon.getLiveId(), coupon.getGoodsId());
+        return cacheAsideR(cacheKey, LiveKeysConstant.TTL_LIVE_GOODS,
+                () -> liveCouponService.curCoupon(coupon));
+    }
 }

+ 14 - 1
fs-user-app/src/main/java/com/fs/app/facade/impl/LiveFacadeServiceImpl.java

@@ -214,11 +214,24 @@ public class LiveFacadeServiceImpl extends BaseController implements LiveFacadeS
                 .collect(Collectors.toList());
         LiveMsgVo liveMsg = loadTopMsgFromCache(liveId);
 
+        // 当前展示中的优惠券(WS 开启且剩余数量>0 时写入缓存)
+        Map<String, Object> coupon = iLiveCouponService.getCurrentCouponCache(liveId);
+        if (coupon != null && coupon.get("remainCount") != null) {
+            try {
+                if (Long.parseLong(String.valueOf(coupon.get("remainCount"))) <= 0) {
+                    coupon = null;
+                }
+            } catch (NumberFormatException ignored) {
+                // ignore
+            }
+        }
+
         return R.ok()
                 .put("red", redConfs)
                 .put("lottery", lotteryConfs)
                 .put("goods", liveConfigVo.getLiveGoodsVo())
-                .put("topMsg", liveMsg);
+                .put("topMsg", liveMsg)
+                .put("coupon", coupon);
     }
 
     private LiveConfigVo loadLiveConfigFromCache(Long liveId) {