Browse Source

Merge remote-tracking branch 'origin/红德堂-test' into 红德堂

wangxy 1 week ago
parent
commit
c5e8e1b348
100 changed files with 5664 additions and 501 deletions
  1. 11 4
      fs-admin/src/main/java/com/fs/his/controller/FsUserController.java
  2. 29 0
      fs-admin/src/main/java/com/fs/hisStore/task/ExpressTask.java
  3. 584 0
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  4. 88 6
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  5. 3 3
      fs-admin/src/main/java/com/fs/live/controller/LiveAutoTaskController.java
  6. 57 4
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  7. 4 0
      fs-admin/src/main/java/com/fs/live/controller/LiveCouponController.java
  8. 80 1
      fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java
  9. 102 9
      fs-admin/src/main/java/com/fs/live/controller/LiveHealthOrderController.java
  10. 181 33
      fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java
  11. 4 0
      fs-admin/src/main/java/com/fs/live/controller/LiveVideoController.java
  12. 307 0
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  13. 0 183
      fs-admin/src/main/java/com/fs/task/LiveTask.java
  14. 11 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  15. 16 6
      fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java
  16. 3 3
      fs-company/src/main/java/com/fs/company/controller/live/LiveAutoTaskController.java
  17. 57 21
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  18. 17 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveCouponController.java
  19. 79 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  20. 61 37
      fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java
  21. 117 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveWatchLogController.java
  22. 298 0
      fs-company/src/main/java/com/fs/company/controller/live/OrderController.java
  23. 0 1
      fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java
  24. 1 1
      fs-live-app/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  25. 11 8
      fs-live-app/src/main/java/com/fs/live/controller/LiveController.java
  26. 0 2
      fs-live-app/src/main/java/com/fs/live/controller/LiveDataController.java
  27. 98 0
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  28. 501 29
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  29. 9 0
      fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  30. 1 0
      fs-live-app/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java
  31. 4 0
      fs-live-app/src/main/java/com/fs/live/websocket/constant/AttrConstant.java
  32. 19 17
      fs-live-app/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java
  33. 815 67
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  34. 6 5
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  35. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
  36. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseTrafficLog.java
  37. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseWatchLog.java
  38. 5 0
      fs-service/src/main/java/com/fs/course/param/CourseAnalysisParam.java
  39. 5 0
      fs-service/src/main/java/com/fs/course/param/FsCourseTrafficLogParam.java
  40. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java
  41. 5 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogStatisticsListParam.java
  42. 1 0
      fs-service/src/main/java/com/fs/course/param/FsUserCourseVideoAddKfUParam.java
  43. 1 0
      fs-service/src/main/java/com/fs/course/param/FsUserCourseVideoFinishUParam.java
  44. 5 0
      fs-service/src/main/java/com/fs/course/param/PeriodCountParam.java
  45. 6 0
      fs-service/src/main/java/com/fs/course/param/newfs/FsUserCourseAddCompanyUserParam.java
  46. 1 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java
  47. 57 20
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  48. 4 4
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  49. 4 4
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiNewServiceImpl.java
  50. 4 4
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiServiceImpl.java
  51. 15 0
      fs-service/src/main/java/com/fs/his/dto/FsUserBindSalesParamDTO.java
  52. 4 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  53. 12 4
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  54. 97 2
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  55. 7 4
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  56. 11 0
      fs-service/src/main/java/com/fs/his/vo/UserOpenIdVO.java
  57. 49 0
      fs-service/src/main/java/com/fs/hisStore/enums/LiveEnum.java
  58. 12 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductAttrValueScrmMapper.java
  59. 118 0
      fs-service/src/main/java/com/fs/hisStore/mapper/MergedOrderMapper.java
  60. 6 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderParam.java
  61. 21 0
      fs-service/src/main/java/com/fs/hisStore/param/FsUsePackageScrmSendParam.java
  62. 31 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesDeliveryParam.java
  63. 43 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesParam.java
  64. 23 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesQueryParam.java
  65. 22 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesRevokeParam.java
  66. 25 0
      fs-service/src/main/java/com/fs/hisStore/param/MergedOrderDeleteParam.java
  67. 3 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsExpressScrmService.java
  68. 83 0
      fs-service/src/main/java/com/fs/hisStore/service/IMergedOrderService.java
  69. 52 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsExpressScrmServiceImpl.java
  70. 363 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/MergedOrderServiceImpl.java
  71. 78 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsMergedOrderListQueryVO.java
  72. 144 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java
  73. 124 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java
  74. 103 0
      fs-service/src/main/java/com/fs/hisStore/vo/MergedAfterSalesVO.java
  75. 15 0
      fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/request/V2TradePaymentScanpayRefundRequest.java
  76. 8 0
      fs-service/src/main/java/com/fs/live/domain/Live.java
  77. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveAutoTask.java
  78. 58 0
      fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java
  79. 6 2
      fs-service/src/main/java/com/fs/live/domain/LiveCoupon.java
  80. 4 0
      fs-service/src/main/java/com/fs/live/domain/LiveCouponIssue.java
  81. 7 0
      fs-service/src/main/java/com/fs/live/domain/LiveData.java
  82. 8 0
      fs-service/src/main/java/com/fs/live/domain/LiveMsg.java
  83. 4 0
      fs-service/src/main/java/com/fs/live/domain/LiveOrder.java
  84. 66 0
      fs-service/src/main/java/com/fs/live/domain/LiveTagConfig.java
  85. 8 0
      fs-service/src/main/java/com/fs/live/domain/LiveUserFirstEntry.java
  86. 6 0
      fs-service/src/main/java/com/fs/live/domain/LiveVideo.java
  87. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java
  88. 89 0
      fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java
  89. 16 0
      fs-service/src/main/java/com/fs/live/domain/LiveWatchUser.java
  90. 37 0
      fs-service/src/main/java/com/fs/live/enums/LiveGoodsAddErrorEnum.java
  91. 52 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  92. 11 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponIssueUserMapper.java
  93. 4 1
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponMapper.java
  94. 1 1
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java
  95. 20 2
      fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java
  96. 8 0
      fs-service/src/main/java/com/fs/live/mapper/LiveGoodsMapper.java
  97. 88 6
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  98. 5 2
      fs-service/src/main/java/com/fs/live/mapper/LiveMsgMapper.java
  99. 3 0
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java
  100. 8 1
      fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java

+ 11 - 4
fs-admin/src/main/java/com/fs/his/controller/FsUserController.java

@@ -13,6 +13,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.his.domain.FsUserAddress;
 import com.fs.his.domain.FsUserCompanyUserTransferTask;
+import com.fs.his.dto.FsUserBindSalesParamDTO;
 import com.fs.his.dto.FsUserDTO;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.param.FsUserAddIntegralTemplateParam;
@@ -22,10 +23,7 @@ import com.fs.his.dto.FsUserTransferImportDTO;
 import com.fs.his.service.IFsUserCompanyUserTransferTaskService;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.utils.PhoneUtil;
-import com.fs.his.vo.FsUserCompanyUserTransferTaskDetailVO;
-import com.fs.his.vo.FsUserCompanyUserTransferTaskVO;
-import com.fs.his.vo.FsUserVO;
-import com.fs.his.vo.UserVo;
+import com.fs.his.vo.*;
 import com.fs.qw.dto.UserProjectDTO;
 import com.fs.store.param.h5.FsUserPageListParam;
 import com.fs.store.vo.h5.FsUserPageListVO;
@@ -158,6 +156,15 @@ public class FsUserController extends BaseController
         return util.exportExcel(listDTO, "用户数据");
     }
 
+    @PreAuthorize("@ss.hasPermi('his:user:exportOpenId')")
+    @GetMapping("/exportOpenId")
+    public  AjaxResult exportOpenIdList(){
+        List<UserOpenIdVO> list = fsUserService.selectOpenIdList();
+        ExcelUtil<UserOpenIdVO> util = new ExcelUtil<UserOpenIdVO>(UserOpenIdVO.class);
+        return util.exportExcel(list, "用户openId数据");
+    }
+
+
     @Autowired
     private ISysRoleService sysRoleService;
     private SysRole isCheckPermission() {

+ 29 - 0
fs-admin/src/main/java/com/fs/hisStore/task/ExpressTask.java

@@ -0,0 +1,29 @@
+package com.fs.hisStore.task;
+
+
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 物流信息定时任务
+ */
+@Slf4j
+@Component("expressTask")
+public class ExpressTask {
+
+    @Autowired
+    private IFsStoreOrderScrmService fsStoreOrderScrmService;
+
+    public void syncExpressToWx(){
+        fsStoreOrderScrmService.syncExpressToWx();
+    }
+
+
+    //定时任务刷新订单结算状态
+    public void refreshOrderSettlementStatus(){
+        fsStoreOrderScrmService.refreshOrderSettlementStatus();
+    }
+
+}

+ 584 - 0
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -0,0 +1,584 @@
+package com.fs.hisStore.task;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.vo.RedPacketMoneyVO;
+import com.fs.course.mapper.FsCourseRedPacketLogMapper;
+import com.fs.erp.domain.ErpDeliverys;
+import com.fs.erp.domain.ErpOrderQuery;
+import com.fs.erp.dto.ErpOrderQueryRequert;
+import com.fs.erp.dto.ErpOrderQueryResponse;
+import com.fs.erp.service.FsJstAftersalePushScrmService;
+import com.fs.erp.service.IErpOrderService;
+import com.fs.his.config.FsSysConfig;
+import com.fs.his.dto.ExpressInfoDTO;
+import com.fs.his.service.IFsExpressService;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
+import com.fs.hisStore.dto.DateComparisonConfigDTO;
+import com.fs.hisStore.enums.ShipperCodeEnum;
+import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
+import com.fs.hisStore.param.FsStoreOrderAddTuiMoneyParam;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.hisStore.service.IFsStoreProductScrmService;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.service.HuiFuService;
+import com.fs.live.domain.LiveAfterSales;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderItem;
+import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.mapper.LiveOrderItemMapper;
+import com.fs.live.mapper.LiveOrderMapper;
+import com.fs.live.mapper.LiveOrderPaymentMapper;
+import com.fs.live.param.LiveAfterSalesAudit1Param;
+import com.fs.live.param.LiveAfterSalesParam;
+import com.fs.live.param.LiveAfterSalesProductParam;
+import com.fs.live.service.*;
+import com.fs.pay.pay.dto.OrderQueryDTO;
+import com.fs.pay.service.IPayService;
+import com.fs.store.config.StoreConfig;
+import com.fs.system.service.ISysConfigService;
+import com.fs.ybPay.domain.OrderResult;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static com.fs.hisStore.constants.StoreConstants.DELIVERY;
+
+/**
+ * 定时任务调度测试
+ *
+ * @author fs
+ */
+@Slf4j
+@Component("liveTask")
+public class LiveTask {
+    @Autowired
+    private RedisTemplate redisTemplate;
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private ILiveOrderService liveOrderService;
+
+    @Autowired
+    private ILiveCouponService liveCouponService;
+
+    @Autowired
+    private ILiveCouponIssueService liveCouponIssueService;
+    @Autowired
+    private IFsStoreProductScrmService fsStoreProductScrmService;
+
+    @Autowired
+    private ILiveAfterSalesService liveAfterSalesService;
+
+    @Autowired
+    private ILiveOrderItemService liveOrderItemService;
+
+    @Autowired
+    private ILiveOrderPaymentService liveOrderPaymentService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    @Qualifier("erpOrderServiceImpl")
+    private IErpOrderService gyOrderService;
+
+    @Autowired
+    @Qualifier("wdtErpOrderServiceImpl")
+    private IErpOrderService wdtOrderService;
+
+    @Autowired
+    @Qualifier("hzOMSErpOrderServiceImpl")
+    private IErpOrderService hzOMSOrderService;
+
+    @Autowired
+    @Qualifier("dfOrderServiceImpl")
+    private IErpOrderService dfOrderService;
+
+    @Autowired
+    @Qualifier("JSTErpOrderServiceImpl")
+    private IErpOrderService jSTOrderService;
+
+    @Autowired
+    @Qualifier("k9OrderScrmServiceImpl")
+    private IErpOrderService k9OrderService;
+
+    @Autowired
+    private ConfigUtil configUtil;
+
+    @Autowired
+    IErpOrderService erpOrderService;
+
+    @Autowired
+    private LiveOrderMapper liveOrderMapper;
+
+    @Autowired
+    private LiveOrderItemMapper liveOrderItemMapper;
+
+    @Autowired
+    private LiveOrderPaymentMapper liveOrderPaymentMapper;
+
+    @Autowired
+    private IPayService ybPayService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private IFsExpressService expressService;
+
+    @Autowired
+    private FsCourseRedPacketLogMapper fsCourseRedPacketLogMapper;
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    private HuiFuService huiFuService;
+
+    @Autowired
+    private IFsStoreOrderScrmService orderService;
+
+    @Autowired
+    private FsJstAftersalePushScrmService fsJstAftersalePushScrmService;
+
+    // 聚水潭 推送售后信息
+    public void pushJst(){
+        fsJstAftersalePushScrmService.pushJst();
+    }
+
+
+    // 订单银行回调数据丢失补偿
+    public void recoveryBankOrder() {
+        // 查询出来最近15分钟的订单 待支付 未退款
+        List<LiveOrder> list = liveOrderService.selectBankOrder();
+        if(list == null || list.isEmpty()) return;
+        for (LiveOrder liveOrder : list) {
+            List<LiveOrderPayment> liveOrderPayments = liveOrderPaymentMapper.selectLiveOrderPaymentByOrderId(liveOrder.getOrderId());
+            if(liveOrderPayments == null || liveOrderPayments.isEmpty()) continue;
+            for (LiveOrderPayment payment : liveOrderPayments) {
+                V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+                request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                request.setOrgHfSeqId(payment.getTradeNo());
+                HuiFuQueryOrderResult o = null;
+                try {
+                    o = huiFuService.queryOrder(request);
+                } catch (Exception e) {
+                    log.error("查询失败:"+e.getMessage());
+                    continue;
+                }
+                log.info("汇付返回"+o);
+                if ("00000000".equals(o.getResp_code()) && "S".equals(o.getTrans_stat())) {
+                    String[] order=o.getOrg_req_seq_id().split("-");
+                    if ("live".equals(order[0])) {
+                        liveOrderService.payConfirm(1, null, order[1], o.getOrg_hf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    }
+                }
+            }
+        }
+    }
+
+
+    public void PushErp() throws ParseException {
+        List<Long> ids = liveOrderMapper.selectOrderIdByNoErp();
+        if(ids == null) return;
+        if (ids.size() > 50) {
+            ids = ids.subList(0, 50);
+        }
+//        liveOrderService.batchUpdateTimeIds(ids);
+        // 单个异常影响全部,跳过异常单子
+        for (Long id : ids) {
+            try {
+                liveOrderService.createOmsOrder(id);
+            } catch (Exception e) {
+                log.error("创建直播oms订单失败:"+id);
+                log.error("创建直播oms订单失败:"+e.getMessage());
+            }
+
+        }
+    }
+
+    public void redPacketSubMoney() throws Exception {
+        List<RedPacketMoneyVO> redPacketMoneyVOS = fsCourseRedPacketLogMapper.selectFsCourseRedPacketLogByCompany();
+        for (RedPacketMoneyVO redPacketMoneyVO : redPacketMoneyVOS) {
+            companyService.subtractCompanyMoney(redPacketMoneyVO.getMoney(), redPacketMoneyVO.getCompanyId());
+        }
+    }
+
+    public void redPacketAddMoney() throws Exception {
+        List<RedPacketMoneyVO> redPacketMoneyVOS = fsCourseRedPacketLogMapper.selectFsCourseAddRedPacketLogByCompany();
+        for (RedPacketMoneyVO redPacketMoneyVO : redPacketMoneyVOS) {
+            companyService.addRedPacketCompanyMoney(redPacketMoneyVO.getMoney(), redPacketMoneyVO.getCompanyId());
+        }
+    }
+
+
+    //定时任务刷新微信订单结算状态
+    public void refreshOrderSettlementStatus(){
+        liveOrderService.refreshOrderSettlementStatus();
+    }
+
+
+
+    //每5分钟执行一次
+    public void deliveryOp() {
+        List<LiveOrder> list = liveOrderService.selectUpdateExpress();
+        if(list == null || list.isEmpty()) return;
+
+        for (LiveOrder order : list) {
+            order.setUpdateTime(new Date());
+            liveOrderService.updateLiveOrder(order);
+            ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+            request.setCode(order.getExtendOrderId());
+            IErpOrderService erpOrderService = getErpOrderService();
+            ErpOrderQueryResponse response = erpOrderService.getLiveOrder(request);
+            if(!response.getSuccess() && "429".equals(response.getCode())){
+                break;
+            }
+            if (erpOrderService != dfOrderService) {
+                if (response.getOrders() != null && !response.getOrders().isEmpty()) {
+                    for (ErpOrderQuery orderQuery : response.getOrders()) {
+                        if (orderQuery.getDeliverys() != null && !orderQuery.getDeliverys().isEmpty()) {
+                            for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+                                if (delivery.getDelivery() && StringUtils.isNotEmpty(delivery.getMail_no())) {
+                                    //更新商订单状态 删除REDIS
+                                    liveOrderService.deliveryOrder(order.getOrderCode(), delivery.getMail_no(), delivery.getExpress_code(), delivery.getExpress_name());
+                                    redisCache.deleteObject(DELIVERY + ":" + order.getExtendOrderId());
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+    }
+
+    public void couponOp() {
+        // 直播优惠券过期处理,如果有对应方法则调用
+        // liveCouponService.updateFsCouponByExpire();
+    }
+
+    //退款自动处理 24小时未审核自动审核通过 每小时执行一次
+    public void refundOp() {
+        //获取所有退款申请
+        List<LiveAfterSales> list = liveAfterSalesService.selectLiveAfterSalesByDoAudit();
+        if (list != null) {
+            for (LiveAfterSales afterSales : list) {
+                //仅退款
+                if (afterSales.getRefundType().equals(0)) {
+                    LiveAfterSalesAudit1Param audit1Param = new LiveAfterSalesAudit1Param();
+                    audit1Param.setSalesId(afterSales.getId());
+                    audit1Param.setOperator("平台");
+                    liveAfterSalesService.audit1(audit1Param);
+                }
+            }
+        }
+    }
+
+    //每天执行一次
+    public void userMoneyOp() {
+        // 直播订单完成7天后给用户返现,如果有对应方法则调用
+        // List<LiveOrder> list = liveOrderService.selectLiveOrderListByFinish7Day();
+        // if (list != null) {
+        //     for (LiveOrder order : list) {
+        //         userService.addMoney(order);
+        //     }
+        // }
+    }
+
+    //每30秒执行一次
+    public void orderItemSyncOp() {
+//         同步订单项JSON,如果有对应方法则调用
+         List<LiveOrder> list = liveOrderService.selectLiveOrderItemJson();
+         for (LiveOrder storeOrder : list) {
+             LiveOrderItem parmOrderItem = new LiveOrderItem();
+             parmOrderItem.setOrderId(storeOrder.getOrderId());
+             List<LiveOrderItem> listOrderItem = liveOrderItemService.selectLiveOrderItemList(parmOrderItem);
+             if (listOrderItem.size() > 0) {
+                 String itemJson = JSONUtil.toJsonStr(listOrderItem);
+                 storeOrder.setItemJson(itemJson);
+                 liveOrderMapper.updateLiveOrderItemJson(storeOrder);
+             }
+         }
+    }
+
+    public void returnDeliveryId() {
+        IErpOrderService erpOrderService = getErpOrderService();
+        // 获取ERP订单号列表,如果有对应方法则调用
+        // List<String> list = liveOrderMapper.selectErpCode();
+        // for (String s : list) {
+        //     ErpOrderQueryRequert request = new ErpOrderQueryRequert();
+        //     request.setCode(s);
+        //     ErpOrderQueryResponse response = erpOrderService.getOrder(request);
+        //     if (response.getOrders() != null && response.getOrders().size() > 0) {
+        //         for (ErpOrderQuery orderQuery : response.getOrders()) {
+        //             if (orderQuery.getDeliverys() != null && orderQuery.getDeliverys().size() > 0) {
+        //                 for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
+        //                     if (delivery.getDelivery() && StringUtils.isNotEmpty(delivery.getMail_no())) {
+        //                         LiveOrder order = new LiveOrder();
+        //                         order.setExtendOrderId(s);
+        //                         order.setDeliverySn(delivery.getMail_no());
+        //                         order.setStatus(2);
+        //                         liveOrderMapper.updateDelivery(order);
+        //                     }
+        //                 }
+        //             }
+        //         }
+        //     }
+        // }
+    }
+
+    public void changeStatus() {
+//         获取需要更新物流状态的订单ID列表,如果有对应方法则调用
+//         List<Long> list = liveOrderMapper.selectOrderId();
+//         for (Long orderId : list) {
+//             LiveOrder order = liveOrderMapper.selectLiveOrderByOrderId(String.valueOf(orderId));
+//             String lastFourNumber = "";
+//             if (order.getDeliverySn() != null && order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue())) {
+//                 lastFourNumber = order.getUserPhone();
+//                 if (lastFourNumber != null && lastFourNumber.length() == 11) {
+//                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+//                 }
+//             }
+//             ExpressInfoDTO dto = expressService.getExpressInfo(order.getOrderCode(), order.getDeliverySn(), order.getDeliverySn(), lastFourNumber);
+//             LiveOrder map = new LiveOrder();
+//             map.setDeliveryStatus(Integer.parseInt(dto.getState()));
+//             map.setOrderId(orderId);
+//             map.setDeliveryType(dto.getStateEx());
+//             liveOrderMapper.updateLiveOrder(map);
+//         }
+    }
+
+    public void subCompanyMoney() {
+        // 获取需要扣减公司金额的支付ID列表,如果有对应方法则调用
+        // List<Long> list = liveOrderPaymentMapper.selectPaymentIds();
+        // for (Long paymentId : list) {
+        //     LiveOrderPayment payment = liveOrderPaymentService.selectLiveOrderPaymentByPaymentId(paymentId);
+        //     if (payment.getCompanyId() != null && payment.getCompanyId() > 0) {
+        //         companyService.subCompanyPaymentMoney(payment);
+        //     }
+        // }
+    }
+
+    public void updateOrderItem() throws ParseException {
+//        List<Long> ids = liveOrderService.selectOrderIdByNoErp();
+//        for (Long id : ids) {
+//            liveOrderService.createOmsOrder(id);
+//        }
+    }
+
+    //每天执行一次
+    public void syncExpress() {
+        List<Long> ids = liveOrderService.selectSyncExpressIds();
+        for (Long id : ids) {
+            liveOrderService.syncExpress(id);
+        }
+    }
+
+    public void returnPayStatus() {
+        // 获取需要查询支付状态的支付ID列表,如果有对应方法则调用
+        // List<String> ids = liveOrderPaymentMapper.selectPayStatusIds();
+        // for (String id : ids) {
+        //     OrderQueryDTO o = new OrderQueryDTO();
+        //     o.setUpOrderId(id);
+        //     OrderResult orderResult = ybPayService.getOrder(o);
+        //     if ("0".equals(orderResult.getState())) {
+        //         String[] order = orderResult.getLowOrderId().split("-");
+        //         if (orderResult.getStatus().equals("100")) {
+        //             switch (order[0]) {
+        //                 case "live":
+        //                     liveOrderService.payConfirm(1, null, order[1], o.getUpOrderId(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
+        //                 case "live_remain":
+        //                     liveOrderService.payConfirm(1, null, order[1], o.getUpOrderId(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
+        //                 case "payment":
+        //                     liveOrderPaymentService.payConfirm(order[1], o.getUpOrderId(), orderResult.getBankTrxId(), orderResult.getBankOrderId());
+        //             }
+        //         }
+        //     }
+        // }
+    }
+
+    public void AddTuiMoney() {
+        // 获取需要添加推荐金额的订单ID列表,如果有对应方法则调用
+        // List<Long> ids = liveOrderMapper.selectAddTuiMoney();
+        // for (Long id : ids) {
+        //     FsStoreOrderAddTuiMoneyParam param = new FsStoreOrderAddTuiMoneyParam();
+        //     param.setOrderId(id);
+        //     liveOrderService.addTuiMoney(param);
+        // }
+    }
+
+    public void selectPayMoneyLessOne() {
+        // 获取支付金额小于1的订单列表,如果有对应方法则调用
+        // List<LiveOrder> list = liveOrderMapper.selectPayMoneyLessOne();
+        // for (LiveOrder order : list) {
+        //     LiveAfterSalesParam param = new LiveAfterSalesParam();
+        //     param.setOrderCode(order.getOrderCode());
+        //     param.setServiceType(0);
+        //     param.setRefundAmount(order.getPayMoney());
+        //     param.setReasons("超时未处理,自动申请退款");
+        //     List<LiveAfterSalesProductParam> productParams = new ArrayList<>();
+        //     List<LiveOrderItem> items = liveOrderItemMapper.selectLiveOrderItemByOrderId(order.getOrderId());
+        //     for (LiveOrderItem item : items) {
+        //         LiveAfterSalesProductParam param1 = new LiveAfterSalesProductParam();
+        //         param1.setProductId(item.getProductId());
+        //         param1.setNum(item.getNum());
+        //         productParams.add(param1);
+        //     }
+        //     param.setProductList(productParams);
+        //     liveAfterSalesService.applyForAfterSales(order.getUserId(), param);
+        // }
+    }
+
+    public void deleteCustomer() {
+        // 删除客户逻辑
+    }
+
+    private IErpOrderService getErpOrderService() {
+        //判断是否开启erp
+        IErpOrderService erpOrderService = null;
+        FsSysConfig erpConfig = configUtil.getSysConfig();
+        Integer erpOpen = erpConfig.getErpOpen();
+        if (erpOpen != null && erpOpen == 1) {
+            //判断erp类型
+            Integer erpType = erpConfig.getErpType();
+            if (erpType != null) {
+                if (erpType == 1) {
+                    //管易
+                    erpOrderService = gyOrderService;
+                } else if (erpType == 2) {
+                    //旺店通
+                    erpOrderService = wdtOrderService;
+                } else if (erpType == 3) {
+                    //代服
+                    erpOrderService = hzOMSOrderService;
+                } else if (erpType == 4) {
+                    //瀚智
+                    erpOrderService = dfOrderService;
+                } else if (erpType == 5) {
+                    erpOrderService = jSTOrderService;
+                } else if (erpType == 6) {
+                    erpOrderService = k9OrderService;
+                }
+            }
+        }
+        return erpOrderService;
+    }
+
+    /**
+     * 提醒证件到期任务
+     */
+    public void remindCertValidation() {
+        log.info("提醒店铺证件到期任务执行... 当前时间: {}", LocalTime.now());
+
+        // 从配置表获取需要比较的表和字段
+        List<DateComparisonConfigDTO> tablesToCheck = jdbcTemplate.query(
+                "SELECT table_name, date_column,in_advance,user_column,phone_column,remind_words,platform,cert_type" +
+                        " FROM date_comparison_config", (rs, rowNum) -> {
+                    return DateComparisonConfigDTO.builder()
+                            .tableName(rs.getString("table_name"))//表名
+                            .certType(rs.getString("cert_type"))//证件类型
+                            .dateColumn(rs.getString("date_column"))//日期字段
+                            .userColumn(rs.getString("user_column"))//用户字段
+                            .remindWords(rs.getString("remindWords"))//提醒内容
+                            .phoneColumn(rs.getString("phone_column"))//提醒手机
+                            .inAdvance(rs.getInt("inAdvance"))//提前天数
+                            .platform(rs.getString("platform")).build();//平台
+                });
+
+        tablesToCheck.forEach(dto -> {
+            //获取证件失效日期字段小于当前时间加提前天数的用户和电话号码
+            String sql = String.format("SELECT %s , %s " +
+                            "FROM %s " +
+                            "WHERE %s >= DATE_SUB(CURDATE(), INTERVAL %d DAY)",
+                    dto.getUserColumn(),
+                    dto.getPhoneColumn(),
+                    dto.getTableName(),
+                    dto.getDateColumn(),
+                    dto.getInAdvance()
+            );
+            List<Map<String, Object>> users = jdbcTemplate.queryForList(sql);
+            users.forEach(user -> {
+                String userName = (String) user.get(dto.getUserColumn());
+                String phone = (String) user.get(dto.getPhoneColumn());
+                String remindWords = String.format("【%s平台提示】尊敬的%s用户,店铺%s证件即将到期,请及时处理!",
+                        dto.getPlatform(),
+                        userName,
+                        dto.getCertType());
+                // 使用phone发送remindWords短信
+                // TODO 发送通知
+            });
+        });
+    }
+
+    /**
+     * 禁用店铺
+     */
+    public void disable() {
+        log.info("禁用店铺任务执行... 当前时间: {}", LocalTime.now());
+        // 从配置表获取需要禁用的表和字段
+        List<DateComparisonConfigDTO> toDisable = jdbcTemplate.query(
+                "SELECT table_name, date_column,invalid_expression,status_column" +
+                        " FROM date_comparison_config " +
+                        " WHERE is_do_invalid = '1'", (rs, rowNum) -> DateComparisonConfigDTO.builder()
+                        .tableName(rs.getString("table_name"))//表名
+                        .dateColumn(rs.getString("date_column"))//日期字段
+                        .invalidExpression(rs.getString("invalid_expression"))//失效表达式
+                        .statusColumn(rs.getString("status_column"))//状态字段
+                        .build());
+
+        toDisable.forEach(dto -> {
+            //更新证件失效日期字段小于当前时间的数据
+            String sql = String.format("UPDATE %s " +
+                            "SET %s = %s " +
+                            "WHERE %s < CURDATE()",
+                    dto.getTableName(),
+                    dto.getStatusColumn(),
+                    dto.getInvalidExpression(),
+                    dto.getDateColumn());
+            jdbcTemplate.update(sql);
+        });
+    }
+
+    public void getOrderDeliveryStatus() {
+        IErpOrderService erpOrderService = getErpOrderService();
+        List<LiveOrder> orders = null;
+        if (erpOrderService != null && erpOrderService == dfOrderService) {
+            // 获取已发货订单列表,如果有对应方法则调用
+             orders = liveOrderMapper.selectShippedOrder();
+             if (orders != null && !orders.isEmpty()) {
+                 List<CompletableFuture<Void>> futures = new ArrayList<>();
+                 for (LiveOrder order : orders) {
+                     CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+                         erpOrderService.getOrderLiveDeliveryStatus(order);
+                     });
+                     futures.add(future);
+                 }
+                 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+             }
+        }
+    }
+}

+ 88 - 6
fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java

@@ -1,5 +1,6 @@
 package com.fs.live.controller;
 
+import cn.hutool.core.date.DateTime;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -11,8 +12,11 @@ import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.web.service.TokenService;
+import com.fs.his.domain.FsStoreAfterSalesLogs;
 import com.fs.his.domain.FsUser;
+import com.fs.his.enums.FsStoreAfterSalesStatusEnum;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.vo.FsStoreOrderItemExportRefundZMVO;
 import com.fs.live.domain.LiveAfterSales;
 import com.fs.live.domain.LiveAfterSalesItem;
 import com.fs.live.domain.LiveAfterSalesLogs;
@@ -28,11 +32,13 @@ import com.fs.live.service.ILiveOrderService;
 import com.fs.live.vo.LiveAfterSalesVo;
 import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.text.ParseException;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * 售后记录Controller
@@ -57,11 +63,14 @@ public class LiveAfterSalesController extends BaseController
     private IFsUserService userService;
     @Autowired
     private ILiveOrderService orderService;
+    @Value("${cloud_host.company_name}")
+    private String signProjectName;
+
 
     /**
      * 获取售后记录详细信息
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:query')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:query')")
     @GetMapping(value = "/{id}")
     public R getInfo(@PathVariable("id") Long id)
     {
@@ -79,11 +88,15 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 查询售后记录列表
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:list')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:list')")
     @GetMapping("/list")
     public TableDataInfo list(LiveAfterSalesVo liveAfterSales)
     {
         startPage();
+        // 将productName映射到productNameQuery用于查询
+        if (liveAfterSales.getProductName() != null && !liveAfterSales.getProductName().isEmpty()) {
+            liveAfterSales.setProductNameQuery(liveAfterSales.getProductName());
+        }
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(ParseUtils.parsePhone(liveAfterSalesVo.getUserPhone()));
@@ -94,7 +107,7 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 导出售后记录列表
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:export')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:export')")
     @Log(title = "售后记录", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(LiveAfterSalesVo liveAfterSales)
@@ -102,6 +115,54 @@ public class LiveAfterSalesController extends BaseController
         PageHelper.clearPage();
         PageHelper.startPage(1, 10000, "");
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
+        if("北京卓美".equals(signProjectName)){
+            List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
+                    .map(vo -> {
+                        FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
+                        try {
+                            zmvo.setPayCode(vo.getPayCode());
+                            zmvo.setOrderCode(vo.getOrderCode());
+                            zmvo.setStatus(vo.getOrderStatus().toString());
+                            zmvo.setUserId(vo.getUserId());
+                            zmvo.setProductName(vo.getProductName());
+                            zmvo.setBarCode(vo.getProductBarCode());
+                            zmvo.setSku(vo.getSku());
+                            zmvo.setNum(vo.getNum());
+                            zmvo.setPrice(vo.getPrice());
+                            zmvo.setCost(vo.getCost());
+//                            zmvo.setFPrice("");
+                            zmvo.setPayMoney(vo.getPayMoney());
+                            zmvo.setPayPostage(vo.getTotalPostage());
+                            zmvo.setCateName(vo.getCateName());
+                            zmvo.setRealName(vo.getUserName());
+                            zmvo.setUserPhone(vo.getUserPhone());
+                            zmvo.setUserAddress(vo.getUserAddress());
+                            zmvo.setCreateTime(vo.getCreateTime());
+                            zmvo.setPayTime(vo.getOrderPayTime());
+                            zmvo.setDeliverySn(vo.getOrderDeliverySn());
+                            zmvo.setDeliveryName(vo.getOrderDeliveryName());
+                            zmvo.setDeliveryId(vo.getOrderDeliveryId());
+                            zmvo.setCompanyName(vo.getCompanyName());
+                            zmvo.setCompanyUserNickName(vo.getCompanyUserNickName());
+                            zmvo.setRefundTime(vo.getCreateTime());
+//                            zmvo.setAfterSalesNumber
+                            zmvo.setRefundMoney(vo.getRefundAmount());
+                            zmvo.setBankTransactionId(vo.getBankTransactionId());
+                            zmvo.setReasons(vo.getReasons());
+                            zmvo.setExplains(vo.getExplains());
+                        } catch (Exception e) {
+                            // 处理异常
+                            e.printStackTrace();
+                        }
+                        return zmvo;
+                    })
+                    .collect(Collectors.toList());
+            for (FsStoreOrderItemExportRefundZMVO vo : zmvoList){
+                vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            }
+            ExcelUtil<FsStoreOrderItemExportRefundZMVO> util = new ExcelUtil<FsStoreOrderItemExportRefundZMVO>(FsStoreOrderItemExportRefundZMVO.class);
+            return util.exportExcel(zmvoList, "退款订单导出");
+        }
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(liveAfterSalesVo.getUserPhone() == null ? "" : liveAfterSalesVo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
             liveAfterSalesVo.setPhoneNumber(liveAfterSalesVo.getPhoneNumber() == null ? "" : liveAfterSalesVo.getPhoneNumber().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
@@ -114,7 +175,7 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 新增售后记录
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:add')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:add')")
     @Log(title = "售后记录", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody LiveAfterSales liveAfterSales)
@@ -125,18 +186,29 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 修改售后记录
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:edit')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:edit')")
     @Log(title = "售后记录", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody LiveAfterSales liveAfterSales)
     {
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        //操作记录
+        LiveAfterSalesLogs logs = new LiveAfterSalesLogs();
+        logs.setChangeTime(new DateTime());
+        logs.setChangeType(2);
+        logs.setOperator(loginUser.getUser().getNickName());
+        logs.setStoreAfterSalesId(liveAfterSales.getId());
+        logs.setChangeMessage(FsStoreAfterSalesStatusEnum.STATUS_2.getDesc());
+        liveAfterSales.setStatus(FsStoreAfterSalesStatusEnum.STATUS_2.getValue());
+        liveAfterSalesLogsService.insertLiveAfterSalesLogs(logs);
         return toAjax(liveAfterSalesService.updateLiveAfterSales(liveAfterSales));
     }
 
     /**
      * 删除售后记录
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:remove')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:remove')")
     @Log(title = "售后记录", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)
@@ -180,4 +252,14 @@ public class LiveAfterSalesController extends BaseController
         param.setOperator(loginUser.getUser().getNickName());
         return liveAfterSalesService.cancel(param);
     }
+
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSales:refund')")
+    @PostMapping("/handleImmediatelyRefund")
+    public R handleImmediatelyRefund(@RequestBody LiveAfterSalesRefundParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setOperator(loginUser.getUser().getNickName());
+        return liveAfterSalesService.handleImmediatelyRefund(param.getOrderId());
+    }
+
 }

+ 3 - 3
fs-admin/src/main/java/com/fs/live/controller/LiveAutoTaskController.java

@@ -66,7 +66,7 @@ public class LiveAutoTaskController extends BaseController
         return getDataTable(list);
     }
 
-    @PreAuthorize("@ss.hasPermi('live:task:list')")
+//    @PreAuthorize("@ss.hasPermi('live:task:list')")
     @GetMapping("/consoleList")
     public TableDataInfo consoleList(LiveAutoTask liveAutoTask)
     {
@@ -116,9 +116,9 @@ public class LiveAutoTaskController extends BaseController
 //    @PreAuthorize("@ss.hasPermi('shop:task:edit')")
     @Log(title = "直播间自动化任务配置", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody LiveAutoTask liveAutoTask)
+    public R edit(@RequestBody LiveAutoTask liveAutoTask)
     {
-        return toAjax(liveAutoTaskService.updateLiveAutoTask(liveAutoTask));
+        return liveAutoTaskService.updateLiveAutoTask(liveAutoTask);
     }
 
     /**

+ 57 - 4
fs-admin/src/main/java/com/fs/live/controller/LiveController.java

@@ -4,17 +4,30 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.vo.CompanyVO;
+import com.fs.framework.web.service.TokenService;
+import com.fs.hisStore.task.LiveTask;
+import com.fs.hisStore.task.MallStoreTask;
 import com.fs.live.domain.Live;
 import com.fs.live.service.ILiveService;
 import com.fs.live.vo.LiveListVo;
-import com.fs.task.LiveTask;
+import com.fs.qw.domain.QwTagGroup;
+import com.fs.qw.service.IQwTagGroupService;
+import com.fs.qw.service.impl.QwUserServiceImpl;
+import com.fs.qw.vo.QwOptionsVO;
+import com.fs.qw.vo.QwTagGroupListVO;
+import com.hc.openapi.tool.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.text.ParseException;
 import java.util.List;
 import java.util.Map;
 
@@ -26,11 +39,20 @@ import java.util.Map;
  */
 @RestController
 @RequestMapping("/live/live")
+@Slf4j
 public class LiveController extends BaseController {
     @Autowired
     private ILiveService liveService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    QwUserServiceImpl qwUserService;
+
     @Autowired
-    private LiveTask liveTask;
+    private IQwTagGroupService qwTagGroupService;
+
 
     /**
      * 查询直播列表
@@ -62,7 +84,7 @@ public class LiveController extends BaseController {
     @PreAuthorize("@ss.hasPermi('live:live:query')")
     @GetMapping(value = "/{liveId}")
     public AjaxResult getInfo(@PathVariable("liveId") Long liveId) {
-        return AjaxResult.success(liveService.selectLiveByLiveId(liveId));
+        return AjaxResult.success(liveService.selectLiveDbByLiveId(liveId));
     }
 
     /**
@@ -82,6 +104,8 @@ public class LiveController extends BaseController {
     @Log(title = "直播", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody Live live) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        log.info("loginUser:{},update:{}", loginUser.getUserId(), JSON.toJSONString( live));
         return toAjax(liveService.updateLive(live));
     }
 
@@ -92,7 +116,9 @@ public class LiveController extends BaseController {
     @Log(title = "直播", businessType = BusinessType.DELETE)
     @DeleteMapping("/{liveIds}")
     public AjaxResult remove(@PathVariable Long[] liveIds) {
-        return toAjax(liveService.deleteLiveByLiveIds(liveIds));
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        log.info("loginUser:{},update:{}", loginUser.getUserId(), JSON.toJSONString( liveIds));
+        return toAjax(liveService.deleteLiveByLiveIds(liveIds, new Live()));
     }
 
     @PreAuthorize("@ss.hasPermi('live:live:query')")
@@ -115,6 +141,8 @@ public class LiveController extends BaseController {
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
     @PostMapping("/handleShelfOrUn")
     public R handleShelfOrUn(@RequestBody LiveListVo listVo) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        log.info("loginUser:{},update:{}", loginUser.getUserId(), JSON.toJSONString( listVo));
         return liveService.handleShelfOrUnAdmin(listVo);
     }
 
@@ -124,6 +152,8 @@ public class LiveController extends BaseController {
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
     @PostMapping("/handleDeleteSelected")
     public R handleDeleteSelected(@RequestBody LiveListVo listVo) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        log.info("loginUser:{},update:{}", loginUser.getUserId(), JSON.toJSONString( listVo));
         return liveService.handleDeleteSelectedAdmin(listVo);
     }
     /**
@@ -168,4 +198,27 @@ public class LiveController extends BaseController {
     }
 
 
+    /**
+     * 获取公司下拉列表
+     * @return
+     */
+    @GetMapping("/getCompanyDropList")
+    public R getCompanyDropList(){
+        List<CompanyVO> companyDropList = liveService.getCompanyDropList();
+        return R.ok().put("data",companyDropList);
+    }
+
+    @GetMapping("/getQwCorpList/{companyId}")
+    public R getQwCorpList(@PathVariable Long companyId){
+        List<QwOptionsVO> qwOptionsVOS = qwUserService.selectQwCompanyListOptionsVOByCompanyId(companyId);
+        return R.ok().put("data",qwOptionsVOS);
+    }
+
+    @GetMapping("/getTagsListByCorpId")
+    public TableDataInfo getTagsListByCorpId(QwTagGroup qwTagGroup){
+        startPage();
+        List<QwTagGroupListVO> list = qwTagGroupService.selectQwTagGroupListVO(qwTagGroup);
+        return getDataTable(list);
+    }
+
 }

+ 4 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveCouponController.java

@@ -122,6 +122,8 @@ public class LiveCouponController extends BaseController
         issue.setIsPermanent(0);
         issue.setStatus(1);
         issue.setCreateTime(new Date());
+        // 继承优惠券的领取上限字段
+        issue.setLimitReceiveCount(coupon.getLimitReceiveCount());
         return toAjax( liveCouponIssueService.insertLiveCouponIssue(issue));
     }
 
@@ -143,6 +145,8 @@ public class LiveCouponController extends BaseController
             issue.setIsPermanent(0);
             issue.setStatus(1);
             issue.setCreateTime(new Date());
+            // 继承优惠券的领取上限字段
+            issue.setLimitReceiveCount(coupon.getLimitReceiveCount());
             liveCouponIssueService.insertLiveCouponIssue(issue);
         }
         return R.ok();

+ 80 - 1
fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java

@@ -1,12 +1,19 @@
 package com.fs.live.controller;
 
+import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.live.domain.LiveData;
+import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.LiveUserFirstVo;
+import com.fs.live.vo.LiveUserDetailExportVO;
+import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -17,7 +24,7 @@ import java.util.Map;
 
 
 @RestController
-@RequestMapping("/live/liveData")
+@RequestMapping("/liveData/liveData")
 public class LiveDataController extends BaseController {
 
     @Autowired
@@ -41,6 +48,17 @@ public class LiveDataController extends BaseController {
         return getDataTable(list);
     }
 
+    /**
+     * 查询新直播数据列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:list')")
+    @PostMapping("/listLiveData")
+    public R listLiveData(@RequestBody LiveDataParam param, HttpServletRequest request)
+    {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        return liveDataService.listLiveData(param);
+    }
+
     /**
      * 查询直播数据列表
      * */
@@ -96,5 +114,66 @@ public class LiveDataController extends BaseController {
         return R.ok(liveViewData);
     }
 
+    /**
+     * 查询直播间详情数据(SQL方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailBySql")
+    public R getLiveDataDetailBySql(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailBySql(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(SQL方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListBySql")
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId) {
+        return liveDataService.getLiveUserDetailListBySql(liveId,null,null);
+    }
+
+    /**
+     * 查询直播间详情数据(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailByServer")
+    public R getLiveDataDetailByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailByServer(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListByServer")
+    public R getLiveUserDetailListByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveUserDetailListByServer(liveId);
+    }
+
+    /**
+     * 导出直播间用户详情数据
+     * @param liveId 直播间ID
+     * @return Excel文件
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
+    @Log(title = "直播间用户详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportLiveUserDetail")
+    public AjaxResult exportLiveUserDetail(@RequestParam Long liveId) {
+        List<LiveUserDetailExportVO> list = liveDataService.exportLiveUserDetail(liveId,null,null);
+        if (list == null || list.isEmpty()) {
+            return AjaxResult.error("未找到用户详情数据");
+        }
+
+        ExcelUtil<LiveUserDetailExportVO> util = new ExcelUtil<>(LiveUserDetailExportVO.class);
+        return util.exportExcel(list, "直播间用户详情数据");
+    }
 
 }

+ 102 - 9
fs-admin/src/main/java/com/fs/live/controller/LiveHealthOrderController.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.CloudHostUtils;
@@ -23,20 +24,15 @@ import com.fs.live.param.LiveOrderParam;
 import com.fs.live.service.ILiveOrderDfService;
 import com.fs.live.service.ILiveOrderItemService;
 import com.fs.live.service.ILiveOrderService;
-import com.fs.live.vo.LiveOrderErpExportVO;
-import com.fs.live.vo.LiveOrderItemExportVO;
-import com.fs.live.vo.LiveOrderListAndStatisticsVo;
-import com.fs.live.vo.LiveOrderVO;
+import com.fs.live.vo.*;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**`
  * 订单Controller
@@ -318,9 +314,106 @@ public class LiveHealthOrderController extends BaseController {
         return util.exportExcel(list, "订单明细数据");
     }
 
+
+
+    // 允许的文件扩展名
+    private static final String[] ALLOWED_EXCEL_EXTENSIONS = {".xlsx", ".xls"};
+
+    // 最大文件大小(5MB)
+    private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
+
+    /**
+     * 下载订单发货导入模板
+     */
     @GetMapping("/importDeliveryNoteExpressTemplate")
-    public AjaxResult importTemplate() {
+    public AjaxResult importDeliveryNoteExpressTemplate() {
         ExcelUtil<LiveOrderDeliveryNoteDTO> util = new ExcelUtil<>(LiveOrderDeliveryNoteDTO.class);
         return util.importTemplateExcel("订单发货导入模板");
     }
+
+    /**
+     * 订单发货批量导入
+     */
+    @Log(title = "发货同步导入", businessType = BusinessType.IMPORT)
+    @PostMapping("/importDeliveryNoteExpress")
+    public R importDeliveryNoteExpress(@RequestParam("file") MultipartFile file, @RequestParam("miniAppId") String miniAppId) {
+        // 1. 检查文件是否为空
+        if (file.isEmpty()) {
+            return R.error("上传的文件不能为空");
+        }
+        // 2. 检查文件大小
+        if (file.getSize() > MAX_FILE_SIZE) {
+            return R.error("文件大小不能超过5MB");
+        }
+        // 3. 检查文件扩展名
+        String fileName = file.getOriginalFilename();
+        if (fileName == null || !isValidExcelFile(fileName)) {
+            return R.error("请上传Excel文件(.xlsx或.xls格式)");
+        }
+
+        ExcelUtil<LiveOrderDeliveryNoteDTO> util = new ExcelUtil<>(LiveOrderDeliveryNoteDTO.class);
+        try {
+            List<LiveOrderDeliveryNoteDTO> dtoList = util.importExcel(file.getInputStream());
+            if (!dtoList.isEmpty()) {
+                if (dtoList.size() > 200) {
+                    return R.error("操作失败,导入数据不能大于200条!");
+                }
+                return liveOrderService.importDeliveryNoteExpress(dtoList,miniAppId);
+            } else {
+                return R.error("操作失败,导入数据不能小于1条!");
+            }
+        } catch (Exception e) {
+            logger.error("导入发货单失败", e);
+            return R.error("导入失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 发货单导出接口
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveOrder:healthExportShippingOrder')")
+    @Log(title = "发货单导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/healthExportShippingOrder")
+    public AjaxResult healthExportShippingOrder(LiveOrderParam param) {
+        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())) {
+            param.setBeginTime(null);
+            param.setEndTime(null);
+        }
+        if (liveOrderService.isEntityNull(param)) {
+            param = new LiveOrderParam();
+        }
+        if (!StringUtils.isEmpty(param.getCreateTimeRange())) {
+            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
+        }
+        if (!StringUtils.isEmpty(param.getPayTimeRange())) {
+            param.setPayTimeList(param.getPayTimeRange().split("--"));
+        }
+        if (!StringUtils.isEmpty(param.getDeliverySendTimeRange())) {
+            param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
+        }
+        if (!StringUtils.isEmpty(param.getDeliveryImportTimeRange())) {
+            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
+        }
+        List<LiveOrderDeliveryNoteExportVO> deliveryNoteExportVOList = liveOrderService.getDeliveryNote(param);
+        ExcelUtil<LiveOrderDeliveryNoteExportVO> util = new ExcelUtil<>(LiveOrderDeliveryNoteExportVO.class);
+        //通过商品ID获取关键字
+        String firstKeyword = deliveryNoteExportVOList.stream()
+                .map(LiveOrderDeliveryNoteExportVO::getKeyword)
+                .findFirst()
+                .orElse("无订单");
+        String fileName = "077AC" + firstKeyword + new java.text.SimpleDateFormat("yyyyMMdd").format(new Date());
+        return util.exportExcel(deliveryNoteExportVOList, fileName);
+    }
+
+    /**
+     * 检查文件是否为有效的Excel文件
+     */
+    private boolean isValidExcelFile(String fileName) {
+        for (String ext : ALLOWED_EXCEL_EXTENSIONS) {
+            if (fileName.toLowerCase().endsWith(ext)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

+ 181 - 33
fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java

@@ -6,6 +6,7 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ParseUtils;
@@ -30,22 +31,29 @@ import com.fs.his.enums.FsStoreOrderLogEnum;
 import com.fs.his.service.IFsDfAccountService;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.hisStore.config.FsErpConfig;
 import com.fs.hisStore.dto.StoreOrderExpressExportDTO;
 import com.fs.hisStore.param.*;
+import com.fs.hisStore.service.IFsExpressScrmService;
+import com.fs.hisStore.task.ExpressTask;
+import com.fs.hisStore.task.LiveTask;
+import com.fs.hisStore.vo.FsStoreOrderItemExportZMVO;
 import com.fs.hisStore.vo.FsStoreOrderVO;
 import com.fs.live.domain.*;
+import com.fs.live.dto.LiveOrderCustomerExportDTO;
+import com.fs.live.dto.LiveOrderDeliveryNoteDTO;
 import com.fs.live.dto.LiveOrderExpressExportDTO;
+import com.fs.live.param.LiveOrderParam;
+import com.fs.live.vo.LiveOrderDeliveryNoteExportVO;
 import com.fs.live.enums.LiveOrderCancleReason;
 import com.fs.live.param.LiveOrderScrmSetErpPhoneParam;
 import com.fs.live.service.*;
-import com.fs.live.vo.LiveGoodsVo;
-import com.fs.live.vo.LiveOrderPaymentVo;
-import com.fs.live.vo.LiveOrderTimeVo;
-import com.fs.live.vo.LiveOrderVO;
+import com.fs.live.vo.*;
+import com.fs.qw.utils.RSAUtils;
 import com.fs.store.domain.FsStoreDelivers;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
-import com.fs.task.LiveTask;
 import io.swagger.annotations.ApiOperation;
 import org.apache.http.util.Asserts;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -55,8 +63,10 @@ import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Date;
@@ -90,13 +100,12 @@ public class LiveOrderController extends BaseController
     @Autowired
     private TokenService tokenService;
     @Autowired
-    private IFsExpressService expressService;
+    private IFsExpressScrmService expressService;
 //    @Autowired
 //    private FsWarehousesMapper fsWarehousesMapper;
     @Autowired
     IErpOrderService erpOrderService;
-    @Autowired
-    private LiveTask liveTask;
+
 
 
     @Autowired
@@ -108,6 +117,32 @@ public class LiveOrderController extends BaseController
     @Autowired
     private ILiveOrderDfService liveOrderDfService;
 
+    @Autowired
+    private LiveTask liveTask;
+    @Autowired
+    @Qualifier("erpOrderServiceImpl")
+    private IErpOrderService gyOrderService;
+
+    @Autowired
+    @Qualifier("wdtErpOrderServiceImpl")
+    private IErpOrderService wdtOrderService;
+    @Autowired
+    @Qualifier("hzOMSErpOrderServiceImpl")
+    private IErpOrderService hzOMSErpOrderService;
+    @Autowired
+    @Qualifier("dfOrderServiceImpl")
+    private IErpOrderService dfOrderService;
+    @Autowired
+    @Qualifier("k9OrderScrmServiceImpl")
+    private IErpOrderService k9OrderService;
+    @Autowired
+    @Qualifier("JSTErpOrderServiceImpl")
+    private IErpOrderService jSTOrderService;
+    @Autowired
+    private ConfigUtil configUtil;
+
+
+
 
     @GetMapping("/importTemplate")
     public AjaxResult importTemplate() {
@@ -144,6 +179,98 @@ public class LiveOrderController extends BaseController
         return util.exportExcel(list, "订单数据");
     }
 
+    /**
+     * 查询订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveOrder:list')")
+    @GetMapping("/listZm")
+    public TableDataInfo listZm(LiveOrder liveOrder)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
+        for (LiveOrderVoZm vo : list){
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+
+            // 财务独特字段
+            if (loginUser.getPermissions().contains("live:liveOrder:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                vo.setCostPrice(vo.getCostPrice());
+                vo.setFPrice(vo.getCostPrice().multiply(BigDecimal.valueOf(Long.parseLong(vo.getTotalNum()))));
+            } else {
+                vo.setCostPrice(BigDecimal.ZERO);
+                vo.setFPrice(BigDecimal.ZERO);
+                vo.setPayDelivery(BigDecimal.ZERO);
+                vo.setBarCode("");
+                vo.setCateName("");
+                vo.setBankTransactionId("");
+            }
+            vo.setCost(vo.getCostPrice());
+
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveOrder:export')")
+    @Log(title = "订单", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportZm")
+    public AjaxResult exportZm(LiveOrder liveOrder)
+    {
+        List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        for (LiveOrderVoZm vo : list){
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            // 财务独特字段
+            if (loginUser.getPermissions().contains("live:liveOrder:finance") || loginUser.getPermissions().contains("*:*:*")) {
+                vo.setCostPrice(vo.getCost());
+                vo.setFPrice(vo.getCostPrice().multiply(BigDecimal.valueOf(Long.parseLong(vo.getTotalNum()))));
+            } else {
+                vo.setCostPrice(BigDecimal.ZERO);
+                vo.setFPrice(BigDecimal.ZERO);
+                vo.setBankTransactionId("");
+            }
+            vo.setCost(vo.getCostPrice());
+        }
+        ExcelUtil<LiveOrderVoZm> util = new ExcelUtil<LiveOrderVoZm>(LiveOrderVoZm.class);
+        return util.exportExcel(list, "订单数据");
+    }
+
+    /**
+     * 导出订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveOrder:export')")
+    @Log(title = "订单", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportZmNew")
+    public AjaxResult exportZmNew(LiveOrder liveOrder){
+        List<FsStoreOrderItemExportZMVO> list = liveOrderService.selectLiveOrderListZmNew(liveOrder);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        for (FsStoreOrderItemExportZMVO vo : list){
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+//            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+//            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            // 财务独特字段
+            if (loginUser.getPermissions().contains("live:liveOrder:finance") || loginUser.getPermissions().contains("*:*:*")) {
+//                vo.setCostPrice(vo.getCost());
+                vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getNum())));
+            } else {
+                vo.setCost(BigDecimal.ZERO);
+                vo.setFPrice(BigDecimal.ZERO);
+                vo.setBankTransactionId("");
+            }
+//            vo.setCost(vo.getCostPrice());
+        }
+        ExcelUtil<FsStoreOrderItemExportZMVO> util = new ExcelUtil<FsStoreOrderItemExportZMVO>(FsStoreOrderItemExportZMVO.class);
+        return util.exportExcel(list, "订单数据");
+    }
     /**
      * 获取订单详细信息
      */
@@ -226,12 +353,7 @@ public class LiveOrderController extends BaseController
         return getDataTable(list);
     }
 
-    @GetMapping("/test")
-    public R test()
-    {
-        liveTask.updateExpress();
-        return R.ok();
-    }
+
     @PreAuthorize("@ss.hasPermi('live:liveOrder:refundOrderMoney')")
     @Log(title = "退款", businessType = BusinessType.UPDATE)
 //    @PreAuthorize("@ss.hasPermi('live:liveOrder:refundOrderMoney')")
@@ -334,8 +456,7 @@ public class LiveOrderController extends BaseController
     @ApiOperation("物流查询")
     @PostMapping("/getExpressByDeliverId")
     public R getExpressByDeliverId(@Validated @RequestBody FsStoreOrderExpressParam param, HttpServletRequest request){
-//        return expressService.getLiveExpressByDeliverId(param);
-        return R.ok();
+        return expressService.getLiveExpressByDeliverId(param);
     }
 
     @PreAuthorize("@ss.hasPermi('live:liveOrder:auditPayRemain')")
@@ -367,20 +488,41 @@ public class LiveOrderController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:liveOrder:getEroOrder')")
     @GetMapping("/getEroOrder")
     public R getEroOrder(@RequestParam("extendOrderId") String extendOrderId) {
+        IErpOrderService erpOrderService = getErpService();
         ErpOrderQueryRequert request = new ErpOrderQueryRequert();
         request.setCode(extendOrderId);
-        if(StringUtils.isEmpty(extendOrderId)) return R.error("物流订单ID为空!");
-
-        LiveOrder order = liveOrderService.selectLiveOrderByExtendId(extendOrderId);
-
-        // 根据仓库code找erp
-//        if(com.fs.common.utils.StringUtils.isNotBlank(order.getStoreHouseCode())){
-//            String erp = fsWarehousesMapper.selectErpByCode(order.getStoreHouseCode());
-//            ErpContextHolder.setErpType(erp);
-//        }
-
-//        ErpOrderQueryResponse response = erpOrderService.getOrderLive(request);
-        return R.ok().put("data","123");
+        ErpOrderQueryResponse response = erpOrderService.getLiveOrder(request);
+        return R.ok().put("data",response);
+    }
+    private IErpOrderService getErpService(){
+        //判断是否开启erp
+        IErpOrderService erpOrderService = null;
+        FsErpConfig erpConfig = configUtil.getErpConfig();
+        Integer erpOpen = erpConfig.getErpOpen();
+        if (erpOpen != null && erpOpen == 1) {
+            //判断erp类型
+            Integer erpType = erpConfig.getErpType();
+            if (erpType != null) {
+                if (erpType == 1){
+                    //管易
+                    erpOrderService =  gyOrderService;
+                } else if (erpType == 2){
+                    //旺店通
+                    erpOrderService =  wdtOrderService;
+                } else if (erpType == 3){
+                    //
+                    erpOrderService =  hzOMSErpOrderService;
+                } else if (erpType == 4){
+                    //代服
+                    erpOrderService =  dfOrderService;
+                }else if(erpType == 5){
+                    erpOrderService=jSTOrderService;
+                }else if(erpType == 6){
+                    erpOrderService=k9OrderService;
+                }
+            }
+        }
+        return erpOrderService;
     }
 
     @Log(title = "冻结、解冻佣金", businessType = BusinessType.UPDATE)
@@ -419,8 +561,12 @@ public class LiveOrderController extends BaseController
     {
         logger.info("手动推管易 订单号: {}",orderCode);
         LiveOrder order=liveOrderService.selectOrderIdByOrderCode(orderCode);
-        liveOrderService.createOmsOrder(order.getOrderId());
-        return R.ok();
+        if (orderPaymentService.selectByBussinessId(order.getOrderId()) != null) {
+            liveOrderService.createOmsOrder(order.getOrderId());
+            return R.ok();
+        }
+        return R.error("订单未支付!");
+
     }
 
     @Log(title = "同步管易物流单号", businessType = BusinessType.UPDATE)
@@ -571,9 +717,11 @@ public class LiveOrderController extends BaseController
                     orderLogsService.create(orderId, FsStoreOrderLogEnum.SET_PUSH_ACCOUNT.getValue(),
                             nickName + " " +FsStoreOrderLogEnum.SET_PUSH_ACCOUNT.getDesc() + ":" + df.getLoginAccount());
                 }
-                liveOrderService.createOmsOrder(orderId);
-                orderLogsService.create(orderId, FsStoreOrderLogEnum.PUSH_ORDER_ERP.getValue(),
-                        nickName + " " +FsStoreOrderLogEnum.PUSH_ORDER_ERP.getDesc() + ":" + df.getLoginAccount());
+                if (orderPaymentService.selectByBussinessId(orderId) != null) {
+                    liveOrderService.createOmsOrder(orderId);
+                    orderLogsService.create(orderId, FsStoreOrderLogEnum.PUSH_ORDER_ERP.getValue(),
+                            nickName + " " +FsStoreOrderLogEnum.PUSH_ORDER_ERP.getDesc() + ":" + df.getLoginAccount());
+                }
             } catch (ParseException e) {
                 throw new RuntimeException(e);
             }

+ 4 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveVideoController.java

@@ -6,6 +6,9 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.config.cloud.CloudHostProper;
+import com.fs.hisStore.enums.CompanyEnum;
+import com.fs.hisStore.enums.LiveEnum;
 import com.fs.live.domain.LiveVideo;
 import com.fs.live.service.ILiveVideoService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -89,6 +92,7 @@ public class LiveVideoController extends BaseController
     @PostMapping
     public AjaxResult add(@RequestBody LiveVideo liveVideo)
     {
+
         return toAjax(liveVideoService.insertLiveVideo(liveVideo));
     }
 

+ 307 - 0
fs-admin/src/main/java/com/fs/live/controller/OrderController.java

@@ -0,0 +1,307 @@
+package com.fs.live.controller;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.web.service.TokenService;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.hisStore.dto.StoreOrderProductDTO;
+import com.fs.hisStore.service.IMergedOrderService;
+import com.fs.hisStore.vo.FsStoreOrderItemExportVO;
+import com.fs.live.param.MergedOrderQueryParam;
+import com.fs.live.vo.MergedOrderVO;
+import com.fs.live.vo.MergedOrderExportVO;
+import com.fs.common.utils.poi.ExcelUtil;
+import org.springframework.beans.BeanUtils;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 合并订单Controller
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Api("合并订单管理")
+@RestController
+@RequestMapping("/order")
+public class OrderController extends BaseController
+{
+    @Autowired
+    private IMergedOrderService mergedOrderService;
+    // 设置最大导出数量限制为20000条
+    private static final int maxExportCount = 20000;
+
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询合并订单列表
+     */
+    @ApiOperation("查询合并订单列表")
+    @GetMapping("/list")
+    public TableDataInfo list(MergedOrderQueryParam param)
+    {
+        startPage();
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        for (MergedOrderVO vo : list) {
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setPhone(ParseUtils.parsePhone(vo.getPhone()));
+            vo.setSalesPhone(ParseUtils.parsePhone(vo.getSalesPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出合并订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:order:export')")
+    @ApiOperation("导出合并订单列表")
+    @Log(title = "合并订单", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                    System.out.println(e.getMessage());
+                }
+            }
+        }
+
+        // 转换为导出VO
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, false,loginUser);
+        
+        // 如果数据量在限制范围内,正常导出
+        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
+        return util.exportExcel(exportList, "合并订单数据");
+    }
+
+    /**
+     * 导出合并订单(明文)
+     */
+    @ApiOperation("导出合并订单(明文)")
+    @Log(title = "合并订单(明文)", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('live:order:exportAll')")
+    @GetMapping("/exportDetails")
+    public AjaxResult exportDetails(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                }
+            }
+            //
+
+        }
+
+        // 转换为导出VO(明文模式,不脱敏)
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, true,loginUser);
+
+        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
+        return util.exportExcel(exportList, "合并订单(明文)");
+    }
+
+    /**
+     * 导出合并订单明细
+     */
+    // 预留接口
+    @PreAuthorize("@ss.hasPermi('live:order:exportOther')")
+    @ApiOperation("导出合并订单明细")
+    @Log(title = "合并订单明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportItems")
+    public AjaxResult exportItems(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单明细");
+    }
+
+
+
+    /**
+     * 导出合并订单明细(明文)
+     */
+    // 预留接口
+    @PreAuthorize("@ss.hasPermi('live:order:exportOther')")
+    @ApiOperation("导出合并订单明细(明文)")
+    @Log(title = "合并订单明细(明文)", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportItemsDetails")
+    public AjaxResult exportItemsDetails(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单明细(明文)");
+    }
+
+    /**
+     * 导出合并订单发货单
+     */
+    @ApiOperation("导出合并订单发货单")
+    @Log(title = "合并订单发货单", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportShipping")
+    public AjaxResult exportShipping(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单发货单");
+    }
+
+    /**
+     * 将 MergedOrderVO 转换为 MergedOrderExportVO
+     * @param list 原始数据列表
+     * @param isPlainText 是否为明文模式(true:不脱敏,false:脱敏)
+     * @return 导出VO列表
+     */
+    private List<MergedOrderExportVO> convertToExportVO(List<MergedOrderVO> list, boolean isPlainText,LoginUser loginUser)
+    {
+        if (list == null || list.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return list.stream().map(vo -> {
+            MergedOrderExportVO exportVO = new MergedOrderExportVO();
+            
+            // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
+            exportVO.setOrderCode(vo.getOrderCode());
+            exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
+            exportVO.setUserId(vo.getUserId());
+            
+            // 产品信息
+            exportVO.setProductName(vo.getProductName());
+            exportVO.setBarCode(vo.getBarCode());
+            exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
+            exportVO.setTotalNum(vo.getTotalNum());
+            exportVO.setPrice(vo.getTotalPrice()); // 产品价格使用订单总价
+            exportVO.setCost(vo.getCost());
+            exportVO.setFPrice(vo.getCost() != null ? vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())) : BigDecimal.ZERO); // 结算价,合并订单暂无此字段
+            exportVO.setPayPostage(vo.getPayDelivery());
+            exportVO.setCateName(vo.getCateName());
+            // 收货信息
+            exportVO.setRealName(vo.getRealName());
+            if (isPlainText) {
+                exportVO.setUserPhone(vo.getUserPhone());
+                exportVO.setUserAddress(vo.getUserAddress());
+            } else {
+                exportVO.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+                exportVO.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            }
+            
+            // 时间信息
+            exportVO.setCreateTime(vo.getCreateTime());
+            exportVO.setPayTime(vo.getPayTime());
+            
+            // 物流信息
+            exportVO.setDeliverySn(vo.getDeliveryCode()); // 快递公司编号,合并订单暂无此字段
+            exportVO.setDeliveryName(vo.getDeliveryName()); // 快递公司,合并订单暂无此字段
+            exportVO.setDeliveryId(vo.getDeliveryId());
+            
+            // 公司和销售信息
+            exportVO.setCompanyName(vo.getCompanyName());
+            exportVO.setCompanyUserNickName(vo.getCompanyUserNickName());
+            
+            // 套餐信息
+            exportVO.setPackageName(null); // 套餐名称,合并订单暂无此字段
+            exportVO.setGroupBarCode(null); // 组合码,合并订单暂无此字段
+            
+            // 凭证信息
+            exportVO.setIsUpload(null); // 是否上传凭证,合并订单暂无此字段
+            exportVO.setUploadTime(null); // 上传时间,合并订单暂无此字段
+            
+            // 档期信息
+            exportVO.setScheduleName(null); // 归属档期,合并订单暂无此字段
+            
+            // 银行交易流水号
+            exportVO.setBankTransactionId(vo.getBankTransactionId());
+            
+            // 金额信息
+            exportVO.setTotalPrice(vo.getTotalPrice());
+            exportVO.setPayPrice(vo.getPayPrice());
+            exportVO.setPayMoney(vo.getPayMoney());
+            exportVO.setPayPostage(vo.getPayDelivery()); // 额外运费,合并订单暂无此字段
+            exportVO.setPayDelivery(vo.getPayDelivery());
+            if ((loginUser.getPermissions().contains("order:finance") || loginUser.getPermissions().contains("*:*:*") ) && !Objects.isNull(vo.getCost())) {
+                vo.setFPrice(vo.getCost().multiply(BigDecimal.valueOf(vo.getTotalNum())));
+            } else {
+                vo.setPayPostage(BigDecimal.ZERO);
+                vo.setCost(BigDecimal.ZERO);
+                vo.setFPrice(BigDecimal.ZERO);
+                vo.setBankTransactionId("");
+            }
+            
+            return exportVO;
+        }).collect(Collectors.toList());
+    }
+}

+ 0 - 183
fs-admin/src/main/java/com/fs/task/LiveTask.java

@@ -1,183 +0,0 @@
-package com.fs.task;
-
-
-import com.fs.erp.domain.ErpDeliverys;
-import com.fs.erp.domain.ErpOrderQuery;
-import com.fs.erp.dto.ErpOrderQueryRequert;
-import com.fs.erp.dto.ErpOrderQueryResponse;
-import com.fs.erp.service.FsJstAftersalePushService;
-import com.fs.erp.service.IErpOrderService;
-import com.fs.his.service.IFsExpressService;
-import com.fs.live.domain.LiveAfterSales;
-import com.fs.live.domain.LiveOrder;
-import com.fs.live.param.LiveAfterSalesAudit1Param;
-import com.fs.live.service.ILiveAfterSalesService;
-import com.fs.live.service.ILiveOrderLogsService;
-import com.fs.live.service.ILiveOrderService;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang.ObjectUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import java.text.ParseException;
-import java.util.List;
-
-/**
- * 定时任务调度
- * @author fs
- */
-@Component("liveTask")
-public class LiveTask {
-    Logger logger = LoggerFactory.getLogger(LiveTask.class);
-
-
-    @Autowired
-    private ILiveOrderService liveOrderService;
-
-    @Autowired
-    private ILiveAfterSalesService afterSalesService;
-
-
-    @Autowired
-    private IErpOrderService erpOrderService;
-
-
-    @Autowired
-    private IFsExpressService expressService;
-
-
-    @Autowired
-    private ILiveOrderLogsService orderLogsService;
-
-
-//    @Autowired
-//    private FsWarehousesMapper fsWarehousesMapper;
-
-    @Autowired
-    public FsJstAftersalePushService fsJstAftersalePushService;
-
-    /**
-     * 超时订单自动取消
-     */
-    public void orderCancel(){
-        liveOrderService.orderCancel();
-    }
-
-
-    /**
-     * 发货任务
-     */
-    public void deliveryOp() {
-//        List<LiveOrder> list = liveOrderService.selectDeliverPenddingData();
-//
-//        for (LiveOrder order : list) {
-//            String orderCode = order.getOrderCode();
-//            ErpOrderQueryRequert request = new ErpOrderQueryRequert();
-//            request.setCode(order.getExtendOrderId());
-//
-//            try {
-//                // 根据仓库code找erp
-//                if (com.fs.common.utils.StringUtils.isNotBlank(order.getStoreHouseCode())) {
-//                    String erp = fsWarehousesMapper.selectErpByCode(order.getStoreHouseCode());
-//                    ErpContextHolder.setErpType(erp);
-//                }
-//                ErpOrderQueryResponse response = erpOrderService.getOrderLive(request);
-//                if (CollectionUtils.isNotEmpty(response.getOrders())) {
-//                    for (ErpOrderQuery orderQuery : response.getOrders()) {
-//                        if (CollectionUtils.isNotEmpty(orderQuery.getDeliverys())) {
-//                            // 部分发货或者全部发货
-//                            if (ObjectUtils.equals(orderQuery.getDelivery_state(), 1) || ObjectUtils.equals(orderQuery.getDelivery_state(), 2)) {
-//
-//                                orderLogsService.create(order.getOrderId(), OrderLogEnum.DELIVERY_GOODS.getValue(),
-//                                        OrderLogEnum.DELIVERY_GOODS.getDesc());
-//
-//                                for (ErpDeliverys delivery : orderQuery.getDeliverys()) {
-//
-//                                    FsExpress express = expressService.selectFsExpressByOmsCode(delivery.getExpress_code());
-//                                    if (express == null) {
-//                                        logger.warn("当前express_code: {} 不存在!", delivery.getExpress_code());
-//                                        continue;
-//                                    }
-//
-//                                    if (delivery.getDelivery()) {
-//                                        liveOrderService.deliveryOrder(orderCode, delivery.getMail_no(),
-//                                                delivery.getExpress_code(), delivery.getExpress_name());
-//                                    }
-//                                }
-//
-//                                logger.info("订单 {} 发货信息同步成功", order.getOrderCode());
-//                            }
-//                        }
-//                    }
-//                }
-//            } catch (Exception e) {
-//                logger.error(String.format("[发货任务]调用erp查询接口失败!原因: %s", e));
-//            }
-//
-//        }
-    }
-
-
-        /**
-         * 退款自动处理 24小时未审核自动审核通过 每小时执行一次
-         */
-    public void refundOp() {
-        //获取所有退款申请
-        List<LiveAfterSales> list = afterSalesService.selectLiveAfterSalesByDoAudit();
-        if (list != null) {
-            for (LiveAfterSales afterSales : list) {
-                //仅退款
-//                if (afterSales.getServiceType().equals(0)) {
-                LiveAfterSalesAudit1Param audit1Param = new LiveAfterSalesAudit1Param();
-                audit1Param.setSalesId(afterSales.getId());
-                audit1Param.setOperator("平台");
-                afterSalesService.audit1(audit1Param);
-//                }
-            }
-        }
-    }
-
-    /**
-     * 批量推管易
-     * @throws ParseException 解析异常
-     */
-    public void updateOrderItem() throws ParseException {
-        List<Long> ids = liveOrderService.selectOrderIdByNoErp();
-        for (Long id : ids) {
-            try{
-                liveOrderService.createOmsOrder(id);
-            }catch (Exception e){
-                logger.error("推送管易失败 {}",id,e);
-            }
-        }
-    }
-
-
-    /**
-     * 同步物流状态
-     */
-    public void syncExpress() {
-        List<Long> ids = liveOrderService.selectSyncExpressIds();
-        for (Long id : ids) {
-            liveOrderService.syncExpress(id);
-        }
-    }
-
-    /**
-     * 更新发货状态
-     */
-    public void updateExpress() {
-        List<LiveOrder> list = liveOrderService.selectUpdateExpress();
-
-        for (LiveOrder order : list) {
-            try{
-                liveOrderService.syncDeliveryOrder(order);
-            }catch (Exception e) {
-                logger.error("获取订单是否发货失败!原因: ",e);
-            }
-        }
-
-    }
-}

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

@@ -25,7 +25,18 @@ public class LiveKeysConstant {
     public static final String LIVE_HOME_PAGE_CONFIG_RED = "live:config:%s:red:%s"; //红包记录
     public static final String LIVE_HOME_PAGE_CONFIG_COUPON = "live:config:%s:coupon:%s"; //优惠券记录
     public static final String LIVE_HOME_PAGE_CONFIG_DRAW = "live:config:%s:draw:%s"; //抽奖记录
+    public static final String TOP_MSG = "topMsg"; //抽奖记录
 
+    public static final String LIVE_FLAG_CACHE = "live:flag:%s"; //直播间直播/回放状态缓存
+    public static final Integer LIVE_FLAG_CACHE_EXPIRE = 60; //直播间状态缓存过期时间(秒)
+
+    public static final String LIVE_DATA_CACHE = "live:data:%s"; //直播间数据缓存
+    public static final Integer LIVE_DATA_CACHE_EXPIRE = 300; //直播间数据缓存过期时间(秒)
+
+    public static final String PRODUCT_DETAIL_CACHE = "product:detail:%s"; //商品详情缓存
+    public static final Integer PRODUCT_DETAIL_CACHE_EXPIRE = 300; //商品详情缓存过期时间(秒)
+
+    public static final String LIVE_TAG_MARK_CACHE = "live:tag:mark:%s"; //直播间打标签缓存,存储直播间ID、开始时间和视频时长
 
 
 }

+ 16 - 6
fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java

@@ -8,6 +8,8 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.security.SecurityUtils;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.live.domain.LiveAfterSales;
@@ -50,11 +52,17 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 查询售后记录列表
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:list')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:list')")
     @GetMapping("/list")
     public TableDataInfo list(LiveAfterSalesVo liveAfterSales)
     {
         startPage();
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        liveAfterSales.setCompanyId(user.getCompanyId());
+        // 将productName映射到productNameQuery用于查询
+        if (liveAfterSales.getProductName() != null && !liveAfterSales.getProductName().isEmpty()) {
+            liveAfterSales.setProductNameQuery(liveAfterSales.getProductName());
+        }
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(ParseUtils.parsePhone(liveAfterSalesVo.getUserPhone()));
@@ -65,12 +73,14 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 导出售后记录列表
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:export')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:export')")
     @Log(title = "售后记录", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(LiveAfterSalesVo liveAfterSales)
     {
         PageHelper.startPage(1, 10000, "");
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        liveAfterSales.setCompanyId(user.getCompanyId());
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoList(liveAfterSales);
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(liveAfterSalesVo.getUserPhone() == null ? "" : liveAfterSalesVo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
@@ -83,7 +93,7 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 获取售后记录详细信息
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:query')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:query')")
     @GetMapping(value = "/{id}")
     public R getInfo(@PathVariable("id") Long id)
     {
@@ -101,7 +111,7 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 新增售后记录
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:add')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:add')")
     @Log(title = "售后记录", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody LiveAfterSales liveAfterSales)
@@ -112,7 +122,7 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 修改售后记录
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:edit')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:edit')")
     @Log(title = "售后记录", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody LiveAfterSales liveAfterSales)
@@ -123,7 +133,7 @@ public class LiveAfterSalesController extends BaseController
     /**
      * 删除售后记录
      */
-    @PreAuthorize("@ss.hasPermi('live:liveAfteraSales:remove')")
+    @PreAuthorize("@ss.hasPermi('live:liveAfterSales:remove')")
     @Log(title = "售后记录", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)

+ 3 - 3
fs-company/src/main/java/com/fs/company/controller/live/LiveAutoTaskController.java

@@ -55,7 +55,7 @@ public class LiveAutoTaskController extends BaseController
         return getDataTable(list);
     }
 
-    @PreAuthorize("@ss.hasPermi('live:task:list')")
+//    @PreAuthorize("@ss.hasPermi('live:task:list')")
     @GetMapping("/consoleList")
     public TableDataInfo consoleList(LiveAutoTask liveAutoTask)
     {
@@ -116,9 +116,9 @@ public class LiveAutoTaskController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:task:edit')")
     @Log(title = "直播间自动化任务配置", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody LiveAutoTask liveAutoTask)
+    public R edit(@RequestBody LiveAutoTask liveAutoTask)
     {
-        return toAjax(liveAutoTaskService.updateLiveAutoTask(liveAutoTask));
+        return liveAutoTaskService.updateLiveAutoTask(liveAutoTask);
     }
 
     /**

+ 57 - 21
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -15,9 +15,17 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.service.HuiFuService;
 import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveCompanyCode;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.mapper.LiveOrderMapper;
+import com.fs.live.mapper.LiveOrderPaymentMapper;
 import com.fs.live.service.ILiveCompanyCodeService;
+import com.fs.live.service.ILiveOrderService;
 import com.fs.live.service.ILiveService;
 import com.fs.live.vo.LiveListVo;
 import com.fs.system.oss.OSSFactory;
@@ -29,10 +37,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.text.SimpleDateFormat;
+import java.util.*;
 
 /**
  * 直播Controller
@@ -51,6 +57,18 @@ public class LiveController extends BaseController
     @Autowired
     private ILiveCompanyCodeService liveCompanyCodeService;
 
+    /**
+     * 查询未结束直播间
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:list')")
+    @GetMapping("/listToLiveNoEnd")
+    public TableDataInfo listToLiveNoEnd(Live live)
+    {
+        startPage();
+        List<Live> list = liveService.listToLiveNoEnd(live);
+        return getDataTable(list);
+    }
+
     /**
      * 查询直播列表
      */
@@ -122,7 +140,8 @@ public class LiveController extends BaseController
     {
         // 设置企业ID和企业用户ID
         setCompanyId(live);
-        return toAjax(liveService.insertLive(live));
+        return toAjax(1);
+//        return toAjax(liveService.insertLive(live));
     }
 
     /**
@@ -134,7 +153,8 @@ public class LiveController extends BaseController
         CompanyUser user = SecurityUtils.getLoginUser().getUser();
         live.setCompanyUserId(user.getUserId());
         live.setCompanyId(user.getCompanyId());
-        return liveService.finishLive(live);
+        return R.ok();
+//        return liveService.finishLive(live);
     }
 
     /**
@@ -146,7 +166,8 @@ public class LiveController extends BaseController
         CompanyUser user = SecurityUtils.getLoginUser().getUser();
         live.setCompanyUserId(user.getUserId());
         live.setCompanyId(user.getCompanyId());
-        return liveService.copyLive(live);
+        return R.ok();
+//        return liveService.copyLive(live);
     }
 
     /**
@@ -158,7 +179,8 @@ public class LiveController extends BaseController
         CompanyUser user = SecurityUtils.getLoginUser().getUser();
         live.setCompanyUserId(user.getUserId());
         live.setCompanyId(user.getCompanyId());
-        return liveService.startLive(live);
+        return R.ok();
+//        return liveService.startLive(live);
     }
 
     /**
@@ -169,7 +191,12 @@ public class LiveController extends BaseController
     @PutMapping
     public AjaxResult edit(@RequestBody Live live)
     {
-        return toAjax(liveService.updateLive(live));
+        return AjaxResult.success();
+//        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+//        live.setCompanyUserId(user.getUserId());
+//        live.setCompanyId(user.getCompanyId());
+//
+//        return toAjax(liveService.updateLive(live));
     }
 
     /**
@@ -180,7 +207,12 @@ public class LiveController extends BaseController
 	@DeleteMapping("/{liveIds}")
     public AjaxResult remove(@PathVariable Long[] liveIds)
     {
-        return toAjax(liveService.deleteLiveByLiveIds(liveIds));
+        return AjaxResult.success();
+//        Live live = new Live();
+//        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+//        live.setCompanyUserId(user.getUserId());
+//        live.setCompanyId(user.getCompanyId());
+//        return toAjax(liveService.deleteLiveByLiveIds(liveIds, live));
     }
 
     @PreAuthorize("@ss.hasPermi('live:live:query')")
@@ -198,9 +230,10 @@ public class LiveController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
     @PostMapping("/closeLiving")
     public R closeLiving(@RequestBody Map<String, String> payload) {
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        payload.put("userId", loginUser.getUser().getUserId().toString());
-        return liveService.closeLiving(payload);
+        return R.ok();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        payload.put("userId", loginUser.getUser().getUserId().toString());
+//        return liveService.closeLiving(payload);
     }
 
     @PreAuthorize("@ss.hasPermi('live:live:insert')")
@@ -228,7 +261,8 @@ public class LiveController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
     @PostMapping("/startLoopPlay")
     public R startLoopPlay(@RequestBody Live live) {
-        return liveService.startLoopPlay(live);
+        return R.ok();
+//        return liveService.startLoopPlay(live);
     }
 
     /**
@@ -246,8 +280,9 @@ public class LiveController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
     @PostMapping("/handleShelfOrUn")
     public R handleShelfOrUn(@RequestBody LiveListVo listVo) {
-        setListCompanyId(listVo);
-        return liveService.handleShelfOrUn(listVo);
+        return R.ok();
+//        setListCompanyId(listVo);
+//        return liveService.handleShelfOrUn(listVo);
     }
 
     /**
@@ -256,8 +291,9 @@ public class LiveController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:live:edit')")
     @PostMapping("/handleDeleteSelected")
     public R handleDeleteSelected(@RequestBody LiveListVo listVo) {
-        setListCompanyId(listVo);
-        return liveService.handleDeleteSelected(listVo);
+        return R.ok();
+//        setListCompanyId(listVo);
+//        return liveService.handleDeleteSelected(listVo);
     }
 
 
@@ -314,9 +350,9 @@ public class LiveController extends BaseController
         String url="https://api.weixin.qq.com/cgi-bin/stable_token";
         HashMap<String, String> map = new HashMap<>();
         map.put("grant_type","client_credential");
-        // 芳华惠选
-        map.put("appid","wx503cf8ab31f83dd4");
-        map.put("secret","1ba1972363889dcb4a37ecb685744435");
+        // 百域承品
+        map.put("appid","wx44beed5640bcb1ba");
+        map.put("secret","1bfcfa420f741801575a74d94752d014");
         String accessToken = HttpUtils.endApi(url, null, map);
         // 创建Gson对象
         Gson gson = new Gson();

+ 17 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveCouponController.java

@@ -73,6 +73,19 @@ public class LiveCouponController extends BaseController
         return AjaxResult.success(liveCouponService.selectLiveCouponById(couponId));
     }
 
+    /**
+     * 查询优惠券列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveCoupon:list')")
+    @GetMapping("/listOn")
+    public TableDataInfo listOn(@RequestParam("liveId") Long liveId)
+    {
+        startPage();
+        List<LiveCoupon> list = liveCouponService.listOn(liveId);
+        return getDataTable(list);
+    }
+
+
     /**
      * 新增优惠券
      */
@@ -122,6 +135,8 @@ public class LiveCouponController extends BaseController
         issue.setIsPermanent(0);
         issue.setStatus(1);
         issue.setCreateTime(new Date());
+        // 继承优惠券的领取上限字段
+        issue.setLimitReceiveCount(coupon.getLimitReceiveCount());
         return toAjax( liveCouponIssueService.insertLiveCouponIssue(issue));
     }
 
@@ -143,6 +158,8 @@ public class LiveCouponController extends BaseController
             issue.setIsPermanent(0);
             issue.setStatus(1);
             issue.setCreateTime(new Date());
+            // 继承优惠券的领取上限字段
+            issue.setLimitReceiveCount(coupon.getLimitReceiveCount());
             liveCouponIssueService.insertLiveCouponIssue(issue);
         }
         return R.ok();

+ 79 - 2
fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
@@ -14,11 +15,14 @@ import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.ColumnsConfigVo;
+import com.fs.live.vo.LiveUserDetailExportVO;
+import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -37,6 +41,79 @@ public class LiveDataController extends BaseController
     @Autowired
     private TokenService tokenService;
 
+    /**
+     * 查询直播间详情数据(SQL方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailBySql")
+    public R getLiveDataDetailBySql(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailBySql(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(SQL方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListBySql")
+    public R getLiveUserDetailListBySql(@RequestParam Long liveId, HttpServletRequest request) {
+        CompanyUser user = tokenService.getLoginUser(request).getUser();
+        if ("00".equals(user.getUserType())) {
+            return liveDataService.getLiveUserDetailListBySql(liveId,user.getCompanyId(),null);
+        }
+        return liveDataService.getLiveUserDetailListBySql(liveId,user.getCompanyId(),user.getUserId());
+    }
+
+    /**
+     * 查询直播间详情数据(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveDataDetailByServer")
+    public R getLiveDataDetailByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveDataDetailByServer(liveId);
+    }
+
+    /**
+     * 查询直播间用户详情列表(查询数据服务器处理方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @GetMapping("/getLiveUserDetailListByServer")
+    public R getLiveUserDetailListByServer(@RequestParam Long liveId) {
+        return liveDataService.getLiveUserDetailListByServer(liveId);
+    }
+
+
+    /**
+     * 导出直播间用户详情数据
+     * @param liveId 直播间ID
+     * @return Excel文件
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
+    @Log(title = "直播间用户详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportLiveUserDetail")
+    public AjaxResult exportLiveUserDetail(@RequestParam Long liveId, HttpServletRequest request) {
+        CompanyUser user = tokenService.getLoginUser(request).getUser();
+        List<LiveUserDetailExportVO> list = new ArrayList<>();
+        if ("00".equals(user.getUserType())) {
+            list = liveDataService.exportLiveUserDetail(liveId, user.getCompanyId(), null);
+        } else {
+            list = liveDataService.exportLiveUserDetail(liveId,user.getCompanyId(),user.getUserId());
+        }
+        if (list == null || list.isEmpty()) {
+            return AjaxResult.error("未找到用户详情数据");
+        }
+
+        ExcelUtil<LiveUserDetailExportVO> util = new ExcelUtil<>(LiveUserDetailExportVO.class);
+        return util.exportExcel(list, "直播间用户详情数据");
+    }
+
     /**
      * 直播数据页面卡片数据
      */
@@ -66,8 +143,8 @@ public class LiveDataController extends BaseController
     @PostMapping("/listLiveData")
     public R listLiveData(@RequestBody LiveDataParam param, HttpServletRequest request)
     {
-        param.setCompanyId(tokenService.getLoginUser(request).getUser().getCompanyId());
-        startPage();
+//        param.setCompanyId(tokenService.getLoginUser(request).getUser().getCompanyId());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
         return liveDataService.listLiveData(param);
     }
 

+ 61 - 37
fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java

@@ -1,5 +1,6 @@
 package com.fs.company.controller.live;
 
+import cn.hutool.core.util.StrUtil;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -8,6 +9,7 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
@@ -18,7 +20,10 @@ import com.fs.his.domain.FsUser;
 import com.fs.his.param.FsStoreOrderBindCustomerParam;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.dto.ExpressInfoDTO;
+import com.fs.hisStore.enums.ShipperCodeEnum;
 import com.fs.hisStore.param.FsStoreOrderExpressParam;
+import com.fs.hisStore.service.IFsExpressScrmService;
 import com.fs.live.domain.*;
 import com.fs.live.enums.LiveOrderCancleReason;
 import com.fs.live.param.LiveOrderExpressParam;
@@ -28,6 +33,7 @@ import com.fs.live.service.ILiveOrderPaymentService;
 import com.fs.live.service.ILiveOrderService;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.live.vo.LiveOrderTimeVo;
+import com.fs.live.vo.LiveOrderVoZm;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -35,9 +41,12 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 
+import static com.fs.his.utils.PhoneUtil.decryptPhone;
+
 
 /**
  * 订单Controller
@@ -65,7 +74,7 @@ public class LiveOrderController extends BaseController
     @Autowired
     private TokenService tokenService;
     @Autowired
-    private IFsExpressService expressService;
+    private IFsExpressScrmService expressService;
 
 
 //    @Autowired
@@ -109,6 +118,49 @@ public class LiveOrderController extends BaseController
         return R.ok().put("payments",payments);
     }
 
+    /**
+     * 查询订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveOrder:list')")
+    @GetMapping("/listZm")
+    public TableDataInfo listZm(LiveOrder liveOrder)
+    {
+        startPage();
+        liveOrder.setCompanyId(SecurityUtils.getLoginUser().getUser().getCompanyId());
+        List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
+        for (LiveOrderVoZm vo : list){
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+            vo.setCostPrice(BigDecimal.ZERO);
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveOrder:export')")
+    @Log(title = "订单", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportZm")
+    public AjaxResult exportZm(LiveOrder liveOrder)
+    {
+        liveOrder.setCompanyId(SecurityUtils.getLoginUser().getUser().getCompanyId());
+        List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
+        for (LiveOrderVoZm vo : list){
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+            vo.setCostPrice(BigDecimal.ZERO);
+        }
+        ExcelUtil<LiveOrderVoZm> util = new ExcelUtil<LiveOrderVoZm>(LiveOrderVoZm.class);
+        return util.exportExcel(list, "订单数据");
+    }
+
 
 //    @Log(title = "订单凭证上传", businessType = BusinessType.UPDATE)
 ////    @PreAuthorize("@ss.hasPermi('live:liveOrder:uploadCredentials')")
@@ -159,8 +211,8 @@ public class LiveOrderController extends BaseController
     @PostMapping("/getExpressByDeliverId")
     public R getExpressByDeliverId(@Validated @RequestBody FsStoreOrderExpressParam param, HttpServletRequest request){
 
-//        return expressService.getLiveExpressByDeliverId(param);
-        return R.ok();
+        return expressService.getLiveExpressByDeliverId(param);
+//        return R.ok();
     }
 
     /**
@@ -242,40 +294,12 @@ public class LiveOrderController extends BaseController
     /**
      * 查看物流状态
      * */
-//    @PreAuthorize("@ss.hasPermi('live:liveOrder:express')")
-//    @GetMapping(value = "/getExpress/{id}")
-//    public R getExpress(@PathVariable("id") String id)
-//    {
-//        LiveOrder order=liveOrderService.selectLiveOrderByOrderId(id);
-//        ExpressInfoDTO expressInfoDTO=null;
-//        if(StringUtils.isNotEmpty(order.getDeliverySn())){
-//            String lastFourNumber = "";
-//            if (order.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue())) {
-//
-//                lastFourNumber = order.getUserPhone();
-//                if (lastFourNumber.length() == 11) {
-//                    lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
-//                }else if (lastFourNumber.length()>11){
-//                    String jm = decryptPhone(lastFourNumber);
-//                    lastFourNumber = StrUtil.sub(jm, jm.length(), -4);
-//                }
-//            }
-//            expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliveryCode(),order.getDeliverySn(),lastFourNumber);
-//
-//            if((expressInfoDTO.getStateEx()!=null&&expressInfoDTO.getStateEx().equals("0"))&&(expressInfoDTO.getState()!=null&&expressInfoDTO.getState().equals("0"))){
-//                lastFourNumber = "19923690275";
-//                if (order.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue())) {
-//                    if (lastFourNumber.length() == 11) {
-//                        lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
-//                    }
-//                }
-//
-//                expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliveryCode(),order.getDeliverySn(),lastFourNumber);
-//
-//            }
-//        }
-//        return R.ok().put("data",expressInfoDTO);
-//    }
+    @PreAuthorize("@ss.hasPermi('live:liveOrder:express')")
+    @GetMapping(value = "/getExpress/{id}")
+    public R getExpress(@PathVariable("id") String id)
+    {
+        return liveOrderService.syncExpress(Long.valueOf(id));
+    }
 
     /**
      * 支付订单

+ 117 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveWatchLogController.java

@@ -0,0 +1,117 @@
+package com.fs.company.controller.live;
+
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.live.vo.LiveWatchLogListVO;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.service.ILiveWatchLogService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 直播看课记录Controller
+ * 
+ * @author fs
+ * @date 2025-12-12
+ */
+@RestController
+@RequestMapping("/live/liveWatchLog")
+public class LiveWatchLogController extends BaseController
+{
+    @Autowired
+    private ILiveWatchLogService liveWatchLogService;
+    @Autowired
+    private TokenService tokenService;
+    /**
+     * 查询直播看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveWatchLog liveWatchLog)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if(null == loginUser) {
+           throw new RuntimeException("用户信息错误");
+        }
+        Long companyId = loginUser.getCompany().getCompanyId();
+        liveWatchLog.setCompanyId(companyId);
+        List<LiveWatchLogListVO> list = liveWatchLogService.selectLiveWatchLogListInfo(liveWatchLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出直播看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:export')")
+    @Log(title = "直播看课记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveWatchLog liveWatchLog)
+    {
+        List<LiveWatchLog> list = liveWatchLogService.selectLiveWatchLogList(liveWatchLog);
+        ExcelUtil<LiveWatchLog> util = new ExcelUtil<LiveWatchLog>(LiveWatchLog.class);
+        return util.exportExcel(list, "直播看课记录数据");
+    }
+
+    /**
+     * 获取直播看课记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+    {
+        return AjaxResult.success(liveWatchLogService.selectLiveWatchLogByLogId(logId));
+    }
+
+    /**
+     * 新增直播看课记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:add')")
+    @Log(title = "直播看课记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveWatchLog liveWatchLog)
+    {
+        return toAjax(liveWatchLogService.insertLiveWatchLog(liveWatchLog));
+    }
+
+    /**
+     * 修改直播看课记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:edit')")
+    @Log(title = "直播看课记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveWatchLog liveWatchLog)
+    {
+        return toAjax(liveWatchLogService.updateLiveWatchLog(liveWatchLog));
+    }
+
+    /**
+     * 删除直播看课记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveWatchLog:remove')")
+    @Log(title = "直播看课记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{logIds}")
+    public AjaxResult remove(@PathVariable Long[] logIds)
+    {
+        return toAjax(liveWatchLogService.deleteLiveWatchLogByLogIds(logIds));
+    }
+
+}

+ 298 - 0
fs-company/src/main/java/com/fs/company/controller/live/OrderController.java

@@ -0,0 +1,298 @@
+package com.fs.company.controller.live;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ParseUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.security.SecurityUtils;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.hisStore.dto.StoreOrderProductDTO;
+import com.fs.hisStore.service.IMergedOrderService;
+import com.fs.hisStore.vo.FsStoreOrderItemExportVO;
+import com.fs.live.param.MergedOrderQueryParam;
+import com.fs.live.vo.MergedOrderVO;
+import com.fs.live.vo.MergedOrderExportVO;
+import com.fs.common.utils.poi.ExcelUtil;
+import org.springframework.beans.BeanUtils;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 合并订单Controller
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Api("合并订单管理")
+@RestController
+@RequestMapping("/order")
+public class OrderController extends BaseController
+{
+    @Autowired
+    private IMergedOrderService mergedOrderService;
+    // 设置最大导出数量限制为20000条
+    private static final int maxExportCount = 20000;
+
+
+
+    /**
+     * 查询合并订单列表
+     */
+    @ApiOperation("查询合并订单列表")
+    @GetMapping("/list")
+    public TableDataInfo list(MergedOrderQueryParam param)
+    {
+        if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
+            return getDataTable(new ArrayList<>());
+        }
+
+        startPage();
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        for (MergedOrderVO vo : list) {
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setPhone(ParseUtils.parsePhone(vo.getPhone()));
+            vo.setSalesPhone(ParseUtils.parsePhone(vo.getSalesPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出合并订单列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:order:export')")
+    @ApiOperation("导出合并订单列表")
+    @Log(title = "合并订单", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(MergedOrderQueryParam param)
+    {
+        if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
+            return AjaxResult.error("请选择导出订单类型!");
+        }
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        param.setCompanyId(user.getCompanyId());
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                    System.out.println(e.getMessage());
+                }
+            }
+        }
+
+        // 转换为导出VO
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, false);
+
+        // 如果数据量在限制范围内,正常导出
+        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
+        return util.exportExcel(exportList, "合并订单数据");
+    }
+
+    /**
+     * 导出合并订单(明文)
+     */
+    @PreAuthorize("@ss.hasPermi('live:order:exportAll')")
+    @ApiOperation("导出合并订单(明文)")
+    @Log(title = "合并订单(明文)", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportDetails")
+    public AjaxResult exportDetails(MergedOrderQueryParam param)
+    {
+        if(param.getOrderTypeFilter() == null || param.getOrderTypeFilter().equals("2")){
+            return AjaxResult.error("请选择导出订单类型!");
+        }
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        param.setCompanyId(user.getCompanyId());
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        for (MergedOrderVO vo : list) {
+            if (!StringUtils.isEmpty(vo.getItemJson())) {
+                try {
+                    StoreOrderProductDTO orderProductDTO = JSONObject.parseObject(vo.getItemJson(), StoreOrderProductDTO.class);
+                    BeanUtil.copyProperties(orderProductDTO, vo);
+                } catch (Exception e) {
+                }
+            }
+        }
+        // 转换为导出VO(明文模式,不脱敏)
+        List<MergedOrderExportVO> exportList = convertToExportVO(list, true);
+
+        ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
+        return util.exportExcel(exportList, "合并订单(明文)");
+    }
+
+    /**
+     * 导出合并订单明细
+     */
+    @ApiOperation("导出合并订单明细")
+    @Log(title = "合并订单明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportItems")
+    public AjaxResult exportItems(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单明细");
+    }
+
+
+
+    /**
+     * 导出合并订单明细(明文)
+     */
+    @ApiOperation("导出合并订单明细(明文)")
+    @Log(title = "合并订单明细(明文)", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportItemsDetails")
+    public AjaxResult exportItemsDetails(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单明细(明文)");
+    }
+
+    /**
+     * 导出合并订单发货单
+     */
+    @ApiOperation("导出合并订单发货单")
+    @Log(title = "合并订单发货单", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportShipping")
+    public AjaxResult exportShipping(MergedOrderQueryParam param)
+    {
+        // 先查询数据,限制查询20001条,用于判断是否超过限制
+        PageHelper.startPage(1, maxExportCount + 1);
+        List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
+        // 如果查询结果超过20000条,返回错误提示
+        if (list != null && list.size() > maxExportCount) {
+            return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
+        }
+        ExcelUtil<MergedOrderVO> util = new ExcelUtil<>(MergedOrderVO.class);
+        return util.exportExcel(list, "合并订单发货单");
+    }
+
+    /**
+     * 将 MergedOrderVO 转换为 MergedOrderExportVO
+     * @param list 原始数据列表
+     * @param isPlainText 是否为明文模式(true:不脱敏,false:脱敏)
+     * @return 导出VO列表
+     */
+    private List<MergedOrderExportVO> convertToExportVO(List<MergedOrderVO> list, boolean isPlainText)
+    {
+        if (list == null || list.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return list.stream().map(vo -> {
+            MergedOrderExportVO exportVO = new MergedOrderExportVO();
+
+            // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
+            exportVO.setOrderCode(vo.getOrderCode());
+            exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
+            exportVO.setUserId(vo.getUserId());
+
+            // 产品信息
+            exportVO.setProductName(vo.getProductName());
+            exportVO.setBarCode(vo.getBarCode());
+            exportVO.setProductSpec(StringUtils.isEmpty(vo.getProductSpec()) ? "默认" : vo.getProductSpec());
+            exportVO.setTotalNum(vo.getTotalNum());
+            exportVO.setPrice(vo.getTotalPrice()); // 产品价格使用订单总价
+            exportVO.setCost(BigDecimal.ZERO);
+            exportVO.setFPrice(BigDecimal.ZERO); // 结算价,合并订单暂无此字段
+            exportVO.setPayPostage(vo.getPayDelivery());
+            exportVO.setCateName(vo.getCateName());
+
+            // 收货信息
+            exportVO.setRealName(vo.getRealName());
+            exportVO.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            exportVO.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            // 时间信息
+            exportVO.setCreateTime(vo.getCreateTime());
+            exportVO.setPayTime(vo.getPayTime());
+
+            // 物流信息
+            exportVO.setDeliverySn(vo.getDeliveryCode()); // 快递公司编号,合并订单暂无此字段
+            exportVO.setDeliveryName(vo.getDeliveryName()); // 快递公司,合并订单暂无此字段
+            exportVO.setDeliveryId(vo.getDeliveryId());
+
+            // 公司和销售信息
+            exportVO.setCompanyName(vo.getCompanyName());
+            exportVO.setCompanyUserNickName(vo.getCompanyUserNickName());
+
+            // 套餐信息
+            exportVO.setPackageName(null); // 套餐名称,合并订单暂无此字段
+            exportVO.setGroupBarCode(null); // 组合码,合并订单暂无此字段
+
+            // 凭证信息
+            exportVO.setIsUpload(null); // 是否上传凭证,合并订单暂无此字段
+            exportVO.setUploadTime(null); // 上传时间,合并订单暂无此字段
+
+            // 档期信息
+            exportVO.setScheduleName(null); // 归属档期,合并订单暂无此字段
+
+            // 银行交易流水号
+            exportVO.setBankTransactionId(vo.getBankTransactionId());
+
+            // 金额信息
+            exportVO.setTotalPrice(vo.getTotalPrice());
+            exportVO.setPayPrice(vo.getPayPrice());
+            exportVO.setPayMoney(vo.getPayMoney());
+            exportVO.setPayPostage(vo.getPayDelivery()); // 额外运费,合并订单暂无此字段
+            exportVO.setPayDelivery(vo.getPayDelivery());
+
+            return exportVO;
+        }).collect(Collectors.toList());
+    }
+}

+ 0 - 1
fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java

@@ -33,7 +33,6 @@ public class LiveWatchUserAspect {
         try {
             String methodName = joinPoint.getSignature().getName();
             Object[] args = joinPoint.getArgs();
-            log.info("直播观看用户数据发生变化,方法: {}, 参数: {}", methodName, Arrays.toString(args));
             // 提取liveId并处理缓存更新
             Set<Long> liveIds = extractLiveIds(methodName, args);
             for (Long liveId : liveIds) {

+ 1 - 1
fs-live-app/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java

@@ -73,7 +73,7 @@ public class RateLimiterAspect
             {
                 throw new ServiceException("访问过于频繁,请稍后再试");
             }
-            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
+
         }
         catch (ServiceException e)
         {

+ 11 - 8
fs-live-app/src/main/java/com/fs/live/controller/LiveController.java

@@ -77,7 +77,7 @@ public class LiveController {
 		live.setLiveId(Long.valueOf(params.get("stream_id")));
 		live.setStatus(3);
 		live.setFinishTime(LocalDateTime.now());
-		liveService.updateLiveEntity(live);
+//		liveService.updateLiveEntity(live);
 		return R.ok();
 //		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=673,
 //				errcode=1, errmsg=The push client actively stopped the push, event_time=1755571239,
@@ -120,20 +120,23 @@ public class LiveController {
 
 	@PostMapping("/videoUpload")
 	public R videoUpload(HttpServletRequest request, @RequestBody  Map<String, Object> params) {
+		String videoUrl = "https://bjzmkytcpv.ylrzcloud.com/";
 		log.info("请求参数:{}", params);
 		if(!params.containsKey("WorkflowExecution")) return R.error("参数错误");
 
 		LinkedHashMap<String,Object> result = (LinkedHashMap<String,Object>) params.get("WorkflowExecution");
 		String string = result.get("Object").toString();
-		videoService.updateFinishStatus("https://fs-1319721001.cos.ap-chongqing.myqcloud.com/" + string.replace(".mp4", ".m3u8"));
+		videoService.updateFinishStatus(string.replace(".mp4", ".m3u8"));
 
 		return R.ok();
-//		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=673,
-//				errcode=1, errmsg=The push client actively stopped the push, event_time=1755571239,
-//				event_type=0, height=1080, idc_id=38, node=113.250.23.118, push_duration=1051237,
-//				sequence=721865018844564968, set_id=2, stream_id=673,
-//				stream_param=txSecret=A3EF362C9484D3D091C2E9B08C2C08CB&txTime=68A53145,
-//				user_ip=113.248.98.28, width=1920}
+//		{EventName=WorkflowFinish, WorkflowExecution={RunId=i07d29fabdaed11f0ac79525400de87bb, BucketId=bjzmky-1323137866,
+//				Object=course/20251112/1762939096674.mp4, CosHeaders=[{Key=Content-Type, Value=video/mp4}],
+//				WorkflowId=w1d48658c808643bd98bc0f0761ab38a7, WorkflowName=720视频转码, CreateTime=2025-12-17 10:06:42+0800,
+//				State=Success, Tasks=[{Type=Transcode, CreateTime=2025-12-17 10:06:42+0800, EndTime=2025-12-17 10:06:57+0800,
+//				State=Success, JobId=j07dd4e30daed11f084a9fd5b71d7a75a, Name=Transcode_1765251580610, TemplateId=t025ee3d5a5ea14d32a5a3d52d4c28c7d3,
+//				TemplateName=H264-HLS-标清, ResultInfo={ObjectCount=1, ObjectInfo=[
+//						{ObjectName=course/20251112/1762939096674-720.m3u8, ObjectUrl=https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com/course/20251112/1762939096674-720.m3u8}]}}]}}
+
 
 	}
 

+ 0 - 2
fs-live-app/src/main/java/com/fs/live/controller/LiveDataController.java

@@ -15,7 +15,6 @@ public class LiveDataController extends BaseController {
 
     @Autowired
     private RedisCache redisCache;
-
     /**
      * 点赞
      * */
@@ -23,7 +22,6 @@ public class LiveDataController extends BaseController {
     public R like(@PathVariable("liveId") Long liveId) {
         //直播间总点赞数
         Long increment = redisCache.incr("live:like:" + liveId, 1);
-
         return R.ok().put("like",increment);
     }
 }

+ 98 - 0
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -0,0 +1,98 @@
+package com.fs.live.task;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.service.WebSocketServer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 直播完课积分定时任务
+ */
+@Slf4j
+@Component
+public class LiveCompletionPointsTask {
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
+
+    @Autowired
+    private WebSocketServer webSocketServer;
+
+    @Autowired
+    private ILiveService liveService;
+
+    /**
+     * 定时检查观看时长并创建完课记录(兜底机制)
+     * 每分钟执行一次
+     * 优化:防重复推送 + 只查询开启了完课积分的直播间
+     */
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void checkCompletionStatus() {
+        try {
+            // 只查询开启了完课积分配置的直播间
+            List<Live> activeLives = liveService.selectLiveListWithCompletionPointsEnabled();
+            
+            if (activeLives == null || activeLives.isEmpty()) {
+                log.debug("当前没有开启完课积分的直播间");
+                return;
+            }
+            
+            log.info("开始检查完课状态, 开启完课积分的直播间数量: {}", activeLives.size());
+
+            for (Live live : activeLives) {
+                try {
+                    Long liveId = live.getLiveId();
+                    
+                    // 使用Hash结构获取该直播间所有用户的观看时长
+                    String hashKey = "live:watch:duration:hash:" + liveId;
+                    Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
+                    
+                    if (userDurations == null || userDurations.isEmpty()) {
+                        log.warn("直播间没有观看时长数据, liveId={}, liveName={}, Redis Key: {}, userDurations={}", 
+                                liveId, live.getLiveName(), hashKey, userDurations);
+                        continue;
+                    }
+                    
+                    log.info("直播间有观看数据, liveId={}, liveName={}, 用户数: {}", 
+                            liveId, live.getLiveName(), userDurations.size());
+                    
+                    // 3. 逐个用户处理
+                    for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+                        try {
+                            Long userId = Long.parseLong(entry.getKey().toString());
+                            Long duration = Long.parseLong(entry.getValue().toString());  // 从 Redis 直接获取观看时长
+                            
+                            completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, duration);
+
+                        } catch (Exception e) {
+                            log.error("处理用户完课状态失败, liveId={}, userId={}", liveId, entry.getKey(), e);
+                        }
+                    }
+                    
+                } catch (Exception e) {
+                    log.error("处理直播间完课状态失败, liveId={}", live.getLiveId(), e);
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("检查完课状态定时任务执行失败", e);
+        }
+    }
+}

+ 501 - 29
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -71,6 +71,12 @@ public class Task {
     private ILiveRedConfService liveRedConfService;
     @Autowired
     private ILiveCouponIssueService liveCouponIssueService;
+    @Autowired
+    private ILiveVideoService liveVideoService;
+    @Autowired
+    private ILiveWatchLogService liveWatchLogService;
+    @Autowired
+    private ILiveUserFirstEntryService liveUserFirstEntryService;
 
     @Autowired
     public FsJstAftersalePushService fsJstAftersalePushService;
@@ -142,6 +148,11 @@ public class Task {
                 }
             }
         });
+        if(!liveList.isEmpty()){
+            for (Live live : liveList) {
+                liveService.updateLiveEntity(live);
+            }
+        }
         String key = "live:auto_task:";
         if (!startLiveList.isEmpty()) {
             for (Live live : startLiveList) {
@@ -154,10 +165,38 @@ public class Task {
                     collect.forEach(liveAutoTask -> {
                         liveAutoTask.setCreateTime(null);
                         liveAutoTask.setUpdateTime(null);
-                        redisCache.redisTemplate.opsForZSet().add(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
-                        redisCache.redisTemplate.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
+                        redisCache.zSetAdd(key + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
+                        redisCache.expire(key+live.getLiveId(), 1, TimeUnit.DAYS);
                     });
                 }
+                
+                // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
+                try {
+                    // 获取视频时长
+                    Long videoDuration = 0L;
+                    List<LiveVideo> videos = liveVideoService.listByLiveId(live.getLiveId(), 1);
+                    if (CollUtil.isNotEmpty(videos)) {
+                        videoDuration = videos.stream()
+                                .filter(v -> v.getDuration() != null)
+                                .mapToLong(LiveVideo::getDuration)
+                                .sum();
+                    }
+                    
+                    // 如果视频时长大于0,将直播间信息存入Redis
+                    if (videoDuration > 0 && live.getStartTime() != null) {
+                        Map<String, Object> tagMarkInfo = new HashMap<>();
+                        tagMarkInfo.put("liveId", live.getLiveId());
+                        tagMarkInfo.put("startTime", live.getStartTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli());
+                        tagMarkInfo.put("videoDuration", videoDuration);
+                        
+                        String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
+                        redisCache.setCacheObject(tagMarkKey, JSON.toJSONString(tagMarkInfo), 24, TimeUnit.HOURS);
+                        log.info("直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}", 
+                                live.getLiveId(), live.getStartTime(), videoDuration);
+                    }
+                } catch (Exception e) {
+                    log.error("写入直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
+                }
             }
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
@@ -170,7 +209,7 @@ public class Task {
                 webSocketServer.broadcastMessage(live.getLiveId(), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
                 List<LiveAutoTask> collect = liveAutoTasks.stream().filter(liveAutoTask -> liveAutoTask.getLiveId().equals(live.getLiveId())).collect(Collectors.toList());
                 if (!collect.isEmpty()) {
-                    redisCache.redisTemplate.delete(key + live.getLiveId());
+                    redisCache.deleteObject(key + live.getLiveId());
                     collect.forEach(liveAutoTask -> {
                         liveAutoTask.setCreateTime(null);
                         liveAutoTask.setUpdateTime(null);
@@ -178,15 +217,20 @@ public class Task {
                     });
                 }
                 webSocketServer.removeLikeCountCache(live.getLiveId());
+                
+                // 删除打标签缓存
+                try {
+                    String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
+                    redisCache.deleteObject(tagMarkKey);
+                    log.info("直播间结束,已删除打标签缓存: liveId={}", live.getLiveId());
+                } catch (Exception e) {
+                    log.error("删除直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
+                }
             }
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
         }
-        if(!liveList.isEmpty()){
-            for (Live live : liveList) {
-                liveService.updateLiveEntity(live);
-            }
-        }
+
     }
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveLotteryTask", scene = "task")
@@ -460,27 +504,58 @@ public class Task {
             return;
         liveDatas.forEach(liveData ->{
 
-            Long resultLikeCount = getAsLong(redisCache, "live:like:" + liveData.getLiveId());
-            resultLikeCount = resultLikeCount > 0L ? resultLikeCount : liveData.getLikes();
-            redisCache.setCacheObject("live:like:" + liveData.getLiveId(), resultLikeCount.intValue());
-            liveData.setLikes(
-                    resultLikeCount
-            );
-
-       /* for (Long liveId : liveIds) {
-            LiveData liveData = liveDataService.selectLiveDataByLiveId(liveId);
-            if (liveData == null) {
-                continue; // 防止空指针异常
-            }*/
-
-
-            // 从 redis 获取数据,并提供默认值,避免 NPE
-            liveData.setPageViews(
-                    Math.max( liveData.getPageViews(), Optional.ofNullable(redisCache.incr(PAGE_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
-            );
-            liveData.setTotalViews(
-                    Math.max( liveData.getTotalViews(), Optional.ofNullable(redisCache.incr(TOTAL_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
-            );
+            Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveData.getLiveId());
+            Integer liveFlag = flagMap.get("liveFlag");
+
+            // 判断是直播还是回放
+            if (liveFlag != null && liveFlag == 1) {
+                // 直播:更新 likes 和 totalViews
+                Long resultLikeCount = getAsLong(redisCache, "live:like:" + liveData.getLiveId());
+                resultLikeCount = resultLikeCount > 0L ? resultLikeCount : liveData.getLikes();
+                redisCache.setCacheObject("live:like:" + liveData.getLiveId(), resultLikeCount.intValue());
+                liveData.setLikes(resultLikeCount);
+
+                // 从 redis 获取数据,并提供默认值,避免 NPE
+                liveData.setPageViews(
+                        Math.max( liveData.getPageViews(), Optional.ofNullable(redisCache.incr(PAGE_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                );
+                liveData.setTotalViews(
+                        Math.max( liveData.getTotalViews(), Optional.ofNullable(redisCache.incr(TOTAL_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                );
+            } else {
+                // 回放:使用 Redis 中的数据减去直播的数据,得到回放的数据
+                String likeKey = "live:like:" + liveData.getLiveId();
+                String totalViewsKey = TOTAL_VIEWS_KEY + liveData.getLiveId();
+                
+                // 从 Redis 获取总数据(直播+回放)
+                Long totalLikeCount = getAsLong(redisCache, likeKey);
+                Long totalViewCount = getAsLong(redisCache, totalViewsKey);
+                
+                // 获取数据库中直播的数据
+                Long liveLikeCount = liveData.getLikes() != null ? liveData.getLikes() : 0L;
+                Long liveViewCount = liveData.getTotalViews() != null ? liveData.getTotalViews() : 0L;
+                
+                // 回放数据 = Redis总数据 - 直播数据
+                Long replayLikeNum = totalLikeCount - liveLikeCount;
+                Long replayViewNum = totalViewCount - liveViewCount;
+                
+                // 确保回放数据不为负数
+                if (replayLikeNum < 0L) {
+                    replayLikeNum = 0L;
+                }
+                if (replayViewNum < 0L) {
+                    replayViewNum = 0L;
+                }
+                
+                // 更新回放数据
+                liveData.setReplayLikeNum(replayLikeNum);
+                liveData.setReplayViewNum(replayViewNum);
+
+                // 从 redis 获取数据,并提供默认值,避免 NPE
+                liveData.setPageViews(
+                        Math.max( liveData.getPageViews(), Optional.ofNullable(redisCache.incr(PAGE_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L))
+                );
+            }
             liveData.setUniqueVisitors(
                     /*Optional.ofNullable(redisCache.getCacheSet(UNIQUE_VISITORS_KEY + liveId))
                             .map(Set::size)  // 获取集合大小
@@ -547,4 +622,401 @@ public class Task {
     public void updateRedQuantityNum() {
         liveRedConfService.updateRedQuantityNum();
     }
+
+    /**
+     * 定时扫描开启的直播间,检查是否到了打标签的时间
+     * 每10秒执行一次
+     */
+    @Scheduled(cron = "0/10 * * * * ?")
+    @DistributeLock(key = "scanLiveTagMark", scene = "task")
+    public void scanLiveTagMark() {
+        try {
+
+            // 获取所有打标签缓存的key
+            String pattern = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, "*");
+            Set<String> keys = redisCache.redisTemplate.keys(pattern);
+            
+            if (keys == null || keys.isEmpty()) {
+                return;
+            }
+            
+            long currentTimeMillis = System.currentTimeMillis();
+            LocalDateTime now = LocalDateTime.now();
+            List<Long> processedLiveIds = new ArrayList<>();
+            Date nowDate = new Date();
+            for (String key : keys) {
+                try {
+                    // 从Redis获取直播间信息
+                    Object cacheValue = redisCache.getCacheObject(key);
+                    if (cacheValue == null) {
+                        continue;
+                    }
+                    
+                    String jsonStr = cacheValue.toString();
+                    JSONObject tagMarkInfo = JSON.parseObject(jsonStr);
+                    Long liveId = tagMarkInfo.getLong("liveId");
+                    Long startTimeMillis = tagMarkInfo.getLong("startTime");
+                    Long videoDuration = tagMarkInfo.getLong("videoDuration");
+                    
+                    if (liveId == null || startTimeMillis == null || videoDuration == null || videoDuration <= 0) {
+                        log.warn("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}", 
+                                key, liveId, startTimeMillis, videoDuration);
+                        continue;
+                    }
+                    
+                    // 查询直播间信息
+                    Live live = liveService.selectLiveDbByLiveId(liveId);
+                    if (live == null || live.getStartTime() == null) {
+                        continue;
+                    }
+                    // 计算结束时间:开始时间 + 视频时长(秒转毫秒)
+                    long endTimeMillis = startTimeMillis + (videoDuration * 1000);
+
+                    // 如果当前时间已经超过了结束时间,执行打标签操作
+                    if (currentTimeMillis >= endTimeMillis) {
+                        // 查询当前直播间的在线用户(liveFlag = 1, replayFlag = 0)
+                        LiveWatchUser queryUser = new LiveWatchUser();
+                        queryUser.setLiveId(liveId);
+                        queryUser.setLiveFlag(1);
+                        queryUser.setReplayFlag(0);
+                        List<LiveWatchUser> liveUsers = liveWatchUserService.selectLiveWatchUserList(queryUser);
+
+                        if (liveUsers != null && !liveUsers.isEmpty()) {
+
+                            List<LiveWatchUser> updateLiveUsers = new ArrayList<>(); // 需要更新的直播用户
+                            List<LiveWatchUser> replayUsers = new ArrayList<>(); // 回放用户数据
+
+                            for (LiveWatchUser liveUser : liveUsers) {
+                                Long userId = liveUser.getUserId();
+                                if (userId == null) {
+                                    continue;
+                                }
+
+                                // 1. 计算并更新直播用户的在线时长
+                                // 优先从 Redis 获取进入时间
+                                String entryTimeKey = String.format("live:user:entry:time:%s:%s", liveId, userId);
+                                Long entryTime = redisCache.getCacheObject(entryTimeKey);
+
+                                // 如果没有 Redis 记录,使用数据库中的 updateTime
+                                if (entryTime == null) {
+                                    if (liveUser.getUpdateTime() != null) {
+                                        entryTime = liveUser.getUpdateTime().getTime();
+                                    } else if (liveUser.getCreateTime() != null) {
+                                        entryTime = liveUser.getCreateTime().getTime();
+                                    }
+                                }
+
+                                // 计算当前观看时长(秒)
+                                long currentWatchDuration = 0L;
+                                if (entryTime != null) {
+                                    currentWatchDuration = (currentTimeMillis - entryTime) / 1000;
+                                    if (currentWatchDuration < 0) {
+                                        currentWatchDuration = 0L;
+                                    }
+                                }
+
+                                // 加上历史在线时长
+                                Long historyOnlineSeconds = liveUser.getOnlineSeconds();
+                                if (historyOnlineSeconds == null) {
+                                    historyOnlineSeconds = 0L;
+                                }
+                                long totalOnlineSeconds = historyOnlineSeconds + currentWatchDuration;
+
+                                // 更新直播用户的在线时长
+                                liveUser.setOnlineSeconds(totalOnlineSeconds);
+                                liveUser.setUpdateTime(nowDate);
+                                updateLiveUsers.add(liveUser);
+
+                                // 2. 生成回放用户数据(liveFlag = 0, replayFlag = 1),在线时长从0开始
+                                LiveWatchUser replayUser = new LiveWatchUser();
+                                replayUser.setLiveId(liveUser.getLiveId());
+                                replayUser.setUserId(liveUser.getUserId());
+                                replayUser.setMsgStatus(liveUser.getMsgStatus());
+                                replayUser.setOnline(liveUser.getOnline());
+                                replayUser.setOnlineSeconds(0L); // 回放观看时长从0开始,重新计时
+                                replayUser.setGlobalVisible(liveUser.getGlobalVisible());
+                                replayUser.setSingleVisible(liveUser.getSingleVisible());
+                                replayUser.setLiveFlag(0); // 回放标记
+                                replayUser.setReplayFlag(1); // 回放标记
+                                replayUser.setLocation(liveUser.getLocation());
+                                replayUser.setCreateTime(nowDate);
+                                replayUser.setUpdateTime(nowDate);
+                                replayUsers.add(replayUser);
+                            }
+
+                            // 批量更新直播用户的在线时长
+                            if (!updateLiveUsers.isEmpty()) {
+                                int batchSize = 500;
+                                for (int i = 0; i < updateLiveUsers.size(); i += batchSize) {
+                                    int end = Math.min(i + batchSize, updateLiveUsers.size());
+                                    List<LiveWatchUser> batch = updateLiveUsers.subList(i, end);
+                                    liveWatchUserService.batchUpdateLiveWatchUser(batch);
+                                }
+                            }
+
+                            // 批量插入回放用户数据
+                            if (!replayUsers.isEmpty()) {
+                                int batchSize = 500;
+                                for (int i = 0; i < replayUsers.size(); i += batchSize) {
+                                    int end = Math.min(i + batchSize, replayUsers.size());
+                                    List<LiveWatchUser> batch = replayUsers.subList(i, end);
+                                    liveWatchUserService.batchInsertLiveWatchUser(batch);
+                                }
+                            }
+
+                            // 清理直播间状态缓存
+                            liveWatchUserService.clearLiveFlagCache(liveId);
+                        }
+
+                        // 标记为已处理,稍后删除缓存
+                        processedLiveIds.add(liveId);
+                        // 调用打标签方法
+                        liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
+                    }
+                } catch (Exception e) {
+                    log.error("处理直播间打标签缓存异常: key={}, error={}", key, e.getMessage(), e);
+                }
+            }
+            
+            // 删除已处理的直播间缓存
+            for (Long liveId : processedLiveIds) {
+                try {
+                    String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, liveId);
+                    redisCache.deleteObject(tagMarkKey);
+                } catch (Exception e) {
+                    log.error("删除直播间打标签缓存失败: liveId={}, error={}", liveId, e.getMessage(), e);
+                }
+            }
+        } catch (Exception e) {
+            log.error("扫描直播间打标签任务异常: error={}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 实时扫描用户直播数据,根据用户的直播在线时长更新观看记录状态
+     * 每30秒执行一次
+     */
+    @Scheduled(cron = "0/30 * * * * ?")
+    @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
+    public void scanLiveWatchUserStatus() {
+        try {
+            // 查询所有正在直播的直播间
+            List<Live> activeLives = liveService.selectNoEndLiveList();
+            if (activeLives == null || activeLives.isEmpty()) {
+                return;
+            }
+
+            for (Live live : activeLives) {
+                try {
+                    Long liveId = live.getLiveId();
+                    if (liveId == null) {
+                        continue;
+                    }
+                    // 获取直播间的直播/回放状态
+                    Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+                    Integer liveFlag = flagMap.get("liveFlag");
+                    // 只处理直播状态的用户(liveFlag = 1)
+                    if (liveFlag == null || liveFlag != 1) {
+                        continue;
+                    }
+                    // 查询该直播间的在线用户(liveFlag = 1, replayFlag = 0)
+                    LiveWatchUser queryUser = new LiveWatchUser();
+                    queryUser.setLiveId(liveId);
+                    queryUser.setLiveFlag(1);
+                    queryUser.setReplayFlag(0);
+                    queryUser.setOnline(0); // 在线用户
+                    List<LiveWatchUser> onlineUsers = liveWatchUserService.selectLiveWatchUserList(queryUser);
+                    if (onlineUsers == null || onlineUsers.isEmpty()) {
+                        continue;
+                    }
+                    // 获取直播视频总时长
+                    List<LiveVideo> videos = liveVideoService.listByLiveIdWithCache(liveId, 1);
+                    long totalVideoDuration = 0L;
+                    if (videos != null && !videos.isEmpty()) {
+                        totalVideoDuration = videos.stream()
+                                .filter(v -> v.getDuration() != null)
+                                .mapToLong(LiveVideo::getDuration)
+                                .sum();
+                    }
+
+                    // 处理每个在线用户
+                    for (LiveWatchUser user : onlineUsers) {
+                        try {
+                            Long userId = user.getUserId();
+                            if (userId == null) {
+                                continue;
+                            }
+
+                            // 获取用户的在线观看时长
+                            Long onlineSeconds = user.getOnlineSeconds();
+                            if (onlineSeconds == null || onlineSeconds <= 0) {
+                                continue;
+                            }
+                            
+                            // 获取用户的 companyId 和 companyUserId
+                            LiveUserFirstEntry liveUserFirstEntry =
+                                    liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
+                            if (liveUserFirstEntry == null) {
+                                continue;
+                            }
+                            
+                            Long qwUserId = liveUserFirstEntry.getQwUserId();
+                            Long externalContactId = liveUserFirstEntry.getExternalContactId();
+
+                            if (qwUserId == null || qwUserId <= 0 || externalContactId == null || externalContactId <= 0) {
+                                continue;
+                            }
+
+                            // 使用 updateLiveWatchLogTypeByDuration 的逻辑更新观看记录状态
+                            updateLiveWatchLogTypeByDuration(liveId, userId, qwUserId, externalContactId,
+                                    onlineSeconds, totalVideoDuration);
+                            
+                        } catch (Exception e) {
+                            log.error("处理用户观看记录状态异常: liveId={}, userId={}, error={}",
+                                    liveId, user.getUserId(), e.getMessage(), e);
+                        }
+                    }
+                    
+                } catch (Exception e) {
+                    log.error("处理直播间观看记录状态异常: liveId={}, error={}",
+                            live.getLiveId(), e.getMessage(), e);
+                }
+            }
+        } catch (Exception e) {
+            log.error("实时扫描用户直播数据任务异常: error={}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 根据在线时长更新 LiveWatchLog 的 logType(复用 WebSocketServer 中的逻辑)
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param qwUserId 邀请人id
+     * @param  exId 外部人id
+     * @param onlineSeconds 在线时长(秒)
+     * @param totalVideoDuration 视频总时长(秒)
+     */
+    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long qwUserId,
+                                                   Long exId, Long onlineSeconds, long totalVideoDuration) {
+        try {
+            // 查询 LiveWatchLog
+            LiveWatchLog queryLog = new LiveWatchLog();
+            queryLog.setLiveId(liveId);
+            queryLog.setUserId(userId);
+            queryLog.setQwUserId(String.valueOf(qwUserId));
+            queryLog.setExternalContactId(exId);
+
+            List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+            if (logs == null || logs.isEmpty()) {
+                return;
+            }
+
+            Date now = new Date();
+            for (LiveWatchLog log : logs) {
+                if (log.getLogType() != null && log.getLogType() == 2) {
+                    continue;
+                }
+                boolean needUpdate = false;
+                Integer newLogType = log.getLogType();
+
+                // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断)
+                if (onlineSeconds <= 180) { // 3分钟 = 180秒
+                    newLogType = 4;
+                    needUpdate = true;
+                }
+                // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
+                    newLogType = 2;
+                    log.setFinishTime(now);
+                    needUpdate = true;
+                }
+                // 如果直播视频 >= 20分钟且 < 40分钟,在线时长 >= 20分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 1200 && totalVideoDuration < 2400 && onlineSeconds >= 1200) { // 20分钟 = 1200秒
+                    newLogType = 2;
+                    log.setFinishTime(now);
+                    needUpdate = true;
+                }
+
+                // 如果 logType 已经是 2(完课),不再更新
+                if (needUpdate) {
+                    log.setLogType(newLogType);
+                    liveWatchLogService.updateLiveWatchLog(log);
+                }
+            }
+        } catch (Exception e) {
+            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}",
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 批量同步Redis中的观看时长到数据库
+     * 每2分钟执行一次,减少数据库压力
+     */
+//    @Scheduled(cron = "0 0/2 * * * ?")
+//    @DistributeLock(key = "batchSyncWatchDuration", scene = "task")
+//    public void batchSyncWatchDuration() {
+//        try {
+//            log.info("开始批量同步观看时长到数据库");
+//
+//            // 优化:从所有直播间的Hash中批量获取数据
+//            List<Live> activeLives = liveService.selectNoEndLiveList();
+//
+//            if (activeLives == null || activeLives.isEmpty()) {
+//                log.debug("当前没有活跃的直播间");
+//                return;
+//            }
+//
+//            int totalCount = 0;
+//            int successCount = 0;
+//            int failCount = 0;
+//
+//            // 逐个直播间处理
+//            for (Live live : activeLives) {
+//                try {
+//                    Long liveId = live.getLiveId();
+//
+//                    // 使用Hash结构存储每个直播间的观看时长
+//                    String hashKey = "live:watch:duration:hash:" + liveId;
+//                    Map<Object, Object> userDurations = redisCache.redisTemplate.opsForHash().entries(hashKey);
+//
+//                    if (userDurations == null || userDurations.isEmpty()) {
+//                        continue;
+//                    }
+//
+//                    // 获取直播/回放标记(一次查询,所有用户复用)
+//                    Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+//                    Integer liveFlag = flagMap.get("liveFlag");
+//                    Integer replayFlag = flagMap.get("replayFlag");
+//
+//                    // 批量处理该直播间的所有用户
+//                    for (Map.Entry<Object, Object> entry : userDurations.entrySet()) {
+//                        try {
+//                            Long userId = Long.parseLong(entry.getKey().toString());
+//                            Long duration = Long.parseLong(entry.getValue().toString());
+//
+//                            totalCount++;
+//
+//                            // 异步更新数据库
+//                            liveWatchUserService.updateWatchDuration(liveId, userId, liveFlag, replayFlag, duration);
+//                            successCount++;
+//
+//                        } catch (Exception e) {
+//                            failCount++;
+//                            log.error("同步用户观看时长失败: liveId={}, userId={}, error={}",
+//                                    liveId, entry.getKey(), e.getMessage());
+//                        }
+//                    }
+//
+//                } catch (Exception e) {
+//                    log.error("处理直播间观看时长失败: liveId={}, error={}", live.getLiveId(), e.getMessage());
+//                }
+//            }
+//
+//            log.info("批量同步观看时长完成: 总数={}, 成功={}, 失败={}", totalCount, successCount, failCount);
+//
+//        } catch (Exception e) {
+//            log.error("批量同步观看时长任务异常", e);
+//        }
+//    }
 }

+ 9 - 0
fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java

@@ -52,6 +52,15 @@ public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
         if (parameterMap.containsKey(AttrConstant.COMPANY_USER_ID)) {
             userProperties.put(AttrConstant.COMPANY_USER_ID, Long.valueOf(parameterMap.get(AttrConstant.COMPANY_USER_ID).get(0)));
         }
+        if (parameterMap.containsKey(AttrConstant.LOCATION)) {
+            userProperties.put(AttrConstant.LOCATION, parameterMap.get(AttrConstant.LOCATION).get(0));
+        }
+        if (parameterMap.containsKey(AttrConstant.QW_USER_ID)) {
+            userProperties.put(AttrConstant.QW_USER_ID, parameterMap.get(AttrConstant.QW_USER_ID).get(0));
+        }
+        if (parameterMap.containsKey(AttrConstant.EXTERNAL_CONTACT_ID)) {
+            userProperties.put(AttrConstant.EXTERNAL_CONTACT_ID, parameterMap.get(AttrConstant.EXTERNAL_CONTACT_ID).get(0));
+        }
 
         // 验证token
         if (parameterMap.containsKey(tokenKey)) {

+ 1 - 0
fs-live-app/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java

@@ -33,5 +33,6 @@ public class SendMsgVo {
     private String avatar;
     private boolean on = false;
     private Integer status;
+    private Integer duration;
 
 }

+ 4 - 0
fs-live-app/src/main/java/com/fs/live/websocket/constant/AttrConstant.java

@@ -12,9 +12,13 @@ public class AttrConstant {
     public static final String SIGNATURE = "signature";
     public static final String COMPANY_ID = "companyId";
     public static final String COMPANY_USER_ID = "companyUserId";
+    public static final String LOCATION = "location";
+    public static final String QW_USER_ID = "qwUserId";
+    public static final String EXTERNAL_CONTACT_ID = "externalContactId";
 
     // 定义 AttributeKey 保存必要参数
     public static final AttributeKey<Long> ATTR_LIVE_ID = AttributeKey.valueOf(LIVE_ID);
     public static final AttributeKey<Long> ATTR_USER_ID = AttributeKey.valueOf(USER_ID);
     public static final AttributeKey<Long> ATTR_USER_TYPE = AttributeKey.valueOf(USER_TYPE);
+    public static final AttributeKey<String> ATTR_LOCATION = AttributeKey.valueOf(USER_TYPE);
 }

+ 19 - 17
fs-live-app/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java

@@ -3,6 +3,8 @@ package com.fs.live.websocket.handle;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.domain.FsUserScrm;
+import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.websocket.bean.SendMsgVo;
 import com.fs.live.websocket.constant.AttrConstant;
 import com.fs.common.core.domain.R;
@@ -41,7 +43,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
     private final static ILiveService liveService = SpringUtils.getBean(ILiveService.class);
     private final static ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
     private final static ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
-    private final static IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
+    private final static IFsUserScrmService fsUserService = SpringUtils.getBean(IFsUserScrmService.class);
 
     /**
      * 处理握手
@@ -51,13 +53,14 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
      */
     @Override
     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
-        log.debug("事件");
+
         // 处理 WebSocket 握手完成事件
         if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
             Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
             Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
             Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
 
+
             if (Objects.isNull(liveService.selectLiveByLiveId(liveId))) {
                 ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 未找到直播间")).addListener(ChannelFutureListener.CLOSE);
                 return;
@@ -69,17 +72,17 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
             roomGroup.add(ctx.channel());
 
             if (userType == 0) {
+
+
+                FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
                 // 加入房间
-                liveWatchUserService.join(liveId, userId);
+                LiveWatchUser liveWatchUser = liveWatchUserService.joinWithoutLocation(fsUser,liveId, userId);
                 room.put(userId, ctx.channel());
-
-                FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
                 if (Objects.isNull(fsUser)) {
                     ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 用户信息错误")).addListener(ChannelFutureListener.CLOSE);
                     return;
                 }
 
-                LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
 
                 SendMsgVo sendMsgVo = new SendMsgVo();
                 sendMsgVo.setLiveId(liveId);
@@ -87,7 +90,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                 sendMsgVo.setUserType(userType);
                 sendMsgVo.setCmd("entry");
                 sendMsgVo.setMsg("用户进入");
-                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUser));
                 sendMsgVo.setNickName(fsUser.getNickname());
                 sendMsgVo.setAvatar(fsUser.getAvatar());
 
@@ -97,7 +100,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                 adminRoom.add(ctx.channel());
             }
 
-            log.debug("加入webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
         }
     }
 
@@ -154,7 +156,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
      */
     @Override
     protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
-        log.debug("接收到消息 data: {}", textWebSocketFrame.text());
+
         Long liveId = channelHandlerContext.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
         Long userType = channelHandlerContext.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
 
@@ -175,8 +177,9 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
                     liveMsg.setCreateTime(new Date());
 
                     if (userType == 0) {
-                        LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
-                        if(liveWatchUser.getMsgStatus() == 1){
+                        Map<String, Integer> liveFlagWithCache = liveWatchUserService.getLiveFlagWithCache(liveId);
+                        LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserByFlag(msg.getLiveId(), msg.getUserId(), liveFlagWithCache.get("liveFlag"),  liveFlagWithCache.get("replayFlag"));
+                        if(liveWatchUser != null && liveWatchUser.getMsgStatus() == 1){
                             sendMessage(channelHandlerContext.channel(), JSONObject.toJSONString(R.error("你以被禁言")));
                             return;
                         }
@@ -203,7 +206,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
      */
     @Override
     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
-        log.debug("断开连接");
+
         Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
         Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
         Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
@@ -217,15 +220,15 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
         ChannelGroup roomGroup = getRoomGroup(liveId);
 
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
-            liveWatchUserService.close(liveId, userId);
+            FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
+            LiveWatchUser close = liveWatchUserService.close(fsUser,liveId, userId);
             room.remove(userId);
 
             if (room.isEmpty()) {
                 rooms.remove(liveId);
             }
 
-            LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
+
 
             SendMsgVo sendMsgVo = new SendMsgVo();
             sendMsgVo.setLiveId(liveId);
@@ -233,7 +236,7 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
             sendMsgVo.setUserType(userType);
             sendMsgVo.setCmd("out");
             sendMsgVo.setMsg("用户离开");
-            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+            sendMsgVo.setData(JSONObject.toJSONString(close));
             sendMsgVo.setNickName(fsUser.getNickname());
             sendMsgVo.setAvatar(fsUser.getAvatar());
 
@@ -250,7 +253,6 @@ public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFr
             roomGroups.remove(liveId);
         }
 
-        log.debug("断开webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
 
     }
 

+ 815 - 67
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -4,11 +4,18 @@ package com.fs.live.websocket.service;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.LiveKeysConstant;
+import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.date.DateUtil;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.domain.FsUserScrm;
+import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.config.ProductionWordFilter;
 import com.fs.live.mapper.LiveCouponMapper;
+import com.fs.live.vo.LiveWatchUserEntry;
+import com.fs.live.domain.LiveWatchLog;
+import com.fs.live.domain.LiveVideo;
 import com.fs.live.websocket.auth.WebSocketConfigurator;
 import com.fs.live.websocket.bean.SendMsgVo;
 import com.fs.common.core.domain.R;
@@ -20,17 +27,20 @@ import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.DateUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
 import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
+import java.io.EOFException;
 import java.io.IOException;
+import java.time.LocalDateTime;
 import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
 
@@ -46,11 +56,21 @@ public class WebSocketServer {
     private final static ConcurrentHashMap<Long, ConcurrentHashMap<Long, Session>> rooms = new ConcurrentHashMap<>();
     // 管理端连接
     private final static ConcurrentHashMap<Long, CopyOnWriteArrayList<Session>> adminRooms = new ConcurrentHashMap<>();
+
+    // Session发送锁,避免同一会话并发发送消息
+    private final static ConcurrentHashMap<String, Lock> sessionLocks = new ConcurrentHashMap<>();
+    // 心跳超时缓存:key=sessionId,value=最后心跳时间戳
+    private final static ConcurrentHashMap<String, Long> heartbeatCache = new ConcurrentHashMap<>();
+    // 心跳超时时间(毫秒):3分钟无心跳则认为超时
+    private final static long HEARTBEAT_TIMEOUT = 2 * 60 * 1000;
+    // admin房间消息发送线程池(单线程,保证串行化)
+    private final static ConcurrentHashMap<Long, ExecutorService> adminExecutors = new ConcurrentHashMap<>();
+
     private final RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
     private final ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
     private final ILiveService liveService = SpringUtils.getBean(ILiveService.class);
     private final ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
-    private final IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
+    private final IFsUserScrmService fsUserService = SpringUtils.getBean(IFsUserScrmService.class);
     private final ILiveDataService liveDataService = SpringUtils.getBean(ILiveDataService.class);
     private final ProductionWordFilter productionWordFilter = SpringUtils.getBean(ProductionWordFilter.class);
     private final ILiveRedConfService liveRedConfService =  SpringUtils.getBean(ILiveRedConfService.class);
@@ -59,6 +79,13 @@ public class WebSocketServer {
     private final ILiveUserFirstEntryService liveUserFirstEntryService =  SpringUtils.getBean(ILiveUserFirstEntryService.class);
     private final ILiveCouponIssueService liveCouponIssueService =  SpringUtils.getBean(ILiveCouponIssueService.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);
+    private final ILiveCompletionPointsRecordService completionPointsRecordService = SpringUtils.getBean(ILiveCompletionPointsRecordService.class);
+    private static Random random = new Random();
+    
+    // Redis key 前缀:用户进入直播间时间
+    private static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
 
     // 直播间在线用户缓存
 //    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
@@ -72,6 +99,10 @@ public class WebSocketServer {
         long liveId = (long) userProperties.get("liveId");
         long userId = (long) userProperties.get("userId");
         long userType = (long) userProperties.get("userType");
+        long qwUserId = -1;
+        long externalContactId = -1;
+        String location = (String) userProperties.get("location");  // 获取location参数
+
         Live live = liveService.selectLiveByLiveId(liveId);
         if (live == null) {
             throw new BaseException("未找到直播间");
@@ -84,6 +115,12 @@ public class WebSocketServer {
         if (!Objects.isNull(userProperties.get("companyUserId"))) {
             companyUserId = (long) userProperties.get("companyUserId");
         }
+        if (!Objects.isNull(userProperties.get("qwUserId"))) {
+            qwUserId = (long) userProperties.get("qwUserId");
+        }
+        if (!Objects.isNull(userProperties.get("externalContactId"))) {
+            externalContactId = (long) userProperties.get("externalContactId");
+        }
 
 
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
@@ -91,13 +128,18 @@ public class WebSocketServer {
 
         // 记录连接信息 管理员不记录
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
 
-            LiveWatchUser liveWatchUserVO = liveWatchUserService.join(liveId, userId);
+            LiveWatchUser liveWatchUserVO = liveWatchUserService.join(fsUser,liveId, userId, location);
             room.put(userId, session);
+            
+            // 存储用户进入直播间的时间到 Redis(用于计算在线时长)
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            redisCache.setCacheObject(entryTimeKey, System.currentTimeMillis(), 24, TimeUnit.HOURS);
+            
             // 直播间浏览量 +1
             redisCache.incr(PAGE_VIEWS_KEY + liveId, 1);
 
@@ -129,26 +171,47 @@ public class WebSocketServer {
             if (isFirstViewer) {
                 redisCache.incr(UNIQUE_VIEWERS_KEY + liveId, 1);
             }
-            LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(liveId, userId);
-            liveWatchUserVO.setMsgStatus(liveWatchUser.getMsgStatus());
-            SendMsgVo sendMsgVo = new SendMsgVo();
-            sendMsgVo.setLiveId(liveId);
-            sendMsgVo.setUserId(userId);
-            sendMsgVo.setUserType(userType);
-            sendMsgVo.setCmd("entry");
-            sendMsgVo.setMsg("用户进入");
-            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
-            sendMsgVo.setNickName(fsUser.getNickname());
-            sendMsgVo.setAvatar(fsUser.getAvatar());
-            // 广播连接消息
-            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            liveWatchUserVO.setMsgStatus(liveWatchUserVO.getMsgStatus());
+            if (1 == random.nextInt(10)) {
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setUserType(userType);
+                sendMsgVo.setCmd("entry");
+                sendMsgVo.setMsg("用户进入");
+                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setAvatar(fsUser.getAvatar());
+                // 广播连接消息
+                broadcastWebMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            }
 
             LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+            // 如果用户连上了 socket,并且公司ID和销售ID大于0,更新 LiveWatchLog 的 logType
+
+            if ((qwUserId > 0 && externalContactId > 0) || (liveUserFirstEntry != null && liveUserFirstEntry.getCompanyId() > 0 && liveUserFirstEntry.getCompanyUserId() > 0 )) {
+                // 获取当前直播/回放状态
+                Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+                Integer currentLiveFlag = flagMap.get("liveFlag");
+
+                // 如果当前是直播状态(liveFlag = 1),更新 logType
+                if (currentLiveFlag != null && currentLiveFlag == 1) {
+                    updateLiveWatchLogTypeOnConnect(liveId, userId, qwUserId, externalContactId);
+                }
+            }
+
+
             if (liveUserFirstEntry != null) {
                 // 处理第一次自己进入,第二次扫码销售进入
                 if (liveUserFirstEntry.getCompanyUserId() == -1L && companyUserId != -1L) {
                     liveUserFirstEntry.setCompanyId(companyId);
                     liveUserFirstEntry.setCompanyUserId(companyUserId);
+                    if (qwUserId != -1) {
+                        liveUserFirstEntry.setQwUserId(qwUserId);
+                    }
+                    if (externalContactId!= -1) {
+                        liveUserFirstEntry.setExternalContactId(externalContactId);
+                    }
                     liveUserFirstEntryService.updateLiveUserFirstEntry(liveUserFirstEntry);
                 }
             } else {
@@ -167,21 +230,43 @@ public class WebSocketServer {
                 liveUserFirstEntry.setEntryDate(date);
                 liveUserFirstEntry.setFirstEntryTime(date);
                 liveUserFirstEntry.setUpdateTime( date);
+                if (qwUserId != -1) {
+                    liveUserFirstEntry.setQwUserId(qwUserId);
+                }
+                if (externalContactId!= -1) {
+                    liveUserFirstEntry.setExternalContactId(externalContactId);
+                }
                 liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
             }
+            redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry,1, TimeUnit.HOURS);
 
 
         } else {
             adminRoom.add(session);
+            // 为admin房间创建单线程执行器,保证串行化发送
+            adminExecutors.computeIfAbsent(liveId, k -> Executors.newSingleThreadExecutor());
         }
 
-        log.debug("加入webSocket liveId: {}, userId: {}, 直播间人数: {}, 管理端人数: {}", liveId, userId, room.size(), adminRoom.size());
+        // 初始化Session锁
+        sessionLocks.putIfAbsent(session.getId(), new ReentrantLock());
+        // 初始化心跳时间
+        heartbeatCache.put(session.getId(), System.currentTimeMillis());
+
     }
 
     //关闭连接时调用
     @OnClose
     public void onClose(Session session) {
         Map<String, Object> userProperties = session.getUserProperties();
+        // 获取公司ID和销售ID
+        long companyId = -1L;
+        long companyUserId = -1L;
+        if (!Objects.isNull(userProperties.get("companyId"))) {
+            companyId = (long) userProperties.get("companyId");
+        }
+        if (!Objects.isNull(userProperties.get("companyUserId"))) {
+            companyUserId = (long) userProperties.get("companyUserId");
+        }
 
         long liveId = (long) userProperties.get("liveId");
         long userId = (long) userProperties.get("userId");
@@ -190,14 +275,12 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
-
-            LiveWatchUser liveWatchUserVO = liveWatchUserService.close(liveId, userId);
+            // 计算并更新用户在线时长
             room.remove(userId);
-
             if (room.isEmpty()) {
                 rooms.remove(liveId);
             }
@@ -209,23 +292,38 @@ public class WebSocketServer {
             String onlineUsersSetKey = ONLINE_USERS_SET_KEY + liveId;
             redisCache.redisTemplate.opsForSet().remove(onlineUsersSetKey, String.valueOf(userId));
 
-            SendMsgVo sendMsgVo = new SendMsgVo();
-            sendMsgVo.setLiveId(liveId);
-            sendMsgVo.setUserId(userId);
-            sendMsgVo.setUserType(userType);
-            sendMsgVo.setCmd("out");
-            sendMsgVo.setMsg("用户离开");
-            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
-            sendMsgVo.setNickName(fsUser.getNickname());
-            sendMsgVo.setAvatar(fsUser.getAvatar());
-
-            // 广播离开消息
-            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            LiveWatchUser liveWatchUserVO = liveWatchUserService.close(fsUser,liveId, userId);
+
+
+            // 广播离开消息 添加一个概率问题 摇塞子,1-4 当为1的时候广播消息
+            if (1 == new Random().nextInt(10)) {
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setUserType(userType);
+                sendMsgVo.setCmd("out");
+                sendMsgVo.setMsg("用户离开");
+                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setAvatar(fsUser.getAvatar());
+                broadcastWebMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            }
+
         } else {
             adminRoom.remove(session);
+            // 如果admin房间为空,关闭并清理执行器
+            if (adminRoom.isEmpty()) {
+                ExecutorService executor = adminExecutors.remove(liveId);
+                if (executor != null) {
+                    executor.shutdown();
+                }
+                adminRooms.remove(liveId);
+            }
         }
 
-        log.debug("离开webSocket liveId: {}, userId: {}, 直播间人数: {}, 管理端人数: {}", liveId, userId, room.size(), adminRoom.size());
+        // 清理Session相关资源
+        heartbeatCache.remove(session.getId());
+        sessionLocks.remove(session.getId());
     }
 
     //收到客户端信息
@@ -242,6 +340,64 @@ public class WebSocketServer {
         try {
             switch (msg.getCmd()) {
                 case "heartbeat":
+                    // 更新心跳时间
+                    heartbeatCache.put(session.getId(), System.currentTimeMillis());
+
+                    // 心跳时同步更新观看时长到Redis Hash
+                    long watchUserId = (long) userProperties.get("userId");
+
+
+                    
+                    if (msg.getData() != null && !msg.getData().isEmpty()) {
+                        try {
+                            Long currentDuration = Long.parseLong(msg.getData());
+
+                            Live currentLive = liveService.selectLiveByLiveId(liveId);
+                            if (currentLive == null) {
+                                break;
+                            }
+                            
+                            // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
+                            boolean isLiveStarted = false;
+                            if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
+                                // status=2 表示直播中
+                                isLiveStarted = true;
+                            } else if (currentLive.getStartTime() != null) {
+                                // 判断当前时间是否已超过开播时间
+                                LocalDateTime now = java.time.LocalDateTime.now();
+                                isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
+                            }
+                            
+                            if (!isLiveStarted) {
+                                log.debug("[心跳-观看时长] 直播未开始(开播倒计时中),不统计观看时长, liveId={}, status={}, startTime={}", 
+                                        liveId, currentLive.getStatus(), currentLive.getStartTime());
+                                break;
+                            }
+                            
+                            log.debug("[心跳-观看时长] 直播已开始,统计观看时长, liveId={}, userId={}, duration={}秒", 
+                                    liveId, watchUserId, currentDuration);
+                            
+                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
+                            String hashKey = "live:watch:duration:hash:" + liveId;
+                            String userIdField = String.valueOf(watchUserId);
+                            // 获取现有时长
+                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
+                            // 只有当新的时长更大时才更新
+                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
+                                // 更新Hash中的用户时长
+                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
+                                // 设置过期时间(2小时)
+                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
+
+                                checkAndSendCompletionPointsInRealTime(liveId, watchUserId, currentDuration);
+
+                            }
+                        } catch (Exception e) {
+                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}", 
+                                    liveId, watchUserId, msg.getData(), e);
+                        }
+                    }
+                    
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -256,12 +412,18 @@ public class WebSocketServer {
                     liveMsg.setCreateTime(new Date());
 
                     if (userType == 0) {
-                        LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
-                        if(liveWatchUser.getMsgStatus() == 1){
+                        List<LiveWatchUser> liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
+                        if(!liveWatchUser.isEmpty() && liveWatchUser.get(0).getMsgStatus() == 1){
                             sendMessage(session, JSONObject.toJSONString(R.error("你已被禁言")));
                             return;
                         }
 
+                        Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+                        Integer liveFlag = flagMap.get("liveFlag");
+                        Integer replayFlag = flagMap.get("replayFlag");
+                        liveMsg.setLiveFlag(liveFlag);
+                        liveMsg.setReplayFlag(replayFlag);
+
                         liveMsgService.insertLiveMsg(liveMsg);
                     }
 
@@ -281,6 +443,23 @@ public class WebSocketServer {
                     liveMsg.setAvatar(msg.getAvatar());
                     liveMsg.setMsg(msg.getMsg());
                     liveMsg.setCreateTime(new Date());
+
+                    // 根据直播状态设置live_flag或replay_flag
+                    Live normalMsgLive = liveService.selectLiveByLiveId(msg.getLiveId());
+                    if (normalMsgLive != null && normalMsgLive.getFinishTime() != null) {
+                        Date finishTime = java.sql.Timestamp.valueOf(normalMsgLive.getFinishTime());
+                        if (new Date().after(finishTime)) {
+                            liveMsg.setReplayFlag(1);
+                            liveMsg.setLiveFlag(0);
+                        } else {
+                            liveMsg.setLiveFlag(1);
+                            liveMsg.setReplayFlag(0);
+                        }
+                    } else {
+                        liveMsg.setLiveFlag(1);
+                        liveMsg.setReplayFlag(0);
+                    }
+
                     liveMsgService.insertLiveMsg(liveMsg);
                     msg.setOn(true);
                     msg.setData(JSONObject.toJSONString(liveMsg));
@@ -302,6 +481,25 @@ public class WebSocketServer {
                     // 广播消息
                     broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
+                case "sendTopMsg":
+                    msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
+                    if(StringUtils.isEmpty(msg.getMsg())) return;
+                    liveMsg = new LiveMsg();
+                    liveMsg.setLiveId(msg.getLiveId());
+                    liveMsg.setUserId(msg.getUserId());
+                    liveMsg.setNickName(msg.getNickName());
+                    liveMsg.setAvatar(msg.getAvatar());
+                    liveMsg.setMsg(msg.getMsg());
+                    liveMsg.setEndTime(DateUtils.addMinutes(new Date(),msg.getDuration()).toString());
+                    msg.setOn(true);
+                    msg.setData(JSONObject.toJSONString(liveMsg));
+                    // 广播消息
+                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    // 放在当前活动里面
+                    redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, TOP_MSG));
+                    redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, TOP_MSG), JSONObject.toJSONString(liveMsg));
+                    redisCache.expire(String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG, liveId, TOP_MSG), msg.getDuration(), TimeUnit.MINUTES);
+                    break;
                 case "globalVisible":
                     msg.setOn(true);
                     liveWatchUserService.updateGlobalVisible(liveId, msg.getStatus());
@@ -322,6 +520,9 @@ public class WebSocketServer {
                 case "goods":
                     sendGoodsMessage(msg);
                     break;
+                case "deleteMsg":
+                    deleteMsg(liveId,msg);
+                    break;
                 case "red":
                     processRed(liveId, msg);
                     break;
@@ -342,6 +543,15 @@ public class WebSocketServer {
         }
     }
 
+    private void deleteMsg(long liveId,SendMsgVo msg) {
+        SendMsgVo sendMsgVo = new SendMsgVo();
+        sendMsgVo.setLiveId(liveId);
+        sendMsgVo.setUserType(0L);
+        sendMsgVo.setCmd("deleteMsg");
+        sendMsgVo.setMsg(msg.getMsg());
+        broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+    }
+
     private void processCoupon(long liveId, SendMsgVo msg) {
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Integer status = jsonObject.getInteger("status");
@@ -381,7 +591,6 @@ public class WebSocketServer {
      * 处理红包变动消息
      */
     private void processRed(Long liveId, SendMsgVo msg) {
-        log.debug("redData: {}", msg);
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Integer status = jsonObject.getInteger("status");
         msg.setStatus( status);
@@ -397,7 +606,6 @@ public class WebSocketServer {
      * 处理抽奖变动消息
      */
     private void processLottery(Long liveId, SendMsgVo msg) {
-        log.debug("lotteryData: {}", msg);
         JSONObject jsonObject = JSON.parseObject(msg.getData());
         Integer status = jsonObject.getInteger("status");
         msg.setStatus( status);
@@ -412,7 +620,12 @@ public class WebSocketServer {
     //错误时调用
     @OnError
     public void onError(Session session, Throwable throwable) {
-        log.error("webSocKet连接错误 msg: {}", throwable.getMessage(), throwable);
+
+        try {
+            this.onClose(session);
+        } catch (Exception e) {
+            log.error("webSocket 错误处理失败", e);
+        }
     }
 
     /**
@@ -433,12 +646,36 @@ public class WebSocketServer {
         return adminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
     }
 
-    //发送消息
+    //发送消息(带锁机制,避免并发发送)
     public void sendMessage(Session session, String message) throws IOException {
-        session.getAsyncRemote().sendText(message);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+
+        // 获取Session锁
+        Lock lock = sessionLocks.get(session.getId());
+        if (lock == null) {
+            // 如果锁不存在,创建一个新锁
+            lock = sessionLocks.computeIfAbsent(session.getId(), k -> new ReentrantLock());
+        }
+
+        // 使用锁保证同一Session的消息串行发送
+        lock.lock();
+        try {
+            if (session.isOpen()) {
+                session.getAsyncRemote().sendText(message);
+            }
+        } finally {
+            lock.unlock();
+        }
     }
 
     public void sendIntegralMessage(Long liveId, Long userId,Long scoreAmount) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
         SendMsgVo sendMsgVo = new SendMsgVo();
         sendMsgVo.setLiveId(liveId);
         sendMsgVo.setUserId(userId);
@@ -446,13 +683,36 @@ public class WebSocketServer {
         sendMsgVo.setCmd("Integral");
         sendMsgVo.setMsg("恭喜你成功获得观看奖励:" + scoreAmount + "芳华币");
         sendMsgVo.setData(String.valueOf(scoreAmount));
+
+        if(Objects.isNull( session)) return;
+        // 使用带锁的sendMessage方法,保证线程安全
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } catch (IOException e) {
+            log.error("发送积分消息失败: liveId={}, userId={}, error={}", liveId, userId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 发送完课积分弹窗通知给特定用户
+     */
+    public void sendCompletionPointsMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         Session session = room.get(userId);
-        if(Objects.isNull( session)) return;
+        if (session == null || !session.isOpen()) {
+            return;
+        }
         session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
     }
 
     private void sendBlockMessage(Long liveId, Long userId) {
+
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+
         SendMsgVo sendMsgVo = new SendMsgVo();
         sendMsgVo.setLiveId(liveId);
         sendMsgVo.setUserId(userId);
@@ -460,31 +720,79 @@ public class WebSocketServer {
         sendMsgVo.setCmd("blockUser");
         sendMsgVo.setMsg("账号已被停用");
         sendMsgVo.setData(null);
-        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
-        Session session = room.get(userId);
+
         if(Objects.isNull( session)) return;
-        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        // 使用带锁的sendMessage方法,保证线程安全
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } catch (IOException e) {
+            log.error("发送封禁消息失败: liveId={}, userId={}, error={}", liveId, userId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 广播消息
+     * @param liveId   直播间ID
+     * @param message  消息内容
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
+     */
+    public void broadcastWebMessage(Long liveId, String message) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        
+        if (room.isEmpty()) {
+            return;
+        }
+
+        // 普通用户房间:并行发送(使用快照遍历,避免并发修改)
+        // ConcurrentHashMap 的 entrySet() 是弱一致性的,但为了更安全,我们显式创建快照
+        for (Map.Entry<Long, Session> entry : room.entrySet()) {
+            Session session = entry.getValue();
+            if (session != null && session.isOpen()) {
+                sendWithRetry(session, message, 1);
+            }
+        }
     }
 
     /**
      * 广播消息
      * @param liveId   直播间ID
      * @param message  消息内容
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
      */
     public void broadcastMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
 
-        room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,7);
+        // 普通用户房间:并行发送(使用快照遍历,避免并发修改)
+        if (!room.isEmpty()) {
+            for (Map.Entry<Long, Session> entry : room.entrySet()) {
+                Session session = entry.getValue();
+                if (session != null && session.isOpen()) {
+                    sendWithRetry(session, message, 1);
+                }
             }
-        });
-        adminRoom.forEach(v -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,7);
+        }
+
+        // admin房间:串行发送,使用单线程执行器
+        if (!adminRoom.isEmpty()) {
+            ExecutorService executor = adminExecutors.get(liveId);
+            if (executor != null && !executor.isShutdown()) {
+                executor.submit(() -> {
+                    for (Session session : adminRoom) {
+                        if (session.isOpen()) {
+                            sendWithRetry(session, message, 1);
+                        }
+                    }
+                });
+            } else {
+                // 如果执行器不存在或已关闭,直接发送
+                adminRoom.forEach(v -> {
+                    if (v.isOpen()) {
+                        sendWithRetry(v, message, 1);
+                    }
+                });
             }
-        });
+        }
     }
 
     public void removeLikeCountCache(Long liveId) {
@@ -504,7 +812,6 @@ public class WebSocketServer {
                 String valueStr = cacheObject.toString().trim();
                 current = Integer.parseInt(valueStr);
             } catch (NumberFormatException e) {
-                log.error("点赞数格式错误,liveId: {}, value: {}", liveId, cacheObject, e);
                 continue;
             }
             Integer last = lastLikeCountCache.getOrDefault(liveId, 0);
@@ -520,27 +827,154 @@ public class WebSocketServer {
         lastLikeCountCache.keySet().removeIf(liveId -> !activeLiveIds.contains(liveId));
     }
 
+
+    @Scheduled(fixedRate = 2000)// 每2秒执行一次
+    public void broadcastUserNumMessage() {
+        // 遍历每个直播间
+        for (Map.Entry<Long, ConcurrentHashMap<Long, Session>> entry : rooms.entrySet()) {
+            Long liveId = entry.getKey();
+            ConcurrentHashMap<Long, Session> room = entry.getValue();
+
+            // 统计当前直播间的在线人数
+            int onlineCount = room.size();
+
+            // 构造消息
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setCmd("userCount");
+            sendMsgVo.setData(String.valueOf(onlineCount));
+
+            // 广播当前直播间的在线人数
+            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        }
+    }
+
+
+    /**
+     * 定时清理无效会话(每分钟执行一次)
+     * 检查心跳超时的会话并关闭
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
+     */
+    @Scheduled(fixedRate = 60000) // 每分钟执行一次
+    public void cleanInactiveSessions() {
+        long currentTime = System.currentTimeMillis();
+        int cleanedCount = 0;
+
+        // 遍历所有直播间(使用快照,避免在遍历过程中被修改影响)
+        for (Map.Entry<Long, ConcurrentHashMap<Long, Session>> roomEntry : rooms.entrySet()) {
+            Long liveId = roomEntry.getKey();
+            ConcurrentHashMap<Long, Session> room = roomEntry.getValue();
+            
+            // 如果房间为空,跳过
+            if (room.isEmpty()) {
+                continue;
+            }
+
+            // 检查普通用户会话(使用快照遍历,避免并发修改异常)
+            List<Long> toRemove = new ArrayList<>();
+            // 创建快照,避免在遍历过程中修改原集合
+            for (Map.Entry<Long, Session> userEntry : room.entrySet()) {
+                Long userId = userEntry.getKey();
+                Session session = userEntry.getValue();
+                
+                if (session == null) {
+                    toRemove.add(userId);
+                    continue;
+                }
+                
+                Long lastHeartbeat = heartbeatCache.get(session.getId());
+                if (lastHeartbeat != null && (currentTime - lastHeartbeat) > HEARTBEAT_TIMEOUT) {
+                    toRemove.add(userId);
+                    try {
+                        if (session.isOpen()) {
+                            session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
+                        }
+                        liveWatchUserService.close(null, liveId, userId);
+                    } catch (Exception e) {
+                        log.error("关闭超时会话失败: sessionId={}, liveId={}, userId={}",
+                                session.getId(), liveId, userId, e);
+                    }
+                }
+            }
+
+            // 移除超时的会话
+            if (!toRemove.isEmpty()) {
+                String hashKey = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);
+                for (Long userId : toRemove) {
+                    room.remove(userId);
+                    // 从 Redis hash 中删除无效用户
+                    redisCache.hashDelete(hashKey, String.valueOf(userId));
+                }
+                cleanedCount += toRemove.size();
+            }
+        }
+
+        // 检查admin房间
+        for (Map.Entry<Long, CopyOnWriteArrayList<Session>> adminEntry : adminRooms.entrySet()) {
+            Long liveId = adminEntry.getKey();
+            CopyOnWriteArrayList<Session> adminRoom = adminEntry.getValue();
+
+            List<Session> toRemoveAdmin = new ArrayList<>();
+            for (Session session : adminRoom) {
+                Long lastHeartbeat = heartbeatCache.get(session.getId());
+                if (lastHeartbeat != null && (currentTime - lastHeartbeat) > HEARTBEAT_TIMEOUT) {
+                    toRemoveAdmin.add(session);
+                    try {
+                        if (session.isOpen()) {
+                            session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "心跳超时"));
+                        }
+                    } catch (Exception e) {
+                        log.error("关闭admin超时会话失败: sessionId={}, liveId={}",
+                                session.getId(), liveId, e);
+                    }
+                }
+            }
+
+            // 移除超时的admin会话
+            toRemoveAdmin.forEach(adminRoom::remove);
+            cleanedCount += toRemoveAdmin.size();
+        }
+
+        if (cleanedCount > 0) {
+            if (random.nextInt(10) == 1) {
+                log.info("已清理 {} 个无效会话", cleanedCount);
+            }
+        }
+    }
+
+
     /**
      * 广播点赞消息
      * @param liveId   直播间ID
      * @param message  消息内容
+     * 优化:使用快照遍历,避免在遍历过程中修改集合
      */
     public void broadcastLikeMessage(Long liveId, String message) {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
-        room.forEach((k, v) -> {
-            if (v.isOpen()) {
-                sendWithRetry(v,message,7);
+        
+        if (room.isEmpty()) {
+            return;
+        }
+        
+        // 使用快照遍历,避免并发修改
+        for (Map.Entry<Long, Session> entry : room.entrySet()) {
+            Session session = entry.getValue();
+            if (session != null && session.isOpen()) {
+                sendWithRetry(session, message, 1);
             }
-        });
+        }
     }
 
     private void sendWithRetry(Session session, String message, int maxRetries) {
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+
         int attempts = 0;
         while (attempts < maxRetries) {
             try {
-                if(session.isOpen()) {
-                    session.getAsyncRemote().sendText(message);
-                }
+                // 使用带锁的sendMessage方法,避免并发发送
+                sendMessage(session, message);
                 return;  // 发送成功,退出
             } catch (Exception e) {
                 if (e.getMessage() != null && e.getMessage().contains("TEXT_FULL_WRITING")) {
@@ -552,11 +986,15 @@ public class WebSocketServer {
                         break;
                     }
                 } else {
-                    throw e;
+                    log.error("发送消息失败: sessionId={}, error={}", session.getId(), e.getMessage(), e);
+                    break;
                 }
             }
         }
-        log.info("超过重试次数, 消息 {}",message);
+
+        if (attempts >= maxRetries) {
+            log.warn("超过重试次数({}),放弃发送消息: sessionId={}", maxRetries, session.getId());
+        }
     }
 
 
@@ -618,6 +1056,9 @@ 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());
@@ -629,6 +1070,27 @@ public class WebSocketServer {
                 data.put("couponTime", liveCoupon.getCouponTime());
                 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());
+                Long goodsId = jsonObject.getLong("goodsId");
+                Integer status = jsonObject.getInteger("status");
+                if (goodsId == null || status == null) {
+                    log.error("商品ID或状态为空");
+                    return;
+                }
+                // 更新商品上下架状态
+                liveGoodsService.updateLiveGoodsStatus(goodsId, status);
+                return ;
+                // 更新直播间配置缓存
+//                liveService.asyncToCacheLiveConfig(task.getLiveId());
+                // 查询商品信息并广播
+//                LiveGoodsVo liveGoodsVo = liveGoodsService.selectLiveGoodsVoByGoodsId(goodsId);
+//                if (liveGoodsVo != null) {
+//                    msg.setData(JSON.toJSONString(liveGoodsVo));
+//                    msg.setStatus(status);
+//                }
             }
             msg.setStatus(1);
             broadcastMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)));
@@ -641,4 +1103,290 @@ public class WebSocketServer {
         String key = "live:auto_task:";
         redisCache.redisTemplate.opsForZSet().removeRangeByScore(key + liveId, data, data);
     }
+
+    /**
+     * 计算并更新用户在线时长
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param companyId 公司ID
+     * @param companyUserId 销售ID
+     */
+    private void updateUserOnlineDuration(Long liveId, Long userId, Long companyId, Long companyUserId) {
+        try {
+            // 从 Redis 获取用户进入时间
+            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+            Long entryTime = redisCache.getCacheObject(entryTimeKey);
+            
+            if (entryTime == null) {
+                // 如果没有进入时间记录,可能是旧数据,跳过
+                return;
+            }
+            
+            long currentTimeMillis = System.currentTimeMillis();
+            Date now = new Date();
+            
+            // 计算在线时长(秒)
+            long durationSeconds = (currentTimeMillis - entryTime) / 1000;
+            
+            if (durationSeconds <= 0) {
+                return;
+            }
+            
+            // 获取当前直播/回放状态
+            Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+            Integer currentLiveFlag = flagMap.get("liveFlag");
+            Integer currentReplayFlag = flagMap.get("replayFlag");
+            
+            // 查询用户记录
+            LiveWatchUserEntry liveWatchUser = liveWatchUserService.selectLiveWatchAndCompanyUserByFlag(
+                    liveId, userId, currentLiveFlag, currentReplayFlag);
+            
+            if (liveWatchUser != null) {
+                // 累加在线时长
+                Long onlineSeconds = liveWatchUser.getOnlineSeconds();
+                if (onlineSeconds == null) {
+                    onlineSeconds = 0L;
+                }
+                liveWatchUser.setOnlineSeconds(onlineSeconds + durationSeconds);
+                liveWatchUser.setUpdateTime(now);
+                
+                // 更新数据库
+                liveWatchUserService.updateLiveWatchUserEntry(liveWatchUser);
+                // 如果 LiveWatchUserEntry 存在,并且当前是直播状态(liveFlag = 1),更新 LiveWatchLog
+//                if (currentLiveFlag != null && currentLiveFlag == 1
+//                        && liveWatchUser.getCompanyId() != null && liveWatchUser.getCompanyId() > 0
+//                        && liveWatchUser.getCompanyUserId() != null && liveWatchUser.getCompanyUserId() > 0) {
+//                    updateLiveWatchLogTypeByDuration(liveId, userId,
+//                            liveWatchUser.getCompanyId(), liveWatchUser.getCompanyUserId(),
+//                            liveWatchUser.getOnlineSeconds());
+//                }
+            }
+            
+            // 删除 Redis 中的进入时间记录
+            redisCache.deleteObject(entryTimeKey);
+        } catch (Exception e) {
+            log.error("更新用户在线时长异常:liveId={}, userId={}, error={}", 
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 在连接时更新 LiveWatchLog 的 logType
+     * 如果 logType 类型不是 2,修改 logType 类型为 1(看课中)
+     */
+    private void updateLiveWatchLogTypeOnConnect(Long liveId, Long userId, Long qwUserId, Long externalContactId) {
+        try {
+            LiveWatchLog queryLog = new LiveWatchLog();
+            queryLog.setLiveId(liveId);
+            queryLog.setUserId(userId);
+            queryLog.setQwUserId(String.valueOf(qwUserId));
+            queryLog.setExternalContactId(externalContactId);
+            
+            List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+            if (logs != null && !logs.isEmpty()) {
+                for (LiveWatchLog log : logs) {
+                    // 如果 logType 不是 2(完课),则更新为 1(看课中)
+                    if (log.getLogType() == null || log.getLogType() != 2) {
+                        log.setLogType(1);
+                        liveWatchLogService.updateLiveWatchLog(log);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("更新 LiveWatchLog logType 异常(连接时):liveId={}, userId={}, error={}", 
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 实时更新用户看课状态(在心跳时调用)
+     * 在直播期间实时更新用户的看课状态,而不是等到关闭 WebSocket 或清理无效会话时才更新
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param watchDuration 观看时长(秒)
+     */
+    public void updateWatchLogTypeInRealTime(Long liveId, Long userId, Long watchDuration) {
+        try {
+            // 获取当前直播/回放状态
+            Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(liveId);
+            Integer currentLiveFlag = flagMap.get("liveFlag");
+            
+            // 只在直播状态(liveFlag = 1)时更新
+            if (currentLiveFlag == null || currentLiveFlag != 1) {
+                return;
+            }
+            
+            // 获取用户的 companyId 和 companyUserId(使用带缓存的查询方法)
+            LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
+            if (liveUserFirstEntry == null) {
+                return;
+            }
+            
+            Long companyId = liveUserFirstEntry.getCompanyId();
+            Long companyUserId = liveUserFirstEntry.getCompanyUserId();
+            
+            // 如果 companyId 和 companyUserId 有效,则更新看课状态
+            if (companyId != null && companyId > 0 && companyUserId != null && companyUserId > 0) {
+                // 检查是否达到关键观看时长节点,在这些节点实时更新
+                // 关键节点:3分钟(180秒)、20分钟(1200秒)、30分钟(1800秒)
+                boolean isKeyDuration = (watchDuration == 180 || watchDuration == 1200 || watchDuration == 1800) ||
+                                       (watchDuration > 180 && watchDuration % 60 == 0); // 每分钟更新一次
+                
+                // 使用 Redis 缓存控制更新频率,避免频繁更新数据库
+                // 策略:在关键节点立即更新,其他时候每60秒更新一次
+                String updateLockKey = "live:watch:log:update:lock:" + liveId + ":" + userId;
+                String lastUpdateKey = "live:watch:log:last:duration:" + liveId + ":" + userId;
+                
+                // 获取上次更新的时长
+                Long lastUpdateDuration = redisCache.getCacheObject(lastUpdateKey);
+                
+                // 如果达到关键节点,或者距离上次更新已超过60秒,则更新
+                boolean shouldUpdate = false;
+                if (isKeyDuration) {
+                    // 关键节点立即更新
+                    shouldUpdate = true;
+                } else if (lastUpdateDuration == null || (watchDuration - lastUpdateDuration) >= 60) {
+                    // 每60秒更新一次
+                    shouldUpdate = true;
+                }
+                
+                if (shouldUpdate) {
+                    // 使用分布式锁,避免并发更新(锁超时时间10秒)
+                    Boolean canUpdate = redisCache.setIfAbsent(updateLockKey, "1", 10, TimeUnit.SECONDS);
+                    
+                    if (Boolean.TRUE.equals(canUpdate)) {
+                        // 异步更新,避免阻塞心跳处理
+                        CompletableFuture.runAsync(() -> {
+                            try {
+                                updateLiveWatchLogTypeByDuration(liveId, userId, companyId, companyUserId, watchDuration);
+                                // 更新上次更新的时长
+                                redisCache.setCacheObject(lastUpdateKey, watchDuration, 2, TimeUnit.HOURS);
+                            } catch (Exception e) {
+                                log.error("实时更新看课状态异常:liveId={}, userId={}, error={}", 
+                                        liveId, userId, e.getMessage(), e);
+                            } finally {
+                                // 释放锁
+                                redisCache.deleteObject(updateLockKey);
+                            }
+                        });
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("实时更新看课状态异常:liveId={}, userId={}, error={}", 
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 根据在线时长更新 LiveWatchLog 的 logType
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param companyId 公司ID
+     * @param companyUserId 销售ID
+     * @param onlineSeconds 在线时长(秒)
+     */
+    private void updateLiveWatchLogTypeByDuration(Long liveId, Long userId, Long companyId, 
+                                                   Long companyUserId, Long onlineSeconds) {
+        try {
+            // 获取直播视频总时长(videoType = 1 的视频,使用带缓存的查询方法)
+            List<LiveVideo> videos = liveVideoService.listByLiveIdWithCache(liveId, 1);
+            long totalVideoDuration = 0L;
+            if (videos != null && !videos.isEmpty()) {
+                totalVideoDuration = videos.stream()
+                        .filter(v -> v.getDuration() != null)
+                        .mapToLong(LiveVideo::getDuration)
+                        .sum();
+            }
+            
+            // 查询 LiveWatchLog
+            LiveWatchLog queryLog = new LiveWatchLog();
+            queryLog.setLiveId(liveId);
+            queryLog.setUserId(userId);
+            queryLog.setCompanyId(companyId);
+            queryLog.setCompanyUserId(companyUserId);
+            
+            List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+            if (logs == null || logs.isEmpty()) {
+                return;
+            }
+            Date now = DateUtil.getDate();
+            for (LiveWatchLog log : logs) {
+                boolean needUpdate = false;
+                Integer newLogType = log.getLogType();
+                
+                // ① 如果在线时长 <= 3分钟,修改 logType 为 4(看课中断)
+                if (onlineSeconds <= 180) { // 3分钟 = 180秒
+                    newLogType = 4;
+                    needUpdate = true;
+                }
+                // ③ 如果直播视频 >= 40分钟,在线时长 >= 30分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 2400 && onlineSeconds >= 1800) { // 40分钟 = 2400秒,30分钟 = 1800秒
+                    newLogType = 2;
+                    log.setFinishTime(now);
+                    needUpdate = true;
+                }
+                // 如果直播视频 >= 20分钟且 < 40分钟,在线时长 >= 20分钟,logType 设置为 2(完课)
+                else if (totalVideoDuration >= 1200 && totalVideoDuration < 2400 && onlineSeconds >= 1200) { // 20分钟 = 1200秒
+                    newLogType = 2;
+                    log.setFinishTime(now);
+                    needUpdate = true;
+                }
+                
+                // 如果 logType 已经是 2(完课),不再更新
+                if (needUpdate && (log.getLogType() == null || log.getLogType() != 2)) {
+                    log.setLogType(newLogType);
+                    liveWatchLogService.updateLiveWatchLog(log);
+                }
+            }
+        } catch (Exception e) {
+            log.error("根据在线时长更新 LiveWatchLog logType 异常:liveId={}, userId={}, error={}", 
+                    liveId, userId, e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 实时检查并推送完课积分
+     * 在用户观看时长更新时立即检查是否达到完课条件,达到则立即推送
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @param duration 当前观看时长(秒)
+     */
+    private void checkAndSendCompletionPointsInRealTime(long liveId, long userId, Long duration) {
+        try {
+            log.debug("[实时完课检查] liveId={}, userId={}, duration={}秒", liveId, userId, duration);
+
+            // 1. 调用完课记录服务检查并创建完课记录
+            completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, duration);
+
+            // 2. 查询是否有新的未领取完课记录
+            List<LiveCompletionPointsRecord> unreceivedRecords =
+                completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+
+            if (unreceivedRecords == null || unreceivedRecords.isEmpty()) {
+                // 没有待领取的完课记录
+                return;
+            }
+
+            // 3. 构建推送消息
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setUserId(userId);
+            sendMsgVo.setCmd("completionPoints");
+            sendMsgVo.setMsg("完成任务!");
+            sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
+
+            // 4. 实时推送完课积分弹窗
+            sendCompletionPointsMessage(liveId, userId, sendMsgVo);
+
+            log.info("[实时完课推送] 发送完课积分弹窗通知, liveId={}, userId={}, points={}, duration={}秒",
+                    liveId, userId, unreceivedRecords.get(0).getPointsAwarded(), duration);
+
+        } catch (Exception e) {
+            log.error("[实时完课推送] 实时检查完课积分失败, liveId={}, userId={}, duration={}",
+                    liveId, userId, duration, e);
+        }
+    }
+
 }
+

+ 6 - 5
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -1017,14 +1017,14 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                                 GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
                                 if (vo != null && vo.getId() != null) {
                                     sopLogs.setFsUserId(vo.getFsUserId());
-                                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, vo.getId().toString(), logVo);
+                                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, vo.getId().toString(), logVo,2);
                                 }
                             });
                         } catch (Exception e) {
                             log.error("群聊创建看课记录失败!", e);
                         }
                     } else {
-                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo,2);
                     }
 
                     String sortLink = createLinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
@@ -1061,7 +1061,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     break;
                 //app
                 case "9":
-                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo,1);
 
                     QwCreateLinkByAppVO linkByApp = createLinkByApp(setting, logVo, sendTime, courseId, videoId,
                             qwUserId, companyUserId, companyId, externalId,sopLogs.getCorpId(),qwUserName);
@@ -1073,7 +1073,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     break;
                 //自定义小程序
                 case "10":
-                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo,2);
 
                     Optional<Company> matchedCompany = companies.stream()
                             .filter(company -> String.valueOf(company.getCompanyId()).equals(companyId))
@@ -1453,7 +1453,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
     private void addWatchLogIfNeeded(QwSopLogs sopLogs, Long videoId, Long courseId,
                                      Date sendTime, String qwUserId, String companyUserId,
-                                     String companyId, String externalId,SopUserLogsVo logsVo) {
+                                     String companyId, String externalId,SopUserLogsVo logsVo,Integer watchType) {
         FsCourseWatchLog watchLog = new FsCourseWatchLog();
         watchLog.setVideoId(videoId != null ? videoId.longValue() : null);
         watchLog.setQwExternalContactId(externalId != null ? Long.valueOf(externalId) : null);
@@ -1468,6 +1468,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         watchLog.setUpdateTime(new Date());
         watchLog.setLogType(3);
         watchLog.setUserId(sopLogs.getFsUserId());
+        watchLog.setWatchType(watchType);
         watchLog.setCampPeriodTime(convertStringToDate(logsVo.getStartTime(),"yyyy-MM-dd"));
         enqueueWatchLog(watchLog);
     }

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -259,4 +259,9 @@ public interface CompanyMapper
 
     @Select("select company_id from company where live_show=1")
     List<Long> selectLiveShowCompanyId();
+
+    @Select("select company_id,company_name from company where \n" +
+            " `status` != 0   " +
+            " and is_del != 1 ")
+    List<CompanyVO> getCompanyDropList();
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseTrafficLog.java

@@ -71,6 +71,8 @@ public class FsCourseTrafficLog extends BaseEntity
      */
     private String appId;
 
+    private Integer typeFlag;
+
 //    @JsonFormat(pattern = "yyyy-MM-dd")
 //    private Date time;
 

+ 3 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseWatchLog.java

@@ -91,4 +91,7 @@ public class FsCourseWatchLog extends BaseEntity
     /** im发送消息详情id */
     private Long imMsgSendDetailId;
 
+    //看课方式:1 app  2 小程序
+    private Integer watchType;
+
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/param/CourseAnalysisParam.java

@@ -42,5 +42,10 @@ public class CourseAnalysisParam implements Serializable {
      */
     private  List<Long> companyUserIds;
 
+    /**
+     * 类型标识 1 app  2 小程序'
+     */
+    private  Integer typeFlag;
+
 }
 

+ 5 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseTrafficLogParam.java

@@ -45,4 +45,9 @@ public class FsCourseTrafficLogParam {
      */
     private Long companyUserId;
 
+    /**
+     * 类型标识 0 小程序 1 app
+     */
+    private String typeFlag;
+
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java

@@ -105,4 +105,6 @@ public class FsCourseWatchLogListParam implements Serializable {
     private Long deptId;
     private List<Long> deptIds;
     private String ids;
+
+    private Integer watchType;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogStatisticsListParam.java

@@ -96,6 +96,11 @@ public class FsCourseWatchLogStatisticsListParam extends BaseEntity {
 
     private  List<Long> deptIds;
 
+    /**
+     * 看课方式:1 app  2 小程序
+     */
+    private  Integer watchType;
+
 
 
 }

+ 1 - 0
fs-service/src/main/java/com/fs/course/param/FsUserCourseVideoAddKfUParam.java

@@ -69,5 +69,6 @@ public class FsUserCourseVideoAddKfUParam implements Serializable {
     private String nickName;
 
     private Integer isOpenCourse;
+    private Integer typeFlag; //0 小程序 1 app
 
 }

+ 1 - 0
fs-service/src/main/java/com/fs/course/param/FsUserCourseVideoFinishUParam.java

@@ -25,4 +25,5 @@ public class FsUserCourseVideoFinishUParam implements Serializable {
     private Long periodId;
     private Integer projectId;
     private String appId; // 小程序AppId
+    private Integer typeFlag; //0 小程序 1 app
 }

+ 5 - 0
fs-service/src/main/java/com/fs/course/param/PeriodCountParam.java

@@ -38,5 +38,10 @@ public class PeriodCountParam implements Serializable {
      */
     private  List<Long> companyUserIds;
 
+    /**
+     * 类型标识 1 app  2 小程序'
+     */
+    private  Integer typeFlag;
+
 }
 

+ 6 - 0
fs-service/src/main/java/com/fs/course/param/newfs/FsUserCourseAddCompanyUserParam.java

@@ -1,6 +1,7 @@
 package com.fs.course.param.newfs;
 
 import io.swagger.annotations.ApiModelProperty;
+import io.swagger.models.auth.In;
 import lombok.Data;
 
 import javax.validation.constraints.NotNull;
@@ -42,4 +43,9 @@ public class FsUserCourseAddCompanyUserParam implements Serializable {
      * 营期课程id
      */
     private Long id;
+
+    /**
+     * 来源标识 0小程序 1 app
+     */
+    private Integer typeFlag=0;
 }

+ 1 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java

@@ -270,6 +270,7 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
         courseAnalysisParam.setVideoIdList(param.getVideoIdList());
         courseAnalysisParam.setCompanyId(param.getCompanyId());
         courseAnalysisParam.setCompanyUserIds(param.getCompanyUserIds());
+        courseAnalysisParam.setTypeFlag(param.getTypeFlag());
         //查询营期维度的看课人数和完课人数
         FsPeriodCountVO fsPeriodCountVO = fsUserMapper.selectFsPeriodCountVO(courseAnalysisParam);
         List<FsCourseAnalysisCountVO> courseCountList = fsUserMapper.courseAnalysisWatchLog(courseAnalysisParam);

+ 57 - 20
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -671,7 +671,10 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             if (log.getUserId() == null || log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())) {
                 log.setUserId(param.getUserId());
             }
-
+            //区分课程来源 0 小程序 1 app
+//            if (param.getTypeFlag() != null) {
+//                log.setTypeFlag(param.getTypeFlag());
+//            }
             log.setUpdateTime(new Date());
             //重粉逻辑
             //
@@ -694,6 +697,10 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             //绑定上之后 更新观看记录
             //看课记录中userId为0绑定userId
             log.setUserId(param.getUserId());
+            //区分课程来源
+//            if (param.getTypeFlag() != null) {
+//                log.setTypeFlag(param.getTypeFlag());
+//            }
             log.setUpdateTime(new Date());
             courseWatchLogMapper.updateFsCourseWatchLog(log);
         }
@@ -783,6 +790,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
         log.setQwUserId(Long.valueOf(param.getQwUserId()));
         log.setCreateTime(new Date());
         log.setLogType(3);
+        log.setWatchType(2);
         logger.info("【群聊生成看课记录】:{}", param);
         courseWatchLogMapper.insertFsCourseWatchLog(log);
     }
@@ -863,6 +871,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             if (log.getUserId() == null || log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())) {
                 log.setUserId(param.getUserId());
             }
+//            if (param.getTypeFlag() != null) {
+//                log.setTypeFlag(param.getTypeFlag());
+//            }
             log.setUpdateTime(new Date());
             courseWatchLogMapper.updateFsCourseWatchLog(log);
 
@@ -903,7 +914,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             if (log.getUserId() == null || log.getUserId().equals(0L) || !log.getUserId().equals(param.getUserId())) {
                 log.setUserId(param.getUserId());
             }
-
+//            if (param.getTypeFlag() != null) {
+//                log.setTypeFlag(param.getTypeFlag());
+//            }
             log.setUpdateTime(new Date());
             courseWatchLogMapper.updateFsCourseWatchLog(log);
 
@@ -1005,6 +1018,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
 
             // 处理 UUID 为空的情况
             if (StringUtils.isNotEmpty(trafficLog.getUuId())) {
+                if(param.getTypeFlag()!=null){
+                    trafficLog.setTypeFlag(param.getTypeFlag());
+                }
                 // 插入或更新
                 fsCourseTrafficLogMapper.insertOrUpdateTrafficLog(trafficLog);
                 asyncDeductTraffic(company, trafficLog);
@@ -1970,6 +1986,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             if (watchCourseVideo != null) {
                 FsCourseWatchLog updateLog = new FsCourseWatchLog();
                 updateLog.setUpdateTime(new Date());
+//                if (param.getTypeFlag() != null) {
+//                    updateLog.setTypeFlag(param.getTypeFlag());
+//                }
                 courseWatchLogMapper.updateFsCourseWatchLog(updateLog);
             } else {
                 FsCourseWatchLog fsCourseWatchLog = new FsCourseWatchLog();
@@ -1978,6 +1997,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
                 fsCourseWatchLog.setDuration(0L);
                 fsCourseWatchLog.setCreateTime(new Date());
                 fsCourseWatchLog.setLogType(1);
+//                if (param.getTypeFlag() != null) {
+//                    fsCourseWatchLog.setTypeFlag(param.getTypeFlag());
+//                }
                 courseWatchLogMapper.insertFsCourseWatchLog(fsCourseWatchLog);
                 String redisKey = "h5wxuser:watch:heartbeat:" + param.getUserId() + ":" + param.getVideoId() + ":" + 0;
                 redisCache.setCacheObject(redisKey, LocalDateTime.now().toString());
@@ -1991,20 +2013,23 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
         if (companyUser == null) {
             return ResponseResult.fail(405, "当前销售不存在");
         }
-        //营期公司的开关状态
-        List<Integer> selected = periodCompanyMapper.selectRegistrationSwitchByPeriodId(param.getPeriodId());
-        //课程注册链接开关
-        FsUserCoursePeriodDays fsUserCoursePeriodDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysById(param.getId());
-        if (CollectionUtils.isNotEmpty(selected) &&fsUserCoursePeriodDays != null) {
-            Integer registrationSwitch = fsUserCoursePeriodDays.getRegistrationSwitch();
-            //注册开关开启  未注册的用户不允许看课
-           if((registrationSwitch!=null&&registrationSwitch==1)&&selected.contains(1)){
-               //查询用户是否注册
-               FsSalesUserPeriodRelation fsSalesUserPeriodRelation = periodRelationMapper.selectBySalesUserPeriodAndDays(param.getCompanyUserId(), param.getUserId(), param.getPeriodId(),param.getId());
-               if (fsSalesUserPeriodRelation == null) {
-                   return ResponseResult.fail(506, "当前营期下的用户未绑定该销售,请注册");
-               }
-           }
+        if(param.getTypeFlag()==0){
+            //小程序看课需要判断是否注册
+            //营期公司的开关状态
+            List<Integer> selected = periodCompanyMapper.selectRegistrationSwitchByPeriodId(param.getPeriodId());
+            //课程注册链接开关
+            FsUserCoursePeriodDays fsUserCoursePeriodDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysById(param.getId());
+            if (CollectionUtils.isNotEmpty(selected) &&fsUserCoursePeriodDays != null) {
+                Integer registrationSwitch = fsUserCoursePeriodDays.getRegistrationSwitch();
+                //注册开关开启  未注册的用户不允许看课
+                if((registrationSwitch!=null&&registrationSwitch==1)&&selected.contains(1)){
+                    //查询用户是否注册
+                    FsSalesUserPeriodRelation fsSalesUserPeriodRelation = periodRelationMapper.selectBySalesUserPeriodAndDays(param.getCompanyUserId(), param.getUserId(), param.getPeriodId(),param.getId());
+                    if (fsSalesUserPeriodRelation == null) {
+                        return ResponseResult.fail(506, "当前营期下的用户未绑定该销售,请注册");
+                    }
+                }
+            }
         }
         // 获取课程所属项目id
         FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
@@ -2077,6 +2102,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             updateLog.setPeriodId(param.getPeriodId());
             updateLog.setProject(courseProject);
             updateLog.setUpdateTime(new Date());
+//            if (param.getTypeFlag() != null) {
+//                updateLog.setTypeFlag(param.getTypeFlag());
+//            }
             courseWatchLogMapper.updateFsCourseWatchLog(updateLog);
         } else {
             FsCourseWatchLog fsCourseWatchLog = new FsCourseWatchLog();
@@ -2086,6 +2114,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             fsCourseWatchLog.setCreateTime(new Date());
             fsCourseWatchLog.setLogType(1);
             fsCourseWatchLog.setProject(courseProject);
+//            if (param.getTypeFlag() != null) {
+//                fsCourseWatchLog.setTypeFlag(param.getTypeFlag());
+//            }
             courseWatchLogMapper.insertFsCourseWatchLog(fsCourseWatchLog);
 
             String redisKey = "h5wxuser:watch:heartbeat:" + param.getUserId() + ":" + param.getVideoId() + ":" + param.getCompanyUserId();
@@ -2480,7 +2511,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
         }
 
         //看课记录
-        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId());
+        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId(),2);
 
         //生成小程序链接
         String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, param.getExternalUserId(), 2, null, 0);
@@ -2523,7 +2554,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             domainName = config.getRealLinkDomainName();
         }
 
-        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId());
+        addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), param.getFsUserId(), qwUser, param.getExternalUserId(),2);
 
         String linkByCartLink = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, param.getExternalUserId(), 1, domainName, 0);
 
@@ -2542,7 +2573,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
 
     //插入观看记录
     private void addWatchLogIfNeeded(Long videoId, Long courseId,
-                                     Long fsUserId, QwUser qwUser, Long externalId) {
+                                     Long fsUserId, QwUser qwUser, Long externalId, Integer watchType) {
 
         try {
 
@@ -2558,7 +2589,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             watchLog.setCreateTime(new Date());
             watchLog.setUpdateTime(new Date());
             watchLog.setLogType(3);
-
+            watchLog.setWatchType(watchType);
             if (fsUserId == null) {
                 fsUserId = 0L;
             }
@@ -3124,6 +3155,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             Company company = companyMapper.selectCompanyById(param.getCompanyId());
             // 处理 UUID 为空的情况
             if (StringUtils.isNotEmpty(trafficLog.getUuId())) {
+                if(param.getTypeFlag()!=null){
+                    trafficLog.setTypeFlag(param.getTypeFlag());
+                }
                 // 插入或更新
                 fsCourseTrafficLogMapper.insertOrUpdateTrafficLog(trafficLog);
                 asyncDeductTraffic(company, trafficLog);
@@ -3148,6 +3182,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService {
             log.setCreateTime(new Date());
             log.setLogType(3);
             logger.info("【群聊生成看课记录】:{}", param);
+//            if(param.getTypeFlag()!=null){
+//                log.setTypeFlag(param.getTypeFlag());
+//            }
             courseWatchLogMapper.insertFsCourseWatchLog(log);
         } catch (BeansException e) {
             return R.error("群聊生成看课记录失败!");

+ 4 - 4
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -1355,7 +1355,7 @@ public class AiHookServiceImpl implements AiHookService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     // sendWebSocketMsg(s,msgVo,user,session);
@@ -1386,7 +1386,7 @@ public class AiHookServiceImpl implements AiHookService {
                                 String.valueOf(user.getId()),
                                 String.valueOf(user.getCompanyUserId()),
                                 String.valueOf(user.getCompanyId()),
-                                String.valueOf(session.getQwExtId()));
+                                String.valueOf(session.getQwExtId()),2);
                         if (linkUrl != null && linkUrl.get("url") != null) {
                             String s = (String)linkUrl.get("url");
                             // sendWebSocketMsg(s,msgVo,user,session);
@@ -1896,7 +1896,7 @@ public class AiHookServiceImpl implements AiHookService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebTaskSocketMsg(s,sendId,user);
@@ -1927,7 +1927,7 @@ public class AiHookServiceImpl implements AiHookService {
                             String.valueOf(user.getId()),
                             String.valueOf(user.getCompanyUserId()),
                             String.valueOf(user.getCompanyId()),
-                            String.valueOf(session.getQwExtId()));
+                            String.valueOf(session.getQwExtId()),2);
                     if (linkUrl != null && linkUrl.get("url") != null) {
                         String s = (String)linkUrl.get("url");
                         sendWebTaskSocketMsg(s,sendId,user);

+ 4 - 4
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiNewServiceImpl.java

@@ -552,7 +552,7 @@ public class AiNewServiceImpl implements AiNewService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebSocketMsg(s,msgVo,user,session);
@@ -583,7 +583,7 @@ public class AiNewServiceImpl implements AiNewService {
                                 String.valueOf(user.getId()),
                                 String.valueOf(user.getCompanyUserId()),
                                 String.valueOf(user.getCompanyId()),
-                                String.valueOf(session.getQwExtId()));
+                                String.valueOf(session.getQwExtId()),2);
                         if (linkUrl != null && linkUrl.get("url") != null) {
                             String s = (String)linkUrl.get("url");
                             sendWebSocketMsg(s,msgVo,user,session);
@@ -1070,7 +1070,7 @@ public class AiNewServiceImpl implements AiNewService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebTaskSocketMsg(s,sendId,user);
@@ -1101,7 +1101,7 @@ public class AiNewServiceImpl implements AiNewService {
                             String.valueOf(user.getId()),
                             String.valueOf(user.getCompanyUserId()),
                             String.valueOf(user.getCompanyId()),
-                            String.valueOf(session.getQwExtId()));
+                            String.valueOf(session.getQwExtId()),2);
                     if (linkUrl != null && linkUrl.get("url") != null) {
                         String s = (String)linkUrl.get("url");
                         sendWebTaskSocketMsg(s,sendId,user);

+ 4 - 4
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiServiceImpl.java

@@ -530,7 +530,7 @@ public class AiServiceImpl implements AiService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebSocketMsg(s,msgVo,user,session);
@@ -561,7 +561,7 @@ public class AiServiceImpl implements AiService {
                                 String.valueOf(user.getId()),
                                 String.valueOf(user.getCompanyUserId()),
                                 String.valueOf(user.getCompanyId()),
-                                String.valueOf(session.getQwExtId()));
+                                String.valueOf(session.getQwExtId()),2);
                         if (linkUrl != null && linkUrl.get("url") != null) {
                             String s = (String)linkUrl.get("url");
                             sendWebSocketMsg(s,msgVo,user,session);
@@ -1048,7 +1048,7 @@ public class AiServiceImpl implements AiService {
                         String.valueOf(user.getId()),
                         String.valueOf(user.getCompanyUserId()),
                         String.valueOf(user.getCompanyId()),
-                        String.valueOf(session.getQwExtId()));
+                        String.valueOf(session.getQwExtId()),2);
                 if (linkUrl != null && linkUrl.get("url") != null) {
                     String s = (String)linkUrl.get("url");
                     sendWebTaskSocketMsg(s,sendId,user);
@@ -1079,7 +1079,7 @@ public class AiServiceImpl implements AiService {
                             String.valueOf(user.getId()),
                             String.valueOf(user.getCompanyUserId()),
                             String.valueOf(user.getCompanyId()),
-                            String.valueOf(session.getQwExtId()));
+                            String.valueOf(session.getQwExtId()),2);
                     if (linkUrl != null && linkUrl.get("url") != null) {
                         String s = (String)linkUrl.get("url");
                         sendWebTaskSocketMsg(s,sendId,user);

+ 15 - 0
fs-service/src/main/java/com/fs/his/dto/FsUserBindSalesParamDTO.java

@@ -0,0 +1,15 @@
+package com.fs.his.dto;
+
+import lombok.Data;
+
+@Data
+public class FsUserBindSalesParamDTO {
+    /**
+     * 用户id
+     */
+    private Long userId;
+    /**
+     * 销售id
+     */
+    private Long salesId;
+}

+ 4 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java

@@ -16,6 +16,7 @@ import com.fs.his.param.FsUserParam;
 import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.FsUserExportListVO;
 import com.fs.his.vo.OptionsVO;
+import com.fs.his.vo.UserOpenIdVO;
 import com.fs.hisStore.vo.FsCompanyUserListQueryVO;
 import com.fs.qw.dto.FsUserTransferParamDTO;
 import com.fs.qw.param.QwFsUserParam;
@@ -259,6 +260,9 @@ public interface FsUserMapper
             "</script>"})
     Long selectFsUserExportListVOCount(FsUserParam fsUser);
 
+    @Select("SELECT mp_open_id as openId FROM fs_user WHERE mp_open_id is not null")
+    List<UserOpenIdVO> selectOpenIdList();
+
     @Select("select * from fs_user where phone=#{phone}")
     FsUser selectFsUserByMpOpenId(@Param("phone") String phone);
 

+ 12 - 4
fs-service/src/main/java/com/fs/his/service/IFsUserService.java

@@ -17,10 +17,7 @@ import com.fs.his.domain.FsUserAddress;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FsUserParam;
-import com.fs.his.vo.FsUserVO;
-import com.fs.his.vo.FsUserExportListVO;
-import com.fs.his.vo.FsUserFollowDoctorVO;
-import com.fs.his.vo.UserVo;
+import com.fs.his.vo.*;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.vo.FsCompanyUserListQueryVO;
@@ -142,6 +139,12 @@ public interface IFsUserService
 
     Long selectFsUserExportListVOCount(FsUserParam fsUser);
 
+    /**
+     * 获取所有用户openId
+     * @return
+     */
+    List<UserOpenIdVO> selectOpenIdList();
+
     FsUser selectFsUserByMpOpenId(String openId);
 
     void setRepeatFansTag(FsUserCourseBeMemberParam param);
@@ -256,4 +259,9 @@ public interface IFsUserService
      * 项目会员导出
      */
     void exportProjectUserData(FsUserPageListExportParam param);
+
+    /**
+     * 销售分享app下载链接给用户
+     */
+//    Boolean  bindUserToSales(Long userId, Long salesId);
 }

+ 97 - 2
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java

@@ -23,8 +23,12 @@ import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.util.WechatApi;
 import com.fs.core.utils.OrderCodeUtils;
+import com.fs.event.TemplateBean;
+import com.fs.event.TemplateEvent;
+import com.fs.event.TemplateListenEnum;
 import com.fs.his.domain.*;
 import com.fs.his.enums.BusinessTypeEnum;
+import com.fs.his.enums.FsInquiryOrderStatusEnum;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.enums.PaymentMethodEnum;
 import com.fs.his.mapper.*;
@@ -35,6 +39,9 @@ import com.fs.his.vo.FsIntegralOrderListUVO;
 import com.fs.his.vo.FsIntegralOrderListVO;
 import com.fs.his.vo.FsIntegralOrderPVO;
 import com.fs.his.vo.FsStoreProductDeliverExcelVO;
+import com.fs.huifuPay.domain.HuiFuRefundResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayRefundRequest;
+import com.fs.huifuPay.service.HuiFuService;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.tzBankPay.doman.PayType;
@@ -44,13 +51,17 @@ import com.fs.ybPay.service.IPayService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.redisson.api.RObjectAsync;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.interceptor.TransactionAspectSupport;
 
 import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.*;
@@ -66,6 +77,7 @@ import java.util.concurrent.TimeUnit;
 @Service
 public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
 {
+    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
     @Autowired
     private FsIntegralOrderMapper fsIntegralOrderMapper;
 
@@ -94,6 +106,8 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private IFsStorePaymentService storePaymentService;
     @Autowired
+    private  FsStorePaymentMapper storePaymentMapper;
+    @Autowired
     private IFsIntegralCartService cartService;
     @Autowired
     private ICompanyUserService companyUserService;
@@ -103,6 +117,12 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private IFsUserService userService;
 
+    @Autowired
+    HuiFuService huiFuService;
+
+    @Autowired
+    private ApplicationEventPublisher publisher;
+
     /**
      * 查询积分商品订单
      *
@@ -512,7 +532,6 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
         logs.setBusinessId(order.getOrderId().toString());
         logs.setCreateTime(new Date());
         fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
-
         return R.ok();
     }
 
@@ -679,7 +698,46 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             fsUserIntegralLogs.setBusinessType(2);
             fsUserIntegralLogs.setStatus(0);
             i = fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs);
+            //还原库存
+            if (fsIntegralOrder.getItemJson().startsWith("[") && fsIntegralOrder.getItemJson().endsWith("]")){
+                List<FsIntegralGoods> goodsItem = JSONUtil.toBean(fsIntegralOrder.getItemJson(), new TypeReference<List<FsIntegralGoods>>(){}, true);
+                goodsItem.forEach(goods -> fsIntegralGoodsMapper.addStock(goods.getGoodsId(), Objects.isNull(goods.getNum()) ? 1 : goods.getNum()));
+            } else {
+                FsIntegralGoods integralGoods = JSONUtil.toBean(fsIntegralOrder.getItemJson(), FsIntegralGoods.class);
+                fsIntegralGoodsMapper.addStock(integralGoods.getGoodsId(), Objects.isNull(integralGoods.getNum()) ? 1 : integralGoods.getNum());
+            }
+            //还原金额
+            List<FsStorePayment> payments = storePaymentMapper.selectFsStorePaymentByPay(6,fsIntegralOrder.getOrderId());
+            if(payments!=null&&payments.size()==1){
+                FsStorePayment payment=payments.get(0);
+                V2TradePaymentScanpayRefundRequest request = new V2TradePaymentScanpayRefundRequest();
+                request.setOrdAmt(payment.getPayMoney().toString());
+                request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                request.setReqSeqId("refund-"+payment.getPayCode());
+                Map<String, Object> extendInfoMap = new HashMap<>();
+                extendInfoMap.put("org_req_seq_id", "integral-"+payment.getPayCode());
+                request.setExtendInfo(extendInfoMap);
+                HuiFuRefundResult refund = huiFuService.refund(request);
+                logger.info("积分退款返回结果:积分订单id:"+fsIntegralOrder.getOrderId()+refund);
+                if((refund.getResp_code().equals("00000000")||refund.getResp_code().equals("00000100"))&&(refund.getTrans_stat().equals("S")||refund.getTrans_stat().equals("P"))){
+                    FsStorePayment paymentMap=new FsStorePayment();
+                    paymentMap.setPaymentId(payment.getPaymentId());
+                    paymentMap.setStatus(-1);
+                    paymentMap.setRefundTime(DateUtils.getNowDate());
+                    paymentMap.setRefundMoney(payment.getPayMoney());
+                    storePaymentMapper.updateFsStorePayment(paymentMap);
+                    TemplateBean templateBean = TemplateBean.builder()
+                            .orderId(fsIntegralOrder.getOrderId().toString())
+                            .title("订单已取消")
+                            .remark("您的订单已取消")
+                            .uid(fsIntegralOrder.getUserId())
+                            .templateType(TemplateListenEnum.TYPE_1.getValue())
+                            .build();
+                    publisher.publishEvent(new TemplateEvent(this, templateBean));
+                }
+            }
         }
+
         return i;
     }
 
@@ -713,7 +771,44 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             fsUserIntegralLogs.setBusinessType(2);
             fsUserIntegralLogs.setStatus(0);
             i = fsUserIntegralLogsMapper.insertFsUserIntegralLogs(fsUserIntegralLogs);
-            //todo:库存是否需要退还,待定
+            //还原库存
+            if (fsIntegralOrder.getItemJson().startsWith("[") && fsIntegralOrder.getItemJson().endsWith("]")){
+                List<FsIntegralGoods> goodsItem = JSONUtil.toBean(fsIntegralOrder.getItemJson(), new TypeReference<List<FsIntegralGoods>>(){}, true);
+                goodsItem.forEach(goods -> fsIntegralGoodsMapper.addStock(goods.getGoodsId(), Objects.isNull(goods.getNum()) ? 1 : goods.getNum()));
+            } else {
+                FsIntegralGoods integralGoods = JSONUtil.toBean(fsIntegralOrder.getItemJson(), FsIntegralGoods.class);
+                fsIntegralGoodsMapper.addStock(integralGoods.getGoodsId(), Objects.isNull(integralGoods.getNum()) ? 1 : integralGoods.getNum());
+            }
+            //还原金额
+            List<FsStorePayment> payments = storePaymentMapper.selectFsStorePaymentByPay(6,fsIntegralOrder.getOrderId());
+            if(payments!=null&&payments.size()==1){
+                FsStorePayment payment=payments.get(0);
+                V2TradePaymentScanpayRefundRequest request = new V2TradePaymentScanpayRefundRequest();
+                request.setOrdAmt(payment.getPayMoney().toString());
+                request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                request.setReqSeqId("refund-"+payment.getPayCode());
+                Map<String, Object> extendInfoMap = new HashMap<>();
+                extendInfoMap.put("org_req_seq_id", "integral-"+payment.getPayCode());
+                request.setExtendInfo(extendInfoMap);
+                HuiFuRefundResult refund = huiFuService.refund(request);
+                logger.info("积分退款返回结果:积分订单id:"+fsIntegralOrder.getOrderId()+refund);
+                if((refund.getResp_code().equals("00000000")||refund.getResp_code().equals("00000100"))&&(refund.getTrans_stat().equals("S")||refund.getTrans_stat().equals("P"))){
+                    FsStorePayment paymentMap=new FsStorePayment();
+                    paymentMap.setPaymentId(payment.getPaymentId());
+                    paymentMap.setStatus(-1);
+                    paymentMap.setRefundTime(DateUtils.getNowDate());
+                    paymentMap.setRefundMoney(payment.getPayMoney());
+                    storePaymentMapper.updateFsStorePayment(paymentMap);
+                    TemplateBean templateBean = TemplateBean.builder()
+                            .orderId(fsIntegralOrder.getOrderId().toString())
+                            .title("订单已取消")
+                            .remark("您的订单已取消")
+                            .uid(fsIntegralOrder.getUserId())
+                            .templateType(TemplateListenEnum.TYPE_1.getValue())
+                            .build();
+                    publisher.publishEvent(new TemplateEvent(this, templateBean));
+                }
+            }
         }
         return i;
     }

+ 7 - 4
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -64,10 +64,7 @@ import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserProjectTagService;
 import com.fs.his.service.IFsUserWxService;
 import com.fs.his.utils.PhoneUtil;
-import com.fs.his.vo.FsUserVO;
-import com.fs.his.vo.FsUserExportListVO;
-import com.fs.his.vo.FsUserFollowDoctorVO;
-import com.fs.his.vo.UserVo;
+import com.fs.his.vo.*;
 import com.fs.im.config.ImTypeConfig;
 import com.fs.im.service.OpenIMService;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
@@ -572,6 +569,12 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.selectFsUserExportListVOCount(fsUser);
     }
 
+    @Override
+    public List<UserOpenIdVO> selectOpenIdList() {
+        return fsUserMapper.selectOpenIdList();
+    }
+
+
     @Override
     public FsUser selectFsUserByMpOpenId(String openId) {
         return fsUserMapper.selectFsUserByMpOpenId(openId);

+ 11 - 0
fs-service/src/main/java/com/fs/his/vo/UserOpenIdVO.java

@@ -0,0 +1,11 @@
+package com.fs.his.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+@Data
+public class UserOpenIdVO {
+
+    @Excel(name = "openId")
+    private String openId;
+}

+ 49 - 0
fs-service/src/main/java/com/fs/hisStore/enums/LiveEnum.java

@@ -0,0 +1,49 @@
+package com.fs.hisStore.enums;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * 设置不转码的项目
+ */
+public enum LiveEnum {
+    KANGNIAN_TANG("康年堂"),
+    SIFU_TANG("四福堂"),
+    NMG_MYT("内蒙古一贴"),
+    CQ_TYT("重庆泰医堂"),
+    HDT("弘德堂"),
+    JNMY("金牛明医"),
+    HYT("鹤颜堂"),
+    Z_K("中康");
+
+    private final String companyName;
+
+    LiveEnum(String companyName) {
+        this.companyName = companyName;
+    }
+
+    public String getCompanyName() {
+        return companyName;
+    }
+
+    /**
+     * 静态集合,避免每次调用都重新创建
+     */
+    private static final Set<String> COMPANY_NAMES = Collections.unmodifiableSet(
+            Arrays.stream(values())
+                    .map(LiveEnum::getCompanyName)
+                    .collect(Collectors.toSet())
+    );
+
+    /**
+     * 比较是否存在
+     *
+     * @param companyName
+     * @return
+     */
+    public static boolean contains(String companyName) {
+        return COMPANY_NAMES.contains(companyName);
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductAttrValueScrmMapper.java

@@ -147,4 +147,16 @@ public interface FsStoreProductAttrValueScrmMapper
     void updateFsStoreProductAttrValuePrice(List<Long> ids, double v);
 
     List<FsStoreProductAttrValueScrm> getFsStoreProductAttrValueListInProductId(List<Long> productIds);
+
+    @Update({"<script> " +
+            " UPDATE fs_store_product_attr_value_scrm" +
+            " SET stock = stock + CAST(#{totalNum} AS SIGNED)" +
+            " WHERE product_id = #{productId}" +
+            " AND bar_code IN",
+            "<foreach collection='barCodeList' item='barCode' open='(' separator=',' close=')'>" +
+                    "#{barCode}" +
+                    "</foreach>" +
+                    "</script>"
+    })
+    void incStock(@Param("productId") Long productId,@Param("barCodeList") List<String> barCodeList,@Param("totalNum") String totalNum);
 }

+ 118 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/MergedOrderMapper.java

@@ -0,0 +1,118 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.param.FsMyStoreOrderQueryParam;
+import com.fs.hisStore.param.MergedAfterSalesQueryParam;
+import com.fs.hisStore.vo.FsMergedOrderListQueryVO;
+import com.fs.hisStore.vo.MergedAfterSalesVO;
+import com.fs.live.param.MergedOrderQueryParam;
+import com.fs.live.vo.MergedOrderVO;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 合并订单Mapper接口
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+public interface MergedOrderMapper
+{
+    /**
+     * 查询合并的订单列表(销售订单+商城订单+直播订单)
+     *
+     * @param param 查询参数
+     * @return 合并后的订单列表
+     */
+    List<MergedOrderVO> selectMergedOrderList(@Param("maps") MergedOrderQueryParam param);
+
+    /**
+     * 查询合并的售后列表(商城售后+直播售后)
+     *
+     * @param param 查询参数
+     * @return 合并后的售后列表
+     */
+    List<MergedAfterSalesVO> selectMergedAfterSalesList(@Param("maps") MergedAfterSalesQueryParam param);
+
+    /**
+     * 查询合并的订单列表(商城订单+直播订单)
+     *
+     * @param param 查询参数
+     * @return 合并后的订单列表
+     */
+    @Select({"<script> " +
+            "SELECT * FROM ( " +
+            "  SELECT " +
+            "    o.id, " +
+            "    NULL AS order_id, " +
+            "    NULL AS live_id, " +
+            "    NULL AS after_sales_id, " +
+            "    o.order_code, " +
+            "    o.pay_price, " +
+            "    o.status, " +
+            "    o.is_package, " +
+            "    o.package_json, " +
+            "    o.item_json, " +
+            "    o.delivery_id, " +
+            "    o.finish_time, " +
+            "    o.create_time, " +
+            "    NULL AS total_num, " +
+            "    NULL AS discount_money, " +
+            "    1 AS order_type " +
+            "  FROM fs_store_order_scrm o " +
+            "  WHERE o.is_del = 0 AND o.is_sys_del = 0 " +
+            "  <if test = 'maps.status != null and maps.status != \"\"'> " +
+            "    AND o.status = #{maps.status} " +
+            "  </if> " +
+            "  <if test = 'maps.keyword != null and maps.keyword != \"\"'> " +
+            "    AND o.order_code LIKE CONCAT('%', #{maps.keyword}, '%') " +
+            "  </if> " +
+            "  <if test = 'maps.deliveryStatus != null'> " +
+            "    AND o.delivery_status = #{maps.deliveryStatus} " +
+            "  </if> " +
+            "  <if test = 'maps.userId != null'> " +
+            "    AND o.user_id = #{maps.userId} " +
+            "  </if> " +
+            "  UNION ALL " +
+            "  SELECT " +
+            "    NULL AS id, " +
+            "    o.order_id, " +
+            "    o.live_id, " +
+            "    a.id AS after_sales_id, " +
+            "    o.order_code, " +
+            "    o.pay_price, " +
+            "    o.status, " +
+            "    NULL AS is_package, " +
+            "    NULL AS package_json, " +
+            "    o.item_json, " +
+            "    o.delivery_sn AS delivery_id, " +
+            "    o.finish_time, " +
+            "    o.create_time, " +
+            "    o.total_num, " +
+            "    o.discount_money, " +
+            "    2 AS order_type " +
+            "  FROM live_order o " +
+            "  LEFT JOIN ( " +
+            "    SELECT t.*, ROW_NUMBER() OVER (PARTITION BY t.order_id ORDER BY t.create_time DESC) AS rn " +
+            "    FROM live_after_sales t " +
+            "  ) a ON o.order_id = a.order_id AND a.rn = 1 " +
+            "  WHERE o.is_del = 0 " +
+            "  <if test = 'maps.status != null and maps.status != \"\"'> " +
+            "    AND o.status = #{maps.status} " +
+            "  </if> " +
+            "  <if test = 'maps.keyword != null and maps.keyword != \"\"'> " +
+            "    AND o.order_code LIKE CONCAT('%', #{maps.keyword}, '%') " +
+            "  </if> " +
+            "  <if test = 'maps.deliveryStatus != null'> " +
+            "    AND o.delivery_status = #{maps.deliveryStatus} " +
+            "  </if> " +
+            "  <if test = 'maps.userId != null'> " +
+            "    AND o.user_id = #{maps.userId} " +
+            "  </if> " +
+            ") AS merged_orders " +
+            "ORDER BY create_time DESC " +
+            "</script>"})
+    List<FsMergedOrderListQueryVO> selectMergedOrderListVO(@Param("maps") FsMyStoreOrderQueryParam param);
+}
+

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderParam.java

@@ -113,4 +113,10 @@ public class FsStoreOrderParam extends BaseEntity implements Serializable
 
     private String appId;
 
+    //银行交易流水号
+    private String bankTransactionId;
+
+    private Integer pageNum;
+    private Integer pageSize;
+
 }

+ 21 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsUsePackageScrmSendParam.java

@@ -0,0 +1,21 @@
+package com.fs.hisStore.param;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+@Data
+public class FsUsePackageScrmSendParam {
+
+    /** 优惠劵id */
+    @Excel(name = "套餐id")
+    private Long packageId;
+
+    @Excel(name = "会员ID")
+    private Long userId;
+
+    //发送销售id
+    private Long companyUserId;
+
+    //发送销售公司id
+    private Long companyId;
+}

+ 31 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesDeliveryParam.java

@@ -0,0 +1,31 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * 合并售后物流参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesDeliveryParam implements Serializable
+{
+    private Long userId;
+    
+    /** 售后ID */
+    private Long salesId;
+
+    @NotBlank(message = "物流单号不能为空")
+    private String deliverySn;
+
+    @NotBlank(message = "物流公司不能为空")
+    private String deliveryName;
+
+    /** 售后类型 1商城售后 2直播售后 */
+    private Integer afterSalesType;
+}
+

+ 43 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesParam.java

@@ -0,0 +1,43 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合并售后申请参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesParam implements Serializable
+{
+    /** 订单号 */
+    @NotBlank
+    private String orderCode;
+
+    /** 服务类型 0仅退款1退货退款 */
+    @NotBlank
+    private Integer serviceType;
+
+    /** 申请原因 */
+    @NotBlank
+    private String reasons;
+
+    /** 申请说明 */
+    private String explains;
+
+    /** 申请说明图片 */
+    private String explainImg;
+
+    private BigDecimal refundAmount;
+
+    /** 商品数据 */
+    @NotBlank
+    private List<FsStoreAfterSalesProductParam> productList;
+}
+

+ 23 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesQueryParam.java

@@ -0,0 +1,23 @@
+package com.fs.hisStore.param;
+
+import com.fs.common.param.BaseQueryParam;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 合并售后查询参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesQueryParam extends BaseQueryParam implements Serializable
+{
+    /** 状态 1待处理 2已完成 */
+    private Integer status;
+    
+    /** 用户ID */
+    private Long userId;
+}
+

+ 22 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedAfterSalesRevokeParam.java

@@ -0,0 +1,22 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 合并售后撤销参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesRevokeParam implements Serializable
+{
+    /** 售后ID */
+    private Long salesId;
+
+    /** 售后类型 1商城售后 2直播售后 */
+    private Integer afterSalesType;
+}
+

+ 25 - 0
fs-service/src/main/java/com/fs/hisStore/param/MergedOrderDeleteParam.java

@@ -0,0 +1,25 @@
+package com.fs.hisStore.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+/**
+ * 合并订单删除参数
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedOrderDeleteParam implements Serializable
+{
+    /** 订单ID */
+    @NotNull(message = "订单ID不能为空")
+    private Long orderId;
+
+    /** 订单类型 1商城订单 2直播订单 */
+    @NotNull(message = "订单类型不能为空")
+    private Integer orderType;
+}
+

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

@@ -7,6 +7,7 @@ import com.fs.api.vo.ExpressVO;
 import com.fs.common.core.domain.R;
 import com.fs.hisStore.domain.FsExpressScrm;
 import com.fs.hisStore.dto.ExpressInfoDTO;
+import com.fs.hisStore.param.FsStoreOrderExpressParam;
 
 /**
  * 快递公司Service接口
@@ -78,4 +79,6 @@ public interface IFsExpressScrmService
 
 
     List<ExpressVO> getExpressInfoAPI(ExpressParam param);
+
+    R getLiveExpressByDeliverId(FsStoreOrderExpressParam param);
 }

+ 83 - 0
fs-service/src/main/java/com/fs/hisStore/service/IMergedOrderService.java

@@ -0,0 +1,83 @@
+package com.fs.hisStore.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.hisStore.param.*;
+import com.fs.hisStore.vo.FsMergedOrderListQueryVO;
+import com.fs.hisStore.vo.MergedAfterSalesVO;
+import com.fs.live.param.MergedOrderQueryParam;
+import com.fs.live.vo.MergedOrderVO;
+
+import java.text.ParseException;
+import java.util.List;
+
+/**
+ * 合并订单Service接口
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+public interface IMergedOrderService
+{
+    /**
+     * 查询合并的订单列表(商城订单+直播订单)
+     *
+     * @param param 查询参数
+     * @return 合并后的订单列表
+     */
+    List<FsMergedOrderListQueryVO> selectMergedOrderListVO(FsMyStoreOrderQueryParam param);
+
+    List<MergedOrderVO> selectMergedOrderList(MergedOrderQueryParam param);
+
+    /**
+     * 查询合并的售后列表(商城售后+直播售后)
+     *
+     * @param param 查询参数
+     * @return 合并后的售后列表
+     */
+    List<MergedAfterSalesVO> selectMergedAfterSalesList(MergedAfterSalesQueryParam param);
+
+    /**
+     * 申请售后
+     *
+     * @param userId 用户ID
+     * @param param 售后参数
+     * @return 结果
+     */
+    R applyForAfterSales(String userId, MergedAfterSalesParam param);
+
+    /**
+     * 撤销售后
+     *
+     * @param userId 用户ID
+     * @param param 撤销参数
+     * @return 结果
+     */
+    R revokeAfterSales(String userId, MergedAfterSalesRevokeParam param) throws ParseException;
+
+    /**
+     * 提交物流信息
+     *
+     * @param param 物流参数
+     * @return 结果
+     */
+    R addDelivery(MergedAfterSalesDeliveryParam param);
+
+    /**
+     * 查询售后详情
+     *
+     * @param salesId 售后ID
+     * @param afterSalesType 售后类型 1商城售后 2直播售后
+     * @return 售后详情
+     */
+    MergedAfterSalesVO selectMergedAfterSalesById(Long salesId, Integer afterSalesType);
+
+    /**
+     * 删除订单(逻辑删除)
+     *
+     * @param userId 用户ID
+     * @param param 删除参数
+     * @return 结果
+     */
+    R deleteOrder(String userId, MergedOrderDeleteParam param);
+}
+

+ 52 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsExpressScrmServiceImpl.java

@@ -24,7 +24,11 @@ import com.fs.hisStore.dto.ExpressInfoDTO;
 import com.fs.hisStore.dto.TracesDTO;
 import com.fs.hisStore.enums.ShipperCodeEnum;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.param.FsStoreOrderExpressParam;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.mapper.LiveOrderMapper;
 import com.fs.system.service.ISysConfigService;
+import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -51,6 +55,9 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private LiveOrderMapper liveOrderMapper;
+
     /**
      * 查询快递公司
      * 
@@ -285,6 +292,51 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
 
     }
 
+    @Override
+    public R getLiveExpressByDeliverId(FsStoreOrderExpressParam param) {
+        // 添加日志 - 开始
+        Logger logger = LoggerFactory.getLogger(this.getClass());
+        logger.info("查询物流信息开始,参数:{}", param);
+        LiveOrder liveOrder = liveOrderMapper.selectLiveOrderByOrderId(String.valueOf(param.getOrderId()));
+        // 添加日志 - 订单信息
+        logger.info("查询到订单信息:{}", liveOrder);
+        if (liveOrder == null) {
+            return R.error("未查询到订单信息");
+        }
+        //顺丰轨迹查询处理
+        String lastFourNumber = "";
+        if (StringUtils.equals(liveOrder.getDeliveryCode(),ShipperCodeEnum.SF.getValue()) || StringUtils.equals(liveOrder.getDeliveryCode(),ShipperCodeEnum.ZTO.getValue())) {
+            lastFourNumber = getLastFourNum(liveOrder.getUserPhone());
+            // 添加日志 - 顺丰单号
+            logger.info("顺丰单号处理,获取用户手机号后四位:{}", lastFourNumber);
+        }
+        ExpressInfoDTO dto = null;
+        try {
+            dto = this.getExpressInfo(liveOrder.getOrderCode(),
+                    liveOrder.getDeliveryCode(),
+                    liveOrder.getDeliverySn(),
+                    lastFourNumber);
+            // 添加日志 - 成功获取物流信息
+            logger.info("成功获取物流信息,订单号:{},物流单号:{}, 快递公司ID:{}, 返回数据:{}", liveOrder.getOrderCode(), liveOrder.getDeliveryCode(),liveOrder.getDeliverySn(), dto);
+        } catch (Exception e) {
+            // 添加日志 - 异常
+            logger.error("获取物流信息异常,订单号:{},物流单号:{},快递公司ID:{}", liveOrder.getOrderCode(), liveOrder.getDeliveryCode(), liveOrder.getDeliverySn(), e);
+        }
+        // 添加日志 - 结束
+        logger.info("查询物流信息结束,订单号:{}", param.getOrderId());
+
+        return R.ok().put("data",dto);
+    }
+
+    public static String getLastFourNum(String phone) {
+
+        String lastFourNumber = phone;
+        if (lastFourNumber.length() == 11) {
+            lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+        }
+        return lastFourNumber;
+    }
+
     /**
      * Sign签名生成
      *

+ 363 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/MergedOrderServiceImpl.java

@@ -0,0 +1,363 @@
+package com.fs.hisStore.service.impl;
+
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.hisStore.domain.FsStoreAfterSalesItemScrm;
+import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
+import com.fs.hisStore.enums.OrderInfoEnum;
+import com.fs.hisStore.mapper.MergedOrderMapper;
+import com.fs.hisStore.param.*;
+import com.fs.hisStore.service.IFsStoreAfterSalesItemScrmService;
+import com.fs.hisStore.service.IFsStoreAfterSalesScrmService;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.hisStore.service.IMergedOrderService;
+import com.fs.hisStore.vo.FsMergedOrderListQueryVO;
+import com.fs.hisStore.vo.FsStoreOrderItemVO;
+import com.fs.hisStore.vo.MergedAfterSalesVO;
+import com.fs.live.domain.LiveAfterSales;
+import com.fs.live.domain.LiveAfterSalesItem;
+import com.fs.live.param.LiveAfterSalesDeliveryParam;
+import com.fs.live.param.LiveAfterSalesParam;
+import com.fs.live.param.LiveAfterSalesRevokeParam;
+import com.fs.live.param.MergedOrderQueryParam;
+import com.fs.live.service.ILiveAfterSalesItemService;
+import com.fs.live.service.ILiveAfterSalesService;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.live.vo.MergedOrderVO;
+import com.fs.store.config.StoreConfig;
+import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.text.ParseException;
+import java.util.*;
+
+/**
+ * 合并订单Service实现类
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Service
+public class MergedOrderServiceImpl implements IMergedOrderService
+{
+    @Autowired
+    private MergedOrderMapper mergedOrderMapper;
+
+    /*
+     * 后端合并
+     * */
+    @Override
+    public List<MergedOrderVO> selectMergedOrderList(MergedOrderQueryParam param)
+    {
+        List<MergedOrderVO> list = mergedOrderMapper.selectMergedOrderList(param);
+
+        // 处理商品JSON
+        for (MergedOrderVO vo : list)
+        {
+            if (StringUtils.isNotEmpty(vo.getItemJson()))
+            {
+                try
+                {
+                    JSONArray jsonArray = JSONUtil.parseArray(vo.getItemJson());
+                    if (jsonArray != null && jsonArray.size() > 0)
+                    {
+                        vo.setItems(jsonArray);
+                    }
+                }
+                catch (Exception e)
+                {
+                    // JSON解析失败,忽略
+                }
+            }
+        }
+
+        return list;
+    }
+
+
+    @Autowired
+    private ISysConfigService configService;
+
+    @Autowired
+    private IFsStoreAfterSalesScrmService storeAfterSalesService;
+
+    @Autowired
+    private ILiveAfterSalesService liveAfterSalesService;
+
+    @Autowired
+    private IFsStoreOrderScrmService storeOrderService;
+
+    @Autowired
+    private ILiveOrderService liveOrderService;
+
+    @Autowired
+    private IFsStoreAfterSalesItemScrmService storeAfterSalesItemService;
+
+    @Autowired
+    private ILiveAfterSalesItemService liveAfterSalesItemService;
+
+    /*
+     * 小程序合并
+     * */
+    @Override
+    public List<FsMergedOrderListQueryVO> selectMergedOrderListVO(FsMyStoreOrderQueryParam param)
+    {
+        List<FsMergedOrderListQueryVO> list = mergedOrderMapper.selectMergedOrderListVO(param);
+
+        for (FsMergedOrderListQueryVO vo : list)
+        {
+
+            // 处理商品JSON
+            if (StringUtils.isNotEmpty(vo.getItemJson()))
+            {
+                List<FsStoreOrderItemVO> items = new ArrayList<>();
+                if (2 == vo.getOrderType()) {
+                    FsStoreOrderItemVO bean = JSONUtil.toBean(vo.getItemJson(), FsStoreOrderItemVO.class);
+                    items.add(bean);
+                    vo.setItems(items);
+                }else {
+                    JSONArray jsonArray = JSONUtil.parseArray(vo.getItemJson());
+                    items = JSONUtil.toList(jsonArray, FsStoreOrderItemVO.class);
+                    if (items != null && items.size() > 0)
+                    {
+                        vo.setItems(items);
+                    }
+                }
+
+
+            }
+
+            // 处理是否可以申请售后
+            vo.setIsAfterSales(0);
+            if (vo.getStatus() != null && vo.getStatus().equals(OrderInfoEnum.STATUS_3.getValue()))
+            {
+                // 已完成订单
+                vo.setIsAfterSales(1);
+                if (vo.getFinishTime() != null)
+                {
+                    String json = configService.selectConfigByKey("his.store");
+                    if (StringUtils.isNotEmpty(json))
+                    {
+                        StoreConfig storeConfig = JSONUtil.toBean(json, StoreConfig.class);
+                        if (storeConfig != null && storeConfig.getStoreAfterSalesDay() != null && storeConfig.getStoreAfterSalesDay() > 0)
+                        {
+                            // 判断完成时间是否超过指定时间
+                            Calendar calendar = new GregorianCalendar();
+                            calendar.setTime(vo.getFinishTime());
+                            calendar.add(Calendar.DATE, storeConfig.getStoreAfterSalesDay());
+                            if (calendar.getTime().getTime() < new Date().getTime())
+                            {
+                                vo.setIsAfterSales(0);
+                            }
+                        }
+                    }
+                }
+            }
+            else if (vo.getStatus() != null && (vo.getStatus() == 1 || vo.getStatus() == 2))
+            {
+                vo.setIsAfterSales(1);
+            }
+        }
+
+        return list;
+    }
+
+    @Override
+    public List<MergedAfterSalesVO> selectMergedAfterSalesList(MergedAfterSalesQueryParam param) {
+        List<MergedAfterSalesVO> list = mergedOrderMapper.selectMergedAfterSalesList(param);
+        
+        // 填充售后商品列表
+        for (MergedAfterSalesVO vo : list) {
+            if (vo.getAfterSalesType() != null && vo.getAfterSalesType() == 1) {
+                // 商城售后
+                FsStoreAfterSalesItemScrm itemParam = new FsStoreAfterSalesItemScrm();
+                itemParam.setStoreAfterSalesId(vo.getId());
+                List<FsStoreAfterSalesItemScrm> items = storeAfterSalesItemService.selectFsStoreAfterSalesItemList(itemParam);
+                vo.setItems(items);
+            } else if (vo.getAfterSalesType() != null && vo.getAfterSalesType() == 2) {
+                // 直播售后
+                List<LiveAfterSalesItem> items = liveAfterSalesItemService.selectLiveAfterSalesItemByAfterId(vo.getId());
+                vo.setItems(items);
+            }
+        }
+        
+        return list;
+    }
+
+    @Override
+    public R applyForAfterSales(String userId, MergedAfterSalesParam param) {
+        // 根据订单号判断是商城订单还是直播订单
+        try {
+            // 先尝试查询商城订单
+            com.fs.hisStore.domain.FsStoreOrderScrm storeOrder = storeOrderService.selectFsStoreOrderByOrderCode(param.getOrderCode());
+            if (storeOrder != null) {
+                // 商城订单,调用商城售后服务
+                FsStoreAfterSalesParam storeParam = new FsStoreAfterSalesParam();
+                BeanUtils.copyProperties(param, storeParam);
+                return storeAfterSalesService.applyForAfterSales(Long.parseLong(userId), storeParam);
+            }
+        } catch (Exception e) {
+            // 商城订单不存在,继续尝试直播订单
+        }
+        
+        // 尝试查询直播订单
+        com.fs.live.domain.LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderCode(param.getOrderCode());
+        if (liveOrder != null) {
+            // 直播订单,调用直播售后服务
+            LiveAfterSalesParam liveParam = new LiveAfterSalesParam();
+            liveParam.setOrderCode(param.getOrderCode());
+            liveParam.setServiceType(param.getServiceType());
+            liveParam.setReasons(param.getReasons());
+            liveParam.setExplains(param.getExplains());
+            liveParam.setExplainImg(param.getExplainImg());
+            liveParam.setRefundAmount(param.getRefundAmount());
+            // 转换商品列表
+            if (param.getProductList() != null) {
+                List<com.fs.live.param.LiveAfterSalesProductParam> liveProductList = new ArrayList<>();
+                for (FsStoreAfterSalesProductParam product : param.getProductList()) {
+                    com.fs.live.param.LiveAfterSalesProductParam liveProduct = new com.fs.live.param.LiveAfterSalesProductParam();
+                    liveProduct.setProductId(product.getProductId());
+                    liveProduct.setNum(product.getNum());
+                    liveProductList.add(liveProduct);
+                }
+                liveParam.setProductList(liveProductList);
+            }
+            return liveAfterSalesService.applyForAfterSales(userId, liveParam);
+        }
+        
+        return R.error("订单不存在");
+    }
+
+    @Override
+    public R revokeAfterSales(String userId, MergedAfterSalesRevokeParam param) throws ParseException {
+        if (param.getAfterSalesType() != null && param.getAfterSalesType() == 1) {
+            // 商城售后
+            return storeAfterSalesService.revoke(Long.parseLong(userId), param.getSalesId());
+        } else if (param.getAfterSalesType() != null && param.getAfterSalesType() == 2) {
+            // 直播售后
+            LiveAfterSalesRevokeParam liveParam = new LiveAfterSalesRevokeParam();
+            liveParam.setId(param.getSalesId());
+            return liveAfterSalesService.revoke(userId, liveParam);
+        }
+        return R.error("售后类型错误");
+    }
+
+    @Override
+    public R addDelivery(MergedAfterSalesDeliveryParam param) {
+        if (param.getAfterSalesType() != null && param.getAfterSalesType() == 1) {
+            // 商城售后
+            FsStoreAfterSalesDeliveryParam storeParam = new FsStoreAfterSalesDeliveryParam();
+            storeParam.setUserId(param.getUserId());
+            storeParam.setSalesId(param.getSalesId());
+            storeParam.setDeliverySn(param.getDeliverySn());
+            storeParam.setDeliveryName(param.getDeliveryName());
+            return storeAfterSalesService.addDelivery(storeParam);
+        } else if (param.getAfterSalesType() != null && param.getAfterSalesType() == 2) {
+            // 直播售后
+            LiveAfterSalesDeliveryParam liveParam = new LiveAfterSalesDeliveryParam();
+            liveParam.setUserId(param.getUserId());
+            liveParam.setId(param.getSalesId());
+            liveParam.setDeliverySn(param.getDeliverySn());
+            liveParam.setDeliveryName(param.getDeliveryName());
+            return liveAfterSalesService.addDelivery(liveParam);
+        }
+        return R.error("售后类型错误");
+    }
+
+    @Override
+    public MergedAfterSalesVO selectMergedAfterSalesById(Long salesId, Integer afterSalesType) {
+        MergedAfterSalesVO vo = new MergedAfterSalesVO();
+        
+        if (afterSalesType != null && afterSalesType == 1) {
+            // 商城售后
+            FsStoreAfterSalesScrm storeAfterSales = storeAfterSalesService.selectFsStoreAfterSalesById(salesId);
+            if (storeAfterSales != null) {
+                BeanUtils.copyProperties(storeAfterSales, vo);
+                vo.setAfterSalesType(1);
+                vo.setAfterSalesTypeName("商城售后");
+                vo.setOrderCode(storeAfterSales.getOrderCode());
+                
+                // 填充商品列表
+                FsStoreAfterSalesItemScrm itemParam = new FsStoreAfterSalesItemScrm();
+                itemParam.setStoreAfterSalesId(salesId);
+                List<FsStoreAfterSalesItemScrm> items = storeAfterSalesItemService.selectFsStoreAfterSalesItemList(itemParam);
+                vo.setItems(items);
+            }
+        } else if (afterSalesType != null && afterSalesType == 2) {
+            // 直播售后
+            LiveAfterSales liveAfterSales = liveAfterSalesService.selectLiveAfterSalesById(salesId);
+            if (liveAfterSales != null) {
+                BeanUtils.copyProperties(liveAfterSales, vo);
+                vo.setAfterSalesType(2);
+                vo.setAfterSalesTypeName("直播售后");
+                
+                // 查询订单号
+                com.fs.live.domain.LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderId(String.valueOf(liveAfterSales.getOrderId()));
+                if (liveOrder != null) {
+                    vo.setOrderCode(liveOrder.getOrderCode());
+                }
+                
+                // 填充商品列表
+                List<LiveAfterSalesItem> items = liveAfterSalesItemService.selectLiveAfterSalesItemByAfterId(salesId);
+                vo.setItems(items);
+            }
+        }
+        
+        return vo;
+    }
+
+    @Override
+    public R deleteOrder(String userId, MergedOrderDeleteParam param) {
+        Long orderId = param.getOrderId();
+        Integer orderType = param.getOrderType();
+        
+        if (orderType == null) {
+            return R.error("订单类型不能为空");
+        }
+        
+        if (orderType == 1) {
+            // 商城订单
+            com.fs.hisStore.domain.FsStoreOrderScrm storeOrder = storeOrderService.selectFsStoreOrderById(orderId);
+            if (storeOrder == null) {
+                return R.error("订单不存在");
+            }
+            // 检查订单是否属于当前用户
+            if (!storeOrder.getUserId().equals(Long.parseLong(userId))) {
+                return R.error("无权删除该订单");
+            }
+            // 逻辑删除:设置 isDel = 1
+            storeOrder.setIsDel(1);
+            int result = storeOrderService.updateFsStoreOrder(storeOrder);
+            if (result > 0) {
+                return R.ok("删除成功");
+            } else {
+                return R.error("删除失败");
+            }
+        } else if (orderType == 2) {
+            // 直播订单
+            com.fs.live.domain.LiveOrder liveOrder = liveOrderService.selectLiveOrderByOrderId(String.valueOf(orderId));
+            if (liveOrder == null) {
+                return R.error("订单不存在");
+            }
+            // 检查订单是否属于当前用户
+            if (!liveOrder.getUserId().equals(userId)) {
+                return R.error("无权删除该订单");
+            }
+            // 逻辑删除:设置 isDel = "1"
+            liveOrder.setIsDel("1");
+            int result = liveOrderService.updateLiveOrder(liveOrder);
+            if (result > 0) {
+                return R.ok("删除成功");
+            } else {
+                return R.error("删除失败");
+            }
+        } else {
+            return R.error("订单类型错误");
+        }
+    }
+}
+

+ 78 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsMergedOrderListQueryVO.java

@@ -0,0 +1,78 @@
+package com.fs.hisStore.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 合并订单列表查询VO(商城订单+直播订单)
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class FsMergedOrderListQueryVO implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 订单ID */
+    private Long id;
+
+    /** 订单ID(直播订单使用) */
+    private Long orderId;
+
+    /** 直播ID(直播订单使用) */
+    private Long liveId;
+
+    /** 售后ID(直播订单使用) */
+    private Long afterSalesId;
+
+    /** 订单号 */
+    private String orderCode;
+
+    /** 实际支付金额 */
+    private BigDecimal payPrice;
+
+    /** 订单状态 */
+    private Integer status;
+
+    /** 是否套餐 */
+    private Integer isPackage;
+
+    /** 套餐JSON */
+    private String packageJson;
+
+    /** 商品JSON */
+    private String itemJson;
+
+    /** 物流单号 */
+    private String deliveryId;
+
+    /** 是否可以申请售后 */
+    private Integer isAfterSales;
+
+    /** 完成时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date finishTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 总数量(直播订单使用) */
+    private Integer totalNum;
+
+    /** 优惠金额(直播订单使用) */
+    private Integer discountMoney;
+
+    /** 订单类型:1-商城订单,2-直播订单 */
+    private Integer orderType;
+
+    /** 订单商品列表 */
+    private List<FsStoreOrderItemVO> items;
+}
+

+ 144 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java

@@ -0,0 +1,144 @@
+package com.fs.hisStore.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/4 上午11:43)
+ */
+
+@Data
+public class FsStoreOrderItemExportRefundZMVO implements Serializable  {
+
+
+    private Long itemId;
+
+    /** 订单号 */
+    @Excel(name = "订单号",sort = 1)
+    private String orderCode;
+
+    @Excel(name = "支付单号",sort = 2)
+    private String payCode;
+
+    @Excel(name = "订单状态", dictType = "store_order_status",sort = 10)
+    private String status;
+
+    @Excel(name = "会员ID" ,sort = 20)
+    private Long userId;
+
+    @Excel(name = "产品名称",sort = 30)
+    private String productName;
+
+    @Excel(name = "产品编码",sort =40)
+    private String barCode;
+
+
+    @Excel(name = "规格",sort =50)
+    private String sku;
+
+    @Excel(name = "产品数量",sort =60)
+    private String num;
+
+
+    @Excel(name = "产品价格",sort =70)
+    private String price;
+
+    @Excel(name = "成本价",sort =80)
+    private String cost;
+    @Excel(name = "结算价",sort =90)
+    private BigDecimal FPrice;
+
+    @Excel(name = "实付金额",sort =91)
+    private BigDecimal payMoney;
+
+    @Excel(name = "额外运费",sort =100)
+    private BigDecimal payPostage;
+    private Integer totalNum;
+    @Excel(name = "商品分类",sort =100)
+    private String cateName;
+
+
+    private String jsonInfo;
+
+    /** 用户姓名 */
+    @Excel(name = "收货人姓名",sort =110)
+    private String realName;
+
+    /** 用户电话 */
+    @Excel(name = "收货人电话",sort =120)
+    private String userPhone;
+
+    /** 详细地址 */
+    @Excel(name = "详细地址",sort =130)
+    private String userAddress;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "下单时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 140)
+    private Date createTime;
+    /** 支付时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 150)
+    private Date payTime;
+
+    /** 快递公司编号 */
+    @Excel(name = "快递公司编号",sort = 160)
+    private String deliverySn;
+
+    /** 快递名称/送货人姓名 */
+    @Excel(name = "快递公司",sort = 170)
+    private String deliveryName;
+
+    /** 快递单号/手机号 */
+    @Excel(name = "快递单号",sort = 180)
+    private String deliveryId;
+
+    @Excel(name = "所属公司",sort = 190)
+    private String companyName;
+    @Excel(name = "所属销售",sort = 200)
+    private String companyUserNickName;
+
+    @Excel(name = "套餐名称",sort = 210)
+    private String packageName;
+
+    @Excel(name = "组合码",sort = 210)
+    private String groupBarCode;
+
+    @Excel(name = "是否上传凭证 0:未上传 1:已上传",sort = 210)
+    private Integer isUpload;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "上传时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 220)
+    private Date uploadTime;
+
+    @Excel(name = "归属档期",sort = 230)
+    private String scheduleName;
+
+    //银行交易流水号
+    @Excel(name = "银行交易流水号",sort = 240)
+    private String bankTransactionId;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "退款时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 220)
+    private Date refundTime;
+
+    @Excel(name = "退款数量" ,sort = 230)
+    private Integer afterSalesNumber;
+
+    @Excel(name = "退款金额" ,sort = 240)
+    private BigDecimal refundMoney;
+
+    /** 申请原因 */
+    @Excel(name = "申请原因",sort = 250)
+    private String reasons;
+
+    /** 说明 */
+    @Excel(name = "说明",sort = 260)
+    private String explains;
+
+}

+ 124 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java

@@ -0,0 +1,124 @@
+package com.fs.hisStore.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/4 上午11:43)
+ */
+
+@Data
+public class FsStoreOrderItemExportZMVO implements Serializable  {
+
+
+    private Long itemId;
+
+    /** 订单号 */
+    @Excel(name = "订单号",sort = 1)
+    private String orderCode;
+
+    @Excel(name = "订单状态", dictType = "store_order_status",sort = 10)
+    private String status;
+
+    @Excel(name = "会员ID" ,sort = 20)
+    private Long userId;
+
+    @Excel(name = "产品名称",sort = 30)
+    private String productName;
+
+    @Excel(name = "产品编码",sort =40)
+    private String barCode;
+
+
+    @Excel(name = "规格",sort =50)
+    private String sku;
+
+    @Excel(name = "产品数量",sort =60)
+    private Integer num;
+
+
+    @Excel(name = "产品价格",sort =70)
+    private BigDecimal price;
+
+    @Excel(name = "成本价",sort =80)
+    private BigDecimal cost;
+    @Excel(name = "结算价",sort =90)
+    private BigDecimal FPrice;
+
+    @Excel(name = "实付金额",sort =91)
+    private BigDecimal payMoney;
+
+    @Excel(name = "额外运费",sort =100)
+    private BigDecimal payPostage;
+    private Integer totalNum;
+    @Excel(name = "商品分类",sort =100)
+    private String cateName;
+
+
+    private String jsonInfo;
+
+    /** 用户姓名 */
+    @Excel(name = "收货人姓名",sort =110)
+    private String realName;
+
+    /** 用户电话 */
+    @Excel(name = "收货人电话",sort =120)
+    private String userPhone;
+
+    /** 详细地址 */
+    @Excel(name = "详细地址",sort =130)
+    private String userAddress;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "下单时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 140)
+    private Date createTime;
+    /** 支付时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 150)
+    private Date payTime;
+
+    /** 快递公司编号 */
+    @Excel(name = "快递公司编号",sort = 160)
+    private String deliverySn;
+
+    /** 快递名称/送货人姓名 */
+    @Excel(name = "快递公司",sort = 170)
+    private String deliveryName;
+
+    /** 快递单号/手机号 */
+    @Excel(name = "快递单号",sort = 180)
+    private String deliveryId;
+
+    @Excel(name = "所属公司",sort = 190)
+    private String companyName;
+    @Excel(name = "所属销售",sort = 200)
+    private String companyUserNickName;
+
+    @Excel(name = "套餐名称",sort = 210)
+    private String packageName;
+
+    @Excel(name = "组合码",sort = 210)
+    private String groupBarCode;
+
+    @Excel(name = "是否上传凭证 0:未上传 1:已上传",sort = 210)
+    private Integer isUpload;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "上传时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss",sort = 220)
+    private Date uploadTime;
+
+    @Excel(name = "归属档期",sort = 230)
+    private String scheduleName;
+
+    //银行交易流水号
+    @Excel(name = "银行交易流水号",sort = 240)
+    private String bankTransactionId;
+
+
+}

+ 103 - 0
fs-service/src/main/java/com/fs/hisStore/vo/MergedAfterSalesVO.java

@@ -0,0 +1,103 @@
+package com.fs.hisStore.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 合并售后VO
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Data
+public class MergedAfterSalesVO implements Serializable
+{
+    /** 售后ID */
+    @Excel(name = "售后ID")
+    private Long id;
+
+    /** 订单号 */
+    @Excel(name = "订单号")
+    private String orderCode;
+
+    /** 退款金额 */
+    @Excel(name = "退款金额")
+    private BigDecimal refundAmount;
+
+    /** 服务类型0仅退款1退货退款 */
+    @Excel(name = "服务类型0仅退款1退货退款")
+    private Integer serviceType;
+
+    /** 申请原因 */
+    @Excel(name = "申请原因")
+    private String reasons;
+
+    /** 说明 */
+    @Excel(name = "说明")
+    private String explains;
+
+    /** 说明图片->多个用逗号分割 */
+    @Excel(name = "说明图片->多个用逗号分割")
+    private String explainImg;
+
+    /** 物流公司编码 */
+    @Excel(name = "物流公司编码")
+    private String shipperCode;
+
+    /** 物流单号 */
+    @Excel(name = "物流单号")
+    private String deliverySn;
+
+    /** 物流名称 */
+    @Excel(name = "物流名称")
+    private String deliveryName;
+
+    /** 状态 0已提交等待平台审核 1平台已审核 等待用户发货/退款 2 用户已发货 3退款成功 */
+    @Excel(name = "状态 0已提交等待平台审核 1平台已审核 等待用户发货/退款 2 用户已发货 3退款成功")
+    private Integer status;
+
+    /** 售后状态-0正常1用户取消2商家拒绝 */
+    @Excel(name = "售后状态-0正常1用户取消2商家拒绝")
+    private Integer salesStatus;
+
+    @Excel(name = "订单当前状态")
+    private Integer orderStatus;
+
+    /** 逻辑删除 */
+    @Excel(name = "逻辑删除")
+    private Integer isDel;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 商家收货人 */
+    @Excel(name = "商家收货人")
+    private String consignee;
+
+    /** 商家手机号 */
+    @Excel(name = "商家手机号")
+    private String phoneNumber;
+
+    /** 商家地址 */
+    @Excel(name = "商家地址")
+    private String address;
+
+    private Integer isPackage;
+
+    private String packageJson;
+
+    /** 售后类型 1商城售后 2直播售后 */
+    private Integer afterSalesType;
+
+    /** 售后类型名称 */
+    private String afterSalesTypeName;
+
+    /** 售后商品列表 */
+    private List<?> items;
+}
+

+ 15 - 0
fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/request/V2TradePaymentScanpayRefundRequest.java

@@ -52,6 +52,12 @@ public class V2TradePaymentScanpayRefundRequest extends BaseRequest {
     private String acctSplitBunch;
 
 
+    /**
+     *小程序
+     * **/
+    private String appId;
+
+
     @Override
     public FunctionCodeEnum getFunctionCode() {
         return FunctionCodeEnum.V2_TRADE_PAYMENT_SCANPAY_REFUND;
@@ -124,4 +130,13 @@ public class V2TradePaymentScanpayRefundRequest extends BaseRequest {
     public void setAcctSplitBunch(String acctSplitBunch) {
         this.acctSplitBunch = acctSplitBunch;
     }
+
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
 }

+ 8 - 0
fs-service/src/main/java/com/fs/live/domain/Live.java

@@ -3,14 +3,17 @@ package com.fs.live.domain;
 
 
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import com.fs.live.vo.LiveTagItemVO;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.time.LocalDateTime;
 import java.util.Date;
+import java.util.List;
 
 /**
  * 直播对象 live
@@ -124,5 +127,10 @@ public class   Live extends BaseEntity {
     private Date createTime;
     private String companyName;
     private Long fileSize;
+    private Long videoFileSize;
+    private Long videoDuration;
     private Integer globalVisible;
+
+    @TableField(exist = false)
+    private List<LiveTagItemVO> liveTagList;
 }

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

@@ -29,7 +29,7 @@ public class LiveAutoTask extends BaseEntity{
     @Excel(name = "任务名称")
     private String taskName;
 
-    /** 任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动 */
+    /** 任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动  4-抽奖 5-优惠券 6-自动上下架*/
     @Excel(name = "任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动 4-抽奖")
     private Long taskType;
 

+ 58 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java

@@ -0,0 +1,58 @@
+package com.fs.live.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 直播完课积分领取记录对象 live_completion_points_record
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCompletionPointsRecord extends BaseEntity {
+    
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 直播ID */
+    private Long liveId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 观看时长(秒) */
+    private Long watchDuration;
+
+    /** 视频总时长(秒) */
+    private Long videoDuration;
+
+    /** 完课比例(%) */
+    private BigDecimal completionRate;
+
+    /** 连续完课天数 */
+    private Integer continuousDays;
+
+    /** 获得积分 */
+    private Integer pointsAwarded;
+
+    /** 上次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date lastCompletionDate;
+
+    /** 本次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date currentCompletionDate;
+
+    /** 领取状态 0-未领取 1-已领取 */
+    private Integer receiveStatus;
+
+    /** 领取时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime;
+}

+ 6 - 2
fs-service/src/main/java/com/fs/live/domain/LiveCoupon.java

@@ -59,14 +59,18 @@ public class LiveCoupon extends BaseEntity
     @Excel(name = "套餐分类ids")
     private String packageCateIds;
 
-    /** 优惠券类型 0-通用 1-商品券 */
-    @Excel(name = "优惠券类型 0-通用 1-商品券")
+    /** 优惠券类型 0-普通 1-套餐 2-制单 3.无门槛 */
+    @Excel(name = "优惠券类型")
     private Long type;
 
     /** 是否删除 */
     @Excel(name = "是否删除")
     private Integer isDel;
 
+    /** 限制领取次数(针对无门槛优惠券,每个用户可以领取的最大张数) */
+    @Excel(name = "限制领取次数")
+    private Integer limitReceiveCount;
+
     private Long id;
     private Integer isShow;
     private Long goodsId;

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

@@ -69,4 +69,8 @@ public class LiveCouponIssue extends BaseEntity
     private BigDecimal useMinPrice;
     private Long couponTime;
 
+    /** 限制领取次数(针对无门槛优惠券,每个用户可以领取的最大张数) */
+    @Excel(name = "限制领取次数")
+    private Integer limitReceiveCount;
+
 }

+ 7 - 0
fs-service/src/main/java/com/fs/live/domain/LiveData.java

@@ -76,5 +76,12 @@ public class LiveData{
     @Excel(name = "关注数")
     private Long followNum;
 
+    /** 回放观看人次 */
+    @Excel(name = "回放观看人次")
+    private Long replayViewNum;
+
+    /** 回放点赞数 */
+    @Excel(name = "回放点赞数")
+    private Long replayLikeNum;
 
 }

+ 8 - 0
fs-service/src/main/java/com/fs/live/domain/LiveMsg.java

@@ -44,4 +44,12 @@ public class LiveMsg extends BaseEntity {
 
     @TableField(exist = false)
     private Integer singleVisible;
+
+    /** 直播消息标记:0-否 1-是 */
+    @Excel(name = "直播消息标记")
+    private Integer liveFlag = 0;
+
+    /** 回放消息标记:0-否 1-是 */
+    @Excel(name = "回放消息标记")
+    private Integer replayFlag = 0;
 }

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

@@ -352,4 +352,8 @@ public class LiveOrder extends BaseEntity {
     @TableField(exist = false)
     private Long attrValueId;
 
+    /** 小程序AppId */
+    @Excel(name = "小程序AppId")
+    private String appId;
+
 }

+ 66 - 0
fs-service/src/main/java/com/fs/live/domain/LiveTagConfig.java

@@ -0,0 +1,66 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播间标签配置对象 live_tag_config
+ *
+ * @author fs
+ * @date 2025-12-13
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveTagConfig extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 直播间id */
+    @Excel(name = "直播间id")
+    private Long liveId;
+
+    /** 企微主体id */
+    @Excel(name = "企微主体id")
+    private String corpId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 标记标签行为类型,数据字典live_mark_type */
+    @Excel(name = "标记标签行为类型,数据字典live_mark_type")
+    private Long markType;
+
+    /** 企微标签id */
+    @Excel(name = "企微标签id")
+    private Long qwTagId;
+
+    /** 企微标签真实id */
+    @Excel(name = "企微标签真实id")
+    private String qwTagRealId;
+
+    @Excel(name = "企微标签名称")
+    private String  qwTagName;
+
+    /** 创建人id */
+    @Excel(name = "创建人id")
+    private Long createUserId;
+
+    /** 创建人 */
+    @Excel(name = "创建人")
+    private String createUserName;
+
+    /** 更新人id */
+    @Excel(name = "更新人id")
+    private Long updateUserId;
+
+    /** 更新人 */
+    @Excel(name = "更新人")
+    private String updateUserName;
+
+
+}

+ 8 - 0
fs-service/src/main/java/com/fs/live/domain/LiveUserFirstEntry.java

@@ -33,6 +33,14 @@ public class LiveUserFirstEntry extends BaseEntity{
     @Excel(name = "公司id")
     private Long companyId;
 
+    /** 企微ID */
+    @Excel(name = "企微ID")
+    private Long qwUserId;
+
+    /** 外部联系人ID */
+    @Excel(name = "外部联系人ID")
+    private Long externalContactId;
+
     /** 公司用户id */
     @Excel(name = "公司用户id")
     private Long companyUserId;

+ 6 - 0
fs-service/src/main/java/com/fs/live/domain/LiveVideo.java

@@ -43,4 +43,10 @@ public class LiveVideo extends BaseEntity {
     private Long sort;
     @Excel(name = "转码状态")
     private Integer finishStatus;
+
+
+    /**
+     * 未转码数据
+     */
+    private String lineOne;
 }

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

@@ -27,7 +27,7 @@ public class LiveWatchConfig extends BaseEntity{
     private Boolean enabled;
 
     /** 参与条件 1达到指定观看时长 */
-    @Excel(name = "参与条件 1达到指定观看时长")
+    @Excel(name = "参与条件 1达到指定观看时长 2观看比例达到指定积分")
     private Long participateCondition;
 
     /** 观看时长 */

+ 89 - 0
fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java

@@ -0,0 +1,89 @@
+package com.fs.live.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播看课记录对象 live_watch_log
+ *
+ * @author fs
+ * @date 2025-12-12
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveWatchLog extends BaseEntity{
+
+    /** 日志id */
+    private Long logId;
+
+    /** 用户userId */
+    @Excel(name = "用户userId")
+    private Long userId;
+
+    /** 直播间id */
+    @Excel(name = "直播间id")
+    private Long liveId;
+
+    /** 记录类型 1看课中 2完课 3待看课 4看课中断 */
+    @Excel(name = "记录类型 1看课中 2完课 3待看课 4看课中断")
+    private Integer logType;
+
+    /** 外部联系人id */
+    @Excel(name = "外部联系人id")
+    private Long externalContactId;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 完课时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "完课时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date finishTime;
+
+    /** sop最后创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "sop最后创建时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date sopCreateTime;
+
+    /** 发送小程序appid */
+    @Excel(name = "发送小程序appid")
+    private String sendAppId;
+
+    /** 日志创建来源:1、个人sop,2、群聊sop,3、一键群发 */
+    @Excel(name = "日志创建来源:1、个人sop,2、群聊sop,3、一键群发")
+    private Integer logSource;
+
+    /** 分享人企微id */
+    @Excel(name = "分享人企微id")
+    private String qwUserId;
+    /**
+     * 查看直播类型:1、直播,2、回放
+     */
+    private Integer watchType;
+
+    /**
+     * 企微主体id
+     */
+    private String corpId;
+
+    /**
+     * 直播购买
+     */
+    private Integer liveBuy;
+
+    /**
+     * 回放购买
+     */
+    private Integer replayBuy;
+
+}

+ 16 - 0
fs-service/src/main/java/com/fs/live/domain/LiveWatchUser.java

@@ -50,4 +50,20 @@ public class LiveWatchUser extends BaseEntity {
     private String nickName;
     private String tabName;
 
+    /** 直播进入标记:0-否 1-是 */
+    @Excel(name = "直播进入标记")
+    private Integer liveFlag = 0;
+
+    /** 回放进入标记:0-否 1-是 */
+    @Excel(name = "回放进入标记")
+    private Integer replayFlag = 0;
+
+    /** 用户所在位置 */
+    @Excel(name = "用户所在位置")
+    private String location;
+
+
+    private Integer pageNum;
+    private Integer pageSize;
+
 }

+ 37 - 0
fs-service/src/main/java/com/fs/live/enums/LiveGoodsAddErrorEnum.java

@@ -0,0 +1,37 @@
+package com.fs.live.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 直播商品添加失败原因枚举
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@Getter
+@AllArgsConstructor
+public enum LiveGoodsAddErrorEnum {
+
+    /**
+     * 未上架
+     */
+    NOT_SHELVED("未上架"),
+
+    /**
+     * 未审核
+     */
+    NOT_AUDITED("未审核"),
+
+    /**
+     * 已删除
+     */
+    DELETED("已删除");
+
+    /**
+     * 错误描述
+     */
+    private String desc;
+
+}
+

+ 52 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java

@@ -0,0 +1,52 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课积分记录Mapper接口
+ */
+public interface LiveCompletionPointsRecordMapper {
+
+    /**
+     * 插入完课积分记录
+     */
+    int insertRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 更新完课积分记录
+     */
+    int updateRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 查询用户某天的完课记录
+     */
+    LiveCompletionPointsRecord selectByUserAndDate(@Param("liveId") Long liveId, 
+                                                     @Param("userId") Long userId, 
+                                                     @Param("currentDate") Date currentDate);
+
+    /**
+     * 查询用户最近一次完课记录(不限直播间,用于计算连续天数)
+     */
+    LiveCompletionPointsRecord selectLatestByUser(@Param("userId") Long userId);
+
+    /**
+     * 查询用户未领取的完课记录列表
+     */
+    List<LiveCompletionPointsRecord> selectUnreceivedByUser(@Param("liveId") Long liveId, 
+                                                             @Param("userId") Long userId);
+
+    /**
+     * 查询用户的完课积分领取记录列表
+     */
+    List<LiveCompletionPointsRecord> selectRecordsByUser(@Param("liveId") Long liveId, 
+                                                          @Param("userId") Long userId);
+
+    /**
+     * 根据ID查询
+     */
+    LiveCompletionPointsRecord selectById(@Param("id") Long id);
+}

+ 11 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCouponIssueUserMapper.java

@@ -4,6 +4,8 @@ import java.util.List;
 import com.fs.live.domain.LiveCouponIssueUser;
 import com.fs.live.domain.LiveCouponUser;
 import com.fs.live.param.CouponPO;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 /**
  * 优惠券用户领取记录Mapper接口
@@ -63,4 +65,13 @@ public interface LiveCouponIssueUserMapper
 
 
     List<LiveCouponUser> selectLiveCouponUserByCouponPO(CouponPO coupon);
+
+    /**
+     * 查询用户领取某个优惠券的次数
+     * @param userId 用户ID
+     * @param issueId 优惠券发放ID
+     * @return 领取次数
+     */
+    @Select("SELECT COUNT(1) FROM live_coupon_issue_user WHERE user_id = #{userId} AND issue_id = #{issueId} AND is_del = 0")
+    int countUserReceivedCoupon(@Param("userId") Long userId, @Param("issueId") Long issueId);
 }

+ 4 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveCouponMapper.java

@@ -112,7 +112,7 @@ public interface LiveCouponMapper
             "from live_coupon_issue_relation lcir left join live_coupon_issue lci " +
             "left join live_coupon lc on lc.coupon_id=lci.coupon_id on lci.id = lcir.coupon_issue_id  " +
             "left join live_goods lg on lg.goods_id = lcir.goods_id " +
-            "left join fs_store_product fsp on lg.product_id = fsp.product_id " +
+            "left join fs_store_product_scrm fsp on lg.product_id = fsp.product_id " +
             "where lcir.live_id = #{liveId} and lcir.goods_id is not null")
     List<LiveCoupon> listOn(@Param("liveId") Long liveId);
 
@@ -125,4 +125,7 @@ public interface LiveCouponMapper
     @Select("select * from live_coupon_issue_relation where live_id = #{liveId}")
     List<LiveCouponIssueRelation> selectCouponRelationByLiveId(@Param("liveId") Long liveId);
 
+    @Select("select * from live_coupon_issue_relation where coupon_issue_id = #{couponIssueId}")
+    List<LiveCouponIssueRelation> selectCouponRelationByCouponIssueId(@Param("couponIssueId") Long couponIssueId);
+
 }

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

@@ -68,7 +68,7 @@ public interface LiveCouponUserMapper
     @Select("<script>" +
             "select lcu.* from live_coupon_user lcu where lcu.user_id= #{coupon.userId} " +
             " <if test='coupon.goodsId != null'>" +
-            " and lcu.goods_id= #{coupon.goodsId}" +
+            " and (lcu.goods_id= #{coupon.goodsId} or lcu.goods_id is null or lcu.goods_id = 0)" +
             " </if>" +
             "</script>")
     List<LiveCouponUser> curCoupon(@Param("coupon") CouponPO coupon);

+ 20 - 2
fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java

@@ -3,8 +3,10 @@ package com.fs.live.mapper;
 
 import com.fs.live.domain.LiveData;
 import com.fs.live.vo.LiveDashBoardDataVo;
+import com.fs.live.vo.LiveDataDetailVo;
 import com.fs.live.vo.LiveDataListVo;
 import com.fs.live.vo.LiveDataStatisticsVo;
+import com.fs.live.vo.LiveUserDetailVo;
 import com.fs.live.vo.RecentLiveDataVo;
 import com.fs.live.vo.TrendDataVO;
 import org.apache.ibatis.annotations.Param;
@@ -131,8 +133,10 @@ public interface LiveDataMapper {
     List<Map<String, Object>> getCompanyChartData(@Param("chartStartDate") String chartStartDate,@Param("chartEndDate") String chartEndDate, @Param("format") String format,@Param("category") String category,@Param("companyId") Long companyId);
 
     @Select("SELECT " +
-            "    ld.total_views AS viewNum,                        " +
-            "    ld.likes AS likeNum                        " +
+            "    ld.total_views AS liveViewNum, " +
+            "    ld.replay_view_num AS replayViewNum, " +
+            "    ld.likes AS liveLikeNum, " +
+            "    ld.replay_like_num AS replayLikeNum " +
             "FROM " +
             "    live_data ld " +
             "where ld.live_id=#{liveId}")
@@ -151,4 +155,18 @@ public interface LiveDataMapper {
      * @return 列表数据
      */
     List<LiveDataListVo> selectLiveDataListByLiveIds(@Param("liveIds") List<Long> liveIds);
+
+    /**
+     * 查询直播间详情数据(SQL方式)
+     * @param liveId 直播间ID
+     * @return 详情数据
+     */
+    LiveDataDetailVo selectLiveDataDetailBySql(@Param("liveId") Long liveId);
+
+    /**
+     * 查询直播间用户详情列表(SQL方式)
+     * @param liveId 直播间ID
+     * @return 用户详情列表
+     */
+    List<LiveUserDetailVo> selectLiveUserDetailListBySql(@Param("liveId") Long liveId,@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
 }

+ 8 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveGoodsMapper.java

@@ -149,4 +149,12 @@ public interface LiveGoodsMapper {
             "ELSE 0 END where live_id = #{liveId}" +
             "</script>"})
     void updateLiveIsShow(@Param("goodsId")Long goodsId,@Param("liveId") Long liveId);
+
+    /**
+     * 根据goodsId更新商品上下架状态
+     * @param goodsId 商品ID
+     * @param status 状态:1-上架 0-下架
+     */
+    @Update("update live_goods set status = #{status} where goods_id = #{goodsId}")
+    void updateLiveGoodsStatus(@Param("goodsId") Long goodsId, @Param("status") Integer status);
 }

+ 88 - 6
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -2,6 +2,7 @@ package com.fs.live.mapper;
 
 
 import com.fs.live.domain.Live;
+import com.fs.live.param.LiveDataParam;
 import com.fs.live.vo.LiveListVo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
@@ -75,7 +76,7 @@ public interface LiveMapper
      * @param liveIds 需要删除的数据主键集合
      * @return 结果
      */
-    public int deleteLiveByLiveIds(Long[] liveIds);
+    public int deleteLiveByLiveIds(@Param("liveIds") Long[] liveIds,@Param("live") Live live);
 
     List<Live> liveList();
 
@@ -117,6 +118,15 @@ public interface LiveMapper
     @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1")
     List<Live> selectNoEndLiveList();
 
+    /**
+     * 查询开启了完课积分配置的直播间(用于完课积分定时任务)
+     * @return 直播列表
+     */
+    @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1 " +
+            "and config_json is not null " +
+            "and JSON_EXTRACT(config_json, '$.enabled') = true")
+    List<Live> selectLiveListWithCompletionPointsEnabled();
+
     void updateStatusAndTimeBatchById(@Param("liveList") List<Live> list);
 
     @Select("select * from live where (company_id = #{companyId} or company_id is null)  and is_audit = 1 and is_del = 0 and is_show = 1 and status != 3\n")
@@ -133,15 +143,87 @@ public interface LiveMapper
     @Update("update live set global_visible = #{status} where live_id = #{liveId}")
     void updateGlobalVisible(@Param("liveId")Long liveId,@Param("status") Integer status);
 
-    @Select("select * from live where company_id = #{companyId} and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1")
-    List<Live> listLiveData(@Param("companyId")Long companyId);
-
-    @Select("select count(1) from live where company_id = #{companyId} and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1")
-    int listLiveDataCount(@Param("companyId") Long companyId);
+    @Select({"<script>" +
+            "select * from live where 1=1 " +
+            " <if test='param.companyId!=null' > and company_id = #{param.companyId} </if> and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1 " +
+            " <if test='param.liveName!=null' > and live_name like concat('%' ,#{param.liveName},'%') </if> " +
+            " <if test='param.startTime!=null and param.endTime!=null' > and start_time between #{param.startTime} and  #{param.endTime}  </if> " +
+            " UNION " +
+            "select l.* from live l " +
+            "LEFT JOIN ( " +
+            "    SELECT live_id, SUM(COALESCE(duration, 0)) AS total_duration " +
+            "    FROM live_video " +
+            "    WHERE video_type IN (1, 2) " +
+            "    GROUP BY live_id " +
+            ") video_duration ON l.live_id = video_duration.live_id " +
+            "where 1=1 " +
+            " <if test='param.companyId!=null' > and l.company_id = #{param.companyId} </if> " +
+            "and l.live_type IN (1,2, 3) AND l.status = 2 AND l.is_del = 0 and l.is_audit=1 " +
+            "and l.start_time IS NOT NULL " +
+            "and TIMESTAMPDIFF(SECOND, l.start_time, NOW()) > COALESCE(video_duration.total_duration, 0) " +
+            "and COALESCE(video_duration.total_duration, 0) > 0 " +
+            " <if test='param.liveName!=null' > and l.live_name like concat('%' ,#{param.liveName},'%') </if> " +
+            " <if test='param.startTime!=null and param.endTime!=null' > and l.start_time between #{param.startTime} and  #{param.endTime}  </if> " +
+            "order by create_time desc" +
+            " </script>"})
+    List<Live> listLiveData(@Param("param") LiveDataParam param);
+
+    @Select({"<script>" +
+            "select count(1) from ( " +
+            "select * from live where 1=1 " +
+            " <if test='param.companyId!=null' > and company_id = #{param.companyId} </if> and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1 " +
+            " <if test='param.liveName!=null' > and live_name like concat('%' ,#{param.liveName},'%') </if> " +
+            " <if test='param.startTime!=null and param.endTime!=null' > and start_time between #{param.startTime} and  #{param.endTime}  </if> " +
+            " UNION " +
+            "select l.* from live l " +
+            "LEFT JOIN ( " +
+            "    SELECT live_id, SUM(COALESCE(duration, 0)) AS total_duration " +
+            "    FROM live_video " +
+            "    WHERE video_type IN (1, 2) " +
+            "    GROUP BY live_id " +
+            ") video_duration ON l.live_id = video_duration.live_id " +
+            "where 1=1 " +
+            " <if test='param.companyId!=null' > and l.company_id = #{param.companyId} </if> " +
+            "and l.live_type IN (1,2, 3) AND l.status = 2 AND l.is_del = 0 and l.is_audit=1 " +
+            "and l.start_time IS NOT NULL " +
+            "and TIMESTAMPDIFF(SECOND, l.start_time, NOW()) > COALESCE(video_duration.total_duration, 0) " +
+            "and COALESCE(video_duration.total_duration, 0) > 0 " +
+            " <if test='param.liveName!=null' > and l.live_name like concat('%' ,#{param.liveName},'%') </if> " +
+            " <if test='param.startTime!=null and param.endTime!=null' > and l.start_time between #{param.startTime} and  #{param.endTime}  </if> " +
+            ") as temp " +
+            " </script>"})
+    int listLiveDataCount(@Param("param") LiveDataParam param);
 
 
     List<Live> liveShowList(@Param("companyIds") List<Long> companyIds);
 
     List<Live> selectLiveShowReadyStartLiveList(@Param("companyIds") List<Long> companyIds);
 
+    @Select("select * from live where is_audit = 1 and is_del = 0 and status in (1,2,4) and live_type in (2,3) order by create_time desc")
+    List<Live> liveListAll();
+
+    /**
+     * 查询直播间是直播还是回放状态
+     * 判断标准:当前直播间开始时间 + 视频类型为1的视频时长,如果大于当前时间,返回1,否则返回0
+     * @param liveId 直播间ID
+     * @return 1表示直播中,0表示回放中
+     */
+    @Select("SELECT CASE " +
+            "WHEN l.start_time IS NOT NULL AND " +
+            "     (l.start_time + INTERVAL COALESCE(SUM(CASE WHEN lv.video_type = 1 THEN lv.duration ELSE 0 END), 0) SECOND) > NOW() " +
+            "THEN 1 " +
+            "ELSE 0 " +
+            "END AS liveFlag " +
+            "FROM live l " +
+            "LEFT JOIN live_video lv ON l.live_id = lv.live_id AND lv.video_type = 1 " +
+            "WHERE l.live_id = #{liveId} " +
+            "GROUP BY l.live_id, l.start_time")
+    Integer selectLiveFlagByLiveId(@Param("liveId") Long liveId);
+
+    @Select({"<script>" +
+            " SELECT * FROM live WHERE is_audit = 1 and is_del = 0 and status in (1,2,4) and live_type in (2,3) " +
+            "  <if test='live.liveName!=null' > and live_name like concat('%',#{live.liveName},'%') </if> " +
+            " order by create_time desc" +
+            " </script>"})
+    List<Live> listToLiveNoEnd(@Param("live") Live live);
 }

+ 5 - 2
fs-service/src/main/java/com/fs/live/mapper/LiveMsgMapper.java

@@ -77,8 +77,11 @@ public interface LiveMsgMapper
     @Select("select * from live_msg where live_id = #{liveId} order by create_time desc limit 30")
     List<LiveMsg> listRecentMsg(@Param("liveId")Long liveId);
 
-    @Select("SELECT count(1) as commentNum from live_msg where live_id = #{liveId}")
-    Map<String, Long> selectDashboardCount(@Param("liveId") Long liveId);
+    @Select("SELECT " +
+            "    SUM(CASE WHEN live_flag = 1 THEN 1 ELSE 0 END) AS liveCommentNum, " +
+            "    SUM(CASE WHEN replay_flag = 1 THEN 1 ELSE 0 END) AS replayCommentNum " +
+            "FROM live_msg WHERE live_id = #{liveId}")
+    Map<String, BigDecimal> selectDashboardCount(@Param("liveId") Long liveId);
 
     List<LiveMsg> selectLiveMsgSingleList(LiveMsg liveMsg);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java

@@ -166,4 +166,7 @@ public interface LiveOrderItemMapper {
             " order by o.id desc limit 50000"+
             "</script>"})
     List<LiveOrderItemExportVO> selectFsStoreOrderItemListExportVO(@Param("maps") LiveOrderParam fsStoreOrder);
+
+
+    List<LiveOrderItemListUVO> selectLiveOrderItemListUVOByOrderIds(@Param("orderIds")List<Long> orderIds);
 }

+ 8 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveOrderMapper.java

@@ -1,6 +1,7 @@
 package com.fs.live.mapper;
 
 
+import com.fs.hisStore.vo.FsStoreOrderItemExportZMVO;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.dto.LiveOrderDeliveryNoteDTO;
 import com.fs.live.param.FsMyLiveOrderQueryParam;
@@ -91,6 +92,9 @@ public interface LiveOrderMapper {
     @Update("update live_order set `status`=-3 where order_id=#{orderId}")
     int cancelOrder(Long orderId);
 
+    @Update("update live_order set `status`=-3 where order_code=#{orderCode}")
+    int cancelOrderByCode(@Param("orderCode") String orderCode);
+
     @Select({"<script> " +
             "select * from live_order " +
             "<where>" +
@@ -369,7 +373,8 @@ public interface LiveOrderMapper {
     int batchUpdateErpByOrderIds(@Param("maps")ArrayList<Map<String, String>> maps);
 
     @Select({"<script> " +
-            "select o.order_id,o.total_num,o.create_time, o.discount_money ,o.live_id,o.order_code,o.item_json,o.pay_price,o.status,o.delivery_sn as delivery_id,o.finish_time  from live_order o  " +
+            "select a.id as afterSalesId,o.order_id,o.total_num,o.create_time, o.discount_money ,o.live_id,o.order_code,o.item_json,o.pay_price,o.status,o.delivery_sn as delivery_id,o.finish_time  from live_order o  " +
+            " left join ( SELECT t.*,ROW_NUMBER() OVER (PARTITION BY t.order_id ORDER BY t.create_time DESC) AS rn FROM live_after_sales t ) a ON o.order_id = a.order_id AND a.rn = 1 " +
             "where o.is_del=0 " +
             "<if test = 'maps.status != null and maps.status != \"\"     '> " +
             "and o.status =#{maps.status} " +
@@ -418,6 +423,8 @@ public interface LiveOrderMapper {
 
     List<LiveOrderVoZm> selectLiveOrderListZm(LiveOrder liveOrder);
 
+    List<FsStoreOrderItemExportZMVO> selectLiveOrderListZmNew(LiveOrder liveOrder);
+
     @Select(" SELECT * from live_order WHERE item_json is NULL ORDER BY create_time DESC  LIMIT 30")
     List<LiveOrder> selectLiveOrderItemJson();
 

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