Преглед изворни кода

Merge branch 'refs/heads/master' into master-clq-temp

# Conflicts:
#	fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java
#	fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
caoliqin пре 4 дана
родитељ
комит
05069f5c9d
100 измењених фајлова са 3325 додато и 210 уклоњено
  1. 5 1
      fs-admin/pom.xml
  2. 9 1
      fs-admin/src/main/java/com/fs/company/controller/CompanyWxAccountController.java
  3. 15 1
      fs-admin/src/main/java/com/fs/his/controller/FsCourseCouponUserController.java
  4. 1 1
      fs-admin/src/main/java/com/fs/his/controller/FsExpressCommonController.java
  5. 20 3
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java
  6. 1 1
      fs-admin/src/main/java/com/fs/his/task/Task.java
  7. 30 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreDeliveryAbnormalOrderController.java
  8. 19 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  9. 22 6
      fs-admin/src/main/java/com/fs/live/controller/LiveAutoTaskController.java
  10. 91 0
      fs-admin/src/main/java/com/fs/live/controller/LiveGroupTypeController.java
  11. 103 0
      fs-admin/src/main/java/com/fs/live/controller/LiveRedPacketLogController.java
  12. 68 1
      fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java
  13. 14 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  14. 83 0
      fs-company-app/src/main/java/com/fs/app/controller/LiveController.java
  15. 5 0
      fs-company/pom.xml
  16. 17 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseCouponUserController.java
  17. 213 0
      fs-company/src/main/java/com/fs/company/controller/scrm/ScrmCustomerController.java
  18. 30 9
      fs-company/src/main/java/com/fs/hisStore/controller/FsIntegralOrderController.java
  19. 57 59
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  20. 1 1
      fs-company/src/main/java/com/fs/hisStore/utils/CityTreeUtil.java
  21. 1 1
      fs-company/src/main/java/com/fs/hisStore/vo/CityVO.java
  22. 97 0
      fs-company/src/main/java/com/fs/live/LiveCouponUserController.java
  23. 104 0
      fs-company/src/main/java/com/fs/live/LiveGroupTypeController.java
  24. 111 0
      fs-company/src/main/java/com/fs/live/LiveRedPacketLogController.java
  25. 3 0
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  26. 1 1
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  27. 14 14
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  28. 5 0
      fs-service/pom.xml
  29. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java
  30. 3 1
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java
  31. 5 3
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  32. 3 0
      fs-service/src/main/java/com/fs/core/config/WxMaConfiguration.java
  33. 39 0
      fs-service/src/main/java/com/fs/course/dto/VideoCollectionDTO.java
  34. 8 4
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  35. 57 18
      fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java
  36. 9 3
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  37. 1 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoServiceImpl.java
  38. 3 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoH5DVO.java
  39. 10 0
      fs-service/src/main/java/com/fs/course/vo/FsUserVideoListUVO.java
  40. 2 1
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  41. 137 0
      fs-service/src/main/java/com/fs/framework/liquibase/ChangelogMd5Service.java
  42. 34 0
      fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseAutoConfiguration.java
  43. 102 0
      fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseDependsOnConfigurer.java
  44. 128 0
      fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseMetaRepository.java
  45. 192 0
      fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseMigrationRunner.java
  46. 87 0
      fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseProperties.java
  47. 139 0
      fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseStartupGate.java
  48. 36 0
      fs-service/src/main/java/com/fs/framework/liquibase/MasterDataSourceValidator.java
  49. 10 0
      fs-service/src/main/java/com/fs/his/domain/FsCourseCouponUser.java
  50. 11 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java
  51. 17 0
      fs-service/src/main/java/com/fs/his/mapper/CompanyIntegralOrderMapper.java
  52. 27 7
      fs-service/src/main/java/com/fs/his/mapper/FsCourseCouponUserMapper.java
  53. 7 1
      fs-service/src/main/java/com/fs/his/mapper/FsUserCouponMapper.java
  54. 17 0
      fs-service/src/main/java/com/fs/his/service/ICompanyIntegralOrderService.java
  55. 9 7
      fs-service/src/main/java/com/fs/his/service/IFsCourseCouponUserService.java
  56. 7 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderService.java
  57. 99 0
      fs-service/src/main/java/com/fs/his/service/impl/CompanyIntegralOrderServiceImpl.java
  58. 8 7
      fs-service/src/main/java/com/fs/his/service/impl/FsCourseCouponServiceImpl.java
  59. 13 7
      fs-service/src/main/java/com/fs/his/service/impl/FsCourseCouponUserServiceImpl.java
  60. 9 3
      fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderServiceImpl.java
  61. 89 1
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java
  62. 73 0
      fs-service/src/main/java/com/fs/his/vo/FsCourseCouponUserVO.java
  63. 7 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderExcelVO.java
  64. 6 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderListVO.java
  65. 2 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderPVO.java
  66. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  67. 4 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  68. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsCityScrmMapper.java
  69. 6 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  70. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  71. 12 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  72. 4 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductAddEditParam.java
  73. 6 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsCityScrmService.java
  74. 10 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsExpressScrmService.java
  75. 10 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  76. 26 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsCityScrmServiceImpl.java
  77. 155 13
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsExpressScrmServiceImpl.java
  78. 12 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderItemScrmServiceImpl.java
  79. 161 12
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  80. 1 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java
  81. 54 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreDeliveryAbnormalOrderExportVO.java
  82. 6 2
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderExportVO.java
  83. 6 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java
  84. 6 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportZMVO.java
  85. 16 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderRefundAmountVO.java
  86. 5 0
      fs-service/src/main/java/com/fs/live/domain/Live.java
  87. 15 2
      fs-service/src/main/java/com/fs/live/domain/LiveCouponUser.java
  88. 27 0
      fs-service/src/main/java/com/fs/live/domain/LiveGroupType.java
  89. 7 0
      fs-service/src/main/java/com/fs/live/domain/LiveRedPacketLog.java
  90. 28 2
      fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java
  91. 5 1
      fs-service/src/main/java/com/fs/live/domain/LiveWatchUser.java
  92. 7 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponIssueMapper.java
  93. 35 2
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java
  94. 61 0
      fs-service/src/main/java/com/fs/live/mapper/LiveGroupTypeMapper.java
  95. 5 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  96. 12 1
      fs-service/src/main/java/com/fs/live/param/CouponPO.java
  97. 23 0
      fs-service/src/main/java/com/fs/live/param/FsLiveListParam.java
  98. 26 0
      fs-service/src/main/java/com/fs/live/param/FsLiveSortLinkParam.java
  99. 1 1
      fs-service/src/main/java/com/fs/live/param/LiveAutoTaskImportParam.java
  100. 19 0
      fs-service/src/main/java/com/fs/live/param/LiveCouponVerifyParam.java

+ 5 - 1
fs-admin/pom.xml

@@ -44,7 +44,11 @@
             <artifactId>fs-framework</artifactId>
         </dependency>
 
-
+        <!-- Liquibase(启动模块显式依赖) -->
+        <dependency>
+            <groupId>org.liquibase</groupId>
+            <artifactId>liquibase-core</artifactId>
+        </dependency>
 
         <!-- 定时任务-->
         <dependency>

+ 9 - 1
fs-admin/src/main/java/com/fs/company/controller/CompanyWxAccountController.java

@@ -7,7 +7,9 @@ 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.company.domain.CompanyWxAccount;
+import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyWxAccountService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -17,7 +19,7 @@ import java.util.List;
 
 /**
  * 企微账号Controller
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */
@@ -28,6 +30,9 @@ public class CompanyWxAccountController extends BaseController
     @Autowired
     private ICompanyWxAccountService companyWxEnterpriseAccountService;
 
+    @Autowired
+    private ICompanyUserService userService;
+
     /**
      * 查询企微账号列表
      */
@@ -80,6 +85,9 @@ public class CompanyWxAccountController extends BaseController
     @PostMapping
     public AjaxResult add(@RequestBody CompanyWxAccount companyWxAccount)
     {
+        Long companyUserId = companyWxAccount.getCompanyUserId();
+        CompanyUser companyUser = userService.selectCompanyUserById(companyUserId);
+        companyWxAccount.setCompanyId(companyUser.getCompanyId());
         return toAjax(companyWxEnterpriseAccountService.insertCompanyWxAccount(companyWxAccount));
     }
 

+ 15 - 1
fs-admin/src/main/java/com/fs/his/controller/FsCourseCouponUserController.java

@@ -1,6 +1,8 @@
 package com.fs.his.controller;
 
 import java.util.List;
+
+import com.fs.his.vo.FsCourseCouponUserVO;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -22,7 +24,7 @@ import com.fs.common.core.page.TableDataInfo;
 
 /**
  * 用户看课优惠券Controller
- * 
+ *
  * @author fs
  * @date 2026-05-13
  */
@@ -45,6 +47,18 @@ public class FsCourseCouponUserController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询用户看课优惠券列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:courseCouponUser:list')")
+    @GetMapping("/listVO")
+    public TableDataInfo listVO(FsCourseCouponUser fsCourseCouponUser)
+    {
+        startPage();
+        List<FsCourseCouponUserVO> list = fsCourseCouponUserService.selectFsCourseCouponUserListVO(fsCourseCouponUser);
+        return getDataTable(list);
+    }
+
     /**
      * 导出用户看课优惠券列表
      */

+ 1 - 1
fs-admin/src/main/java/com/fs/his/controller/FsExpressCommonController.java

@@ -89,7 +89,7 @@ public class FsExpressCommonController extends BaseController {
 
 		} else{
 			FsUser newUser=new FsUser();
-			newUser.setPhone(tl.getPhoneNum());
+			newUser.setPhone(encryptPhone(tl.getPhoneNum()));
 			newUser.setNickName("微信用户"+tl.getPhoneNum().substring(tl.getPhoneNum().length()-4));
 			newUser.setStatus(1);
 			newUser.setIntegralStatus(1);

+ 20 - 3
fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java

@@ -1,6 +1,7 @@
 package com.fs.his.controller;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.BeanCopyUtils;
@@ -245,6 +246,8 @@ public class FsIntegralOrderController extends BaseController
             }
 
         }
+
+        fsIntegralOrderService.fillExportWatchAttribution(newFsIntegralOrderListVOS);
         ExcelUtil<FsIntegralOrderListVO> util = new ExcelUtil<>(FsIntegralOrderListVO.class);
         return util.exportExcel(new ArrayList<>(newFsIntegralOrderListVOS), "积分商品订单数据");
     }
@@ -351,9 +354,23 @@ public class FsIntegralOrderController extends BaseController
             if (StringUtils.isNotEmpty(fsIntegralOrder.getDeliverySn())) {
                 String lastFourNumber = "";
                 if (fsIntegralOrder.getDeliveryCode().equals(ShipperCodeEnum.SF.getValue())) {
-
-                    lastFourNumber = fsIntegralOrder.getUserPhone();
-                    if (lastFourNumber.length() == 11) {
+                    if("恒春来".equals(cloudHostProper.getCompanyName())
+                            && ObjectUtil.isNotEmpty(lastFourNumber = fsIntegralOrder.getVirtualPhone())){
+                        if (lastFourNumber.contains("-")) {
+                            String beforeDash = lastFourNumber.split("-")[0];
+                            lastFourNumber = beforeDash.length() >= 4 ? beforeDash.substring(beforeDash.length() - 4) : beforeDash;
+                        }else{
+                            lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
+                        }
+                        expressInfoDTO=expressService.getExpressInfo(fsIntegralOrder.getOrderCode(),fsIntegralOrder.getDeliverySn(),fsIntegralOrder.getDeliverySn(),lastFourNumber);
+                        if(expressInfoDTO!=null && !expressInfoDTO.isSuccess()){
+                            lastFourNumber = StrUtil.sub(fsIntegralOrder.getVirtualPhone(), fsIntegralOrder.getVirtualPhone().length(), -4);
+                            expressInfoDTO=expressService.getExpressInfo(fsIntegralOrder.getOrderCode(),fsIntegralOrder.getDeliverySn(),fsIntegralOrder.getDeliverySn(),lastFourNumber);
+                        }
+                        return R.ok().put("data",expressInfoDTO);
+                    }
+                    // 原逻辑
+                    else if ((lastFourNumber = fsIntegralOrder.getUserPhone()).length() == 11) {
                         lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                     }
                 }

+ 1 - 1
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -2057,7 +2057,7 @@ public class Task {
         List<QwCompany> companies = qwCompanyService.selectQwCompanyList(new QwCompany()); // 获取所有企业
         for (QwCompany company : companies) {
             try {
-                syncService.syncConversationsForCorp(company.getCorpId());
+                syncService.syncConversationsForCorp(company.getCorpId(),null);
             } catch (Exception e) {
                 log.error("同步企业 {} 会话失败", company.getCorpName());
                 log.error(e.getMessage());

+ 30 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreDeliveryAbnormalOrderController.java

@@ -1,9 +1,13 @@
 package com.fs.hisStore.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.domain.model.LoginUser;
+import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.CloudHostUtils;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
@@ -31,6 +35,7 @@ import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStoreOrderStatusScrmService;
 import com.fs.hisStore.service.IFsStorePaymentScrmService;
+import com.fs.hisStore.vo.FsStoreDeliveryAbnormalOrderExportVO;
 import com.fs.hisStore.vo.FsStoreOrderAuditLogVO;
 import com.fs.hisStore.vo.FsStoreOrderVO;
 import com.github.pagehelper.PageHelper;
@@ -130,6 +135,31 @@ public class FsStoreDeliveryAbnormalOrderController extends BaseController {
         return vo;
     }
 
+    /**
+     * 导出物流异常订单
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeDeliveryAbnormalOrder:export')")
+    @Log(title = "物流异常订单", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public AjaxResult export(@RequestBody FsStoreOrderParam param) {
+        normalizeTimeRange(param);
+        param.setDeliveryStatus(4);
+        if (fsStoreOrderService.isEntityNull(param)) {
+            return AjaxResult.error("请筛选数据导出");
+        }
+        List<FsStoreOrderVO> list = fsStoreOrderService.selectFsStoreOrderListVO(param);
+        maskSensitiveFields(list);
+        List<FsStoreDeliveryAbnormalOrderExportVO> exportList = new ArrayList<>();
+        for (FsStoreOrderVO vo : list) {
+            FsStoreDeliveryAbnormalOrderExportVO exportVO = new FsStoreDeliveryAbnormalOrderExportVO();
+            BeanUtils.copyProperties(vo, exportVO);
+            exportList.add(exportVO);
+        }
+        ExcelUtil<FsStoreDeliveryAbnormalOrderExportVO> util =
+                new ExcelUtil<>(FsStoreDeliveryAbnormalOrderExportVO.class);
+        return util.exportExcel(exportList, "物流异常订单");
+    }
+
     /**
      * 获取物流异常订单详情
      */

+ 19 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -5,6 +5,8 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.FSApplication;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
@@ -66,6 +68,7 @@ import com.fs.hisStore.dto.FsStoreOrderPayPostageEditDTO;
 import com.fs.hisStore.dto.StoreOrderExpressExportDTO;
 import com.fs.hisStore.dto.StoreOrderProductDTO;
 import com.fs.hisStore.enums.ShipperCodeEnum;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
@@ -74,10 +77,12 @@ import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysRoleService;
 import com.github.pagehelper.PageHelper;
 import io.swagger.annotations.ApiOperation;
+import org.junit.Test;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -167,6 +172,9 @@ public class FsStoreOrderScrmController extends BaseController {
     @Autowired
     private IFsUserCourseVideoService fsUserCourseVideoService;
 
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderMapper;
+
     @Value("${cloud_host.company_name}")
     private String signProjectName;
 
@@ -777,6 +785,17 @@ public class FsStoreOrderScrmController extends BaseController {
                         lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
                         expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
                     }
+                    if(expressInfoDTO!=null){
+                        FsStoreOrderScrm map = new FsStoreOrderScrm();
+                        map.setDeliveryStatus(Integer.parseInt(expressInfoDTO.getState()));
+                        map.setId(order.getId());
+                        map.setDeliveryType(expressInfoDTO.getStateEx());
+                        fsStoreOrderMapper.updateFsStoreOrder(map);
+                        //如果是正常签收,更新订单状态
+                        if (expressInfoDTO.getState().equals("3") && (expressInfoDTO.getStateEx().equals("301") || expressInfoDTO.getStateEx().equals("302") || expressInfoDTO.getStateEx().equals("304") || expressInfoDTO.getStateEx().equals("311"))) {
+                            fsStoreOrderService.finishOrder(order.getId());
+                        }
+                    }
                     return R.ok().put("data",expressInfoDTO);
                 }
                 // 原逻辑

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

@@ -193,21 +193,37 @@ public class LiveAutoTaskController extends BaseController
     }
 
     public static Date formatTime(String duration) {
-        String[] parts = duration.split(":");
-        if (parts.length != 3) {
+
+        if (duration == null || duration.trim().isEmpty()) {
+            throw new IllegalArgumentException("时间字符串不能为空");
+        }
+
+        // 1. 去除首尾空格
+        String cleaned = duration.trim();
+
+        // 2. 只保留数字和冒号
+        cleaned = cleaned.replaceAll("[^0-9:]", "");
+
+        // 3. 验证格式
+        if (!cleaned.matches("\\d{1,2}:\\d{1,2}:\\d{1,2}")) {
             throw new IllegalArgumentException("时间格式不正确,应为 hh:mm:ss");
         }
+
+        String[] parts = cleaned.split(":");
         int hours = Integer.parseInt(parts[0]);
         int minutes = Integer.parseInt(parts[1]);
         int seconds = Integer.parseInt(parts[2]);
 
-        // 获取当前日期的Calendar实例
+        // 4. 验证数值范围
+        if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) {
+            throw new IllegalArgumentException("时间数值超出有效范围");
+        }
+
         Calendar calendar = Calendar.getInstance();
-        // 设置时分秒
-        calendar.set(Calendar.HOUR_OF_DAY, hours); // 24小时制
+        calendar.set(Calendar.HOUR_OF_DAY, hours);
         calendar.set(Calendar.MINUTE, minutes);
         calendar.set(Calendar.SECOND, seconds);
-        calendar.set(Calendar.MILLISECOND, 0); // 毫秒设为0
+        calendar.set(Calendar.MILLISECOND, 0);
 
         return calendar.getTime();
     }

+ 91 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveGroupTypeController.java

@@ -0,0 +1,91 @@
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.live.domain.LiveGroupType;
+import com.fs.live.service.ILiveGroupTypeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 直播间分组分类Controller
+ *
+ * @author fs
+ * @date 2026-06-16
+ */
+@RestController
+@RequestMapping("/live/liveGroupType")
+public class LiveGroupTypeController extends BaseController
+{
+    @Autowired
+    private ILiveGroupTypeService liveGroupTypeService;
+
+    /**
+     * 查询直播间分组分类列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(LiveGroupType liveGroupType)
+    {
+        startPage();
+        List<LiveGroupType> list = liveGroupTypeService.selectLiveGroupTypeList(liveGroupType);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出直播间分组分类列表
+     */
+    @Log(title = "直播间分组分类", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveGroupType liveGroupType)
+    {
+        List<LiveGroupType> list = liveGroupTypeService.selectLiveGroupTypeList(liveGroupType);
+        ExcelUtil<LiveGroupType> util = new ExcelUtil<LiveGroupType>(LiveGroupType.class);
+        return util.exportExcel(list, "直播间分组分类数据");
+    }
+
+    /**
+     * 获取直播间分组分类详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(liveGroupTypeService.selectLiveGroupTypeById(id));
+    }
+
+    /**
+     * 新增直播间分组分类
+     */
+    @Log(title = "直播间分组分类", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveGroupType liveGroupType)
+    {
+        return toAjax(liveGroupTypeService.insertLiveGroupType(liveGroupType));
+    }
+
+    /**
+     * 修改直播间分组分类
+     */
+    @Log(title = "直播间分组分类", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveGroupType liveGroupType)
+    {
+        return toAjax(liveGroupTypeService.updateLiveGroupType(liveGroupType));
+    }
+
+    /**
+     * 删除直播间分组分类
+     */
+    @Log(title = "直播间分组分类", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(liveGroupTypeService.deleteLiveGroupTypeByIds(ids));
+    }
+}

+ 103 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveRedPacketLogController.java

@@ -0,0 +1,103 @@
+package com.fs.live.controller;
+
+import java.util.List;
+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.LiveRedPacketLog;
+import com.fs.live.service.ILiveRedPacketLogService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 直播红包 记录Controller
+ *
+ * @author fs
+ * @date 2026-06-15
+ */
+@RestController
+@RequestMapping("/live/liveRedPacketLog")
+public class LiveRedPacketLogController extends BaseController
+{
+    @Autowired
+    private ILiveRedPacketLogService liveRedPacketLogService;
+
+    /**
+     * 查询直播红包 记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveRedPacketLog liveRedPacketLog)
+    {
+        startPage();
+        List<LiveRedPacketLog> list = liveRedPacketLogService.selectLiveRedPacketLogList(liveRedPacketLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出直播红包 记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:export')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveRedPacketLog liveRedPacketLog)
+    {
+        List<LiveRedPacketLog> list = liveRedPacketLogService.selectLiveRedPacketLogList(liveRedPacketLog);
+        ExcelUtil<LiveRedPacketLog> util = new ExcelUtil<LiveRedPacketLog>(LiveRedPacketLog.class);
+        return util.exportExcel(list, "直播红包 记录数据");
+    }
+
+    /**
+     * 获取直播红包 记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+    {
+        return AjaxResult.success(liveRedPacketLogService.selectLiveRedPacketLogByLogId(logId));
+    }
+
+    /**
+     * 新增直播红包 记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:add')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveRedPacketLog liveRedPacketLog)
+    {
+        return toAjax(liveRedPacketLogService.insertLiveRedPacketLog(liveRedPacketLog));
+    }
+
+    /**
+     * 修改直播红包 记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:edit')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveRedPacketLog liveRedPacketLog)
+    {
+        return toAjax(liveRedPacketLogService.updateLiveRedPacketLog(liveRedPacketLog));
+    }
+
+    /**
+     * 删除直播红包 记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:remove')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{logIds}")
+    public AjaxResult remove(@PathVariable Long[] logIds)
+    {
+        return toAjax(liveRedPacketLogService.deleteLiveRedPacketLogByLogIds(logIds));
+    }
+}

+ 68 - 1
fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java

@@ -1,17 +1,30 @@
 package com.fs.qw.controller;
 
+import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.exception.CustomException;
 import com.fs.qw.dto.SearchMsgRequest;
 import com.fs.qw.service.ICorporateWeChatSpaceService;
+import com.fs.qw.utils.WeChatSpaceUtil;
 import com.fs.qw.vo.QwSessionConfigVo;
 import com.fs.qw.vo.SearchResultVO;
+import com.fs.qwApi.util.WXBizMsgCrypt;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
+import org.w3c.dom.Document;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 
 /**
  * 企业微信专区-统一前端 API 接口
@@ -24,6 +37,9 @@ public class CorporateWeChatSpaceController extends BaseController {
 
     private final ICorporateWeChatSpaceService weChatSpaceService;
 
+    @Autowired
+    private WeChatSpaceUtil weChatSpaceUtil;
+
     // 会话信息列表
     @GetMapping("/conversations")
     public JSONObject getConversations(
@@ -93,7 +109,7 @@ public class CorporateWeChatSpaceController extends BaseController {
     //获取企业微信专区会话配置
     @GetMapping("/getQwSessionConfig/{corpid}")
     public AjaxResult getQwSessionConfig(@PathVariable String corpid) {
-        QwSessionConfigVo qwSessionConfig = weChatSpaceService.getQwSessionConfigByCorpid(corpid);
+        QwSessionConfigVo qwSessionConfig = weChatSpaceUtil.getQwSessionConfigByCorpid(corpid);
         //敏感信息设置为null
         qwSessionConfig.setPrivateKey(null);
         qwSessionConfig.setAgentSecret(null);
@@ -101,4 +117,55 @@ public class CorporateWeChatSpaceController extends BaseController {
         qwSessionConfig.setAbilityIds(null);
         return AjaxResult.success(qwSessionConfig);
     }
+
+    @GetMapping("/api/wecom/callback/{corpid}")
+    public void verifyUrl(@PathVariable String corpid,
+                          @RequestParam("msg_signature") String msgSignature,
+                          @RequestParam("timestamp") String timestamp,
+                          @RequestParam("nonce") String nonce,
+                          @RequestParam("echostr") String echostr,
+                          HttpServletResponse response) throws Exception {
+        WXBizMsgCrypt wxcpt = new WXBizMsgCrypt("aJlu2JEY7KdL9", "qfz5rCYomxISMojyOgpnbnMWKUyL2JNbGfHCAs8qRJN", corpid);
+        String echoStr = wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr);
+        response.getWriter().write(echoStr);
+        log.info("收到企业微信会话get回调");
+    }
+
+    /**
+     * 企业会话回调
+     * */
+    @PostMapping("/api/wecom/callback/{corpid}")
+    public void receiveNotify(HttpServletRequest request, HttpServletResponse response,@PathVariable String corpid) {
+        try {
+            // 1. 获取请求参数
+            String msgSignature = request.getParameter("msg_signature");
+            String timestamp = request.getParameter("timestamp");
+            String nonce = request.getParameter("nonce");
+
+            // 2. 获取请求体中的加密字符串
+            InputStream inputStream = request.getInputStream();
+            String postData = IOUtils.toString(inputStream, "UTF-8");
+            JSONObject json = JSON.parseObject(postData);
+            String encrypt = json.getString("encrypt");
+
+            // 3. 使用企业微信提供的WXBizMsgCrypt工具类解密
+            WXBizMsgCrypt wxcpt = new WXBizMsgCrypt("aJlu2JEY7KdL9", "qfz5rCYomxISMojyOgpnbnMWKUyL2JNbGfHCAs8qRJN", corpid);
+            String plainText = wxcpt.DecryptMsg(msgSignature, timestamp, nonce, encrypt);
+
+            // 4. 解析明文XML,获取 NotifyId
+            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(new ByteArrayInputStream(plainText.getBytes("UTF-8")));
+            String notifyId = document.getElementsByTagName("NotifyId").item(0).getTextContent();
+
+            // 5. 拿到 notifyId 后,立即触发消息拉取任务
+            weChatSpaceService.syncConversationsByNotifyId(notifyId, corpid);
+
+            // 6. 返回成功响应
+            response.getWriter().write("success");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        log.info("收到企业微信会话post回调");
+    }
 }

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

@@ -40,5 +40,19 @@ public class LiveKeysConstant {
     //记录用户观看直播间信息 直播间id、用户id、外部联系人id、qwUserId
     public static final String LIVE_USER_WATCH_LOG_CACHE = "live:user:watch:log:%s:%s:%s:%s";
 
+    public static final String CACHE_LIVE_INFO = "live:cache:info:%s";
+    public static final String CACHE_LIVE_GOODS_LIST = "live:cache:goods:list:%s:%s:%s";
+    public static final String CACHE_LIVE_MSG_LIST = "live:cache:msg:list:%s";
+    public static final String CACHE_LIVE_VIDEO = "live:cache:video:%s:%s";
+    public static final String CACHE_LIVE_SHOW_GOODS = "live:cache:goods:show:%s";
+    public static final String CACHE_LIVE_STORE = "live:cache:goods:store:%s:%s";
+    public static final String CACHE_USER_ORDER_LIST = "live:cache:order:list:%s:%s";
+    public static final String CACHE_LIVE_VIEW_DATA = "live:cache:view:data:%s";
+    public static final String CACHE_LIVE_RECENT_VIEWERS = "live:cache:recent:viewers:%s";
+    public static final int TTL_LIVE_INFO = 60;
+    public static final int TTL_LIVE_GOODS = 30;
+    public static final int TTL_USER_ORDER = 15;
+    public static final int TTL_LIVE_MSG = 30;
+    public static final int TTL_LIVE_DATA = 30;
 
 }

+ 83 - 0
fs-company-app/src/main/java/com/fs/app/controller/LiveController.java

@@ -0,0 +1,83 @@
+package com.fs.app.controller;
+
+import com.fs.app.annotation.Login;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.ResponseResult;
+import com.fs.live.domain.Live;
+import com.fs.live.param.FsLiveListParam;
+import com.fs.live.param.FsLiveSortLinkParam;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveSortLinkService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Api("直播相关接口")
+@RestController
+@RequestMapping("/app/fs/live")
+@Slf4j
+public class LiveController extends AppBaseController {
+
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private ILiveSortLinkService liveSortLinkService;
+
+
+    @GetMapping("/pageList")
+    @ApiOperation("未开播/直播中直播间分页列表")
+    public ResponseResult<PageInfo<Live>> pageList(FsLiveListParam param) {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<Live> list = liveService.listAppAvailableLive(param);
+        return ResponseResult.ok(new PageInfo<>(list));
+    }
+
+
+    @PostMapping("/liveSortLink")
+    @ApiOperation("生成直播分享链接")
+    public R createLiveSortLink(@RequestBody FsLiveSortLinkParam param) {
+        R liveSortLink = liveSortLinkService.createLiveSortLink(param);
+        Object url = liveSortLink.get("url");
+        if (url == null) {
+            return liveSortLink;
+        }
+        String linkId = liveSortLink.get("linkId").toString();
+        Map<String, Object> map = new HashMap<>();
+        map.put("url", url.toString());
+        map.put("linkId", linkId);
+        return R.ok(map);
+    }
+
+
+    @PostMapping("/liveSortLinkTo")
+    @ApiOperation("生成直播分享短链(APP发课)")
+    public R createLiveSortLinkTo(@RequestBody FsLiveSortLinkParam param) {
+        R liveSortLink = liveSortLinkService.createAppLiveSortLink(param);
+        Object url = liveSortLink.get("url");
+        if (url == null) {
+            return liveSortLink;
+        }
+        String linkId = liveSortLink.get("linkId").toString();
+        Map<String, Object> map = new HashMap<>();
+        map.put("url", url.toString());
+        map.put("linkId", linkId);
+        return R.ok(map);
+    }
+
+    @Login
+    @GetMapping("/getGotoWxAppLiveLink")
+    @ApiOperation("获取跳转微信小程序直播的链接地址")
+    public ResponseResult<String> getGotoWxAppLiveLink(String linkStr, String appid) {
+        return ResponseResult.ok(liveService.getGotoWxAppLiveLink(linkStr, appid));
+    }
+
+}

+ 5 - 0
fs-company/pom.xml

@@ -38,6 +38,11 @@
             <artifactId>swagger-bootstrap-ui</artifactId>
             <version>1.9.3</version>
         </dependency>
+        <!-- Liquibase(启动模块显式依赖) -->
+        <dependency>
+            <groupId>org.liquibase</groupId>
+            <artifactId>liquibase-core</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>com.github.javen205</groupId>

+ 17 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseCouponUserController.java

@@ -6,7 +6,9 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.his.domain.FsCourseCouponUser;
 import com.fs.his.service.IFsCourseCouponUserService;
 import com.fs.his.vo.FsCourseCouponUserRecordVO;
+import com.fs.his.vo.FsCourseCouponUserVO;
 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;
@@ -20,10 +22,25 @@ public class FsCourseCouponUserController extends BaseController {
     @Autowired
     private IFsCourseCouponUserService courseCouponUserService;
 
+    @Autowired
+    private IFsCourseCouponUserService fsCourseCouponUserService;
+
     @GetMapping("/getRecordList")
     public TableDataInfo getRecordList(FsCourseCouponUser courseCouponUser) {
         startPage();
         List<FsCourseCouponUserRecordVO> vos = courseCouponUserService.selectCourseCouponUserRecordList(courseCouponUser);
         return getDataTable(vos);
     }
+
+    /**
+     * 查询用户看课优惠券列表
+     */
+    @GetMapping("/listVO")
+    public TableDataInfo listVO(FsCourseCouponUser fsCourseCouponUser)
+    {
+        startPage();
+        List<FsCourseCouponUserVO> list = fsCourseCouponUserService.selectFsCourseCouponUserListVO(fsCourseCouponUser);
+        return getDataTable(list);
+    }
+
 }

+ 213 - 0
fs-company/src/main/java/com/fs/company/controller/scrm/ScrmCustomerController.java

@@ -0,0 +1,213 @@
+package com.fs.company.controller.scrm;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.controller.BaseController;
+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.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import com.fs.company.vo.CustomerRoboticCallOutCountVO;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.scrm.domain.ScrmCustomerInfo;
+import com.fs.scrm.param.ScrmCustomerInfoQueryParam;
+import com.fs.scrm.service.IScrmCustomerInfoService;
+import com.fs.scrm.vo.ScrmCustomerInfoVO;
+import com.github.pagehelper.PageHelper;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * SCRM客户Controller
+ *
+ * @author fs
+ * @date 2025-06-17
+ */
+@RestController
+@RequestMapping("/scrm/customer")
+public class ScrmCustomerController extends BaseController
+{
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private IScrmCustomerInfoService scrmCustomerInfoService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+
+    // ==================== SCRM客户信息 ====================
+
+    @ApiOperation("获取我的客户列表(联查qw_external_contact + scrm_customer_info)")
+    @GetMapping("/getMyCustomerList")
+    public TableDataInfo getMyCustomerList(ScrmCustomerInfoQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if (!StringUtils.isEmpty(param.getCreateTimeRange()))
+        {
+            String[] times = param.getCreateTimeRange().split("--");
+            if (times.length == 2)
+            {
+                param.setCreateTimeStart(times[0].trim());
+                param.setCreateTimeEnd(times[1].trim());
+            }
+        }
+        List<ScrmCustomerInfoVO> list = scrmCustomerInfoService.selectScrmCustomerInfoList(param);
+        if (list != null)
+        {
+            for (ScrmCustomerInfoVO vo : list)
+            {
+                if (vo.getRemarkMobiles() != null)
+                {
+                    vo.setRemarkMobiles(vo.getRemarkMobiles().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+            }
+            fillScrmCustomerListRoboticCallOutCount(list, param.getCompanyId());
+        }
+        return getDataTable(list);
+    }
+
+    @ApiOperation("修改SCRM客户信息")
+    @Log(title = "SCRM客户", businessType = BusinessType.UPDATE)
+    @PutMapping("/edit")
+    public AjaxResult edit(@RequestBody ScrmCustomerInfo scrmCustomerInfo)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        scrmCustomerInfo.setCompanyId(loginUser.getCompany().getCompanyId());
+        scrmCustomerInfo.setUpdateBy(loginUser.getUsername());
+        return AjaxResult.success(scrmCustomerInfoService.saveOrUpdateScrmCustomerInfo(scrmCustomerInfo) > 0);
+    }
+
+    private void fillScrmCustomerListRoboticCallOutCount(List<ScrmCustomerInfoVO> list, Long companyId)
+    {
+        if (list == null || list.isEmpty())
+        {
+            return;
+        }
+        List<Long> externalContactIds = list.stream().map(ScrmCustomerInfoVO::getExternalContactId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
+        if (externalContactIds.isEmpty())
+        {
+            return;
+        }
+        Map<Long, Long> countMap = companyVoiceRoboticCallLogCallphoneService.countRoboticCallOutByCustomerIds(externalContactIds, companyId).stream()
+                .collect(Collectors.toMap(CustomerRoboticCallOutCountVO::getCustomerId, CustomerRoboticCallOutCountVO::getCallCount, (a, b) -> a));
+        for (ScrmCustomerInfoVO vo : list)
+        {
+            long n = vo.getExternalContactId() == null ? 0L : countMap.getOrDefault(vo.getExternalContactId(), 0L);
+            vo.setRoboticCallOutCount((int) n);
+        }
+    }
+
+    // ==================== 客户跟进记录 ====================
+
+    @ApiOperation("上传跟进记录文件(上传到OSS存储桶)")
+    @PostMapping("/followUp/upload")
+    public AjaxResult uploadFollowUpFile(@RequestParam("file") MultipartFile file,
+                                         @RequestParam("externalContactId") String externalContactId,
+                                         @RequestParam(value = "userId", required = false) Long userId) throws Exception
+    {
+        if (file.isEmpty())
+        {
+            return AjaxResult.error("上传文件不能为空");
+        }
+        String originalFilename = file.getOriginalFilename();
+        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
+        String prefix = originalFilename.substring(0, originalFilename.lastIndexOf("."));
+        CloudStorageService storage = OSSFactory.build();
+        String ossUrl = storage.upload(file.getBytes(), prefix + System.currentTimeMillis() + suffix);
+        Map<String, Object> data = new HashMap<>();
+        data.put("fileName", originalFilename);
+        data.put("fileUrl", ossUrl);
+        data.put("fileSize", file.getSize());
+        data.put("uploadTime", new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
+        return AjaxResult.success(data);
+    }
+
+    @ApiOperation("保存跟进记录和录音(JSON方式)")
+    @PostMapping("/followUp/save")
+    public AjaxResult saveFollowUpRecord(@RequestBody Map<String, Object> params)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long externalContactId = params.get("externalContactId") != null ? Long.valueOf(params.get("externalContactId").toString()) : null;
+        if (externalContactId == null)
+        {
+            return AjaxResult.error("externalContactId不能为空");
+        }
+        ScrmCustomerInfo info = new ScrmCustomerInfo();
+        info.setExternalContactId(externalContactId);
+        info.setCompanyId(loginUser.getCompany().getCompanyId());
+        info.setUpdateBy(loginUser.getUsername());
+        // 跟进记录JSON
+        if (params.get("visitRecords") != null)
+        {
+            info.setVisitRecords(params.get("visitRecords").toString());
+        }
+        // 录音记录JSON
+        if (params.get("recordings") != null)
+        {
+            info.setRecordings(params.get("recordings").toString());
+        }
+        scrmCustomerInfoService.saveOrUpdateScrmCustomerInfo(info);
+        return AjaxResult.success();
+    }
+
+    @ApiOperation("获取跟进统计")
+    @GetMapping("/followUp/statistics")
+    public AjaxResult getFollowUpStatistics(@RequestParam("externalContactId") Long externalContactId)
+    {
+        ScrmCustomerInfo info = scrmCustomerInfoService.selectScrmCustomerInfoByExternalContactId(externalContactId);
+        Map<String, Object> result = new HashMap<>();
+        int unfollowDays = 0;
+        int followCount = 0;
+        if (info != null)
+        {
+            // 统计跟进记录条数
+            if (info.getVisitRecords() != null && !info.getVisitRecords().isEmpty())
+            {
+                try
+                {
+                    List<?> visitList = new com.fasterxml.jackson.databind.ObjectMapper().readValue(info.getVisitRecords(), List.class);
+                    followCount += visitList.size();
+                }
+                catch (Exception ignored) {}
+            }
+            // 统计录音条数
+            if (info.getRecordings() != null && !info.getRecordings().isEmpty())
+            {
+                try
+                {
+                    List<?> recordingList = new com.fasterxml.jackson.databind.ObjectMapper().readValue(info.getRecordings(), List.class);
+                    followCount += recordingList.size();
+                }
+                catch (Exception ignored) {}
+            }
+            // 计算未跟进天数
+            if (info.getLastTime() != null)
+            {
+                long diffMillis = System.currentTimeMillis() - info.getLastTime().getTime();
+                unfollowDays = (int) java.util.concurrent.TimeUnit.MILLISECONDS.toDays(diffMillis);
+            }
+        }
+        result.put("unfollowDays", unfollowDays);
+        result.put("followCount", followCount);
+        return AjaxResult.success(result);
+    }
+
+}

+ 30 - 9
fs-company/src/main/java/com/fs/hisStore/controller/FsIntegralOrderController.java

@@ -19,6 +19,7 @@ import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.his.enums.ShipperCodeEnum;
 import com.fs.his.param.FsIntegralOrderCreateParam;
 import com.fs.his.param.FsIntegralOrderParam;
+import com.fs.his.service.ICompanyIntegralOrderService;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsIntegralOrderService;
 import com.fs.his.vo.FsIntegralOrderListVO;
@@ -47,6 +48,8 @@ public class FsIntegralOrderController extends BaseController
     @Autowired
     private IFsIntegralOrderService fsIntegralOrderService;
     @Autowired
+    private ICompanyIntegralOrderService companyIntegralOrderService;
+    @Autowired
     private IFsExpressService expressService;
     @Autowired
     private TokenService tokenService;
@@ -68,6 +71,33 @@ public class FsIntegralOrderController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 销售端查询积分订单(按最近一次看课记录归属,数据权限同 list)
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralOrder:list')")
+    @GetMapping("/company/list")
+    public TableDataInfo companyList(FsIntegralOrderParam fsIntegralOrder)
+    {
+        applyCompanyDataScope(fsIntegralOrder);
+        startPage();
+        List<FsIntegralOrderListVO> list = companyIntegralOrderService.selectCompanyIntegralOrderList(fsIntegralOrder);
+        for (FsIntegralOrderListVO vo : list) {
+            vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 销售端导出积分订单(与 company/list 相同查询逻辑)
+     */
+    @PreAuthorize("@ss.hasPermi('his:integralOrder:export')")
+    @Log(title = "积分商品订单", businessType = BusinessType.EXPORT)
+    @GetMapping("/company/export")
+    public AjaxResult companyExport(FsIntegralOrderParam fsIntegralOrder) {
+        applyCompanyDataScope(fsIntegralOrder);
+        return companyIntegralOrderService.exportCompanyIntegralOrder(fsIntegralOrder);
+    }
+
     /**
      * 导出积分商品订单列表
      */
@@ -208,15 +238,6 @@ public class FsIntegralOrderController extends BaseController
         if (order == null) {
             throw new ServiceException("订单不存在");
         }
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        if (!loginUser.getCompany().getCompanyId().equals(order.getCompanyId())) {
-            throw new ServiceException("非法操作");
-        }
-        if (!"00".equals(loginUser.getUser().getUserType()) && !hasRoleKey(loginUser, "finance_order_goods")) {
-            if (order.getCompanyUserId() == null || !loginUser.getUser().getUserId().equals(order.getCompanyUserId())) {
-                throw new ServiceException("非法操作");
-            }
-        }
     }
 
     private boolean hasRoleKey(LoginUser loginUser, String roleKey) {

+ 57 - 59
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.FsCompanyApplication;
 import com.fs.common.annotation.DataScope;
 import com.fs.common.annotation.Log;
 import com.fs.common.constant.HttpStatus;
@@ -45,13 +46,16 @@ import com.fs.hisStore.dto.FsStoreOrderPayDeliveryDTO;
 import com.fs.hisStore.dto.StoreOrderProductDTO;
 import com.fs.hisStore.enums.OrderLogEnum;
 import com.fs.hisStore.enums.ShipperCodeEnum;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
 import com.fs.system.service.ISysConfigService;
 import io.swagger.annotations.ApiOperation;
+import org.junit.jupiter.api.Test;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -109,6 +113,8 @@ public class FsStoreOrderScrmController extends BaseController
     private IFsUserCoursePeriodService fsUserCoursePeriodService;
     @Autowired
     private IFsUserCourseVideoService fsUserCourseVideoService;
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderMapper;
 
     /**
      * 查询订单列表
@@ -118,7 +124,7 @@ public class FsStoreOrderScrmController extends BaseController
     public TableDataInfo list(FsStoreOrderParam param)
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        applyCompanyDataScope(param, loginUser);
 
         // 郑多燕需求
         if("广州郑多燕".equals(cloudHostProper.getCompanyName())){
@@ -127,32 +133,14 @@ public class FsStoreOrderScrmController extends BaseController
             if(!companyUsers.isEmpty()){
                 List<Long> companyUserIds = companyUsers.stream().map(CompanyUser::getUserId).collect(Collectors.toList());
                 param.setCompanyUserIds(companyUserIds);
+                param.setCompanyUserId(null);
             } else {
-                // 表示数据权限是本人
                 param.setCompanyUserId(loginUser.getUser().getUserId());
             }
         }
         startPage();
-
-        String configJson = configService.selectConfigByKey("his.store");
-        StoreConfig storeConfig = JSONUtil.toBean(configJson, StoreConfig.class);
-        if(storeConfig != null && Boolean.TRUE.equals(storeConfig.getEnableCompanyOrderMode())){
-            if(!"00".equals(loginUser.getUser().getUserType()) && !hasRoleKey(loginUser, "finance_order_goods")){//非管理员看见自己数据
-                param.setCompanyUserId(loginUser.getUser().getUserId());
-            }
-            param.setIsCompanyOrder(1);//是否销售订单
-        }
-
-
-        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
-            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getPayTimeRange())){
-            param.setPayTimeList(param.getPayTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
-            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
-        }
+        applyCompanyOrderModeIfEnabled(param);
+        fillListTimeRange(param);
         List<FsStoreOrderVO> list = fsStoreOrderService.selectFsStoreOrderListVO(param);
         return getDataTable(list);
     }
@@ -211,24 +199,9 @@ public class FsStoreOrderScrmController extends BaseController
             return AjaxResult.error("请筛选数据导出");
         }
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        param.setCompanyId(loginUser.getCompany().getCompanyId());
-        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
-            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getPayTimeRange())){
-            param.setPayTimeList(param.getPayTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
-            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
-        }
-        String configJson = configService.selectConfigByKey("his.store");
-        StoreConfig storeConfig = JSONUtil.toBean(configJson, StoreConfig.class);
-        if(storeConfig != null && Boolean.TRUE.equals(storeConfig.getEnableCompanyOrderMode())){
-            if(!"00".equals(loginUser.getUser().getUserType()) && !hasRoleKey(loginUser, "finance_order_goods")){//非管理员看见自己数据
-                param.setCompanyUserId(loginUser.getUser().getUserId());
-            }
-            param.setIsCompanyOrder(1);//是否销售订单
-        }
+        applyCompanyDataScope(param, loginUser);
+        applyCompanyOrderModeIfEnabled(param);
+        fillListTimeRange(param);
         List<FsStoreOrderErpExportVO> list = fsStoreOrderService.selectFsStoreOrderListVOByExport(param);
         //对手机号脱敏
         if(list!=null){
@@ -446,6 +419,17 @@ public class FsStoreOrderScrmController extends BaseController
                         lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
                         expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
                     }
+                    if(expressInfoDTO!=null){
+                        FsStoreOrderScrm map = new FsStoreOrderScrm();
+                        map.setDeliveryStatus(Integer.parseInt(expressInfoDTO.getState()));
+                        map.setId(order.getId());
+                        map.setDeliveryType(expressInfoDTO.getStateEx());
+                        fsStoreOrderMapper.updateFsStoreOrder(map);
+                        //如果是正常签收,更新订单状态
+                        if (expressInfoDTO.getState().equals("3") && (expressInfoDTO.getStateEx().equals("301") || expressInfoDTO.getStateEx().equals("302") || expressInfoDTO.getStateEx().equals("304") || expressInfoDTO.getStateEx().equals("311"))) {
+                            fsStoreOrderService.finishOrder(order.getId());
+                        }
+                    }
                     return R.ok().put("data",expressInfoDTO);
                 }
                 // 原逻辑
@@ -538,25 +522,9 @@ public class FsStoreOrderScrmController extends BaseController
             return AjaxResult.error("请筛选数据导出");
         }
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        param.setCompanyId(loginUser.getCompany().getCompanyId());
-
-        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
-            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getPayTimeRange())){
-            param.setPayTimeList(param.getPayTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
-            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
-        }
-        String configJson = configService.selectConfigByKey("his.store");
-        StoreConfig storeConfig = JSONUtil.toBean(configJson, StoreConfig.class);
-        if(storeConfig != null && Boolean.TRUE.equals(storeConfig.getEnableCompanyOrderMode())){
-            if(!"00".equals(loginUser.getUser().getUserType()) && !hasRoleKey(loginUser, "finance_order_goods")){//非管理员看见自己数据
-                param.setCompanyUserId(loginUser.getUser().getUserId());
-            }
-            param.setIsCompanyOrder(1);//是否销售订单
-        }
+        applyCompanyDataScope(param, loginUser);
+        applyCompanyOrderModeIfEnabled(param);
+        fillListTimeRange(param);
         List<FsStoreOrderItemExportVO> list=orderItemService.selectFsStoreOrderItemListExportVO(param);
         //对手机号脱敏
         if(list!=null){
@@ -700,4 +668,34 @@ public class FsStoreOrderScrmController extends BaseController
         }
         return roles.stream().anyMatch(role -> roleKey.equals(role.getRoleKey()));
     }
+
+    /**
+     * 销售端数据权限:限定本公司;非管理员且非财务订单角色仅看本人订单
+     */
+    private void applyCompanyDataScope(FsStoreOrderParam param, LoginUser loginUser) {
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if (!"00".equals(loginUser.getUser().getUserType()) && !hasRoleKey(loginUser, "finance_order_goods")) {
+            param.setCompanyUserId(loginUser.getUser().getUserId());
+        }
+    }
+
+    private void applyCompanyOrderModeIfEnabled(FsStoreOrderParam param) {
+        String configJson = configService.selectConfigByKey("his.store");
+        StoreConfig storeConfig = JSONUtil.toBean(configJson, StoreConfig.class);
+        if (storeConfig != null && Boolean.TRUE.equals(storeConfig.getEnableCompanyOrderMode())) {
+            param.setIsCompanyOrder(1);
+        }
+    }
+
+    private void fillListTimeRange(FsStoreOrderParam param) {
+        if (!StringUtils.isEmpty(param.getCreateTimeRange())) {
+            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
+        }
+        if (!StringUtils.isEmpty(param.getPayTimeRange())) {
+            param.setPayTimeList(param.getPayTimeRange().split("--"));
+        }
+        if (!StringUtils.isEmpty(param.getDeliveryImportTimeRange())) {
+            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
+        }
+    }
 }

+ 1 - 1
fs-company/src/main/java/com/fs/hisStore/utils/CityTreeUtil.java

@@ -1,5 +1,5 @@
 package com.fs.hisStore.utils;
-import com.fs.store.vo.CityVO;
+import com.fs.hisStore.vo.CityVO;
 import org.springframework.util.CollectionUtils;
 
 import java.util.ArrayList;

+ 1 - 1
fs-company/src/main/java/com/fs/hisStore/vo/CityVO.java

@@ -1,4 +1,4 @@
-package com.fs.store.vo;
+package com.fs.hisStore.vo;
 
 import lombok.Getter;
 import lombok.Setter;

+ 97 - 0
fs-company/src/main/java/com/fs/live/LiveCouponUserController.java

@@ -0,0 +1,97 @@
+package com.fs.live;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.live.domain.LiveCouponUser;
+import com.fs.live.service.ILiveCouponUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 优惠券发放记录Controller
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@RestController
+@RequestMapping("/live/coupon/user")
+public class LiveCouponUserController extends BaseController
+{
+    @Autowired
+    private ILiveCouponUserService liveCouponUserService;
+
+    /**
+     * 查询优惠券发放记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:issue:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveCouponUser liveCouponUser)
+    {
+        startPage();
+        List<LiveCouponUser> list = liveCouponUserService.selectLiveCouponUserList(liveCouponUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出优惠券发放记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:issue:export')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveCouponUser liveCouponUser)
+    {
+        List<LiveCouponUser> list = liveCouponUserService.selectLiveCouponUserList(liveCouponUser);
+        ExcelUtil<LiveCouponUser> util = new ExcelUtil<LiveCouponUser>(LiveCouponUser.class);
+        return util.exportExcel(list, "user");
+    }
+
+    /**
+     * 获取优惠券发放记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('live:issue:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(liveCouponUserService.selectLiveCouponUserById(id));
+    }
+
+    /**
+     * 新增优惠券发放记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:issue:add')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveCouponUser liveCouponUser)
+    {
+        return toAjax(liveCouponUserService.insertLiveCouponUser(liveCouponUser));
+    }
+
+    /**
+     * 修改优惠券发放记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:issue:edit')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveCouponUser liveCouponUser)
+    {
+        return toAjax(liveCouponUserService.updateLiveCouponUser(liveCouponUser));
+    }
+
+    /**
+     * 删除优惠券发放记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:issue:remove')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(liveCouponUserService.deleteLiveCouponUserByIds(ids));
+    }
+}

+ 104 - 0
fs-company/src/main/java/com/fs/live/LiveGroupTypeController.java

@@ -0,0 +1,104 @@
+package com.fs.live;
+
+import java.util.List;
+
+import com.fs.live.domain.LiveGroupType;
+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.service.ILiveGroupTypeService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 直播间分组分类Controller
+ *
+ * @author fs
+ * @date 2026-06-16
+ */
+@RestController
+@RequestMapping("/live/liveGroupType")
+public class LiveGroupTypeController extends BaseController
+{
+    @Autowired
+    private ILiveGroupTypeService liveGroupTypeService;
+
+    /**
+     * 查询直播间分组分类列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveGroupType:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveGroupType liveGroupType)
+    {
+        startPage();
+        List<LiveGroupType> list = liveGroupTypeService.selectLiveGroupTypeList(liveGroupType);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出直播间分组分类列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveGroupType:export')")
+    @Log(title = "直播间分组分类", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveGroupType liveGroupType)
+    {
+        List<LiveGroupType> list = liveGroupTypeService.selectLiveGroupTypeList(liveGroupType);
+        ExcelUtil<LiveGroupType> util = new ExcelUtil<LiveGroupType>(LiveGroupType.class);
+        return util.exportExcel(list, "直播间分组分类数据");
+    }
+
+    /**
+     * 获取直播间分组分类详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveGroupType:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(liveGroupTypeService.selectLiveGroupTypeById(id));
+    }
+
+    /**
+     * 新增直播间分组分类
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveGroupType:add')")
+    @Log(title = "直播间分组分类", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveGroupType liveGroupType)
+    {
+        return toAjax(liveGroupTypeService.insertLiveGroupType(liveGroupType));
+    }
+
+    /**
+     * 修改直播间分组分类
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveGroupType:edit')")
+    @Log(title = "直播间分组分类", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveGroupType liveGroupType)
+    {
+        return toAjax(liveGroupTypeService.updateLiveGroupType(liveGroupType));
+    }
+
+    /**
+     * 删除直播间分组分类
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveGroupType:remove')")
+    @Log(title = "直播间分组分类", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(liveGroupTypeService.deleteLiveGroupTypeByIds(ids));
+    }
+}

+ 111 - 0
fs-company/src/main/java/com/fs/live/LiveRedPacketLogController.java

@@ -0,0 +1,111 @@
+package com.fs.live;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.live.domain.LiveRedPacketLog;
+import com.fs.live.service.ILiveRedPacketLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 直播红包 记录Controller
+ *
+ * @author fs
+ * @date 2026-06-15
+ */
+@RestController
+@RequestMapping("/live/liveRedPacketLog")
+public class LiveRedPacketLogController extends BaseController
+{
+    @Autowired
+    private ILiveRedPacketLogService liveRedPacketLogService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询直播红包 记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveRedPacketLog liveRedPacketLog)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        liveRedPacketLog.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        startPage();
+        List<LiveRedPacketLog> list = liveRedPacketLogService.selectLiveRedPacketLogList(liveRedPacketLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出直播红包 记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:export')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveRedPacketLog liveRedPacketLog)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        liveRedPacketLog.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        List<LiveRedPacketLog> list = liveRedPacketLogService.selectLiveRedPacketLogList(liveRedPacketLog);
+        ExcelUtil<LiveRedPacketLog> util = new ExcelUtil<LiveRedPacketLog>(LiveRedPacketLog.class);
+        return util.exportExcel(list, "直播红包 记录数据");
+    }
+
+    /**
+     * 获取直播红包 记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+    {
+        return AjaxResult.success(liveRedPacketLogService.selectLiveRedPacketLogByLogId(logId));
+    }
+
+    /**
+     * 新增直播红包 记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:add')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveRedPacketLog liveRedPacketLog)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        liveRedPacketLog.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(liveRedPacketLogService.insertLiveRedPacketLog(liveRedPacketLog));
+    }
+
+    /**
+     * 修改直播红包 记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:edit')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveRedPacketLog liveRedPacketLog)
+    {
+        return toAjax(liveRedPacketLogService.updateLiveRedPacketLog(liveRedPacketLog));
+    }
+
+    /**
+     * 删除直播红包 记录
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveRedPacketLog:remove')")
+    @Log(title = "直播红包 记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{logIds}")
+    public AjaxResult remove(@PathVariable Long[] logIds)
+    {
+        return toAjax(liveRedPacketLogService.deleteLiveRedPacketLogByLogIds(logIds));
+    }
+}

+ 3 - 0
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -394,6 +394,9 @@ public class Task {
         }
     }
 
+    /**
+    * 执行自动化任务 脚本
+    */
     private void processAutoTask(Set<String> range) {
         for (String liveAutoTask : range) {
             LiveAutoTask task = JSON.parseObject(liveAutoTask, LiveAutoTask.class);

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

@@ -1120,7 +1120,7 @@ public class WebSocketServer {
     }
 
 
-    @Scheduled(fixedRate = 2000)// 每2秒执行一次
+    @Scheduled(fixedRate = 10000)// 每10秒执行一次
     public void broadcastUserNumMessage() {
         Set<Long> activeLiveIds = new HashSet<>();
         // 遍历每个直播间

+ 14 - 14
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -30,6 +30,8 @@ import com.fs.his.service.IFsIntegralCountService;
 import com.fs.his.service.IFsIntegralGoodsService;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.utils.qrcode.QRCodeUtils;
+import com.fs.live.param.LiveRedPacketParam;
+import com.fs.live.service.ILiveRedPacketLogService;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.QwContactWayMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
@@ -206,22 +208,20 @@ public class CommonController {
     @Autowired
     private IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
 
-    @GetMapping("/test")
-    public R test(){
-
-        Long userId = 10568613L;
-        String AppId = "";
-
-        if (AppId == null || AppId.isEmpty()) {
-            return goodsService.getCourseIntegralGoods(userId);
-        }
 
-        String integralGoods = fsCoursePlaySourceConfigService.selectCoursePlaySourceConfigByAppIdGoods(AppId);
-        if (!StringUtil.strIsNullOrEmpty(integralGoods)) {
-            return goodsService.getCourseIntegralGoodsByMiniApp(AppId, userId, integralGoods);
-        }
+    @Autowired
+    private ILiveRedPacketLogService redPacketLogService;
 
-        return goodsService.getCourseIntegralGoods(userId);
+    @GetMapping("/test")
+    public R test(){
+        LiveRedPacketParam param=new LiveRedPacketParam();
+        param.setUserId(12858L);
+        param.setLiveId(91L);
+        param.setCompanyUserId(9605L);
+        param.setCompanyId(321L);
+        param.setAppId("ww890da2ce046e357c");
+
+        return  redPacketLogService.sendLiveReward(param);
     }
 
     /**

+ 5 - 0
fs-service/pom.xml

@@ -42,6 +42,11 @@
             <groupId>com.fs</groupId>
             <artifactId>fs-common</artifactId>
         </dependency>
+        <!-- Liquibase(启动模块显式依赖,避免 IDE/devtools classpath 缺失) -->
+        <dependency>
+            <groupId>org.liquibase</groupId>
+            <artifactId>liquibase-core</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.bytedeco</groupId>
             <artifactId>javacv</artifactId>

+ 1 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java

@@ -101,7 +101,7 @@ public interface CompanyConfigMapper
             "fs_course_play_source_config\n" +
             "where \n" +
             "is_del = 0 \n" +
-            "and status = 0" +
+            "and status = 0 \n" +
             "and name like '%app%' ")
     List<CompanyMiniAppVO> getCompanyMiniAppAllList();
 

+ 3 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java

@@ -72,7 +72,6 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
      */
     List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallLogCallphoneListData(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 
-    List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId);
 
     List<CalleeRoboticCallOutCountVO> countRoboticCallOutByCalleeIds(List<Long> calleeIds, Long roboticId, Long companyId);
 
@@ -88,4 +87,7 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
     CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
 
     List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    List<CustomerRoboticCallOutCountVO> countRoboticCallOutByCustomerIds(List<Long> customerIds, Long companyId);
+
 }

+ 5 - 3
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -29,6 +29,7 @@ import com.fs.enums.NodeTypeEnum;
 import com.fs.enums.TaskTypeEnum;
 import com.fs.his.config.CidPhoneConfig;
 import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.*;
@@ -268,12 +269,13 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         try{
             CompanyConfig companyConfig = companyConfigMapper.selectCompanyConfigByKey(robotic.getCompanyId(), "cId.config");
             CidPhoneConfig cidConf = JSONObject.parseObject(companyConfig.getConfigValue(), CidPhoneConfig.class);
-            if(null != cidConf && StringUtils.isNotBlank(cidConf.getCallbackUrl())){
+            if(null != cidConf && !StringUtil.strIsNullOrEmpty(cidConf.getCallbackUrl())){
                 callBackUrl = cidConf.getCallbackUrl();
             }
+
             //读取总后台配置
-            if(StringUtils.isBlank(callBackUrl)){
-                String s = configService.selectConfigByKey("cId.config");
+            if(StringUtil.strIsNullOrEmpty(callBackUrl)){
+                String s = configService.selectConfigByKey("cid.config");
                 JSONObject obj = JSONObject.parseObject(s);
                 if(null != obj && obj.containsKey("callbackUrl") && StringUtils.isNotBlank(obj.getString("callbackUrl"))){
                     callBackUrl = obj.getString("callbackUrl");

+ 3 - 0
fs-service/src/main/java/com/fs/core/config/WxMaConfiguration.java

@@ -16,6 +16,7 @@ import com.fs.course.config.CourseMaConfig;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
+import com.fs.framework.liquibase.LiquibaseStartupGate;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.google.common.collect.Lists;
@@ -30,6 +31,7 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
 import org.yaml.snakeyaml.events.Event;
 
 import javax.annotation.PostConstruct;
@@ -44,6 +46,7 @@ import java.util.stream.Collectors;
 @Slf4j
 @Configuration
 @ComponentScan("com.fs.system.mapper")
+@DependsOn(LiquibaseStartupGate.BEAN_NAME)
 public class WxMaConfiguration {
     private final WxMaConfig properties;
 

+ 39 - 0
fs-service/src/main/java/com/fs/course/dto/VideoCollectionDTO.java

@@ -0,0 +1,39 @@
+package com.fs.course.dto;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+@Data
+public class VideoCollectionDTO {
+    /** 合集ID */
+    private Long collectionId;
+
+    private Long userId;
+    /** 合集标题 */
+    @Excel(name = "合集标题")
+    private String title;
+
+    /** 合集描述 */
+    @Excel(name = "合集描述")
+    private String description;
+
+    /** 合集封面图 */
+    @Excel(name = "合集封面图")
+    private String coverUrl;
+
+    /** 达人ID */
+    @Excel(name = "达人ID")
+    private Long talentId;
+
+    /** 状态:1正常 0下架 */
+    @Excel(name = "状态:1正常 0下架")
+    private Long status;
+
+    /** 合集标签 */
+    @Excel(name = "合集标签")
+    private String tags;
+
+    private int count;
+
+    private Boolean isFavorite;
+}

+ 8 - 4
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -142,10 +142,10 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     @Select("select l.*,v.title,c.course_name from fs_course_watch_log l LEFT JOIN fs_user_course_video v ON v.video_id = l.video_id LEFT JOIN fs_user_course c ON c.course_id = l.course_id WHERE l.qw_external_contact_id =#{id} ORDER BY l.create_time  desc LIMIT 1  ")
     FsCourseWatchLogVO selectFsCourseWatchLogByExtId(Long id);
 
-    @Select("select l.qw_external_contact_id,l.company_id,l.company_user_id,l.log_type,ext.`level`,l.qw_user_id FROM fs_course_watch_log l LEFT JOIN qw_external_contact ext ON ext.id =l.qw_external_contact_id  where l.sop_id=#{SopId} and  date(l.create_time)= CURDATE() and l.log_type =3 and l.video_id not in (select video_id from fs_user_course_video WHERE is_first=1 ) ")
+    @Select("select l.qw_external_contact_id,l.company_id,l.company_user_id,l.log_type,ext.`level`,l.qw_user_id FROM fs_course_watch_log l LEFT JOIN qw_external_contact ext ON ext.id =l.qw_external_contact_id  where l.sop_id=#{SopId} and  AND l.create_time >= CURDATE() AND l.create_time < CURDATE() + INTERVAL 1 DAY  and l.log_type =3 and l.video_id not in (select video_id from fs_user_course_video WHERE is_first=1 ) ")
     List<FsCourseWatchLogTaskVO> selectFsCourseWatchLogByDaySopId3(@Param("SopId")String SopId);
 
-    @Select("select l.qw_external_contact_id,l.company_id,l.company_user_id,l.log_type,ext.`level`,l.qw_user_id FROM fs_course_watch_log l LEFT JOIN qw_external_contact ext ON ext.id =l.qw_external_contact_id  where l.sop_id=#{SopId} and  date(l.create_time)= CURDATE() and l.log_type =4 and l.video_id not in (select video_id from fs_user_course_video WHERE is_first=1 ) ")
+    @Select("select l.qw_external_contact_id,l.company_id,l.company_user_id,l.log_type,ext.`level`,l.qw_user_id FROM fs_course_watch_log l LEFT JOIN qw_external_contact ext ON ext.id =l.qw_external_contact_id  where l.sop_id=#{SopId} and  AND l.create_time >= CURDATE() AND l.create_time < CURDATE() + INTERVAL 1 DAY and l.log_type =4 and l.video_id not in (select video_id from fs_user_course_video WHERE is_first=1 ) ")
     List<FsCourseWatchLogTaskVO> selectFsCourseWatchLogByDaySopId4(@Param("SopId")String SopId);
 
 //    @Select("   SELECT\n" +
@@ -627,8 +627,12 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     List<WatchLogDTO> selectFsCourseWatchLog30DayByExtId(@Param("extId") Long extId);
 
     @Select("SELECT * FROM fs_course_watch_log " +
-            "WHERE log_type = 2 AND send_finish_msg = 0 and send_type = 2 " +
-            "AND send_type = 2 and finish_time >= #{startDate} AND finish_time < #{endDate} and log_id > #{maxId} order by log_id asc  " +
+            "WHERE send_type = 2 " +
+            "AND log_type = 2 " +
+            "AND send_finish_msg = 0 " +
+            "AND finish_time >= #{startDate} AND finish_time < #{endDate} " +
+            "AND log_id > #{maxId} " +
+            "ORDER BY log_id ASC " +
             "LIMIT #{limit}")
     List<FsCourseWatchLog> selectFsCourseWatchLogFinishBatchByDate(
             @Param("startDate") Date startDate,

+ 57 - 18
fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java

@@ -134,20 +134,38 @@ public interface FsUserVideoMapper
     @Update("update fs_user_video set favorite_num=favorite_num-1 where video_id=#{videoId}")
     int minusFavorite(Long videoId);
 
-    @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.user_id,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
-            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num from fs_user_video v " +
-            "left join fs_user_talent t on t.talent_id = v.talent_id " +
-            " left join fs_package p on p.package_id = v.product_id " +
-            "where v.is_del = 0 and v.status = 1  " +
-            " and v.is_audit = 1 " +
-            "<if test = ' maps.keyword!=null and maps.keyword != \"\" '> " +
-            "and v.title like CONCAT('%',#{maps.keyword},'%') " +
-            "</if>" +
-            " order by RAND() "+
-            "</script>"})
-    List<FsUserVideoListUVO> selectFsUserVideoListUVO(@Param("maps") FsUserVideoListUParam param);
+//    @Select({"<script> " +
+//            "select v.video_id as id,v.title,v.description as msg,t.user_id,t.nick_name as username,t.avatar as headImg, " +
+//            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
+//            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num from fs_user_video v " +
+//            "left join fs_user_talent t on t.talent_id = v.talent_id " +
+//            " left join fs_package p on p.package_id = v.product_id " +
+//            "where v.is_del = 0 and v.status = 1  " +
+//            " and v.is_audit = 1 " +
+//            "<if test = ' maps.keyword!=null and maps.keyword != \"\" '> " +
+//            "and v.title like CONCAT('%',#{maps.keyword},'%') " +
+//            "</if>" +
+//            " order by RAND() "+
+//            "</script>"})
+//    List<FsUserVideoListUVO> selectFsUserVideoListUVO(@Param("maps") FsUserVideoListUParam param);
+@Select({"<script> " +
+        "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+        "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
+        "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num " +
+        "<if test = 'maps.userId != null'> " +
+        ",CASE WHEN EXISTS (SELECT 1 FROM fs_user_talent_follow tf WHERE tf.talent_id = v.talent_id AND tf.user_id = #{maps.userId}) THEN '1' ELSE '0' END as isFollow " +
+        "</if> " +
+        "from fs_user_video v " +
+        "left join fs_user_talent t on t.talent_id = v.talent_id " +
+        " left join fs_package p on p.package_id = v.product_id " +
+        "where v.is_del = 0 and v.status = 1  " +
+        " and v.is_audit = 1 " +
+        "<if test = ' maps.keyword!=null and maps.keyword != \"\" '> " +
+        "and v.title like CONCAT('%',#{maps.keyword},'%') " +
+        "</if>" +
+        " order by v.create_time desc "+
+        "</script>"})
+List<FsUserVideoListUVO> selectFsUserVideoListUVO(@Param("maps") FsUserVideoListUParam param);
 
     @Update("UPDATE fs_user_video SET comments = comments+#{commentCount} WHERE video_id = #{videoId}")
     void updateCommentCount(@Param("videoId") Long videoId, @Param("commentCount") Integer commentCount);
@@ -254,10 +272,31 @@ public interface FsUserVideoMapper
             "where v.is_del = 0 and  v.status = 1 and f.user_id = #{userId}")
     int countFavoriteVideos(@Param("userId") Long userId);
 
+//    @Select({"<script> " +
+//            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+//            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
+//            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num,v.is_audit,v.status from fs_user_video v " +
+//            "left join fs_user_talent t on t.talent_id = v.talent_id " +
+//            " left join fs_package p on p.package_id = v.product_id " +
+//            "where v.is_del = 0 and (" +
+//            "(#{oneSelf} = true and (v.is_audit = -1 or v.is_audit = 0 or v.is_audit = 1)) or " +
+//            "(#{oneSelf} = false and v.is_audit = 1 and v.status = 1)" +
+//            ") " +
+//            "<if test = ' talentId!=null and talentId != \"\" '> " +
+//            "and v.talent_id = #{talentId}" +
+//            " order by v.create_time" +
+//            "</if>" +
+//            "</script>"})
+//    List<FsUserVideoListUVO> selectFsUserVideoListUVOByUser(@Param("talentId") Long talentId, @Param("oneSelf") boolean oneSelf);
+
     @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
-            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num,v.is_audit,v.status from fs_user_video v " +
+            "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
+            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num,v.is_audit,v.fail_reason,v.status" +
+            "<if test = 'userId != null'> " +
+            ",CASE WHEN EXISTS (SELECT 1 FROM fs_user_talent_follow tf WHERE tf.talent_id = v.talent_id AND tf.user_id = #{userId}) THEN '1' ELSE '0' END as isFollow " +
+            "</if> " +
+            "from fs_user_video v " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +
             "where v.is_del = 0 and (" +
@@ -269,7 +308,7 @@ public interface FsUserVideoMapper
             " order by v.create_time" +
             "</if>" +
             "</script>"})
-    List<FsUserVideoListUVO> selectFsUserVideoListUVOByUser(@Param("talentId") Long talentId, @Param("oneSelf") boolean oneSelf);
+    List<FsUserVideoListUVO> selectFsUserVideoListUVOByUser(@Param("talentId") Long talentId, @Param("oneSelf") boolean oneSelf, @Param("userId") Long userId);
 
     @Select({"<script> " +
             "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +

+ 9 - 3
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -148,7 +148,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     private static final String SHORT_LINK_PREFIX = "/courseH5/pages/course/learning?s=";
     // 排除看课数量限制的公司集合
     private static final Set<String> EXCLUDE_PROJECTS = new HashSet<>(Arrays.asList(
-            "福本源", "宽益堂", "叮当国医", "易行健","河山医院", "鸿森堂"
+            "福本源", "宽益堂", "叮当国医", "易行健","河山医院", "鸿森堂", "北京卓美"
     ));
     @Autowired
     ICompanyService companyService;
@@ -1618,6 +1618,10 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 return R.error(401, "授权后可继续!");
             }
 
+            if("wxcf4c7fb36154bf30".equals(param.getAppId())){
+                config.setRewardType(3);
+            }
+
             log.info("奖励类型:{}", config.getRewardType());
             // 根据奖励类型发放不同奖励
             switch (config.getRewardType()) {
@@ -1639,7 +1643,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     if (!Objects.equals(sendRed.get("code"), 200)) {
                         return sendRed;
                     }
-                    return sendIntegralReward(param, user, watchLog, config);
+                    R r = sendIntegralReward(param, user, watchLog, config);
+                    r.put("res",sendRed);
+                    return r;
                 default:
                     return R.error("参数错误!");
             }
@@ -2536,7 +2542,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         logger.info("发放奖励====================》看课记录,{}", log);
 
         // 红德堂不要积分转红包
-        if (CloudHostUtils.hasCloudHostName("弘德堂")) {
+        if (CloudHostUtils.hasCloudHostName("弘德堂") || CloudHostUtils.hasCloudHostName("恒春来")) {
             return R.ok("奖励发放成功").put("rewardType", config.getRewardType());
         }
 

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

@@ -383,7 +383,7 @@ public class FsUserVideoServiceImpl implements IFsUserVideoService {
 
     @Override
     public List<FsUserVideoListUVO> selectFsUserVideoListUVOByUser(Long talentId, boolean oneSelf, Long userId) {
-        List<FsUserVideoListUVO> list = fsUserVideoMapper.selectFsUserVideoListUVOByUser(talentId, oneSelf);
+        List<FsUserVideoListUVO> list = fsUserVideoMapper.selectFsUserVideoListUVOByUser(talentId, oneSelf,userId);
         /*if (param != null && param.getUserId() != null) {
             Long userId = param.getUserId();
             list = selectLikesAndFavorites(userId, list);

+ 3 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoH5DVO.java

@@ -60,6 +60,9 @@ public class FsUserCourseVideoH5DVO extends BaseEntity
      */
     private String showProduct;
 
+    private Integer isDel;
+    private Integer isOnPut;
+
     /**
      * 商品列表
      */

+ 10 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserVideoListUVO.java

@@ -1,6 +1,7 @@
 package com.fs.course.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.course.dto.VideoCollectionDTO;
 import lombok.Data;
 
 import java.util.Date;
@@ -10,6 +11,8 @@ import java.util.List;
 public class FsUserVideoListUVO {
     private String id; // _id
     private Long userId;
+    private Long talentId;//视频发布的达人id
+    private String isFollow;//当前用户是否关注发布视频的达人
     private String username;//2.视频拥有者名称
     private String headImg;//3.发布者头像
     private String cover;//视频封面
@@ -39,4 +42,11 @@ public class FsUserVideoListUVO {
     private Integer uploadType;
     private Long shares;
     private String addNum; //随机添加数
+    private Integer isAudit;
+    private Integer status;
+    private String failReason;
+    private Boolean isInCollection;
+    //合集信息
+    private VideoCollectionDTO videoCollectionDTO;
+    private String thumbnail;
 }

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

@@ -412,7 +412,8 @@ public class AiHookServiceImpl implements AiHookService {
         }
         if(user.getFastGptRoleId()==null){
             log.error("未绑定角色");
-            return userIsReply(sender, uid, user);
+            return R.ok();
+            //return userIsReply(sender, uid, user);
         }
 
         if(user.getAiStatus() == 1){

+ 137 - 0
fs-service/src/main/java/com/fs/framework/liquibase/ChangelogMd5Service.java

@@ -0,0 +1,137 @@
+package com.fs.framework.liquibase;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.util.StreamUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * 计算 changelog 目录 bundle MD5。
+ */
+public class ChangelogMd5Service {
+
+    private static final Logger log = LoggerFactory.getLogger(ChangelogMd5Service.class);
+
+    private final LiquibaseProperties properties;
+    private final PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+
+    public ChangelogMd5Service(LiquibaseProperties properties) {
+        this.properties = properties;
+    }
+
+    public BundleMd5Result computeBundleMd5() {
+        try {
+            String pattern = buildScanPattern();
+            Resource[] resources = resourceResolver.getResources(pattern);
+            List<Resource> matched = new ArrayList<>();
+            for (Resource resource : resources) {
+                if (resource.exists() && resource.isReadable() && !isDirectoryResource(resource)
+                        && matchExtension(resource.getFilename())
+                        && isActiveChangelogResource(resource)) {
+                    matched.add(resource);
+                }
+            }
+            matched.sort(Comparator.comparing(this::resourceKey));
+
+            StringBuilder payload = new StringBuilder();
+            for (Resource resource : matched) {
+                payload.append(resourceKey(resource)).append('\n');
+                payload.append(StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8));
+                payload.append('\n');
+            }
+            String md5 = md5Hex(payload.toString());
+            log.info("Liquibase bundle MD5 计算完成: md5={}, fileCount={}", md5, matched.size());
+            return new BundleMd5Result(md5, matched.size());
+        } catch (IOException ex) {
+            throw new IllegalStateException("计算 Liquibase changelog bundle MD5 失败", ex);
+        }
+    }
+
+    private String buildScanPattern() {
+        String root = properties.getChangelogRoot();
+        if (root.startsWith("classpath:")) {
+            root = root.substring("classpath:".length());
+        }
+        if (!root.endsWith("/")) {
+            root = root + "/";
+        }
+        return "classpath*:" + root + "**/*";
+    }
+
+    private boolean isDirectoryResource(Resource resource) throws IOException {
+        return resource.getURL().toString().endsWith("/");
+    }
+
+    private boolean isActiveChangelogResource(Resource resource) {
+        try {
+            String path = resource.getURI().toString().replace('\\', '/');
+            return !path.contains("/draft/");
+        } catch (IOException ex) {
+            return true;
+        }
+    }
+
+    private String resourceKey(Resource resource) {
+        try {
+            return resource.getURI().toString();
+        } catch (IOException ex) {
+            return resource.getDescription();
+        }
+    }
+
+    private boolean matchExtension(String filename) {
+        int index = filename.lastIndexOf('.');
+        if (index < 0) {
+            return false;
+        }
+        String ext = filename.substring(index + 1).toLowerCase(Locale.ROOT);
+        for (String allowed : properties.getIncludeExtensions()) {
+            if (allowed.equalsIgnoreCase(ext)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String md5Hex(String content) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("MD5");
+            byte[] hash = digest.digest(content.getBytes(StandardCharsets.UTF_8));
+            StringBuilder builder = new StringBuilder(hash.length * 2);
+            for (byte value : hash) {
+                builder.append(String.format("%02x", value));
+            }
+            return builder.toString();
+        } catch (NoSuchAlgorithmException ex) {
+            throw new IllegalStateException("MD5 算法不可用", ex);
+        }
+    }
+
+    public static final class BundleMd5Result {
+        private final String md5;
+        private final int fileCount;
+
+        public BundleMd5Result(String md5, int fileCount) {
+            this.md5 = md5;
+            this.fileCount = fileCount;
+        }
+
+        public String getMd5() {
+            return md5;
+        }
+
+        public int getFileCount() {
+            return fileCount;
+        }
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseAutoConfiguration.java

@@ -0,0 +1,34 @@
+package com.fs.framework.liquibase;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.Environment;
+
+import javax.sql.DataSource;
+
+/**
+ * Liquibase 启动门禁:不依赖 @ConditionalOnClass,避免 devtools 重启类加载器导致 gate Bean 未注册。
+ */
+@Configuration
+@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
+@ConditionalOnBean(name = "masterDataSource")
+@ConditionalOnProperty(prefix = "fs.liquibase", name = "enabled", havingValue = "true", matchIfMissing = true)
+@EnableConfigurationProperties(LiquibaseProperties.class)
+public class LiquibaseAutoConfiguration {
+
+    @Bean(name = LiquibaseStartupGate.BEAN_NAME)
+    @DependsOn("masterDataSource")
+    public LiquibaseStartupGate liquibaseStartupGate(
+            @Qualifier("masterDataSource") DataSource masterDataSource,
+            LiquibaseProperties properties,
+            Environment environment) {
+        return new LiquibaseStartupGate(masterDataSource, properties, environment);
+    }
+}

+ 102 - 0
fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseDependsOnConfigurer.java

@@ -0,0 +1,102 @@
+package com.fs.framework.liquibase;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.core.PriorityOrdered;
+import org.springframework.core.env.Environment;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * 为业务 {@code @Configuration} 统一追加对 Liquibase 门禁 Bean 的依赖,
+ * 确保数据库 migration 完成后再初始化 WxMaConfiguration 等配置类。
+ */
+public class LiquibaseDependsOnConfigurer implements BeanFactoryPostProcessor, PriorityOrdered, EnvironmentAware {
+
+    private Environment environment;
+
+    public LiquibaseDependsOnConfigurer() {
+    }
+
+    @Override
+    public void setEnvironment(Environment environment) {
+        this.environment = environment;
+    }
+
+    @Override
+    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+        if (!environment.getProperty("fs.liquibase.enabled", Boolean.class, true)) {
+            return;
+        }
+        if (!ClassUtils.isPresent("liquibase.Liquibase", getClass().getClassLoader())) {
+            return;
+        }
+        if (!beanFactory.containsBeanDefinition("masterDataSource")) {
+            return;
+        }
+
+        int affected = 0;
+        for (String beanName : beanFactory.getBeanDefinitionNames()) {
+            if (LiquibaseStartupGate.BEAN_NAME.equals(beanName) || isLiquibaseBean(beanName)) {
+                continue;
+            }
+            BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
+            String beanClassName = resolveBeanClassName(definition);
+            if (!shouldDependOnLiquibase(beanClassName)) {
+                continue;
+            }
+            appendDependsOn(definition, LiquibaseStartupGate.BEAN_NAME);
+            affected++;
+        }
+        if (affected > 0) {
+            // 日志在 Bean 创建前无法使用 Slf4j 注入,此处仅做 debug 级别输出到标准日志框架由后续 gate 打印
+        }
+    }
+
+    private boolean isLiquibaseBean(String beanName) {
+        return beanName.startsWith("liquibase") || beanName.contains("Liquibase");
+    }
+
+    private String resolveBeanClassName(BeanDefinition definition) {
+        if (StringUtils.hasText(definition.getBeanClassName())) {
+            return definition.getBeanClassName();
+        }
+        return null;
+    }
+
+    private boolean shouldDependOnLiquibase(String beanClassName) {
+        if (!StringUtils.hasText(beanClassName) || !beanClassName.startsWith("com.fs.")) {
+            return false;
+        }
+        if (beanClassName.startsWith("com.fs.framework.liquibase.")) {
+            return false;
+        }
+        if (beanClassName.endsWith("DataSourceConfig") || beanClassName.endsWith("DruidConfig")) {
+            return false;
+        }
+        return beanClassName.endsWith("Configuration");
+    }
+
+    private void appendDependsOn(BeanDefinition definition, String dependsOnBean) {
+        String[] existing = definition.getDependsOn();
+        Set<String> merged = new LinkedHashSet<>();
+        if (existing != null) {
+            merged.addAll(Arrays.asList(existing));
+        }
+        if (merged.add(dependsOnBean)) {
+            definition.setDependsOn(merged.toArray(new String[0]));
+        }
+    }
+
+    @Override
+    public int getOrder() {
+        return PriorityOrdered.LOWEST_PRECEDENCE;
+    }
+}

+ 128 - 0
fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseMetaRepository.java

@@ -0,0 +1,128 @@
+package com.fs.framework.liquibase;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.regex.Pattern;
+
+/**
+ * 读写 database_init_meta 表中的 bundle MD5。
+ */
+public class LiquibaseMetaRepository {
+
+    private static final Logger log = LoggerFactory.getLogger(LiquibaseMetaRepository.class);
+    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+$");
+    private static final long META_ROW_ID = 1L;
+
+    private final LiquibaseProperties properties;
+
+    public LiquibaseMetaRepository(LiquibaseProperties properties) {
+        this.properties = properties;
+    }
+
+    public String loadBundleMd5(DataSource dataSource) {
+        assertTableName();
+        String sql = "SELECT bundle_md5 FROM " + properties.getMetaTable() + " WHERE id = ?";
+        try (Connection connection = dataSource.getConnection();
+             PreparedStatement statement = connection.prepareStatement(sql)) {
+            statement.setLong(1, META_ROW_ID);
+            try (ResultSet rs = statement.executeQuery()) {
+                if (rs.next()) {
+                    return rs.getString("bundle_md5");
+                }
+            }
+        } catch (SQLException ex) {
+            if (isTableNotExists(ex)) {
+                log.info("Liquibase 元数据表 {} 尚未创建,视为首次接入", properties.getMetaTable());
+                return null;
+            }
+            throw new IllegalStateException("读取 Liquibase MD5 元数据失败", ex);
+        }
+        return null;
+    }
+
+    public void saveBundleMd5(DataSource dataSource, String bundleMd5, int fileCount, String moduleName) {
+        assertTableName();
+        String sql = "INSERT INTO " + properties.getMetaTable()
+                + " (id, bundle_md5, file_count, last_module, update_time, remark) "
+                + "VALUES (?, ?, ?, ?, ?, ?) "
+                + "ON DUPLICATE KEY UPDATE bundle_md5 = VALUES(bundle_md5), "
+                + "file_count = VALUES(file_count), "
+                + "last_module = VALUES(last_module), "
+                + "update_time = VALUES(update_time), "
+                + "remark = VALUES(remark)";
+        try (Connection connection = dataSource.getConnection();
+             PreparedStatement statement = connection.prepareStatement(sql)) {
+            statement.setLong(1, META_ROW_ID);
+            statement.setString(2, bundleMd5);
+            statement.setInt(3, fileCount);
+            statement.setString(4, moduleName);
+            statement.setTimestamp(5, new Timestamp(System.currentTimeMillis()));
+            statement.setString(6, "Liquibase startup gate");
+            statement.executeUpdate();
+            log.info("Liquibase bundle MD5 已写入 {}: md5={}", properties.getMetaTable(), bundleMd5);
+        } catch (SQLException ex) {
+            if (isTableNotExists(ex)) {
+                throw new IllegalStateException(
+                        "Liquibase 元数据表 " + properties.getMetaTable() + " 不存在,请先执行 baseline changeset", ex);
+            }
+            throw new IllegalStateException("写入 Liquibase MD5 元数据失败", ex);
+        }
+    }
+
+    public boolean metaTableExists(DataSource dataSource) {
+        assertTableName();
+        try (Connection connection = dataSource.getConnection();
+             Statement statement = connection.createStatement();
+             ResultSet rs = statement.executeQuery("SHOW TABLES LIKE '" + properties.getMetaTable() + "'")) {
+            return rs.next();
+        } catch (SQLException ex) {
+            throw new IllegalStateException("检测 Liquibase 元数据表失败", ex);
+        }
+    }
+
+    /**
+     * 兜底创建 MD5 元数据表(已有库首次接入或 changeset 被 MARK_RAN 跳过时使用)。
+     */
+    public void ensureMetaTable(DataSource dataSource) {
+        if (metaTableExists(dataSource)) {
+            return;
+        }
+        assertTableName();
+        String ddl = "CREATE TABLE IF NOT EXISTS " + properties.getMetaTable() + " ("
+                + "id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',"
+                + "bundle_md5 VARCHAR(32) NOT NULL COMMENT 'changelog bundle MD5',"
+                + "bundle_version VARCHAR(64) NULL COMMENT '可选版本号',"
+                + "file_count INT NOT NULL COMMENT '参与计算的文件数',"
+                + "last_module VARCHAR(64) NULL COMMENT '最后成功校验的模块',"
+                + "update_time DATETIME NOT NULL COMMENT 'MD5 更新时间',"
+                + "remark VARCHAR(255) NULL COMMENT '备注',"
+                + "PRIMARY KEY (id)"
+                + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Liquibase changelog bundle MD5 元数据'";
+        try (Connection connection = dataSource.getConnection();
+             Statement statement = connection.createStatement()) {
+            statement.execute(ddl);
+            log.info("已自动创建 Liquibase 元数据表 {}", properties.getMetaTable());
+        } catch (SQLException ex) {
+            throw new IllegalStateException("创建 Liquibase 元数据表失败", ex);
+        }
+    }
+
+    private void assertTableName() {
+        if (!TABLE_NAME_PATTERN.matcher(properties.getMetaTable()).matches()) {
+            throw new IllegalStateException("非法的 Liquibase 元数据表名: " + properties.getMetaTable());
+        }
+    }
+
+    private boolean isTableNotExists(SQLException ex) {
+        String message = ex.getMessage();
+        return message != null && (message.contains("doesn't exist") || message.contains("does not exist"));
+    }
+}

+ 192 - 0
fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseMigrationRunner.java

@@ -0,0 +1,192 @@
+package com.fs.framework.liquibase;
+
+import liquibase.Liquibase;
+import liquibase.changelog.ChangeSet;
+import liquibase.database.Database;
+import liquibase.database.DatabaseFactory;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.resource.ClassLoaderResourceAccessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 执行 Liquibase 迁移并统计 pending changeset。
+ */
+public class LiquibaseMigrationRunner {
+
+    private static final Logger log = LoggerFactory.getLogger(LiquibaseMigrationRunner.class);
+
+    private final LiquibaseProperties properties;
+
+    public LiquibaseMigrationRunner(LiquibaseProperties properties) {
+        this.properties = properties;
+    }
+
+    public MigrationAudit audit(DataSource dataSource) {
+        configureLockWait();
+        try (Connection connection = dataSource.getConnection()) {
+            Database database = createDatabase(connection);
+            Liquibase liquibase = createLiquibase(database);
+            List<ChangeSet> all = liquibase.getDatabaseChangeLog().getChangeSets();
+            List<ChangeSet> pending = liquibase.listUnrunChangeSets(null);
+            Set<String> parsedKeys = toChangeSetKeys(all);
+            Set<String> pendingKeys = toChangeSetKeys(pending);
+            Set<String> executedKeys = loadExecutedChangeSetKeys(dataSource);
+            Set<String> missingInDb = new LinkedHashSet<>(parsedKeys);
+            missingInDb.removeAll(executedKeys);
+
+            log.info("Liquibase 审计: 已解析={}, 待执行={}, DATABASECHANGELOG 已记录={}, 缺失记录={}",
+                    parsedKeys.size(), pendingKeys.size(), executedKeys.size(), missingInDb.size());
+            for (ChangeSet changeSet : pending) {
+                log.info("待执行 changeset -> id={}, author={}, file={}",
+                        changeSet.getId(), changeSet.getAuthor(), changeSet.getFilePath());
+            }
+            for (String missing : missingInDb) {
+                log.warn("DATABASECHANGELOG 缺失 changeset -> {}", missing);
+            }
+            return new MigrationAudit(parsedKeys.size(), pendingKeys.size(), parsedKeys, pendingKeys, missingInDb);
+        } catch (Exception ex) {
+            throw new IllegalStateException("Liquibase migration 审计失败", ex);
+        }
+    }
+
+    public int countPendingChangeSets(DataSource dataSource) {
+        return audit(dataSource).getPendingTotal();
+    }
+
+    public void update(DataSource dataSource) {
+        configureLockWait();
+        try (Connection connection = dataSource.getConnection()) {
+            connection.setAutoCommit(false);
+            Database database = createDatabase(connection);
+            Liquibase liquibase = createLiquibase(database);
+            try {
+                int totalChangeSets = liquibase.getDatabaseChangeLog().getChangeSets().size();
+                log.info("开始执行 Liquibase update, changelog={}, 已解析 changeset 总数={}",
+                        properties.getChangelog(), totalChangeSets);
+                if (totalChangeSets == 0) {
+                    throw new IllegalStateException(
+                            "Liquibase 未解析到任何 changeset,请检查 master changelog 是否正确(SQL master 不支持 --include,需使用 XML)");
+                }
+                liquibase.update((String) null);
+                connection.commit();
+                log.info("Liquibase update 执行成功");
+            } catch (Exception ex) {
+                try {
+                    connection.rollback();
+                } catch (Exception rollbackEx) {
+                    log.warn("Liquibase update 回滚失败", rollbackEx);
+                }
+                throw new IllegalStateException("Liquibase 迁移失败,已回滚,禁止启动", ex);
+            }
+        } catch (IllegalStateException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new IllegalStateException("Liquibase 迁移失败,已回滚,禁止启动", ex);
+        }
+    }
+
+    public Set<String> loadExecutedChangeSetKeys(DataSource dataSource) {
+        Set<String> keys = new LinkedHashSet<>();
+        String sql = "SELECT ID, AUTHOR FROM DATABASECHANGELOG";
+        try (Connection connection = dataSource.getConnection();
+             Statement statement = connection.createStatement();
+             ResultSet rs = statement.executeQuery(sql)) {
+            while (rs.next()) {
+                keys.add(rs.getString("AUTHOR") + ":" + rs.getString("ID"));
+            }
+            return keys;
+        } catch (SQLException ex) {
+            if (isTableNotExists(ex)) {
+                return keys;
+            }
+            throw new IllegalStateException("读取 DATABASECHANGELOG 失败", ex);
+        }
+    }
+
+    private Set<String> toChangeSetKeys(List<ChangeSet> changeSets) {
+        Set<String> keys = new LinkedHashSet<>();
+        for (ChangeSet changeSet : changeSets) {
+            keys.add(changeSet.getAuthor() + ":" + changeSet.getId());
+        }
+        return keys;
+    }
+
+    private Database createDatabase(Connection connection) throws Exception {
+        return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+    }
+
+    private Liquibase createLiquibase(Database database) throws Exception {
+        ClassLoader classLoader = getClass().getClassLoader();
+        if (classLoader.getResource(properties.getChangelog()) == null) {
+            throw new IllegalStateException("未找到 master changelog: " + properties.getChangelog());
+        }
+        Liquibase liquibase = new Liquibase(properties.getChangelog(), new ClassLoaderResourceAccessor(), database);
+        int parsed = liquibase.getDatabaseChangeLog().getChangeSets().size();
+        if (parsed == 0) {
+            throw new IllegalStateException(
+                    "Liquibase 未解析到 changeset,请检查 SQL 文件是否包含 "
+                            + "'--liquibase formatted sql' 及 '--changeset author:id' 格式");
+        }
+        return liquibase;
+    }
+
+    private void configureLockWait() {
+        System.setProperty("liquibase.changelogLockWaitTimeInMinutes",
+                String.valueOf(properties.getLockWaitTimeoutMinutes()));
+    }
+
+    private boolean isTableNotExists(SQLException ex) {
+        String message = ex.getMessage();
+        return message != null && (message.contains("doesn't exist") || message.contains("does not exist"));
+    }
+
+    public static final class MigrationAudit {
+        private final int parsedTotal;
+        private final int pendingTotal;
+        private final Set<String> parsedKeys;
+        private final Set<String> pendingKeys;
+        private final Set<String> missingInDatabase;
+
+        public MigrationAudit(int parsedTotal,
+                              int pendingTotal,
+                              Set<String> parsedKeys,
+                              Set<String> pendingKeys,
+                              Set<String> missingInDatabase) {
+            this.parsedTotal = parsedTotal;
+            this.pendingTotal = pendingTotal;
+            this.parsedKeys = parsedKeys;
+            this.pendingKeys = pendingKeys;
+            this.missingInDatabase = missingInDatabase;
+        }
+
+        public int getParsedTotal() {
+            return parsedTotal;
+        }
+
+        public int getPendingTotal() {
+            return pendingTotal;
+        }
+
+        public Set<String> getParsedKeys() {
+            return parsedKeys;
+        }
+
+        public Set<String> getPendingKeys() {
+            return pendingKeys;
+        }
+
+        public Set<String> getMissingInDatabase() {
+            return missingInDatabase;
+        }
+    }
+}

+ 87 - 0
fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseProperties.java

@@ -0,0 +1,87 @@
+package com.fs.framework.liquibase;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Arrays;
+import java.util.List;
+
+@ConfigurationProperties(prefix = "fs.liquibase")
+public class LiquibaseProperties {
+
+    /** 是否启用启动门禁 */
+    private boolean enabled = true;
+
+    /** 校验失败是否禁止启动 */
+    private boolean failFast = true;
+
+    /** Liquibase 主 changelog(classpath 相对路径) */
+    private String changelog = "db/changelog/db.changelog-master.xml";
+
+    /** bundle MD5 扫描根路径 */
+    private String changelogRoot = "classpath:db/changelog/";
+
+    /** MD5 元数据表名 */
+    private String metaTable = "database_init_meta";
+
+    /** Liquibase 全局锁等待时间(分钟) */
+    private int lockWaitTimeoutMinutes = 2;
+
+    /** 参与 MD5 计算的文件扩展名 */
+    private List<String> includeExtensions = Arrays.asList("sql", "xml", "yaml", "yml");
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public boolean isFailFast() {
+        return failFast;
+    }
+
+    public void setFailFast(boolean failFast) {
+        this.failFast = failFast;
+    }
+
+    public String getChangelog() {
+        return changelog;
+    }
+
+    public void setChangelog(String changelog) {
+        this.changelog = changelog;
+    }
+
+    public String getChangelogRoot() {
+        return changelogRoot;
+    }
+
+    public void setChangelogRoot(String changelogRoot) {
+        this.changelogRoot = changelogRoot;
+    }
+
+    public String getMetaTable() {
+        return metaTable;
+    }
+
+    public void setMetaTable(String metaTable) {
+        this.metaTable = metaTable;
+    }
+
+    public int getLockWaitTimeoutMinutes() {
+        return lockWaitTimeoutMinutes;
+    }
+
+    public void setLockWaitTimeoutMinutes(int lockWaitTimeoutMinutes) {
+        this.lockWaitTimeoutMinutes = lockWaitTimeoutMinutes;
+    }
+
+    public List<String> getIncludeExtensions() {
+        return includeExtensions;
+    }
+
+    public void setIncludeExtensions(List<String> includeExtensions) {
+        this.includeExtensions = includeExtensions;
+    }
+}

+ 139 - 0
fs-service/src/main/java/com/fs/framework/liquibase/LiquibaseStartupGate.java

@@ -0,0 +1,139 @@
+package com.fs.framework.liquibase;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.env.Environment;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+import javax.sql.DataSource;
+
+/**
+ * 应用启动门禁:校验 master 数据源、执行 pending migration、校验 changeset 执行记录、同步 bundle MD5。
+ */
+public class LiquibaseStartupGate implements InitializingBean {
+
+    public static final String BEAN_NAME = "liquibaseStartupGate";
+
+    private static final Logger log = LoggerFactory.getLogger(LiquibaseStartupGate.class);
+
+    private final DataSource masterDataSource;
+    private final LiquibaseProperties properties;
+    private final Environment environment;
+
+    public LiquibaseStartupGate(DataSource masterDataSource,
+                                LiquibaseProperties properties,
+                                Environment environment) {
+        this.masterDataSource = masterDataSource;
+        this.properties = properties;
+        this.environment = environment;
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+        runGate();
+    }
+
+    public void runGate() {
+        if (!properties.isEnabled()) {
+            log.info("Liquibase 启动门禁已关闭 (fs.liquibase.enabled=false)");
+            return;
+        }
+
+        assertLiquibaseOnClasspath();
+
+        MasterDataSourceValidator masterDataSourceValidator = new MasterDataSourceValidator();
+        ChangelogMd5Service changelogMd5Service = new ChangelogMd5Service(properties);
+        LiquibaseMetaRepository liquibaseMetaRepository = new LiquibaseMetaRepository(properties);
+        LiquibaseMigrationRunner liquibaseMigrationRunner = new LiquibaseMigrationRunner(properties);
+        String moduleName = environment.getProperty("spring.application.name", "unknown-module");
+        log.info("模块 {} 开始 Liquibase 启动门禁校验(优先于业务 Bean 初始化)", moduleName);
+
+        masterDataSourceValidator.assertMasterExists(masterDataSource);
+
+        ChangelogMd5Service.BundleMd5Result bundleMd5Result = changelogMd5Service.computeBundleMd5();
+        String fileMd5 = bundleMd5Result.getMd5();
+
+        boolean metaTableExists = liquibaseMetaRepository.metaTableExists(masterDataSource);
+        String dbMd5 = liquibaseMetaRepository.loadBundleMd5(masterDataSource);
+        LiquibaseMigrationRunner.MigrationAudit audit = liquibaseMigrationRunner.audit(masterDataSource);
+        boolean md5Mismatch = StringUtils.hasText(dbMd5) && !fileMd5.equals(dbMd5);
+
+        assertChangeSetsParsable(audit);
+
+        if (audit.getPendingTotal() > 0) {
+            log.info("检测到 {} 条未执行 Liquibase changeset", audit.getPendingTotal());
+        }
+        if (!audit.getMissingInDatabase().isEmpty()) {
+            log.warn("检测到 {} 条 changeset 未写入 DATABASECHANGELOG", audit.getMissingInDatabase().size());
+        }
+        if (md5Mismatch) {
+            log.info("changelog bundle MD5 已变化: dbMd5={}, fileMd5={}", dbMd5, fileMd5);
+        }
+
+        boolean needUpdate = audit.getPendingTotal() > 0
+                || !audit.getMissingInDatabase().isEmpty()
+                || !metaTableExists
+                || !StringUtils.hasText(dbMd5)
+                || md5Mismatch;
+
+        if (needUpdate) {
+            liquibaseMigrationRunner.update(masterDataSource);
+            bootstrapMetaIfNeeded(liquibaseMetaRepository);
+            audit = liquibaseMigrationRunner.audit(masterDataSource);
+            assertMigrationCompleted(audit);
+        } else {
+            assertMigrationCompleted(audit);
+        }
+
+        ChangelogMd5Service.BundleMd5Result latestMd5 = changelogMd5Service.computeBundleMd5();
+        liquibaseMetaRepository.saveBundleMd5(
+                masterDataSource, latestMd5.getMd5(), latestMd5.getFileCount(), moduleName);
+
+        if (md5Mismatch) {
+            log.info("changelog 文件变更且 migration 校验通过,bundle MD5 已同步: {} -> {}",
+                    dbMd5, latestMd5.getMd5());
+        } else {
+            log.info("模块 {} Liquibase 启动门禁通过,bundle MD5={}", moduleName, latestMd5.getMd5());
+        }
+    }
+
+    private void assertChangeSetsParsable(LiquibaseMigrationRunner.MigrationAudit audit) {
+        if (audit.getParsedTotal() == 0) {
+            abort("Liquibase 未解析到任何 changeset,请确认 master changelog 使用 XML include 且已正确引用 SQL 文件");
+        }
+    }
+
+    private void assertMigrationCompleted(LiquibaseMigrationRunner.MigrationAudit audit) {
+        if (audit.getPendingTotal() > 0) {
+            abort("Liquibase update 后仍有 " + audit.getPendingTotal() + " 条未执行 changeset: "
+                    + audit.getPendingKeys());
+        }
+        if (!audit.getMissingInDatabase().isEmpty()) {
+            abort("以下 changeset 未写入 DATABASECHANGELOG: " + audit.getMissingInDatabase());
+        }
+    }
+
+    private void abort(String message) {
+        if (properties.isFailFast()) {
+            throw new IllegalStateException(message);
+        }
+        log.error("Liquibase 启动门禁校验失败(fail-fast=false): {}", message);
+    }
+
+    private void bootstrapMetaIfNeeded(LiquibaseMetaRepository liquibaseMetaRepository) {
+        if (!liquibaseMetaRepository.metaTableExists(masterDataSource)) {
+            liquibaseMetaRepository.ensureMetaTable(masterDataSource);
+        }
+    }
+
+    private void assertLiquibaseOnClasspath() {
+        if (!ClassUtils.isPresent("liquibase.Liquibase", getClass().getClassLoader())) {
+            throw new IllegalStateException(
+                    "已启用 fs.liquibase,但 classpath 未找到 liquibase-core。"
+                            + "请在启动模块 pom.xml 添加 org.liquibase:liquibase-core 依赖,"
+                            + "并关闭 devtools 后完整重启(或配置 spring-devtools restart.include.liquibase)");
+        }
+    }
+}

+ 36 - 0
fs-service/src/main/java/com/fs/framework/liquibase/MasterDataSourceValidator.java

@@ -0,0 +1,36 @@
+package com.fs.framework.liquibase;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+/**
+ * 校验 MySQL MASTER 主数据源是否可用。
+ */
+public class MasterDataSourceValidator {
+
+    private static final Logger log = LoggerFactory.getLogger(MasterDataSourceValidator.class);
+
+    public static final String ERROR_MESSAGE = "未检测到主数据源mysql的master";
+
+    public void assertMasterExists(DataSource masterDataSource) {
+        if (masterDataSource == null) {
+            throw new IllegalStateException(ERROR_MESSAGE);
+        }
+        try (Connection connection = masterDataSource.getConnection()) {
+            if (connection.isClosed()) {
+                throw new IllegalStateException(ERROR_MESSAGE);
+            }
+            try (Statement statement = connection.createStatement()) {
+                statement.execute("SELECT 1");
+            }
+        } catch (SQLException ex) {
+            log.error("主数据源 mysql master 连接失败", ex);
+            throw new IllegalStateException(ERROR_MESSAGE, ex);
+        }
+    }
+}

+ 10 - 0
fs-service/src/main/java/com/fs/his/domain/FsCourseCouponUser.java

@@ -58,4 +58,14 @@ public class FsCourseCouponUser {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     @TableField(fill = FieldFill.INSERT_UPDATE)
     private Date updateTime;
+
+
+    /** 类型  1课程发的 2直播发的 */
+    @Excel(name = "类型  1课程发的 2直播发的")
+    private Integer sendType;
+
+
+    /** 直播间id */
+    @Excel(name = "直播间id")
+    private Long liveId;
 }

+ 11 - 0
fs-service/src/main/java/com/fs/his/domain/FsIntegralOrder.java

@@ -121,6 +121,14 @@ public class FsIntegralOrder
     @Excel(name = "销售公司ID")
     private Long companyId;
 
+    @TableField(exist = false)
+    @Excel(name = "所属公司")
+    private String companyName;
+
+    @TableField(exist = false)
+    @Excel(name = "所属销售")
+    private String companyUserNickName;
+
     private String loginAccount;
 
     private String remark;
@@ -145,4 +153,7 @@ public class FsIntegralOrder
     @ApiModelProperty("商品对应的数量(多个)")
     private String quantityCart;
 
+    //虚拟手机号
+    private String virtualPhone;
+
 }

+ 17 - 0
fs-service/src/main/java/com/fs/his/mapper/CompanyIntegralOrderMapper.java

@@ -0,0 +1,17 @@
+package com.fs.his.mapper;
+
+import com.fs.his.param.FsIntegralOrderParam;
+import com.fs.his.vo.FsIntegralOrderListVO;
+
+import java.util.List;
+
+/**
+ * 销售端积分订单(按最近看课记录归属)
+ */
+public interface CompanyIntegralOrderMapper {
+
+    /**
+     * 查询积分订单列表,数据权限与归属均按最近一次看课记录
+     */
+    List<FsIntegralOrderListVO> selectCompanyIntegralOrderList(FsIntegralOrderParam param);
+}

+ 27 - 7
fs-service/src/main/java/com/fs/his/mapper/FsCourseCouponUserMapper.java

@@ -5,19 +5,20 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.his.domain.FsCourseCouponUser;
 import com.fs.his.vo.CourseCouponUserListUVO;
 import com.fs.his.vo.FsCourseCouponUserRecordVO;
+import com.fs.his.vo.FsCourseCouponUserVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
 /**
  * 用户看课优惠券Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-05-13
  */
 public interface FsCourseCouponUserMapper extends BaseMapper<FsCourseCouponUser>{
     /**
      * 查询用户看课优惠券
-     * 
+     *
      * @param id 用户看课优惠券主键
      * @return 用户看课优惠券
      */
@@ -25,15 +26,34 @@ public interface FsCourseCouponUserMapper extends BaseMapper<FsCourseCouponUser>
 
     /**
      * 查询用户看课优惠券列表
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 用户看课优惠券集合
      */
     List<FsCourseCouponUser> selectFsCourseCouponUserList(FsCourseCouponUser fsCourseCouponUser);
 
+    @Select("<script>" +
+            "select cu.id,cu.coupon_id,cu.user_id,cu.limit_time,cu.start_time," +
+            "cu.create_time,cu.update_time,cu.status,fu.nick_name,cc.title as couponTitle " +
+            "from fs_course_coupon_user cu " +
+            "LEFT JOIN fs_course_coupon cc on cu.coupon_id = cc.id " +
+            "LEFT JOIN fs_user fu on cu.user_id = fu.user_id " +
+            "<where>" +
+            "    <if test='data != null'>" +
+            "    <if test='data.couponId != null'> and cu.coupon_id = #{data.couponId}</if>" +
+            "    <if test='data.userId != null'> and cu.user_id = #{data.userId}</if>" +
+            "    <if test='data.status != null'> and cu.status = #{data.status}</if>" +
+            "    <if test='data.sendType != null'> and cu.send_type = #{data.sendType}</if>" +
+            "    <if test='data.liveId != null'> and cu.live_id = #{data.liveId}</if>" +
+            "  </if>" +
+            "</where>" +
+            "order by cu.create_time desc" +
+            "</script>")
+    List<FsCourseCouponUserVO> selectFsCourseCouponUserListVO(@Param("data") FsCourseCouponUser fsCourseCouponUser);
+
     /**
      * 新增用户看课优惠券
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 结果
      */
@@ -41,7 +61,7 @@ public interface FsCourseCouponUserMapper extends BaseMapper<FsCourseCouponUser>
 
     /**
      * 修改用户看课优惠券
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 结果
      */
@@ -49,7 +69,7 @@ public interface FsCourseCouponUserMapper extends BaseMapper<FsCourseCouponUser>
 
     /**
      * 删除用户看课优惠券
-     * 
+     *
      * @param id 用户看课优惠券主键
      * @return 结果
      */
@@ -57,7 +77,7 @@ public interface FsCourseCouponUserMapper extends BaseMapper<FsCourseCouponUser>
 
     /**
      * 批量删除用户看课优惠券
-     * 
+     *
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */

+ 7 - 1
fs-service/src/main/java/com/fs/his/mapper/FsUserCouponMapper.java

@@ -67,7 +67,13 @@ public interface FsUserCouponMapper
     public int deleteFsUserCouponByIds(Long[] ids);
 
     @Select({"<script> " +
-            " select uc.*,u.nick_name,u.phone,c.title,c.price,c.coupon_type,su.nick_name sendUserName,com.company_name,comu.user_name  company_user_name FROM fs_user_coupon uc LEFT JOIN fs_user u ON uc.user_id=u.user_id LEFT JOIN fs_coupon c on c.coupon_id=uc.coupon_id LEFT JOIN sys_user su ON su.user_id =uc.send_user_id LEFT JOIN company com ON com.company_id=uc.company_id LEFT JOIN company_user comu on comu.user_id=uc.company_user_id "+
+            " select uc.*,u.nick_name,u.phone,c.title,c.price,c.coupon_type,su.nick_name sendUserName,com.company_name,comu.user_name  company_user_name" +
+            " FROM fs_user_coupon uc  " +
+            "LEFT JOIN fs_user u ON uc.user_id=u.user_id " +
+            "LEFT JOIN fs_coupon c on c.coupon_id=uc.coupon_id " +
+            "LEFT JOIN sys_user su ON su.user_id =uc.send_user_id " +
+            "LEFT JOIN company com ON com.company_id=uc.company_id " +
+            "LEFT JOIN company_user comu on comu.user_id=uc.company_user_id "+
             " <where>  \n" +
             "            <if test=\"couponId != null \"> and uc.coupon_id = #{couponId}</if>\n" +
             "            <if test=\"userId != null \"> and uc.user_id = #{userId}</if>\n" +

+ 17 - 0
fs-service/src/main/java/com/fs/his/service/ICompanyIntegralOrderService.java

@@ -0,0 +1,17 @@
+package com.fs.his.service;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.his.param.FsIntegralOrderParam;
+import com.fs.his.vo.FsIntegralOrderListVO;
+
+import java.util.List;
+
+/**
+ * 销售端积分订单(按最近看课记录归属)
+ */
+public interface ICompanyIntegralOrderService {
+
+    List<FsIntegralOrderListVO> selectCompanyIntegralOrderList(FsIntegralOrderParam param);
+
+    AjaxResult exportCompanyIntegralOrder(FsIntegralOrderParam param);
+}

+ 9 - 7
fs-service/src/main/java/com/fs/his/service/IFsCourseCouponUserService.java

@@ -6,17 +6,18 @@ import com.fs.common.core.domain.R;
 import com.fs.his.domain.FsCourseCouponUser;
 import com.fs.his.vo.CourseCouponUserListUVO;
 import com.fs.his.vo.FsCourseCouponUserRecordVO;
+import com.fs.his.vo.FsCourseCouponUserVO;
 
 /**
  * 用户看课优惠券Service接口
- * 
+ *
  * @author fs
  * @date 2026-05-13
  */
 public interface IFsCourseCouponUserService extends IService<FsCourseCouponUser>{
     /**
      * 查询用户看课优惠券
-     * 
+     *
      * @param id 用户看课优惠券主键
      * @return 用户看课优惠券
      */
@@ -24,15 +25,16 @@ public interface IFsCourseCouponUserService extends IService<FsCourseCouponUser>
 
     /**
      * 查询用户看课优惠券列表
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 用户看课优惠券集合
      */
     List<FsCourseCouponUser> selectFsCourseCouponUserList(FsCourseCouponUser fsCourseCouponUser);
+    List<FsCourseCouponUserVO> selectFsCourseCouponUserListVO(FsCourseCouponUser fsCourseCouponUser);
 
     /**
      * 新增用户看课优惠券
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 结果
      */
@@ -40,7 +42,7 @@ public interface IFsCourseCouponUserService extends IService<FsCourseCouponUser>
 
     /**
      * 修改用户看课优惠券
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 结果
      */
@@ -48,7 +50,7 @@ public interface IFsCourseCouponUserService extends IService<FsCourseCouponUser>
 
     /**
      * 批量删除用户看课优惠券
-     * 
+     *
      * @param ids 需要删除的用户看课优惠券主键集合
      * @return 结果
      */
@@ -56,7 +58,7 @@ public interface IFsCourseCouponUserService extends IService<FsCourseCouponUser>
 
     /**
      * 删除用户看课优惠券信息
-     * 
+     *
      * @param id 用户看课优惠券主键
      * @return 结果
      */

+ 7 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralOrderService.java

@@ -115,4 +115,11 @@ public interface IFsIntegralOrderService
     void createErpOrder(Long orderId) throws ParseException;
 
     FsIntegralOrderImportResultVO importOrderStatusData(List<FsIntegralOrderExcelVO> list);
+
+    /**
+     * 导出时按用户最近一次看课记录填充所属公司、所属销售
+     */
+    void fillExportWatchAttribution(List<FsIntegralOrderListVO> list);
+
+    void fillOrderExportWatchAttribution(List<FsIntegralOrder> list);
 }

+ 99 - 0
fs-service/src/main/java/com/fs/his/service/impl/CompanyIntegralOrderServiceImpl.java

@@ -0,0 +1,99 @@
+package com.fs.his.service.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.mapper.CompanyIntegralOrderMapper;
+import com.fs.his.param.FsIntegralOrderParam;
+import com.fs.his.service.ICompanyIntegralOrderService;
+import com.fs.his.vo.FsIntegralOrderListVO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.fs.his.utils.PhoneUtil.decryptAutoPhoneMk;
+
+/**
+ * 销售端积分订单(按最近看课记录归属)
+ */
+@Service
+public class CompanyIntegralOrderServiceImpl implements ICompanyIntegralOrderService {
+
+    private static final Logger log = LoggerFactory.getLogger(CompanyIntegralOrderServiceImpl.class);
+
+    @Autowired
+    private CompanyIntegralOrderMapper companyIntegralOrderMapper;
+
+    @Override
+    public List<FsIntegralOrderListVO> selectCompanyIntegralOrderList(FsIntegralOrderParam param) {
+        return companyIntegralOrderMapper.selectCompanyIntegralOrderList(param);
+    }
+
+    @Override
+    public AjaxResult exportCompanyIntegralOrder(FsIntegralOrderParam param) {
+        List<FsIntegralOrderListVO> list = companyIntegralOrderMapper.selectCompanyIntegralOrderList(param);
+        List<FsIntegralOrderListVO> exportList = buildExportRows(list);
+        ExcelUtil<FsIntegralOrderListVO> util = new ExcelUtil<>(FsIntegralOrderListVO.class);
+        return util.exportExcel(exportList, "积分商品订单数据");
+    }
+
+    /**
+     * 列表与导出共用:解密手机号并解析商品明细
+     */
+    public List<FsIntegralOrderListVO> buildExportRows(List<FsIntegralOrderListVO> list) {
+        List<FsIntegralOrderListVO> exportList = new ArrayList<>();
+        for (FsIntegralOrderListVO vo : list) {
+            vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+            if (StringUtils.isEmpty(vo.getItemJson())) {
+                exportList.add(vo);
+                continue;
+            }
+            try {
+                if (vo.getItemJson().startsWith("[")) {
+                    JSONArray jsonArray = JSONArray.parseArray(vo.getItemJson());
+                    if (jsonArray == null || jsonArray.isEmpty()) {
+                        exportList.add(vo);
+                        continue;
+                    }
+                    for (int i = 0; i < jsonArray.size(); i++) {
+                        FsIntegralOrderListVO row = BeanCopyUtils.copy(vo, FsIntegralOrderListVO.class);
+                        if (row == null) {
+                            continue;
+                        }
+                        fillGoodsFromJson(row, jsonArray.getJSONObject(i));
+                        exportList.add(row);
+                    }
+                } else if (vo.getItemJson().startsWith("{")) {
+                    fillGoodsFromJson(vo, JSONObject.parseObject(vo.getItemJson()));
+                    exportList.add(vo);
+                } else {
+                    exportList.add(vo);
+                }
+            } catch (Exception e) {
+                log.warn("解析商品信息失败,订单编号:{}, 商品信息:{}", vo.getOrderCode(), vo.getItemJson());
+                exportList.add(vo);
+            }
+        }
+        return exportList;
+    }
+
+    private void fillGoodsFromJson(FsIntegralOrderListVO vo, JSONObject goods) {
+        if (goods == null) {
+            return;
+        }
+        if (goods.getString("goodsName") != null) {
+            vo.setGoodsName(goods.getString("goodsName"));
+            vo.setNum(goods.getString("num"));
+        }
+        if (goods.getString("barCode") != null) {
+            vo.setBarCode(goods.getString("barCode"));
+        }
+    }
+}

+ 8 - 7
fs-service/src/main/java/com/fs/his/service/impl/FsCourseCouponServiceImpl.java

@@ -30,7 +30,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 /**
  * 课程优惠券Service业务层处理
- * 
+ *
  * @author fs
  * @date 2026-05-13
  */
@@ -53,7 +53,7 @@ public class FsCourseCouponServiceImpl extends ServiceImpl<FsCourseCouponMapper,
     private FsCourseWatchLogMapper courseWatchLogMapper;
     /**
      * 查询课程优惠券
-     * 
+     *
      * @param id 课程优惠券主键
      * @return 课程优惠券
      */
@@ -65,7 +65,7 @@ public class FsCourseCouponServiceImpl extends ServiceImpl<FsCourseCouponMapper,
 
     /**
      * 查询课程优惠券列表
-     * 
+     *
      * @param fsCourseCoupon 课程优惠券
      * @return 课程优惠券
      */
@@ -77,7 +77,7 @@ public class FsCourseCouponServiceImpl extends ServiceImpl<FsCourseCouponMapper,
 
     /**
      * 新增课程优惠券
-     * 
+     *
      * @param fsCourseCoupon 课程优惠券
      * @return 结果
      */
@@ -91,7 +91,7 @@ public class FsCourseCouponServiceImpl extends ServiceImpl<FsCourseCouponMapper,
 
     /**
      * 修改课程优惠券
-     * 
+     *
      * @param fsCourseCoupon 课程优惠券
      * @return 结果
      */
@@ -104,7 +104,7 @@ public class FsCourseCouponServiceImpl extends ServiceImpl<FsCourseCouponMapper,
 
     /**
      * 批量删除课程优惠券
-     * 
+     *
      * @param ids 需要删除的课程优惠券主键
      * @return 结果
      */
@@ -116,7 +116,7 @@ public class FsCourseCouponServiceImpl extends ServiceImpl<FsCourseCouponMapper,
 
     /**
      * 删除课程优惠券信息
-     * 
+     *
      * @param id 课程优惠券主键
      * @return 结果
      */
@@ -371,6 +371,7 @@ public class FsCourseCouponServiceImpl extends ServiceImpl<FsCourseCouponMapper,
             couponUser.setLimitTime(fsCourseCoupon.getLimitTime());
             couponUser.setCreateTime(new Date());
             couponUser.setLogId(log.getLogId());
+            couponUser.setSendType(1);
             int i = courseCouponUserMapper.insertFsCourseCouponUser(couponUser);
 
             if (i > 0) {

+ 13 - 7
fs-service/src/main/java/com/fs/his/service/impl/FsCourseCouponUserServiceImpl.java

@@ -10,6 +10,7 @@ import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.his.vo.CourseCouponUserListUVO;
 import com.fs.his.vo.FsCourseCouponUserRecordVO;
+import com.fs.his.vo.FsCourseCouponUserVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.his.mapper.FsCourseCouponUserMapper;
@@ -18,7 +19,7 @@ import com.fs.his.service.IFsCourseCouponUserService;
 
 /**
  * 用户看课优惠券Service业务层处理
- * 
+ *
  * @author fs
  * @date 2026-05-13
  */
@@ -27,7 +28,7 @@ public class FsCourseCouponUserServiceImpl extends ServiceImpl<FsCourseCouponUse
 
     /**
      * 查询用户看课优惠券
-     * 
+     *
      * @param id 用户看课优惠券主键
      * @return 用户看课优惠券
      */
@@ -39,7 +40,7 @@ public class FsCourseCouponUserServiceImpl extends ServiceImpl<FsCourseCouponUse
 
     /**
      * 查询用户看课优惠券列表
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 用户看课优惠券
      */
@@ -49,9 +50,14 @@ public class FsCourseCouponUserServiceImpl extends ServiceImpl<FsCourseCouponUse
         return baseMapper.selectFsCourseCouponUserList(fsCourseCouponUser);
     }
 
+    @Override
+    public List<FsCourseCouponUserVO> selectFsCourseCouponUserListVO(FsCourseCouponUser fsCourseCouponUser) {
+        return baseMapper.selectFsCourseCouponUserListVO(fsCourseCouponUser);
+    }
+
     /**
      * 新增用户看课优惠券
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 结果
      */
@@ -64,7 +70,7 @@ public class FsCourseCouponUserServiceImpl extends ServiceImpl<FsCourseCouponUse
 
     /**
      * 修改用户看课优惠券
-     * 
+     *
      * @param fsCourseCouponUser 用户看课优惠券
      * @return 结果
      */
@@ -77,7 +83,7 @@ public class FsCourseCouponUserServiceImpl extends ServiceImpl<FsCourseCouponUse
 
     /**
      * 批量删除用户看课优惠券
-     * 
+     *
      * @param ids 需要删除的用户看课优惠券主键
      * @return 结果
      */
@@ -89,7 +95,7 @@ public class FsCourseCouponUserServiceImpl extends ServiceImpl<FsCourseCouponUse
 
     /**
      * 删除用户看课优惠券信息
-     * 
+     *
      * @param id 用户看课优惠券主键
      * @return 结果
      */

+ 9 - 3
fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderServiceImpl.java

@@ -517,7 +517,7 @@ public class FsInquiryOrderServiceImpl implements IFsInquiryOrderService
                         }
                     } else if ("appPay".equals(payment.getPayMode()) && "wx".equals(payment.getPayTypeCode())) {
                         MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(payment.getMerConfigId());
-                        FsPayConfig fsPayConfig = com.alibaba.fastjson.JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+                        FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
 
                         // 处理微信退款
                         WxPayService wxPayService = getWxPayService(fsPayConfig);
@@ -1774,12 +1774,18 @@ public class FsInquiryOrderServiceImpl implements IFsInquiryOrderService
         BeanUtils.copyProperties(patient,dto);
         FsPackagePatientDTO patJson = JSON.parseObject(packageOrder.getPatientJson(),FsPackagePatientDTO.class);
         long currentTimeMillis = System.currentTimeMillis();
-        long ageInMillis = currentTimeMillis - patJson.getBirthday();
+
+        long ageInMillis = 0;
+        if(packageOrder.getPatientJson()!=null){
+            ageInMillis = currentTimeMillis - patJson.getBirthday();
+        }
         long ageInSeconds = ageInMillis / 1000;
         long ageInYears = ageInSeconds / (365 * 24 * 3600);
         dto.setAge(ageInYears+"");
         // 使用 SimpleDateFormat 格式化日期对象
-        dto.setBirthday(new SimpleDateFormat("yyyy-MM-dd").format(patient.getBirthday()));
+        if(patient != null && patient.getBirthday() != null){
+            dto.setBirthday(new SimpleDateFormat("yyyy-MM-dd").format(patient.getBirthday()));
+        }
         FsInquiryOrder order=new FsInquiryOrder();
         order.setOrderSn(packageOrder.getOrderSn());
         order.setOrderType(2);

+ 89 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralOrderServiceImpl.java

@@ -20,6 +20,8 @@ import com.fs.common.exception.CustomException;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.*;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.cache.ICompanyCacheService;
+import com.fs.company.cache.ICompanyUserCacheService;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyDept;
 import com.fs.company.domain.CompanyUser;
@@ -27,6 +29,8 @@ import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.service.ICompanyDeptService;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.core.utils.OrderCodeUtils;
 import com.fs.erp.domain.ErpOrder;
@@ -172,6 +176,15 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
     @Autowired
     private ICompanyDeptService companyDeptService;
 
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Autowired
+    private ICompanyCacheService companyCacheService;
+
+    @Autowired
+    private ICompanyUserCacheService companyUserCacheService;
+
     @Autowired
     private IFsExpressService expressService;
 
@@ -815,8 +828,80 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
             processGoodsInfo(order, fsIntegralOrderSet);
         }
 
+        List<FsIntegralOrder> exportList = new ArrayList<>(fsIntegralOrderSet);
+        fillOrderExportWatchAttribution(exportList);
         ExcelUtil<FsIntegralOrder> util = new ExcelUtil<>(FsIntegralOrder.class);
-        return util.exportExcel(new ArrayList<>(fsIntegralOrderSet), "积分商品订单数据");
+        return util.exportExcel(exportList, "积分商品订单数据");
+    }
+
+    @Override
+    public void fillExportWatchAttribution(List<FsIntegralOrderListVO> list) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        Map<Long, WatchAttribution> cache = new HashMap<>();
+        for (FsIntegralOrderListVO vo : list) {
+            if (vo.getUserId() == null) {
+                continue;
+            }
+            WatchAttribution attribution = cache.computeIfAbsent(vo.getUserId(), this::resolveWatchAttribution);
+            vo.setCompanyName(attribution.getCompanyName());
+            vo.setCompanyUserNickName(attribution.getCompanyUserNickName());
+        }
+    }
+
+    @Override
+    public void fillOrderExportWatchAttribution(List<FsIntegralOrder> list) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        Map<Long, WatchAttribution> cache = new HashMap<>();
+        for (FsIntegralOrder order : list) {
+            if (order.getUserId() == null) {
+                continue;
+            }
+            WatchAttribution attribution = cache.computeIfAbsent(order.getUserId(), this::resolveWatchAttribution);
+            order.setCompanyName(attribution.getCompanyName());
+            order.setCompanyUserNickName(attribution.getCompanyUserNickName());
+        }
+    }
+
+    private WatchAttribution resolveWatchAttribution(Long userId) {
+        FsCourseWatchLog latestLog = fsCourseWatchLogMapper.selectLatestWatchLogByUserId(userId);
+        if (latestLog == null) {
+            return WatchAttribution.empty();
+        }
+        String companyName = null;
+        String companyUserNickName = null;
+        if (latestLog.getCompanyId() != null) {
+            companyName = companyCacheService.selectCompanyNameById(latestLog.getCompanyId());
+        }
+        if (latestLog.getCompanyUserId() != null) {
+            companyUserNickName = companyUserCacheService.selectCompanyUserNameUserById(latestLog.getCompanyUserId());
+        }
+        return new WatchAttribution(companyName, companyUserNickName);
+    }
+
+    private static class WatchAttribution {
+        private final String companyName;
+        private final String companyUserNickName;
+
+        WatchAttribution(String companyName, String companyUserNickName) {
+            this.companyName = companyName;
+            this.companyUserNickName = companyUserNickName;
+        }
+
+        static WatchAttribution empty() {
+            return new WatchAttribution(null, null);
+        }
+
+        String getCompanyName() {
+            return companyName;
+        }
+
+        String getCompanyUserNickName() {
+            return companyUserNickName;
+        }
     }
 
     @Override
@@ -1258,6 +1343,9 @@ public class FsIntegralOrderServiceImpl implements IFsIntegralOrderService
                 if (StringUtils.isNotBlank(vo.getDeliverySn())) {
                     param.setDeliverySn(vo.getDeliverySn());
                 }
+                if (StringUtils.isNotBlank(vo.getVirtualPhone())) {
+                    param.setVirtualPhone(vo.getVirtualPhone());
+                }
 
                 param.setDeliveryStatus((vo.getDeliveryStatus()==null|| vo.getDeliveryStatus().isEmpty())?null:Integer.valueOf(vo.getDeliveryStatus()));
                 param.setDeliveryType(vo.getDeliveryType().isEmpty()?null:vo.getDeliveryType());

+ 73 - 0
fs-service/src/main/java/com/fs/his/vo/FsCourseCouponUserVO.java

@@ -0,0 +1,73 @@
+package com.fs.his.vo;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户看课优惠券对象 fs_course_coupon_user
+ *
+ * @author fs
+ * @date 2026-05-13
+ */
+@Data
+public class FsCourseCouponUserVO {
+
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 优惠券id */
+    @Excel(name = "优惠券id")
+    private Long couponId;
+    private String couponTitle;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    private String nickName;
+
+
+    /** 有效期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "有效期", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date limitTime;
+
+    /** 开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date startTime;
+
+    /** 核销状态 0-未核销 1-已核销 */
+    @Excel(name = "核销状态 0-未核销 1-已核销")
+    private Integer status;
+
+    /**
+     * 看课记录ID
+     */
+    private Long logId;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+
+    /** 类型  1课程发的 2直播发的 */
+    @Excel(name = "类型  1课程发的 2直播发的")
+    private Integer sendType;
+
+
+    /** 直播间id */
+    @Excel(name = "直播间id")
+    private Long liveId;
+}

+ 7 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderExcelVO.java

@@ -27,6 +27,13 @@ public class FsIntegralOrderExcelVO implements Serializable {
     /** shou */
     @Excel(name = "收货人电话")
     private String userPhone;
+
+    /**
+     * 虚拟手机号
+     */
+    @Excel(name = "虚拟手机号", width = 30, sort = 45)
+    private String virtualPhone;
+
     /** 详情地址 */
     @Excel(name = "详情地址(例:广东省 韶关市 仁化县 亨特中心22楼)使用空格分割")
     private String userAddress;

+ 6 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderListVO.java

@@ -88,6 +88,12 @@ public class FsIntegralOrderListVO {
      * **/
     private Long companyId;
 
+    @Excel(name = "所属公司")
+    private String companyName;
+
+    @Excel(name = "所属销售")
+    private String companyUserNickName;
+
     private BigDecimal payMoney;
 
     private String erpPhone;

+ 2 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralOrderPVO.java

@@ -81,4 +81,6 @@ public class FsIntegralOrderPVO extends BaseEntity
     private String deliveryType;
 
     private Integer deliveryStatus;
+
+    private String virtualPhone;
 }

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

@@ -269,6 +269,9 @@ public class FsStoreOrderScrm extends BaseEntity
     @Excel(name = "发货时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
     private Date deliverySendTime;
 
+    /** 物流订阅成功的发货人手机号 */
+    private String senderPhone;
+
     //凭证
     private String certificates;
 

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java

@@ -322,6 +322,10 @@ public class FsStoreProductScrm extends BaseEntity
     @Excel(name = "退货地址")
     private String returnAddress;
 
+    /** 发货人手机号,多个逗号分隔,最多3个 */
+    @Excel(name = "发货人手机号")
+    private String senderPhones;
+
 
     /** 原产地 */
     @Excel(name = "原产地")

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

@@ -73,4 +73,7 @@ public interface FsCityScrmMapper
 
     @Select("select city_id from fs_city where city_name = #{name} limit 1")
     String selectCityIdByName(@Param("name") String name);
+
+    @Select("select id, city_id, level, parent_id, lng, lat from fs_city where city_id = #{cityId} limit 1")
+    FsCityScrm selectFsCityByCityId(@Param("cityId") String cityId);
 }

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

@@ -10,6 +10,7 @@ import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
 import com.fs.hisStore.param.FsStoreAfterSalesQueryParam;
 import com.fs.hisStore.vo.FsStoreAfterSalesQueryVO;
 import com.fs.hisStore.vo.FsStoreAfterSalesVO;
+import com.fs.hisStore.vo.FsStoreOrderRefundAmountVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -324,6 +325,11 @@ public interface FsStoreAfterSalesScrmMapper
             "WHERE sas.order_code = #{orderCode} AND sas.status = 4 AND sas.is_del = 0 LIMIT 1")
     FsStoreAfterSalesScrm selectFsStoreAfterSalesByOrderCode(@Param("orderCode") String orderCode);
 
+    /**
+     * 按订单号批量查询退款金额(仅 order_code、refund_amount,财务已审核且未删除)
+     */
+    List<FsStoreOrderRefundAmountVO> selectRefundAmountByOrderCodes(@Param("orderCodes") List<String> orderCodes);
+
     /**
      * 售后详情(含一级/二级原因展示文案,来自落库字段)
      */

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java

@@ -77,7 +77,7 @@ public interface FsStoreOrderItemScrmMapper
 
     @Select({"<script> " +
             "select i.*,o.user_id,u.nick_name,psps.cost,o.pay_postage,o.total_num,o.status,fspcs.cate_name, o.real_name,o.user_phone,o.user_address,o.create_time,o.pay_time,o.delivery_sn,o.delivery_name,o.delivery_id, c.company_name ,cu.nick_name as company_user_nick_name ,cu.phonenumber as company_usere_phonenumber,o.upload_time ,CASE WHEN o.certificates IS NULL OR o.certificates = '' THEN 0 ELSE 1 END AS is_upload,p.title as package_name,cts.name as scheduleName,sp_latest.pay_money, sp_latest.bank_transaction_id as bankTransactionId, o.delivery_send_time," +
-            " o.order_code, o.pay_price, o.pay_money, o.deduction_price,o.pay_delivery, o.order_type,psps.price " +
+            " o.order_code, o.pay_price, o.pay_money, o.deduction_price,o.pay_delivery, o.order_type, o.refund_price as refundPrice, psps.price " +
             ", CASE o.is_audit WHEN 1 THEN '是' ELSE '否' END AS isAudit " +
             ", sas.audit_remark as auditRemark, sas.reason_level1_text as reasonValue1, sas.reason_level2_text as reasonValue2 " +
             ", o.delivery_type" +

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

@@ -512,6 +512,18 @@ public interface FsStoreOrderScrmMapper
      */
     @Update("UPDATE fs_store_order_scrm SET outer_oi_id = #{outerOiId} WHERE order_code = #{orderCode}")
     int updateOuterOiIdByOrderCode(@Param("orderCode") String orderCode, @Param("outerOiId") String outerOiId);
+
+    /**
+     * 根据订单号更新物流订阅成功的发货人手机号
+     */
+    @Update("UPDATE fs_store_order_scrm SET sender_phone = #{senderPhone} WHERE order_code = #{orderCode}")
+    int updateSenderPhoneByOrderCode(@Param("orderCode") String orderCode, @Param("senderPhone") String senderPhone);
+
+    /**
+     * 根据订单号查询物流订阅成功的发货人手机号
+     */
+    @Select("SELECT sender_phone FROM fs_store_order_scrm WHERE order_code = #{orderCode} LIMIT 1")
+    String selectSenderPhoneByOrderCode(@Param("orderCode") String orderCode);
     @Update("update fs_store_order_scrm set status=-3 where id=#{orderId}")
     int cancelOrder(Long orderId);
     @Select({"<script> " +

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductAddEditParam.java

@@ -268,6 +268,10 @@ public class FsStoreProductAddEditParam implements Serializable
     @Excel(name = "退货地址")
     private String returnAddress;
 
+    /** 发货人手机号,多个逗号分隔,最多3个 */
+    @Excel(name = "发货人手机号")
+    private String senderPhones;
+
 
     private Integer isDrug;
 

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

@@ -64,4 +64,10 @@ public interface IFsCityScrmService
     List<FsCityScrm> selectFsCitys();
 
     List<CitysAreaVO> getCitysArea();
+
+    /**
+     * 解析运费匹配用的 cityId 列表:自身 + 各级上级 + 默认全国(0)。
+     * 用户地址常为区县级(如 110105000000),运费模板可能配置为市级(如 110000000000)。
+     */
+    List<String> resolveShippingMatchCityIds(String cityId);
 }

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

@@ -82,6 +82,16 @@ public interface IFsExpressScrmService
 
     void subscribeEspress(String orderCode, String deliverySn, String deliveryId, String userPhone);
 
+    /**
+     * 使用发货人手机号优先订阅物流(最多3个发货人手机号,失败后使用收货人手机号)
+     */
+    void subscribeEspressWithSenderPhones(String orderCode, String deliverySn, String deliveryId, String userPhone, String senderPhones);
+
+    /**
+     * 根据字典配置选择订阅方式
+     */
+    void subscribeExpressSmart(String orderCode, String deliverySn, String deliveryId, String userPhone, String senderPhones);
+
     R parseAddress(String content);
 
 

+ 10 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -214,6 +214,16 @@ public interface IFsStoreOrderScrmService
 
     List<FsStoreOrderErpExportVO> selectFsStoreOrderListVOByExport(FsStoreOrderParam param);
 
+    /**
+     * 填充导出订单的退款类型、退款金额(退款中/已退款订单)
+     */
+    void fillOrderRefundExportInfo(List<? extends FsStoreOrderExportVO> list);
+
+    /**
+     * 填充导出订单明细的退款类型、退款金额(退款中/已退款订单)
+     */
+    void fillOrderItemRefundExportInfo(List<FsStoreOrderItemExportVO> list);
+
     List<FsStoreOrderPromotionExportVO> selectFsPromotionOrderListVOByExport(FsStoreOrderParam param);
 
     List<FsStorePayRemainOrderExportVO> selectFsStorePayRemainOrderListVOByExport(FsStoreOrderParam param);

+ 26 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsCityScrmServiceImpl.java

@@ -1,8 +1,11 @@
 package com.fs.hisStore.service.impl;
 
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.List;
 
 import com.fs.his.vo.CitysAreaVO;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.hisStore.mapper.FsCityScrmMapper;
@@ -103,4 +106,27 @@ public class FsCityScrmServiceImpl implements IFsCityScrmService
     public List<CitysAreaVO> getCitysArea() {
         return fsCityMapper.getCitysArea();
     }
+
+    @Override
+    public List<String> resolveShippingMatchCityIds(String cityId) {
+        LinkedHashSet<String> ids = new LinkedHashSet<>();
+        if (StringUtils.isBlank(cityId)) {
+            ids.add("0");
+            return new ArrayList<>(ids);
+        }
+        String current = cityId.trim();
+        for (int depth = 0; depth < 10; depth++) {
+            if (StringUtils.isBlank(current) || "0".equals(current) || ids.contains(current)) {
+                break;
+            }
+            ids.add(current);
+            FsCityScrm city = fsCityMapper.selectFsCityByCityId(current);
+            if (city == null || StringUtils.isBlank(city.getParentId()) || "0".equals(city.getParentId())) {
+                break;
+            }
+            current = city.getParentId();
+        }
+        ids.add("0");
+        return new ArrayList<>(ids);
+    }
 }

+ 155 - 13
fs-service/src/main/java/com/fs/hisStore/service/impl/FsExpressScrmServiceImpl.java

@@ -3,9 +3,11 @@ package com.fs.hisStore.service.impl;
 import java.net.URLEncoder;
 import java.security.MessageDigest;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
@@ -29,6 +31,8 @@ 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 com.fs.system.service.ISysDictTypeService;
+import com.fs.common.core.domain.entity.SysDictData;
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,6 +52,8 @@ import org.springframework.util.Base64Utils;
 @Service
 public class FsExpressScrmServiceImpl implements IFsExpressScrmService
 {
+    private static final String STORE_EXPRESS_SENDER_PHONE_SUBSCRIBE = "store_express_sender_phone";
+
     Logger logger = LoggerFactory.getLogger(FsExpressScrmServiceImpl.class);
     @Autowired
     private FsExpressScrmMapper fsExpressMapper;
@@ -56,6 +62,9 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private ISysDictTypeService sysDictTypeService;
+
     @Autowired
     private LiveOrderMapper liveOrderMapper;
     /**
@@ -134,11 +143,12 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
 
     @Override
     public ExpressInfoDTO getExpressInfo(String OrderCode, String ShipperCode, String LogisticCode, String lastFourNumber)     {
+        String customerName = resolveExpressQueryPhone(OrderCode, ShipperCode, lastFourNumber);
 
         //处理顺丰查询轨迹需手机号码后4位
         String requestData;
         if (ShipperCode.equals(ShipperCodeEnum.SF.getValue()) || ShipperCode.equals(ShipperCodeEnum.ZTO.getValue())) {
-            requestData = "{'OrderCode':'" + OrderCode + "','ShipperCode':'" + ShipperCode + "','LogisticCode':'" + LogisticCode + "','CustomerName':'" + lastFourNumber + "'}";
+            requestData = "{'OrderCode':'" + OrderCode + "','ShipperCode':'" + ShipperCode + "','LogisticCode':'" + LogisticCode + "','CustomerName':'" + customerName + "'}";
         } else {
             requestData = "{'OrderCode':'" + OrderCode + "','ShipperCode':'" + ShipperCode + "','LogisticCode':'" + LogisticCode + "'}";
         }
@@ -215,7 +225,57 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
 
     @Override
     public void subscribeEspress(String orderCode, String deliverySn, String deliveryId, String userPhone) {
-        //处理顺丰查询轨迹需手机号码后4位
+        ExpressInfoDTO dto = doSubscribeRequest(orderCode, deliverySn, deliveryId, userPhone);
+        if (dto == null || !dto.isSuccess()) {
+            logger.error("订阅物流失败:orderCode={}, deliveryId={}, reason={}",
+                    orderCode, deliveryId, dto != null ? dto.getReason() : "订阅物流返回为空");
+            afterExpressFailure(orderCode, deliveryId, dto != null ? dto.getReason() : "订阅物流返回为空");
+            throw new CustomException(dto != null && StringUtils.isNotBlank(dto.getReason()) ? dto.getReason() : "订阅物流失败");
+        }
+    }
+
+    @Override
+    public void subscribeEspressWithSenderPhones(String orderCode, String deliverySn, String deliveryId, String userPhone, String senderPhones) {
+        List<String> senderPhoneList = parseSenderPhones(senderPhones);
+        if (!senderPhoneList.isEmpty()) {
+            for (String senderPhone : senderPhoneList) {
+                String customerName = resolveCustomerName(deliverySn, senderPhone);
+                ExpressInfoDTO dto = doSubscribeRequest(orderCode, deliverySn, deliveryId, customerName);
+                if (dto != null && dto.isSuccess()) {
+                    logger.info("订阅物流成功(发货人手机号): orderCode={}, phone={}", orderCode, senderPhone);
+                    updateOrderSenderPhoneByOrderCode(orderCode, senderPhone);
+                    return;
+                }
+                if (isPhoneError(dto)) {
+                    logger.warn("发货人手机号订阅失败,尝试下一个: orderCode={}, phone={}, reason={}",
+                            orderCode, senderPhone, dto != null ? dto.getReason() : null);
+                    continue;
+                }
+                logger.warn("订阅物流失败(非手机号错误),改用收货人手机号: orderCode={}, reason={}",
+                        orderCode, dto != null ? dto.getReason() : null);
+                break;
+            }
+        }
+        ExpressInfoDTO receiverDto = doSubscribeRequest(orderCode, deliverySn, deliveryId, userPhone);
+        if (receiverDto == null || !receiverDto.isSuccess()) {
+            logger.error("订阅物流失败(收货人手机号):orderCode={}, deliveryId={}, reason={}",
+                    orderCode, deliveryId, receiverDto != null ? receiverDto.getReason() : "订阅物流返回为空");
+            afterExpressFailure(orderCode, deliveryId, receiverDto != null ? receiverDto.getReason() : "订阅物流返回为空");
+        } else {
+            logger.info("订阅物流成功(收货人手机号): orderCode={}", orderCode);
+        }
+    }
+
+    @Override
+    public void subscribeExpressSmart(String orderCode, String deliverySn, String deliveryId, String userPhone, String senderPhones) {
+        if (isSenderPhoneSubscribeEnabled()) {
+            subscribeEspressWithSenderPhones(orderCode, deliverySn, deliveryId, userPhone, senderPhones);
+        } else {
+            subscribeEspress(orderCode, deliverySn, deliveryId, userPhone);
+        }
+    }
+
+    private ExpressInfoDTO doSubscribeRequest(String orderCode, String deliverySn, String deliveryId, String userPhone) {
         String requestData;
         if (deliverySn.equals(ShipperCodeEnum.SF.getValue()) || deliverySn.equals(ShipperCodeEnum.ZTO.getValue())) {
             requestData = "{'OrderCode':'" + orderCode + "','ShipperCode':'" + deliverySn + "','LogisticCode':'" + deliveryId + "','CustomerName':'" + userPhone + "'}";
@@ -226,25 +286,69 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
         Map<String, Object> params = new HashMap<>();
         try {
             String json = this.configService.selectConfigByKey("his.config");
-            FSSysConfig sysConfig= JSON.parseObject(json,FSSysConfig.class);
+            FSSysConfig sysConfig = JSON.parseObject(json, FSSysConfig.class);
             params.put("RequestData", URLEncoder.encode(requestData, "UTF-8"));
-            params.put("EBusinessID",sysConfig.getKdnId().trim());
-            params.put("RequestType", "8001");
+            params.put("EBusinessID", sysConfig.getKdnId().trim());
+            params.put("RequestType", "8008");
             String dataSign = encrypt(requestData, sysConfig.getKdnKeyId().trim(), "UTF-8");
             params.put("DataSign", URLEncoder.encode(dataSign, "UTF-8"));
             params.put("DataType", "2");
             String result = HttpUtil.post(sysConfig.getKdnSubscribeUrl().trim(), params);
-            logger.info("订阅物流:"+result);
-            ExpressInfoDTO dto = JSONUtil.toBean(result, ExpressInfoDTO.class);
-            if (dto == null || !dto.isSuccess()) {
-                logger.error("订阅物流失败:{}:{}", result, requestData);
-                afterExpressFailure(orderCode, deliveryId, dto != null ? dto.getReason() : "订阅物流返回为空");
+            logger.info("订阅物流:{}", result);
+            return JSONUtil.toBean(result, ExpressInfoDTO.class);
+        } catch (Exception e) {
+            logger.error("订阅物流异常: orderCode={}, deliveryId={}", orderCode, deliveryId, e);
+            return null;
+        }
+    }
+
+    private boolean isSenderPhoneSubscribeEnabled() {
+        List<SysDictData> dictList = sysDictTypeService.selectDictDataByType(STORE_EXPRESS_SENDER_PHONE_SUBSCRIBE);
+        if (dictList == null || dictList.isEmpty()) {
+            return false;
+        }
+        return dictList.stream()
+                .filter(item -> "0".equals(item.getStatus()))
+                .anyMatch(item -> "1".equals(StringUtils.trimToEmpty(item.getDictValue())));
+    }
+
+    private List<String> parseSenderPhones(String senderPhones) {
+        if (StringUtils.isBlank(senderPhones)) {
+            return new ArrayList<>();
+        }
+        return Arrays.stream(senderPhones.split("[,,]"))
+                .map(String::trim)
+                .filter(StringUtils::isNotBlank)
+                .limit(3)
+                .collect(Collectors.toList());
+    }
+
+    private String resolveCustomerName(String deliverySn, String phone) {
+        if (StringUtils.isBlank(phone)) {
+            return "";
+        }
+        if (deliverySn.equals(ShipperCodeEnum.SF.getValue()) || deliverySn.equals(ShipperCodeEnum.ZTO.getValue())) {
+            if (phone.length() == 11) {
+                return StrUtil.sub(phone, phone.length(), -4);
             }
+        }
+        return phone;
+    }
 
-        } catch (Exception e) {
-            afterExpressFailure(orderCode, deliveryId, e.getMessage());
-            throw  new CustomException(e.getMessage());
+    private boolean isPhoneError(ExpressInfoDTO dto) {
+        if (dto == null || dto.isSuccess()) {
+            return false;
         }
+        String reason = StringUtils.trimToEmpty(dto.getReason());
+        if (StringUtils.isBlank(reason)) {
+            return false;
+        }
+        return reason.contains("手机尾号不正确")
+                || reason.contains("手机尾号")
+                || reason.contains("手机")
+                || reason.contains("电话")
+                || reason.contains("CustomerName")
+                || reason.contains("号码");
     }
 
     @Override
@@ -376,6 +480,44 @@ public class FsExpressScrmServiceImpl implements IFsExpressScrmService
         return lastFourNumber;
     }
 
+    /**
+     * 根据订单号查询物流订阅成功的发货人手机号
+     */
+    private String getOrderSenderPhoneByOrderCode(String orderCode) {
+        if (StringUtils.isBlank(orderCode)) {
+            return null;
+        }
+        return fsStoreOrderMapper.selectSenderPhoneByOrderCode(orderCode);
+    }
+
+    /**
+     * 解析物流查询使用的手机号:字典开启且订单有发货人手机号时优先使用,否则使用收货人手机尾号
+     */
+    private String resolveExpressQueryPhone(String orderCode, String shipperCode, String lastFourNumber) {
+        if (!isSenderPhoneSubscribeEnabled() || StringUtils.isBlank(orderCode)) {
+            return lastFourNumber;
+        }
+        String senderPhone = getOrderSenderPhoneByOrderCode(orderCode);
+        if (StringUtils.isBlank(senderPhone)) {
+            return lastFourNumber;
+        }
+        return resolveCustomerName(shipperCode, senderPhone);
+    }
+
+    /**
+     * 根据订单号更新物流订阅成功的发货人手机号
+     */
+    private void updateOrderSenderPhoneByOrderCode(String orderCode, String senderPhone) {
+        if (StringUtils.isBlank(orderCode) || StringUtils.isBlank(senderPhone)) {
+            return;
+        }
+        try {
+            fsStoreOrderMapper.updateSenderPhoneByOrderCode(orderCode, senderPhone);
+        } catch (Exception e) {
+            logger.error("更新订单发货人手机号异常: orderCode={}, senderPhone={}", orderCode, senderPhone, e);
+        }
+    }
+
     /**
      * 快递查询/订阅失败或异常时更新订单物流状态为问题件
      */

+ 12 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderItemScrmServiceImpl.java

@@ -7,14 +7,16 @@ import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.his.vo.FsStoreOrderItemListDVO;
 import com.fs.hisStore.dto.FsStoreCartDTO;
+import com.fs.hisStore.domain.FsStoreOrderItemScrm;
+import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
 import com.fs.hisStore.param.FsStoreOrderParam;
 import com.fs.hisStore.vo.FsStoreOrderItemExportVO;
 import com.fs.hisStore.vo.FsStoreOrderItemVO;
+import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
-import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
-import com.fs.hisStore.domain.FsStoreOrderItemScrm;
-import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
 
 /**
  * 订单详情Service业务层处理
@@ -28,6 +30,10 @@ public class FsStoreOrderItemScrmServiceImpl implements IFsStoreOrderItemScrmSer
     @Autowired
     private FsStoreOrderItemScrmMapper fsStoreOrderItemMapper;
 
+    @Autowired
+    @Lazy
+    private IFsStoreOrderScrmService fsStoreOrderScrmService;
+
     /**
      * 查询订单详情
      *
@@ -112,7 +118,9 @@ public class FsStoreOrderItemScrmServiceImpl implements IFsStoreOrderItemScrmSer
 
     @Override
     public List<FsStoreOrderItemExportVO> selectFsStoreOrderItemListExportVO(FsStoreOrderParam fsStoreOrder) {
-        return fsStoreOrderItemMapper.selectFsStoreOrderItemListExportVO(fsStoreOrder);
+        List<FsStoreOrderItemExportVO> list = fsStoreOrderItemMapper.selectFsStoreOrderItemListExportVO(fsStoreOrder);
+        fsStoreOrderScrmService.fillOrderItemRefundExportInfo(list);
+        return list;
     }
 
     @Override

+ 161 - 12
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -91,6 +91,9 @@ import com.fs.hisStore.param.FsStoreOrderParam;
 import com.fs.hisStore.param.FsStoreOrderPayParam;
 import com.fs.hisStore.vo.*;
 import com.fs.hisStore.vo.FsStoreOrderErpExportVO;
+import com.fs.hisStore.vo.FsStoreOrderExportVO;
+import com.fs.hisStore.vo.FsStoreOrderItemExportVO;
+import com.fs.hisStore.vo.FsStoreOrderRefundAmountVO;
 import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.hisStore.vo.FsStoreOrderVO;
 import com.fs.hisStore.vo.FsStoreProductAttrValueVO;
@@ -293,6 +296,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private IFsShippingTemplatesRegionScrmService shippingTemplatesRegionService;
 
+    @Autowired
+    private IFsCityScrmService fsCityService;
+
     @Autowired
     private IFsShippingTemplatesFreeScrmService shippingTemplatesFreeService;
     @Autowired
@@ -672,6 +678,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             }
             return rows;
         }
+        if (fsStoreOrder.getDeliveryType() != null && "4".equals(fsStoreOrder.getDeliveryType())) {
+            fsStoreOrder.setDeliveryExceptionStatus(1);
+        }
 
         return fsStoreOrderMapper.updateFsStoreOrder(fsStoreOrder);
     }
@@ -2278,7 +2287,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                 }
             }
-            expressService.subscribeEspress(order.getOrderCode(), order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
+            subscribeOrderExpress(order, order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
 
             TemplateBean templateBean = TemplateBean.builder()
                     .orderId(order.getId().toString())
@@ -2307,6 +2316,35 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
     }
 
+    private String resolveSenderPhonesByOrder(FsStoreOrderScrm order) {
+        if (order == null || StringUtils.isBlank(order.getItemJson())) {
+            return null;
+        }
+        try {
+            com.alibaba.fastjson.JSONArray itemArray = JSON.parseArray(order.getItemJson());
+            if (itemArray == null || itemArray.isEmpty()) {
+                return null;
+            }
+            Long productId = itemArray.getJSONObject(0).getLong("productId");
+            if (productId == null) {
+                return null;
+            }
+            FsStoreProductScrm product = productService.selectFsStoreProductById(productId);
+            return product != null ? product.getSenderPhones() : null;
+        } catch (Exception e) {
+            log.warn("解析订单itemJson获取发货人手机号失败: orderCode={}, error={}", order.getOrderCode(), e.getMessage());
+            return null;
+        }
+    }
+
+    private void subscribeOrderExpress(FsStoreOrderScrm order, String deliverySn, String deliveryId, String lastFourNumber) {
+        if (order == null || StringUtils.isBlank(deliverySn) || StringUtils.isBlank(deliveryId)) {
+            return;
+        }
+        String senderPhones = resolveSenderPhonesByOrder(order);
+        expressService.subscribeExpressSmart(order.getOrderCode(), deliverySn, deliveryId, lastFourNumber, senderPhones);
+    }
+
     @Override
     public void updateDeliveryOrder(Long id, String deliveryId, String deliverCode, String deliverName) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(id);
@@ -2337,7 +2375,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                         lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                     }
                 }
-                expressService.subscribeEspress(order.getOrderCode(), express.getCode(), deliveryId, lastFourNumber);
+                subscribeOrderExpress(order, express.getCode(), deliveryId, lastFourNumber);
             }
         }
     }
@@ -3972,12 +4010,125 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
     @Override
     public List<FsStoreOrderErpExportVO> selectFsStoreOrderListVOByExport(FsStoreOrderParam param) {
+        List<FsStoreOrderErpExportVO> list;
         if (CloudHostUtils.hasCloudHostName("金牛明医","康年堂")){
-            return fsStoreOrderMapper.selectFsStoreOrderErpListVOByExport(param);
-
+            list = fsStoreOrderMapper.selectFsStoreOrderErpListVOByExport(param);
         } else {
-            return fsStoreOrderMapper.selectFsStoreOrderListVOByExport(param);
+            list = fsStoreOrderMapper.selectFsStoreOrderListVOByExport(param);
+        }
+        fillOrderRefundExportInfo(list);
+        return list;
+    }
 
+    @Override
+    public void fillOrderRefundExportInfo(List<? extends FsStoreOrderExportVO> list) {
+        if (CollectionUtil.isEmpty(list)) {
+            return;
+        }
+        List<String> needLookupOrderCodes = new ArrayList<>();
+        for (FsStoreOrderExportVO vo : list) {
+            if (!isRefundedOrderStatus(vo.getStatus())) {
+                continue;
+            }
+            if (needsAfterSalesRefundLookup(vo.getRefundPrice()) && StrUtil.isNotBlank(vo.getOrderCode())) {
+                needLookupOrderCodes.add(vo.getOrderCode());
+            }
+        }
+        Map<String, BigDecimal> refundAmountMap = batchLoadRefundAmountByOrderCodes(needLookupOrderCodes);
+        for (FsStoreOrderExportVO vo : list) {
+            if (!isRefundedOrderStatus(vo.getStatus())) {
+                continue;
+            }
+            BigDecimal refundAmount = vo.getRefundPrice();
+            if (needsAfterSalesRefundLookup(refundAmount)) {
+                refundAmount = refundAmountMap.get(vo.getOrderCode());
+                if (refundAmount != null) {
+                    vo.setRefundPrice(refundAmount);
+                }
+            }
+            BigDecimal payAmount = vo.getPayMoney() != null ? vo.getPayMoney() : vo.getPayPrice();
+            if (refundAmount != null && payAmount != null) {
+                vo.setRefundType(refundAmount.compareTo(payAmount) < 0 ? "部分退款" : "全额退款");
+            }
+        }
+    }
+
+    private static final int REFUND_AMOUNT_BATCH_SIZE = 1000;
+
+    private Map<String, BigDecimal> batchLoadRefundAmountByOrderCodes(List<String> orderCodes) {
+        if (CollectionUtil.isEmpty(orderCodes)) {
+            return Collections.emptyMap();
+        }
+        List<String> distinctOrderCodes = orderCodes.stream()
+                .filter(StrUtil::isNotBlank)
+                .distinct()
+                .collect(Collectors.toList());
+        if (distinctOrderCodes.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<String, BigDecimal> refundAmountMap = new HashMap<>(distinctOrderCodes.size());
+        for (int i = 0; i < distinctOrderCodes.size(); i += REFUND_AMOUNT_BATCH_SIZE) {
+            int end = Math.min(i + REFUND_AMOUNT_BATCH_SIZE, distinctOrderCodes.size());
+            List<String> batch = distinctOrderCodes.subList(i, end);
+            List<FsStoreOrderRefundAmountVO> rows = fsStoreAfterSalesScrmMapper.selectRefundAmountByOrderCodes(batch);
+            if (CollectionUtil.isEmpty(rows)) {
+                continue;
+            }
+            for (FsStoreOrderRefundAmountVO row : rows) {
+                if (row.getOrderCode() != null && row.getRefundAmount() != null) {
+                    refundAmountMap.put(row.getOrderCode(), row.getRefundAmount());
+                }
+            }
+        }
+        return refundAmountMap;
+    }
+
+    private boolean isRefundedOrderStatus(String status) {
+        if (status == null) {
+            return false;
+        }
+        try {
+            int orderStatus = Integer.parseInt(status);
+            return orderStatus == -1 || orderStatus == -2;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
+    private boolean needsAfterSalesRefundLookup(BigDecimal refundPrice) {
+        return refundPrice == null || refundPrice.compareTo(BigDecimal.ZERO) == 0;
+    }
+
+    @Override
+    public void fillOrderItemRefundExportInfo(List<FsStoreOrderItemExportVO> list) {
+        if (CollectionUtil.isEmpty(list)) {
+            return;
+        }
+        List<String> needLookupOrderCodes = new ArrayList<>();
+        for (FsStoreOrderItemExportVO vo : list) {
+            if (!isRefundedOrderStatus(vo.getStatus())) {
+                continue;
+            }
+            if (needsAfterSalesRefundLookup(vo.getRefundPrice()) && StrUtil.isNotBlank(vo.getOrderCode())) {
+                needLookupOrderCodes.add(vo.getOrderCode());
+            }
+        }
+        Map<String, BigDecimal> refundAmountMap = batchLoadRefundAmountByOrderCodes(needLookupOrderCodes);
+        for (FsStoreOrderItemExportVO vo : list) {
+            if (!isRefundedOrderStatus(vo.getStatus())) {
+                continue;
+            }
+            BigDecimal refundAmount = vo.getRefundPrice();
+            if (needsAfterSalesRefundLookup(refundAmount)) {
+                refundAmount = refundAmountMap.get(vo.getOrderCode());
+            }
+            if (refundAmount != null) {
+                vo.setRefundPrice(refundAmount);
+            }
+            BigDecimal payAmount = vo.getPayMoney() != null ? vo.getPayMoney() : vo.getPayPrice();
+            if (refundAmount != null && payAmount != null) {
+                vo.setRefundType(refundAmount.compareTo(payAmount) < 0 ? "部分退款" : "全额退款");
+            }
         }
     }
 
@@ -4438,9 +4589,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 return storePostage;
             }
             String cityId = userAddress.getCityId();
-            List<String> citys = new ArrayList<>();
-            citys.add(cityId);
-            citys.add("0");//城市包括默认
+            List<String> citys = fsCityService.resolveShippingMatchCityIds(cityId);
             List<Long> tempIds = cartInfo
                     .stream()
                     .map(FsStoreCartQueryVO::getTempId)
@@ -4703,7 +4852,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                                     }
                                 }
                                 logger.info("物流重新订阅:{}", order.getDeliveryId());
-                                expressService.subscribeEspress(order.getOrderCode(), order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
+                                subscribeOrderExpress(order, order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
                             }
 
                         }
@@ -5095,7 +5244,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                         lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                     }
                 }
-                expressService.subscribeEspress(o.getOrderCode(), fsStoreOrder.getDeliveryCode(), fsStoreOrder.getDeliverySn(), lastFourNumber);
+                subscribeOrderExpress(o, fsStoreOrder.getDeliveryCode(), fsStoreOrder.getDeliverySn(), lastFourNumber);
 
                 try {
                     String s = fsPrescribeService.PrescribeImg(o.getPrescribeId());
@@ -5191,7 +5340,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
             }
         }
-        expressService.subscribeEspress(order.getOrderCode(), fsStoreOrder.getDeliveryCode(), fsStoreOrder.getDeliverySn(), lastFourNumber);
+        subscribeOrderExpress(order, fsStoreOrder.getDeliveryCode(), fsStoreOrder.getDeliverySn(), lastFourNumber);
         return i;
     }
 
@@ -5520,7 +5669,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                             lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                         }
                     }
-                    expressService.subscribeEspress(order.getOrderCode(), order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
+                    subscribeOrderExpress(order, order.getDeliverySn(), order.getDeliveryId(), lastFourNumber);
 
                     TemplateBean templateBean = TemplateBean.builder()
                             .orderId(order.getId().toString())

+ 1 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java

@@ -23,10 +23,7 @@ import com.fs.common.annotation.DataScope;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.CustomException;
-import com.fs.common.utils.CloudHostUtils;
-import com.fs.common.utils.DateUtils;
-import com.fs.common.utils.ServletUtils;
-import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.*;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;

+ 54 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreDeliveryAbnormalOrderExportVO.java

@@ -0,0 +1,54 @@
+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.util.Date;
+
+/**
+ * 物流异常订单导出
+ */
+@Data
+public class FsStoreDeliveryAbnormalOrderExportVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @Excel(name = "ID")
+    private Long id;
+
+    @Excel(name = "所属公司")
+    private String companyName;
+
+    @Excel(name = "所属员工")
+    private String companyUserNickName;
+
+    @Excel(name = "订单单号")
+    private String orderCode;
+
+    @Excel(name = "会员手机号")
+    private String userPhone;
+
+    @Excel(name = "订单状态", dictType = "store_order_status")
+    private String status;
+
+    @Excel(name = "物流状态", dictType = "store_order_delivery_status")
+    private Integer deliveryStatus;
+
+    @Excel(name = "物流单号")
+    private String deliveryId;
+
+    @Excel(name = "异常状态", dictType = "store_order_delivery_type")
+    private String deliveryType;
+
+    @Excel(name = "处理状态", readConverterExp = "1=待处理,2=已处理")
+    private Integer deliveryExceptionStatus;
+
+    @Excel(name = "异常备注")
+    private String deliveryExceptionRemark;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 6 - 2
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderExportVO.java

@@ -170,6 +170,10 @@ public class FsStoreOrderExportVO implements Serializable
 ////    @Excel(name = "不退款的理由")
 //    private String refundReason;
 
+    /** 退款类型:全额退款 / 部分退款 */
+    @Excel(name = "退款类型")
+    private String refundType;
+
     /** 退款金额 */
     @Excel(name = "退款金额",cellType= Excel.ColumnType.NUMERIC)
     private BigDecimal refundPrice;
@@ -198,8 +202,8 @@ public class FsStoreOrderExportVO implements Serializable
 //    /** 给用户退了多少积分 */
 //    private BigDecimal backIntegral;
 
-    /** 备注 */
-    @Excel(name = "备注")
+    /** 用户订单备注 */
+    @Excel(name = "用户备注")
     private String mark;
 
 //    /** 是否删除 */

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java

@@ -143,5 +143,11 @@ public class FsStoreOrderItemExportVO implements Serializable
     @Excel(name = "售后备注")
     private String auditRemark;
 
+    @Excel(name = "退款类型")
+    private String refundType;
+
+    @Excel(name = "退款金额", cellType = Excel.ColumnType.NUMERIC)
+    private BigDecimal refundPrice;
+
 
 }

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

@@ -132,4 +132,10 @@ public class FsStoreOrderItemExportZMVO implements Serializable {
 
     @Excel(name = "售后备注")
     private String auditRemark;
+
+    @Excel(name = "退款类型")
+    private String refundType;
+
+    @Excel(name = "退款金额", cellType = Excel.ColumnType.NUMERIC)
+    private BigDecimal refundPrice;
 }

+ 16 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderRefundAmountVO.java

@@ -0,0 +1,16 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 订单导出批量填充退款金额(仅必要字段)
+ */
+@Data
+public class FsStoreOrderRefundAmountVO {
+
+    private String orderCode;
+
+    private BigDecimal refundAmount;
+}

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

@@ -149,4 +149,9 @@ public class   Live extends BaseEntity {
     /** 查询用:为 true 时仅查训练营直播间(training_period_id 非空),总后台审核列表等 */
     @TableField(exist = false)
     private Boolean onlyTrainingCampLive;
+
+    /**
+    * 直播间分类
+    */
+    private Integer liveGroupType;
 }

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

@@ -57,10 +57,23 @@ public class LiveCouponUser extends BaseEntity
     @Excel(name = "获取方式")
     private String type;
 
-    /** 状态(0:未使用,1:已使用, 2:已过期) */
-    @Excel(name = "状态", readConverterExp = "0=:未使用,1:已使用,,2=:已过期")
+    /** 状态(0:未核销,1:已核销, 2:已过期) */
+    @Excel(name = "状态", readConverterExp = "0=:未核销,1:已核销,,2=:已过期")
     private Integer status;
 
+    /** 核销码(用于扫码核销) */
+    @Excel(name = "核销码")
+    private String verifyCode;
+
+    /** 核销销售用户ID */
+    @Excel(name = "核销销售用户ID")
+    private Long verifyUserId;
+
+    /** 核销时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "核销时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date verifyTime;
+
     /** 是否有效 */
     @Excel(name = "是否有效")
     private Integer isFail;

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

@@ -0,0 +1,27 @@
+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_group_type
+ *
+ * @author fs
+ * @date 2026-06-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveGroupType extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 直播间分类 */
+    @Excel(name = "直播间分类")
+    private String liveGroupType;
+
+
+}

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

@@ -69,9 +69,16 @@ public class LiveRedPacketLog extends BaseEntity{
     @Excel(name = "商户ID")
     private String mchId;
 
+
     /** 直播方式 1 app 2浏览器(可能有) */
     @Excel(name = "直播方式 1 app 2浏览器(可能有)")
     private Long wathcType;
 
+    /**
+    * 备注
+    */
+    @Excel(name = "备注")
+    private String remark;
+
 
 }

+ 28 - 2
fs-service/src/main/java/com/fs/live/domain/LiveWatchLog.java

@@ -3,9 +3,11 @@ package com.fs.live.domain;
 import java.util.Date;
 import java.util.Objects;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
+import liquibase.pro.packaged.S;
 import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.EqualsAndHashCode;
@@ -23,14 +25,23 @@ public class LiveWatchLog extends BaseEntity{
     /** 日志id */
     private Long logId;
 
-    /** 用户userId */
-    @Excel(name = "用户userId")
+    /** 会员userId */
+    @Excel(name = "会员userId")
     private Long userId;
 
+    @Excel(name = "会员名称")
+    @TableField(exist = false)
+    private String userIdName;
+
     /** 直播间id */
     @Excel(name = "直播间id")
     private Long liveId;
 
+    /** 直播间名称 */
+    @TableField(exist = false)
+    @Excel(name = "直播间名称")
+    private String liveName;
+
     /** 记录类型 1看课中 2完课 3待看课 4看课中断 */
     @Excel(name = "记录类型 1看课中 2完课 3待看课 4看课中断")
     private Integer logType;
@@ -39,10 +50,20 @@ public class LiveWatchLog extends BaseEntity{
     @Excel(name = "外部联系人id")
     private Long externalContactId;
 
+    /** 外部联系人名称*/
+    @Excel(name = "外部联系人名称")
+    @TableField(exist = false)
+    private String externalContactName;
+
     /** 销售id */
     @Excel(name = "销售id")
     private Long companyUserId;
 
+    /** 销售名称 */
+    @Excel(name = "销售名称")
+    @TableField(exist = false)
+    private String companyUserName;
+
     /** 公司id */
     @Excel(name = "公司id")
     private Long companyId;
@@ -68,6 +89,11 @@ public class LiveWatchLog extends BaseEntity{
     /** 分享人企微id */
     @Excel(name = "分享人企微id")
     private String qwUserId;
+
+    /** 分享人企微名称 */
+    @Excel(name = "分享人企微名称 */")
+    @TableField(exist = false)
+    private String qwUserName;
     /**
      * 查看直播类型:1、直播,2、回放
      */

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

@@ -51,6 +51,10 @@ public class LiveWatchUser extends BaseEntity {
 
     private String nickName;
     private String tabName;
+    /**
+    * 备注
+    */
+    private String remark;
 
     /** 直播进入标记:0-否 1-是 */
     @Excel(name = "直播进入标记")
@@ -73,7 +77,7 @@ public class LiveWatchUser extends BaseEntity {
     */
     private Integer sendType;
     /**
-    * 奖励类型 1红包 2积分 3 设置奖励
+    * 奖励类型 1红包 2积分 3 红包 + 积分 (1|2) 4核销卷 5 红包 + 核销卷 (1|4) 6积分 + 核销卷 (2|4) 7 全部 (1|2|4) 99 未设置奖励
     */
     private Integer rewardType;
 

+ 7 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCouponIssueMapper.java

@@ -1,6 +1,9 @@
 package com.fs.live.mapper;
 
 import java.util.List;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveCouponIssue;
 import com.fs.live.domain.LiveCouponIssueRelation;
 import com.fs.live.param.CouponPO;
@@ -78,4 +81,8 @@ public interface LiveCouponIssueMapper
 
     @Select("select * from live_coupon_issue where coupon_id= #{couponId}")
     LiveCouponIssue selectLiveCouponIssueByCouponId(@Param("couponId") Long couponId);
+
+    @DataSource(DataSourceType.SLAVE)
+    @Select("select * from live_coupon_issue where coupon_id= #{couponId} and status=1  and is_del=0 ")
+    LiveCouponIssue selectLiveCouponIssueByCouponIdByStatus(@Param("couponId") Long couponId);
 }

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

@@ -6,6 +6,7 @@ import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveCouponUser;
 import com.fs.live.param.CouponPO;
+import com.fs.live.vo.LiveCouponUserDetailVo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
@@ -34,6 +35,9 @@ public interface LiveCouponUserMapper
      */
     public List<LiveCouponUser> selectLiveCouponUserList(LiveCouponUser liveCouponUser);
 
+    @DataSource(DataSourceType.SLAVE)
+    public List<LiveCouponUser> selectLiveCouponUserListVO(LiveCouponUser liveCouponUser);
+
     /**
      * 新增优惠券发放记录
      *
@@ -66,15 +70,44 @@ public interface LiveCouponUserMapper
      */
     public int deleteLiveCouponUserByIds(Long[] ids);
 
-    @Select("select lcu.* from live_coupon_user lcu  where lcu.user_id = #{coupon.userId}")
+//    @Select("select lcu.* from live_coupon_user lcu  where lcu.user_id = #{coupon.userId}")
+//    List<LiveCouponUser> selectLiveCouponUserByCouponPO(@Param("coupon") CouponPO coupon);
+
+    @Select("<script>" +
+            "select lcu.* from live_coupon_user lcu where lcu.user_id = #{coupon.userId} and lcu.is_del = 0" +
+            " <if test='coupon.status != null'> and lcu.status = #{coupon.status}</if>" +
+            " order by lcu.create_time desc" +
+            "</script>")
     List<LiveCouponUser> selectLiveCouponUserByCouponPO(@Param("coupon") CouponPO coupon);
 
+
+//    @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} or lcu.goods_id is null or lcu.goods_id = 0)" +
+//            " </if>" +
+//            "</script>")
+//    @DataSource(DataSourceType.SLAVE)
+//    List<LiveCouponUser> curCoupon(@Param("coupon") CouponPO coupon);
+
     @Select("<script>" +
-            "select lcu.* from live_coupon_user lcu where lcu.user_id= #{coupon.userId} " +
+            "select lcu.* from live_coupon_user lcu where lcu.user_id= #{coupon.userId} and lcu.is_del = 0" +
+            " <if test='coupon.status != null'> and lcu.status = #{coupon.status}</if>" +
             " <if test='coupon.goodsId != null'>" +
             " and (lcu.goods_id= #{coupon.goodsId} or lcu.goods_id is null or lcu.goods_id = 0)" +
             " </if>" +
+            " order by lcu.create_time desc" +
             "</script>")
     @DataSource(DataSourceType.SLAVE)
     List<LiveCouponUser> curCoupon(@Param("coupon") CouponPO coupon);
+
+    @Select("select lcu.* from live_coupon_user lcu where lcu.verify_code = #{verifyCode} and lcu.is_del = 0 limit 1")
+    LiveCouponUser selectLiveCouponUserByVerifyCode(@Param("verifyCode") String verifyCode);
+
+    List<LiveCouponUserDetailVo> selectLiveCouponUserDetailList(CouponPO coupon);
+
+    LiveCouponUserDetailVo selectLiveCouponUserDetailById(@Param("id") Long id);
+
+    LiveCouponUserDetailVo selectLiveCouponUserDetailByVerifyCode(@Param("verifyCode") String verifyCode);
+
 }

+ 61 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveGroupTypeMapper.java

@@ -0,0 +1,61 @@
+package com.fs.live.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.live.domain.LiveGroupType;
+
+/**
+ * 直播间分组分类Mapper接口
+ * 
+ * @author fs
+ * @date 2026-06-16
+ */
+public interface LiveGroupTypeMapper extends BaseMapper<LiveGroupType>{
+    /**
+     * 查询直播间分组分类
+     * 
+     * @param id 直播间分组分类主键
+     * @return 直播间分组分类
+     */
+    LiveGroupType selectLiveGroupTypeById(Long id);
+
+    /**
+     * 查询直播间分组分类列表
+     * 
+     * @param liveGroupType 直播间分组分类
+     * @return 直播间分组分类集合
+     */
+    List<LiveGroupType> selectLiveGroupTypeList(LiveGroupType liveGroupType);
+
+    /**
+     * 新增直播间分组分类
+     * 
+     * @param liveGroupType 直播间分组分类
+     * @return 结果
+     */
+    int insertLiveGroupType(LiveGroupType liveGroupType);
+
+    /**
+     * 修改直播间分组分类
+     * 
+     * @param liveGroupType 直播间分组分类
+     * @return 结果
+     */
+    int updateLiveGroupType(LiveGroupType liveGroupType);
+
+    /**
+     * 删除直播间分组分类
+     * 
+     * @param id 直播间分组分类主键
+     * @return 结果
+     */
+    int deleteLiveGroupTypeById(Long id);
+
+    /**
+     * 批量删除直播间分组分类
+     * 
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteLiveGroupTypeByIds(Long[] ids);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -264,6 +264,11 @@ public interface LiveMapper
             " </script>"})
     List<Live> listToLiveNoEndNew(@Param("live") Live live);
 
+    /**
+     * 销售APP:查询未开播、直播中直播间
+     */
+    List<Live> listAppAvailableLive(@Param("live") Live live);
+
     @Select("SELECT COUNT(1) FROM live WHERE is_del = 0 AND training_period_id = #{periodId}")
     int countLiveByTrainingPeriodId(@Param("periodId") Long periodId);
 }

+ 12 - 1
fs-service/src/main/java/com/fs/live/param/CouponPO.java

@@ -1,9 +1,12 @@
 package com.fs.live.param;
 
+import com.fs.common.param.BaseQueryParam;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 @Data
-public class CouponPO {
+@EqualsAndHashCode(callSuper = true)
+public class CouponPO extends BaseQueryParam {
 
     /**
      * 直播间id
@@ -22,5 +25,13 @@ public class CouponPO {
     private Long userId;
     private Long goodsId;
 
+    /** 优惠券发放记录id */
+    private Long couponUserId;
+
+    /** 核销码 */
+    private String verifyCode;
+
+    /** 状态筛选(0:未核销,1:已核销,2:已过期) */
+    private Integer status;
 
 }

+ 23 - 0
fs-service/src/main/java/com/fs/live/param/FsLiveListParam.java

@@ -0,0 +1,23 @@
+package com.fs.live.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(description = "销售APP直播列表查询参数")
+public class FsLiveListParam {
+
+    @ApiModelProperty(value = "页码,默认为1")
+    private Integer pageNum = 1;
+
+    @ApiModelProperty(value = "页大小,默认为10")
+    private Integer pageSize = 10;
+
+    @ApiModelProperty(value = "直播间名称关键字")
+    private String keyword;
+
+    @ApiModelProperty(value = "公司id", hidden = true)
+    private Long companyId;
+
+}

+ 26 - 0
fs-service/src/main/java/com/fs/live/param/FsLiveSortLinkParam.java

@@ -0,0 +1,26 @@
+package com.fs.live.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel(description = "生成直播分享短链入参")
+public class FsLiveSortLinkParam {
+
+    @ApiModelProperty(value = "直播间id", required = true)
+    private Long liveId;
+
+    @ApiModelProperty(value = "公司id", hidden = true)
+    private Long companyId;
+
+    @ApiModelProperty(value = "销售id", hidden = true)
+    private Long companyUserId;
+
+    @ApiModelProperty(value = "企微主体id")
+    private String corpId;
+
+    @ApiModelProperty(value = "链接有效时长(分钟)")
+    private Integer effectiveDuration;
+
+}

+ 1 - 1
fs-service/src/main/java/com/fs/live/param/LiveAutoTaskImportParam.java

@@ -10,7 +10,7 @@ import java.io.Serializable;
 public class LiveAutoTaskImportParam implements Serializable {
 
     @JsonFormat(pattern = "HH:mm:ss")
-    @Excel(name = "时间(格式为 00:00:00)", width = 20, dateFormat = "HH:mm:ss")
+    @Excel(name = "时间(格式为 00:00:00)", width = 20)
     private String registerDate;
 
     @Excel(name = "昵称")

+ 19 - 0
fs-service/src/main/java/com/fs/live/param/LiveCouponVerifyParam.java

@@ -0,0 +1,19 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+/**
+ * 直播优惠券核销参数
+ */
+@Data
+public class LiveCouponVerifyParam {
+
+    /** 核销码(扫码获取) */
+    private String verifyCode;
+
+    /** 优惠券发放记录id(可选,与核销码二选一) */
+    private Long couponUserId;
+
+    /** 销售用户ID(服务端填充) */
+    private Long verifyUserId;
+}

Неке датотеке нису приказане због велике количине промена