Forráskód Böngészése

Merge remote-tracking branch 'origin/master'

zyy 1 napja
szülő
commit
b3b7f49da5
100 módosított fájl, 5857 hozzáadás és 184 törlés
  1. 124 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductActivityController.java
  2. 153 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductDiscountController.java
  3. 152 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductFlashSaleController.java
  4. 60 0
      fs-admin/src/main/java/com/fs/hisStore/task/ActivityExpireTask.java
  5. 33 0
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  6. 114 0
      fs-admin/src/main/java/com/fs/task/SaleBehaviorAnalyzeTask.java
  7. 14 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockData.java
  8. 22 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockDataProvider.java
  9. 711 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockService.java
  10. 26 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityValidateResult.java
  11. 80 8
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java
  12. 60 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java
  13. 5 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  14. 45 0
      fs-company/src/main/java/com/fs/company/controller/fastGpt/FastgptChatQuestionStatisticsController.java
  15. 13 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopController.java
  16. 5 4
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsTaskServiceImpl.java
  17. 6 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java
  18. 6 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java
  19. 8 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflow.java
  20. 6 0
      fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java
  21. 5 5
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  22. 7 5
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java
  23. 15 15
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java
  24. 15 2
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java
  25. 7 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java
  26. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java
  27. 2 2
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  28. 30 7
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  29. 3 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  30. 1 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyMapper.java
  31. 3 2
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java
  32. 10 14
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  33. 9 3
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java
  34. 15 7
      fs-service/src/main/java/com/fs/fastGpt/mapper/FastgptChatQuestionMapper.java
  35. 9 0
      fs-service/src/main/java/com/fs/fastGpt/mapper/FastgptChatQuestionStatisticsMapper.java
  36. 3 0
      fs-service/src/main/java/com/fs/fastGpt/param/FastgptKnowledgeMissCollectParam.java
  37. 15 7
      fs-service/src/main/java/com/fs/fastGpt/service/IFastgptChatQuestionService.java
  38. 1 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  39. 204 31
      fs-service/src/main/java/com/fs/fastGpt/service/impl/FastgptChatQuestionCollectServiceImpl.java
  40. 15 8
      fs-service/src/main/java/com/fs/fastGpt/service/impl/FastgptChatQuestionServiceImpl.java
  41. 65 0
      fs-service/src/main/java/com/fs/fastGpt/vo/FastgptChatQuestionDetailVO.java
  42. 12 0
      fs-service/src/main/java/com/fs/his/enums/AiSaleBehaviorAnalyzsEnum.java
  43. 1 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  44. 4 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  45. 36 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  46. 18 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  47. 154 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductActivity.java
  48. 128 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductDiscount.java
  49. 123 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductFlashSale.java
  50. 12 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  51. 8 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  52. 131 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductActivityMapper.java
  53. 109 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductDiscountMapper.java
  54. 109 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductFlashSaleMapper.java
  55. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  56. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreConfirmOrderParam.java
  57. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java
  58. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductQueryParam.java
  59. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  60. 99 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductActivityService.java
  61. 94 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductDiscountService.java
  62. 94 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductFlashSaleService.java
  63. 8 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  64. 119 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/ActivityStockDataProviderImpl.java
  65. 55 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  66. 527 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  67. 244 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductActivityServiceImpl.java
  68. 144 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductDiscountServiceImpl.java
  69. 144 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductFlashSaleServiceImpl.java
  70. 74 26
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  71. 7 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java
  72. 10 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductQueryVO.java
  73. 20 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsUnsyncOrderVO.java
  74. 2 0
      fs-service/src/main/java/com/fs/ipad/vo/WxBaseVo.java
  75. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwCustomerPropertyMapper.java
  76. 11 3
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  77. 6 0
      fs-service/src/main/java/com/fs/qw/param/QwExternalContactParam.java
  78. 1 2
      fs-service/src/main/java/com/fs/qw/service/impl/QwCustomerPropertyServiceImpl.java
  79. 1 1
      fs-service/src/main/java/com/fs/qw/vo/QwCustomerAiTagVo.java
  80. 1 3
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java
  81. 8 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java
  82. 7 1
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java
  83. 1 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java
  84. 13 0
      fs-service/src/main/java/com/fs/wx/sop/params/SendWxSopMsgParam.java
  85. 14 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java
  86. 0 3
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java
  87. 202 12
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java
  88. 20 4
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java
  89. 8 0
      fs-service/src/main/java/com/fs/wx/sop/vo/WxSopLogsListVO.java
  90. 11 0
      fs-service/src/main/java/com/fs/wx/sop/vo/WxSopMsgVo.java
  91. 7 1
      fs-service/src/main/java/com/fs/wxwork/service/WxIpadService.java
  92. 1 1
      fs-service/src/main/resources/application-config-druid-hst.yml
  93. 2 2
      fs-service/src/main/resources/application-config-druid-sxtb.yml
  94. 4 0
      fs-service/src/main/resources/application-druid-hst.yml
  95. 366 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml
  96. 11 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyMapper.xml
  97. 36 1
      fs-service/src/main/resources/mapper/fastGpt/FastgptChatQuestionMapper.xml
  98. 17 0
      fs-service/src/main/resources/mapper/fastGpt/FastgptChatQuestionStatisticsMapper.xml
  99. 311 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductActivityMapper.xml
  100. 215 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductDiscountMapper.xml

+ 124 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductActivityController.java

@@ -0,0 +1,124 @@
+package com.fs.hisStore.controller;
+
+import java.util.Date;
+import java.util.List;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.service.IFsStoreProductActivityService;
+import com.fs.hisStore.service.IFsStoreProductScrmService;
+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.*;
+
+/**
+ * 商品活动中间表Controller(仅提供商品管理页面弹窗接口)
+ *
+ * @author fs
+ * @date 2026-04-16
+ */
+@Slf4j
+@Api("商品活动设置(秒杀/折扣)")
+@RestController
+@RequestMapping("/store/store/productActivity")
+public class FsStoreProductActivityController extends BaseController {
+
+    @Autowired
+    private IFsStoreProductActivityService activityService;
+
+    @Autowired
+    private IFsStoreProductScrmService storeProductService;
+
+    /**
+     * 根据商品ID查询活动设置(含活动状态、是否进行中)
+     */
+    @ApiOperation("根据商品ID查询活动设置")
+    @GetMapping("/getByProductId/{productId}")
+    public AjaxResult getByProductId(@PathVariable("productId") Long productId) {
+        List<FsStoreProductActivity> list = activityService.selectByProductId(productId);
+
+        // 判断是否有正在进行中的活动
+        FsStoreProductActivity ongoing = activityService.selectOngoingActivity(productId);
+        boolean isOngoing = ongoing != null;
+
+        long now = System.currentTimeMillis();
+        for (FsStoreProductActivity item : list) {
+            // 计算活动状态
+            if (item.getStartTime() != null && item.getEndTime() != null) {
+                long startMs = item.getStartTime().getTime();
+                long endMs = item.getEndTime().getTime();
+                if (now < startMs) {
+                    item.setActivityStatus("not_started");
+                    item.setCountdown((startMs - now) / 1000);
+                } else if (now > endMs) {
+                    item.setActivityStatus("ended");
+                } else {
+                    item.setActivityStatus("ongoing");
+                    item.setCountdown((endMs - now) / 1000);
+                }
+            }
+            item.setIsOngoing(isOngoing);
+        }
+
+        AjaxResult ajax = AjaxResult.success(list);
+        ajax.put("isOngoing", isOngoing);
+        if (isOngoing) {
+            ajax.put("ongoingActivityType", ongoing.getActivityType());
+        }
+        return ajax;
+    }
+
+    /**
+     * 保存活动设置(批量保存/更新)
+     * 请求体: { productId, activityType, activityList: [{specId, originalPrice, flashPrice, discount, discountPrice, stock, startTime, endTime}] }
+     */
+    @ApiOperation("保存活动设置")
+    @PostMapping("/save")
+    public AjaxResult saveActivity(@RequestBody FsStoreProductActivitySaveRequest request) {
+        try {
+            // 校验商品审核状态
+            if (request.getProductId() != null && request.getActivityType() != null && request.getActivityType() != 0) {
+                FsStoreProductScrm product = storeProductService.selectFsStoreProductById(request.getProductId());
+                if (product == null) {
+                    return AjaxResult.error("商品不存在");
+                }
+                if (product.getIsAudit() == null || !"1".equals(product.getIsAudit())) {
+                    return AjaxResult.error("商品审核通过后才能设置活动");
+                }
+            }
+            activityService.saveActivity(request.getProductId(), request.getActivityType(), request.getActivityList());
+            return AjaxResult.success();
+        } catch (RuntimeException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 删除活动设置
+     */
+    @ApiOperation("删除活动设置")
+    @DeleteMapping("/remove/{productId}")
+    public AjaxResult remove(@PathVariable("productId") Long productId) {
+        try {
+            // 删除 = 设置activityType=0
+            activityService.saveActivity(productId, 0, null);
+            return AjaxResult.success();
+        } catch (RuntimeException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 保存活动设置请求体
+     */
+    @lombok.Data
+    public static class FsStoreProductActivitySaveRequest {
+        private Long productId;
+        /** 活动类型:0=无 6=秒杀 7=限时折扣 */
+        private Integer activityType;
+        private List<FsStoreProductActivity> activityList;
+    }
+}

+ 153 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductDiscountController.java

@@ -0,0 +1,153 @@
+package com.fs.hisStore.controller;
+
+import java.util.Date;
+import java.util.List;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.core.redis.service.ActivityStockService;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.framework.web.service.TokenService;
+import com.fs.hisStore.domain.FsStoreProductDiscount;
+import com.fs.hisStore.service.IFsStoreProductDiscountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 限时折扣商品Controller
+ *
+ * @author fs
+ * @date 2026-04-03
+ */
+@RestController
+@RequestMapping("/store/store/productDiscount")
+public class FsStoreProductDiscountController extends BaseController
+{
+    @Autowired
+    private IFsStoreProductDiscountService fsStoreProductDiscountService;
+
+    @Autowired
+    private ActivityStockService activityStockService;
+
+    /**
+     * 查询限时折扣商品列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:productDiscount:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsStoreProductDiscount fsStoreProductDiscount)
+    {
+        startPage();
+        List<FsStoreProductDiscount> list = fsStoreProductDiscountService.selectFsStoreProductDiscountList(fsStoreProductDiscount);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出限时折扣商品列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:productDiscount:export')")
+    @Log(title = "限时折扣商品", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsStoreProductDiscount fsStoreProductDiscount)
+    {
+        List<FsStoreProductDiscount> list = fsStoreProductDiscountService.selectFsStoreProductDiscountList(fsStoreProductDiscount);
+        ExcelUtil<FsStoreProductDiscount> util = new ExcelUtil<>(FsStoreProductDiscount.class);
+        return util.exportExcel(list, "限时折扣商品数据");
+    }
+
+    /**
+     * 获取限时折扣商品详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('store:productDiscount:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsStoreProductDiscountService.selectFsStoreProductDiscountById(id));
+    }
+
+    /**
+     * 新增限时折扣商品
+     */
+    @PreAuthorize("@ss.hasPermi('store:productDiscount:add')")
+    @Log(title = "限时折扣商品", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsStoreProductDiscount fsStoreProductDiscount)
+    {
+        LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null){
+            throw new ServiceException("用户信息不存在!");
+        }
+        fsStoreProductDiscount.setCreateBy(loginUser.getUserId().toString());
+        int result = fsStoreProductDiscountService.insertFsStoreProductDiscount(fsStoreProductDiscount);
+        if (result > 0 && fsStoreProductDiscount.getId() != null) {
+            activityStockService.initDiscountStock(fsStoreProductDiscount.getId(), fsStoreProductDiscount.getStock());
+            if (fsStoreProductDiscount.getStartTime() != null && fsStoreProductDiscount.getEndTime() != null) {
+                activityStockService.initDiscountInfo(
+                        fsStoreProductDiscount.getId(),
+                        fsStoreProductDiscount.getStatus(),
+                        fsStoreProductDiscount.getStartTime().getTime(),
+                        fsStoreProductDiscount.getEndTime().getTime(),
+                        fsStoreProductDiscount.getProductId(),
+                        fsStoreProductDiscount.getStock()
+                );
+            }
+        }
+        return toAjax(result);
+    }
+
+    /**
+     * 修改限时折扣商品
+     */
+    @PreAuthorize("@ss.hasPermi('store:productDiscount:edit')")
+    @Log(title = "限时折扣商品", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsStoreProductDiscount fsStoreProductDiscount)
+    {
+        LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null){
+            throw new ServiceException("用户信息不存在!");
+        }
+        fsStoreProductDiscount.setUpdateBy(loginUser.getUserId().toString());
+        int result = fsStoreProductDiscountService.updateFsStoreProductDiscount(fsStoreProductDiscount);
+        if (result > 0 && fsStoreProductDiscount.getId() != null) {
+            // 活动信息始终更新(状态、时间等)
+            if (fsStoreProductDiscount.getStartTime() != null && fsStoreProductDiscount.getEndTime() != null) {
+                activityStockService.initDiscountInfo(
+                        fsStoreProductDiscount.getId(),
+                        fsStoreProductDiscount.getStatus(),
+                        fsStoreProductDiscount.getStartTime().getTime(),
+                        fsStoreProductDiscount.getEndTime().getTime(),
+                        fsStoreProductDiscount.getProductId(),
+                        fsStoreProductDiscount.getStock()
+                );
+            }
+            // 库存只在明确修改时才重新初始化Redis,避免覆盖已扣减的库存
+            if (fsStoreProductDiscount.getStock() != null) {
+                Integer currentRedisStock = activityStockService.getStock(7, fsStoreProductDiscount.getId());
+                // 只有当请求的库存值与Redis当前库存不一致时才重新初始化(说明管理员确实要改库存)
+                if (!fsStoreProductDiscount.getStock().equals(currentRedisStock.longValue())) {
+                    activityStockService.initDiscountStock(fsStoreProductDiscount.getId(), fsStoreProductDiscount.getStock());
+                }
+            }
+        }
+        return toAjax(result);
+    }
+
+    /**
+     * 删除限时折扣商品
+     */
+    @PreAuthorize("@ss.hasPermi('store:productDiscount:remove')")
+    @Log(title = "限时折扣商品", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsStoreProductDiscountService.deleteFsStoreProductDiscountByIds(ids));
+    }
+}

+ 152 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductFlashSaleController.java

@@ -0,0 +1,152 @@
+package com.fs.hisStore.controller;
+
+import java.util.List;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.core.redis.service.ActivityStockService;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.framework.web.service.TokenService;
+import com.fs.hisStore.domain.FsStoreProductFlashSale;
+import com.fs.hisStore.service.IFsStoreProductFlashSaleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 秒杀商品Controller
+ *
+ * @author fs
+ * @date 2026-04-08
+ */
+@RestController
+@RequestMapping("/store/store/productFlashSale")
+public class FsStoreProductFlashSaleController extends BaseController
+{
+    @Autowired
+    private IFsStoreProductFlashSaleService fsStoreProductFlashSaleService;
+
+    @Autowired
+    private ActivityStockService activityStockService;
+
+    /**
+     * 查询秒杀商品列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:productFlashSale:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsStoreProductFlashSale fsStoreProductFlashSale)
+    {
+        startPage();
+        List<FsStoreProductFlashSale> list = fsStoreProductFlashSaleService.selectFsStoreProductFlashSaleList(fsStoreProductFlashSale);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出秒杀商品列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:productFlashSale:export')")
+    @Log(title = "秒杀商品", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsStoreProductFlashSale fsStoreProductFlashSale)
+    {
+        List<FsStoreProductFlashSale> list = fsStoreProductFlashSaleService.selectFsStoreProductFlashSaleList(fsStoreProductFlashSale);
+        ExcelUtil<FsStoreProductFlashSale> util = new ExcelUtil<>(FsStoreProductFlashSale.class);
+        return util.exportExcel(list, "秒杀商品数据");
+    }
+
+    /**
+     * 获取秒杀商品详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('store:productFlashSale:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsStoreProductFlashSaleService.selectFsStoreProductFlashSaleById(id));
+    }
+
+    /**
+     * 新增秒杀商品
+     */
+    @PreAuthorize("@ss.hasPermi('store:productFlashSale:add')")
+    @Log(title = "秒杀商品", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsStoreProductFlashSale fsStoreProductFlashSale)
+    {
+        LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null){
+            throw new ServiceException("用户信息不存在!");
+        }
+        fsStoreProductFlashSale.setCreateBy(loginUser.getUserId().toString());
+        int result = fsStoreProductFlashSaleService.insertFsStoreProductFlashSale(fsStoreProductFlashSale);
+        if (result > 0 && fsStoreProductFlashSale.getId() != null) {
+            activityStockService.initFlashSaleStock(fsStoreProductFlashSale.getId(), fsStoreProductFlashSale.getStock());
+            if (fsStoreProductFlashSale.getStartTime() != null && fsStoreProductFlashSale.getEndTime() != null) {
+                activityStockService.initFlashSaleInfo(
+                        fsStoreProductFlashSale.getId(),
+                        fsStoreProductFlashSale.getStatus(),
+                        fsStoreProductFlashSale.getStartTime().getTime(),
+                        fsStoreProductFlashSale.getEndTime().getTime(),
+                        fsStoreProductFlashSale.getProductId(),
+                        fsStoreProductFlashSale.getStock()
+                );
+            }
+        }
+        return toAjax(result);
+    }
+
+    /**
+     * 修改秒杀商品
+     */
+    @PreAuthorize("@ss.hasPermi('store:productFlashSale:edit')")
+    @Log(title = "秒杀商品", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsStoreProductFlashSale fsStoreProductFlashSale)
+    {
+        LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
+        if(loginUser == null){
+            throw new ServiceException("用户信息不存在!");
+        }
+        fsStoreProductFlashSale.setUpdateBy(loginUser.getUserId().toString());
+        int result = fsStoreProductFlashSaleService.updateFsStoreProductFlashSale(fsStoreProductFlashSale);
+        if (result > 0 && fsStoreProductFlashSale.getId() != null) {
+            // 活动信息始终更新(状态、时间等)
+            if (fsStoreProductFlashSale.getStartTime() != null && fsStoreProductFlashSale.getEndTime() != null) {
+                activityStockService.initFlashSaleInfo(
+                        fsStoreProductFlashSale.getId(),
+                        fsStoreProductFlashSale.getStatus(),
+                        fsStoreProductFlashSale.getStartTime().getTime(),
+                        fsStoreProductFlashSale.getEndTime().getTime(),
+                        fsStoreProductFlashSale.getProductId(),
+                        fsStoreProductFlashSale.getStock()
+                );
+            }
+            // 库存只在明确修改时才重新初始化Redis,避免覆盖已扣减的库存
+            if (fsStoreProductFlashSale.getStock() != null) {
+                Integer currentRedisStock = activityStockService.getStock(6, fsStoreProductFlashSale.getId());
+                // 只有当请求的库存值与Redis当前库存不一致时才重新初始化(说明管理员确实要改库存)
+                if (!fsStoreProductFlashSale.getStock().equals(currentRedisStock.longValue())) {
+                    activityStockService.initFlashSaleStock(fsStoreProductFlashSale.getId(), fsStoreProductFlashSale.getStock());
+                }
+            }
+        }
+        return toAjax(result);
+    }
+
+    /**
+     * 删除秒杀商品
+     */
+    @PreAuthorize("@ss.hasPermi('store:productFlashSale:remove')")
+    @Log(title = "秒杀商品", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsStoreProductFlashSaleService.deleteFsStoreProductFlashSaleByIds(ids));
+    }
+}

+ 60 - 0
fs-admin/src/main/java/com/fs/hisStore/task/ActivityExpireTask.java

@@ -0,0 +1,60 @@
+package com.fs.hisStore.task;
+
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import com.fs.hisStore.service.IFsStoreProductActivityService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 活动过期兜底定时任务
+ * 每1分钟扫描一次过期活动,自动重置 activity_type 恢复为普通商品
+ *
+ * @author fs
+ * @date 2026-04-22
+ */
+@Slf4j
+@Component("activityExpireTask")
+public class ActivityExpireTask {
+
+    @Autowired
+    private IFsStoreProductActivityService activityService;
+
+    /**
+     * 每1分钟执行一次,处理过期活动
+     */
+//    @Scheduled(fixedRate = 60000)
+    public void handleExpiredActivities() {
+        try {
+            // 1. 查询所有已过期但未重置的活动
+            List<FsStoreProductActivity> expiredList = activityService.selectExpiredList();
+            if (expiredList == null || expiredList.isEmpty()) {
+                return;
+            }
+
+            log.info("[ActivityExpireTask] 发现{}个过期活动,开始处理", expiredList.size());
+
+            // 2. 按商品ID分组处理
+            Map<Long, List<FsStoreProductActivity>> groupByProduct = expiredList.stream()
+                    .collect(Collectors.groupingBy(FsStoreProductActivity::getProductId));
+
+            for (Map.Entry<Long, List<FsStoreProductActivity>> entry : groupByProduct.entrySet()) {
+                try {
+                    activityService.handleExpiredActivity(entry.getKey(), entry.getValue());
+                    log.info("[ActivityExpireTask] 商品{}的过期活动处理完成", entry.getKey());
+                } catch (Exception e) {
+                    log.error("[ActivityExpireTask] 商品{}的过期活动处理失败", entry.getKey(), e);
+                }
+            }
+
+            log.info("[ActivityExpireTask] 本轮过期活动处理完成,共处理{}个商品", groupByProduct.size());
+        } catch (Exception e) {
+            log.error("[ActivityExpireTask] 活动过期定时任务执行异常", e);
+        }
+    }
+}

+ 33 - 0
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -387,6 +387,39 @@ public class MallStoreTask
         }
     }
 
+    /**
+     * 超时未支付自动取消订单(含活动库存回滚)
+     * 建议配置为每分钟执行一次的定时任务
+     */
+    public void cancelUnpayOrder()
+    {
+        try {
+            String json = configService.selectConfigByKey("store.config");
+            if (StringUtils.isEmpty(json)) return;
+            StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+            Integer unPayTime = config.getUnPayTime(); // 分钟
+            if (unPayTime == null || unPayTime <= 0) return;
+
+            // 查询超时未支付的SCRM订单
+            List<FsStoreOrderScrm> orderList = fsStoreOrderMapper.selectUnpayTimeoutOrderList(unPayTime);
+            if (orderList == null || orderList.isEmpty()) return;
+
+            log.info("[cancelUnpayOrder] 发现{}个超时未支付订单,开始取消", orderList.size());
+            for (FsStoreOrderScrm order : orderList) {
+                try {
+                    // cancelOrder内部已处理活动库存回滚(refundStock -> refundActivityStock)
+                    orderService.cancelOrder(order.getId());
+                    log.info("[cancelUnpayOrder] 超时订单取消成功,orderId={}, orderType={}, associatedId={}",
+                            order.getId(), order.getOrderType(), order.getAssociatedId());
+                } catch (Exception e) {
+                    log.error("[cancelUnpayOrder] 超时订单取消失败,orderId={}", order.getId(), e);
+                }
+            }
+        } catch (Exception e) {
+            log.error("[cancelUnpayOrder] 超时取消订单任务异常", e);
+        }
+    }
+
 
     //每天执行一次
     public void userMoneyOp()

+ 114 - 0
fs-admin/src/main/java/com/fs/task/SaleBehaviorAnalyzeTask.java

@@ -0,0 +1,114 @@
+package com.fs.task;
+
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.crm.utils.CrmCustomerAiTagUtil;
+import com.fs.his.enums.AiSaleBehaviorAnalyzsEnum;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+//销售行为分析
+@Component("SaleBehaviorAnalyzeAiTask")
+@RequiredArgsConstructor
+@Slf4j
+public class SaleBehaviorAnalyzeTask {
+
+    private final RedisTemplate<String, String> redisTemplate;
+    private final ICompanyUserService companyUserService;
+
+    public void orderMoney() {
+        log.info("开始获取销售增加的销售额");
+        //增加
+        List<String> rangeAdd = redisTemplate.opsForList().range(AiSaleBehaviorAnalyzsEnum.SALE_ANALYZE_ADD.getRedisKey(), 0, -1);
+        //减少 存的时候传负数
+        List<String> rangeMinus = redisTemplate.opsForList().range(AiSaleBehaviorAnalyzsEnum.SALE_ANALYZE_MINUS.getRedisKey(), 0, -1);
+
+        // 统一判空处理
+        if ((rangeAdd == null || rangeAdd.isEmpty()) && (rangeMinus == null || rangeMinus.isEmpty())) {
+            log.info("无销售额增减任务,结束进程");
+            return;
+        }
+        // 记录任务统计信息
+        log.info("销售额任务统计 - 增加任务数: {}, 减少任务数: {}",
+                rangeAdd == null ? 0 : rangeAdd.size(),
+                rangeMinus == null ? 0 : rangeMinus.size());
+
+//        if (rangeAdd == null || rangeAdd.isEmpty()) {
+//            log.info("无增加销售额任务");
+//        } else {
+//            log.info("增加销售额任务示例: {}", rangeAdd.get(0));
+//        }
+//        if (rangeMinus == null || rangeMinus.isEmpty()) {
+//            log.info("无减少销售额任务");
+//        } else {
+//            log.info("减少销售额任务示例: {}", rangeMinus.get(0));
+//        }
+
+        // key = companyId:companyUserId
+        Map<String, BigDecimal> addSum = sumByCompanyAndUser(rangeAdd);
+        Map<String, BigDecimal> minusSum = sumByCompanyAndUser(rangeMinus);
+
+        // add - minus => 结果集合
+        List<String> netList = new ArrayList<>();
+        Map<String, BigDecimal> netMap = new HashMap<>(addSum);
+        for (Map.Entry<String, BigDecimal> e : minusSum.entrySet()) {
+            netMap.merge(e.getKey(), e.getValue().negate(), BigDecimal::add);
+        }
+        for (Map.Entry<String, BigDecimal> e : netMap.entrySet()) {
+            // 输出格式:companyId:companyUserId:netAmount
+            netList.add(e.getKey() + ":" + e.getValue());
+        }
+
+        log.info("销售额增减汇总完成,分组数: {}", netList.size());
+        // TODO: netList 即“同 companyId 同 companyUserId add相加再减minus”的集合,可用于后续入库/分析
+        for (Map.Entry<String, BigDecimal> entry : netMap.entrySet()){
+            String k = entry.getKey();
+            BigDecimal v = entry.getValue();
+            CompanyUser companyUser = companyUserService.selectCompanyUserById(Long.valueOf(k));
+            if (companyUser == null){
+                log.info("用户ID不存在: " + k);
+                continue;
+            }
+
+        }
+    }
+
+    private static Map<String, BigDecimal> sumByCompanyAndUser(List<String> rows) {
+        Map<String, BigDecimal> sum = new HashMap<>();
+        if (rows == null || rows.isEmpty()) {
+            return sum;
+        }
+        for (String item : rows) {
+            if (item == null || item.trim().isEmpty()) {
+                continue;
+            }
+            List<String> parts = Arrays.asList(item.split(":"));
+            if (parts.size() < 2) {
+                continue;
+            }
+            String companyUserId = parts.get(0);
+            String amountStr = parts.get(1);
+            if ( companyUserId == null || amountStr == null) {
+                continue;
+            }
+            BigDecimal amount;
+            try {
+                amount = new BigDecimal(amountStr);
+            } catch (Exception ignore) {
+                continue;
+            }
+            String key = companyUserId;
+            sum.merge(key, amount, BigDecimal::add);
+        }
+        return sum;
+    }
+}

+ 14 - 0
fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockData.java

@@ -0,0 +1,14 @@
+package com.fs.common.core.redis.service;
+
+import lombok.Data;
+
+@Data
+public class ActivityStockData {
+    private Long id;
+    private Long productId;
+    private Long specId;
+    private Long stock;
+    private Integer status;
+    private Long startTime;
+    private Long endTime;
+}

+ 22 - 0
fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockDataProvider.java

@@ -0,0 +1,22 @@
+package com.fs.common.core.redis.service;
+
+import java.util.List;
+
+public interface ActivityStockDataProvider {
+
+    ActivityStockData loadActivityData(Integer orderType, Long associatedId);
+
+    void updateStockToDb(Integer orderType, Long associatedId, Long redisStock);
+
+    List<ActivityStockData> loadAllActiveActivities(Integer orderType);
+
+    /**
+     * 加载商品规格库存到Redis
+     */
+    void loadProductSpecStock(Long specId);
+
+    /**
+     * 将Redis中的商品规格库存同步回数据库
+     */
+    void updateProductSpecStockToDb(Long specId, long redisStock);
+}

+ 711 - 0
fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockService.java

@@ -0,0 +1,711 @@
+package com.fs.common.core.redis.service;
+
+import com.fs.common.core.redis.RedisCache;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+public class ActivityStockService {
+
+    public final RedisTemplate<String, Object> redisTemplate;
+    public final RedisCache redisCache;
+
+    /** 统一活动信息key前缀 */
+    private static final String ACTIVITY_INFO_KEY = "activity:info:";
+    /** 原商品规格库存key前缀 */
+    private static final String PRODUCT_SPEC_STOCK_KEY = "product:spec:stock:";
+
+    /** 扣减规格库存 Lua脚本(活动不再有独立库存,直接扣规格库存) */
+    private static final DefaultRedisScript<Long> ACTIVITY_STOCK_DEDUCT_SCRIPT;
+
+    static {
+        ACTIVITY_STOCK_DEDUCT_SCRIPT = new DefaultRedisScript<>();
+        // KEYS[1] = 原商品规格库存key(product:spec:stock:{specId})
+        // ARGV[1] = 扣减数量
+        // 返回: >=0 成功(规格剩余库存), -1 库存不足, -2 key不存在, -3 值非数字, -4 扣减数量无效
+        ACTIVITY_STOCK_DEDUCT_SCRIPT.setScriptText(
+                "local deductNum = tonumber(ARGV[1]); " +
+                "if deductNum == nil or deductNum <= 0 then return -4; end " +
+                "if redis.call('exists', KEYS[1]) ~= 1 then return -2; end " +
+                "local stock = tonumber(redis.call('get', KEYS[1])); " +
+                "if stock == nil then return -3; end " +
+                "if stock < deductNum then return -1; end " +
+                "return redis.call('decrby', KEYS[1], deductNum);"
+        );
+        ACTIVITY_STOCK_DEDUCT_SCRIPT.setResultType(Long.class);
+    }
+
+    /** 回滚规格库存 Lua脚本 */
+    private static final DefaultRedisScript<Long> ACTIVITY_STOCK_ROLLBACK_SCRIPT;
+
+    static {
+        ACTIVITY_STOCK_ROLLBACK_SCRIPT = new DefaultRedisScript<>();
+        // KEYS[1] = 原商品规格库存key(product:spec:stock:{specId})
+        // ARGV[1] = 回滚数量
+        ACTIVITY_STOCK_ROLLBACK_SCRIPT.setScriptText(
+                "local rollbackNum = tonumber(ARGV[1]); " +
+                "if rollbackNum == nil or rollbackNum <= 0 then return -4; end " +
+                "if redis.call('exists', KEYS[1]) == 1 then " +
+                "  return redis.call('incrby', KEYS[1], rollbackNum); " +
+                "end " +
+                "return 0;"
+        );
+        ACTIVITY_STOCK_ROLLBACK_SCRIPT.setResultType(Long.class);
+    }
+
+    /** 单独扣减活动库存(无specId时不扣商品库存) */
+    private static final DefaultRedisScript<Long> SINGLE_DEDUCT_SCRIPT;
+
+    static {
+        SINGLE_DEDUCT_SCRIPT = new DefaultRedisScript<>();
+        SINGLE_DEDUCT_SCRIPT.setScriptText(
+                "if redis.call('exists', KEYS[1]) ~= 1 then return -2; end " +
+                "local stock_str = redis.call('get', KEYS[1]); " +
+                "local stock = tonumber(stock_str); " +
+                "if stock == nil then return -3; end " +
+                "local deductNum_str = ARGV[1]; " +
+                "local deductNum = tonumber(deductNum_str); " +
+                "if deductNum == nil or deductNum <= 0 then return -4; end " +
+                "if stock >= deductNum then " +
+                "  return redis.call('decrby', KEYS[1], deductNum); " +
+                "else " +
+                "  return -1; " +
+                "end"
+        );
+        SINGLE_DEDUCT_SCRIPT.setResultType(Long.class);
+    }
+
+    private ActivityStockDataProvider dataProvider;
+
+    public ActivityStockService(RedisTemplate<String, Object> redisTemplate, RedisCache redisCache) {
+        this.redisTemplate = redisTemplate;
+        this.redisCache = redisCache;
+    }
+
+    public void setDataProvider(ActivityStockDataProvider dataProvider) {
+        this.dataProvider = dataProvider;
+    }
+
+    // ==================== 初始化方法 ====================
+
+    /**
+     * 初始化活动信息到Redis(活动不再有独立库存,只存活动信息用于校验)
+     */
+    public void initActivityInfo(Long activityId, Integer status, Long startTime, Long endTime, Long productId, Long specId, Long stock) {
+        String infoKey = ACTIVITY_INFO_KEY + activityId;
+        Map<String, Object> activityInfo = new HashMap<>();
+        activityInfo.put("status", status);
+        activityInfo.put("startTime", startTime);
+        activityInfo.put("endTime", endTime);
+        activityInfo.put("productId", productId);
+        activityInfo.put("specId", specId);
+        redisCache.setCacheMap(infoKey, activityInfo);
+        // 设置过期时间:活动结束后1小时自动清理缓存
+        long ttlMs = endTime - System.currentTimeMillis() + 3600000;
+        if (ttlMs > 0) {
+            redisTemplate.expire(infoKey, ttlMs, TimeUnit.MILLISECONDS);
+        }
+        log.info("活动商品{}活动信息初始化完成,specId={}", activityId, specId);
+    }
+
+    /**
+     * 初始化原商品规格库存到Redis
+     */
+    public void initProductSpecStock(Long specId, Integer stock) {
+        if (specId == null) return;
+        String stockKey = PRODUCT_SPEC_STOCK_KEY + specId;
+        if (!redisCache.hasKey(stockKey)) {
+            redisTemplate.opsForValue().set(stockKey, stock);
+            log.info("商品规格{}库存初始化到Redis完成,库存:{}", specId, stock);
+        }
+    }
+
+    // ==================== 兼容旧接口(已废弃,活动无独立库存) ====================
+
+    /**
+     * @deprecated 活动无独立库存,规格库存用initProductSpecStock初始化
+     */
+    @Deprecated
+    public void initFlashSaleStock(Long flashSaleId, Long stock) {
+        // 不再初始化独立活动库存
+    }
+
+    /**
+     * @deprecated 活动无独立库存,规格库存用initProductSpecStock初始化
+     */
+    @Deprecated
+    public void initDiscountStock(Long discountId, Long stock) {
+        // 不再初始化独立活动库存
+    }
+
+    public void initFlashSaleInfo(Long flashSaleId, Integer status, Long startTime, Long endTime, Long productId, Long stock) {
+        initActivityInfo(flashSaleId, status, startTime, endTime, productId, null, stock);
+    }
+
+    public void initDiscountInfo(Long discountId, Integer status, Long startTime, Long endTime, Long productId, Long stock) {
+        initActivityInfo(discountId, status, startTime, endTime, productId, null, stock);
+    }
+
+    public Integer getFlashSaleStock(Long flashSaleId) {
+        return getStock(6, flashSaleId);
+    }
+
+    public Integer getDiscountStock(Long discountId) {
+        return getStock(7, discountId);
+    }
+
+    // ==================== 核心方法 ====================
+
+    public int getStock(Integer orderType, Long associatedId) {
+        // 活动无独立库存,从活动信息中获取specId查规格库存
+        String infoKey = ACTIVITY_INFO_KEY + associatedId;
+        Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
+        Long specId = null;
+        if (activityInfo != null) {
+            specId = parseLong(activityInfo.get("specId"));
+        }
+        if (specId == null) {
+            loadActivityFromDb(orderType, associatedId);
+            activityInfo = redisCache.getCacheMap(infoKey);
+            if (activityInfo != null) {
+                specId = parseLong(activityInfo.get("specId"));
+            }
+        }
+        if (specId == null) return 0;
+        String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
+        Object stockObj = redisTemplate.opsForValue().get(specStockKey);
+        return parseStockValue(stockObj);
+    }
+
+    /**
+     * Redis Pipeline 批量获取库存
+     * 先从 ACTIVITY_INFO_KEY 获取各活动的 specId,再用 PRODUCT_SPEC_STOCK_KEY Pipeline 批量查规格库存
+     */
+    public Map<Long, Integer> batchGetStock(Integer orderType, List<Long> associatedIds) {
+        if (associatedIds == null || associatedIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<Long, Integer> result = new LinkedHashMap<>();
+        try {
+            //Redis缓存读取每个活动的specId映射
+            Map<Long, Long> specIdMap = new LinkedHashMap<>();
+            for (Long id : associatedIds) {
+                String infoKey = ACTIVITY_INFO_KEY + id;
+                Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
+                if (activityInfo != null) {
+                    Long specId = parseLong(activityInfo.get("specId"));
+                    if (specId != null) {
+                        specIdMap.put(id, specId);
+                    }
+                }
+            }
+
+            // 若specIdMap 为空(活动信息不在Redis),批量从DB加载后逐个getStock降级
+            if (specIdMap.isEmpty()) {
+                log.warn("batchGetStock: 活动信息全部不在Redis,降级逐个加载,orderType={}, size={}", orderType, associatedIds.size());
+                for (Long id : associatedIds) {
+                    loadActivityFromDb(orderType, id);
+                    result.put(id, getStock(orderType, id));
+                }
+                return result;
+            }
+
+            //associatedId 顺序构建 specKey 列表(未命中的记录为 null)
+            List<Long> pipelineIds = new ArrayList<>();
+            List<String> specKeys = new ArrayList<>();
+            for (Long id : associatedIds) {
+                Long specId = specIdMap.get(id);
+                if (specId != null) {
+                    pipelineIds.add(id);
+                    specKeys.add(PRODUCT_SPEC_STOCK_KEY + specId);
+                }
+            }
+
+            //规格库存
+            final List<String> finalSpecKeys = specKeys;
+            List<Object> stockValues = redisTemplate.executePipelined(
+                    (org.springframework.data.redis.core.RedisCallback<Object>) connection -> {
+                        for (String key : finalSpecKeys) {
+                            connection.get(key.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+                        }
+                        return null;
+                    }
+            );
+
+            //结果映射回 associatedId
+            Map<Long, Integer> pipelineResult = new LinkedHashMap<>();
+            List<Long> needFallbackIds = new ArrayList<>();  // Pipeline结果为null需要降级的ID
+            for (int i = 0; i < pipelineIds.size(); i++) {
+                Long id = pipelineIds.get(i);
+                Object value = (i < stockValues.size()) ? stockValues.get(i) : null;
+                if (value == null) {
+                    // product:spec:stock:{specId} key不存在,需要降级从DB加载规格库存
+                    needFallbackIds.add(id);
+                    pipelineResult.put(id, 0);
+                } else {
+                    pipelineResult.put(id, parseStockValue(value));
+                }
+            }
+
+            //合并结果;specIdMap 中没有的 id(缓存缺失)降级逐个查
+            for (Long id : associatedIds) {
+                if (pipelineResult.containsKey(id)) {
+                    result.put(id, pipelineResult.get(id));
+                } else {
+                    //specId,降级加载
+                    loadActivityFromDb(orderType, id);
+                    result.put(id, getStock(orderType, id));
+                }
+            }
+
+            // 对Pipeline返回null的记录,降级从DB加载规格库存并重新获取
+            if (!needFallbackIds.isEmpty()) {
+                log.info("batchGetStock: {}个活动规格库存key不存在,降级加载", needFallbackIds.size());
+                for (Long id : needFallbackIds) {
+                    Long specId = specIdMap.get(id);
+                    if (specId != null && dataProvider != null) {
+                        dataProvider.loadProductSpecStock(specId);
+                    }
+                    // 重新读取规格库存
+                    String specStockKey = PRODUCT_SPEC_STOCK_KEY + specIdMap.get(id);
+                    Object stockObj = redisTemplate.opsForValue().get(specStockKey);
+                    result.put(id, parseStockValue(stockObj));
+                }
+            }
+        } catch (Exception e) {
+            log.error("Pipeline批量获取库存异常,orderType={}", orderType, e);
+            for (Long id : associatedIds) {
+                result.put(id, getStock(orderType, id));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 验证活动是否有效(上架且在活动时间内)
+     */
+    public ActivityValidateResult validateActivityWithDetail(Integer orderType, Long associatedId) {
+        String infoKey = ACTIVITY_INFO_KEY + associatedId;
+        Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
+        if (activityInfo == null || activityInfo.isEmpty()) {
+            log.info("活动信息不存在于Redis,尝试从数据库加载。orderType={}, associatedId={}", orderType, associatedId);
+            boolean loaded = loadActivityFromDb(orderType, associatedId);
+            if (!loaded) {
+                return ActivityValidateResult.fail("活动不存在");
+            }
+            activityInfo = redisCache.getCacheMap(infoKey);
+            if (activityInfo == null || activityInfo.isEmpty()) {
+                return ActivityValidateResult.fail("活动信息加载失败");
+            }
+        }
+        return doValidateActivity(activityInfo);
+    }
+
+    public boolean validateActivity(Integer orderType, Long associatedId) {
+        return validateActivityWithDetail(orderType, associatedId).isValid();
+    }
+
+    private ActivityValidateResult doValidateActivity(Map<String, Object> activityInfo) {
+        Integer status = parseInteger(activityInfo.get("status"));
+        if (status == null || status != 1) {
+            return ActivityValidateResult.fail("活动已下架");
+        }
+        Long startTime = parseLong(activityInfo.get("startTime"));
+        Long endTime = parseLong(activityInfo.get("endTime"));
+        if (startTime == null || endTime == null) {
+            return ActivityValidateResult.fail("活动时间信息不完整");
+        }
+        long now = System.currentTimeMillis();
+        if (now < startTime) {
+            return ActivityValidateResult.fail("活动尚未开始");
+        }
+        if (now > endTime) {
+            return ActivityValidateResult.fail("活动已结束");
+        }
+        return ActivityValidateResult.success();
+    }
+
+    /**
+     * 扣减活动商品库存(直接扣规格库存,活动无独立库存)
+     * @param orderType 活动类型 6=秒杀 7=折扣
+     * @param associatedId 中间表记录ID
+     * @param deductNum 扣减数量
+     * @return 是否成功
+     */
+    public boolean deductStock(Integer orderType, Long associatedId, Integer deductNum) {
+        Integer num = Optional.ofNullable(deductNum).orElse(1);
+
+        if (!validateActivity(orderType, associatedId)) {
+            log.warn("活动校验失败,可能已下架或不在活动时间内。orderType={}, associatedId={}", orderType, associatedId);
+            return false;
+        }
+
+        try {
+            // 从活动信息中获取specId
+            String infoKey = ACTIVITY_INFO_KEY + associatedId;
+            Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
+            Long specId = null;
+            if (activityInfo != null) {
+                specId = parseLong(activityInfo.get("specId"));
+            }
+            if (specId == null || specId <= 0) {
+                log.error("活动规格ID不存在,无法扣减库存。associatedId={}", associatedId);
+                return false;
+            }
+
+            String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
+            if (!redisCache.hasKey(specStockKey)) {
+                if (dataProvider != null) {
+                    dataProvider.loadProductSpecStock(specId);
+                }
+            }
+
+            Long remainingStock = redisTemplate.execute(
+                    ACTIVITY_STOCK_DEDUCT_SCRIPT,
+                    Collections.singletonList(specStockKey),
+                    num
+            );
+
+            log.info("Lua脚本返回值:{}", remainingStock);
+
+            if (remainingStock == null) {
+                log.error("Lua脚本返回null,库存扣减结果未知,orderType={},associatedId={}", orderType, associatedId);
+                return false;
+            }
+
+            if (remainingStock >= 0) {
+                log.info("活动商品库存扣减成功,orderType={},associatedId={},规格剩余库存:{}", orderType, associatedId, remainingStock);
+                // 库存归零时异步标记,通知DB同步
+                if (remainingStock == 0) {
+                    final Long finalAssociatedId = associatedId;
+                    final Integer finalOrderType = orderType;
+                    CompletableFuture.runAsync(() -> {
+                        try {
+                            if (finalOrderType == 6) {
+                                syncFlashSaleStockToDb(finalAssociatedId, 0L);
+                            } else if (finalOrderType == 7) {
+                                syncDiscountStockToDb(finalAssociatedId, 0L);
+                            }
+                        } catch (Exception ex) {
+                            log.error("库存归零异步同步DB异常,orderType={},associatedId={}", finalOrderType, finalAssociatedId, ex);
+                        }
+                    });
+                }
+                return true;
+            } else {
+                String errorMsg = parseErrorCode(remainingStock);
+                log.warn("活动商品扣减失败:orderType={},associatedId={},原因:{}", orderType, associatedId, errorMsg);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("活动库存扣减异常,orderType={},associatedId={}", orderType, associatedId, e);
+            return false;
+        }
+    }
+
+    public CompletableFuture<Boolean> deductStockAsync(Integer orderType, Long associatedId, Integer deductNum) {
+        return CompletableFuture.supplyAsync(() -> deductStock(orderType, associatedId, deductNum));
+    }
+
+    /**
+     * 回滚活动商品库存(直接回滚规格库存,活动无独立库存)
+     */
+    public boolean rollbackStock(Integer orderType, Long associatedId, Integer quantity) {
+        if (orderType == null || associatedId == null || quantity == null || quantity <= 0) {
+            return false;
+        }
+        try {
+            // 从活动信息中获取specId
+            String infoKey = ACTIVITY_INFO_KEY + associatedId;
+            Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
+            Long specId = null;
+            if (activityInfo != null) {
+                specId = parseLong(activityInfo.get("specId"));
+            }
+            // 活动信息key可能因TTL过期被清理,从DB降级加载
+            if (specId == null || specId <= 0) {
+                log.info("rollbackStock: 活动信息不在Redis,尝试从DB加载,orderType={}, associatedId={}", orderType, associatedId);
+                boolean loaded = loadActivityFromDb(orderType, associatedId);
+                if (loaded) {
+                    activityInfo = redisCache.getCacheMap(infoKey);
+                    if (activityInfo != null) {
+                        specId = parseLong(activityInfo.get("specId"));
+                    }
+                }
+            }
+            if (specId == null || specId <= 0) {
+                log.error("rollbackStock: 活动规格ID无法获取,Redis库存回滚失败!orderType={}, associatedId={}, quantity={},需人工处理",
+                        orderType, associatedId, quantity);
+                return false;
+            }
+
+            // 如果规格库存key不存在,先从DB加载初始化
+            String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
+            if (!redisCache.hasKey(specStockKey)) {
+                log.info("rollbackStock: 规格库存key不存在,尝试从DB加载,specId={}", specId);
+                if (dataProvider != null) {
+                    dataProvider.loadProductSpecStock(specId);
+                }
+            }
+
+            Long result = redisTemplate.execute(
+                    ACTIVITY_STOCK_ROLLBACK_SCRIPT,
+                    Collections.singletonList(specStockKey),
+                    quantity
+            );
+
+            if (result != null && result >= 0) {
+                log.info("活动规格库存回滚成功,orderType={},associatedId={},specId={},回滚数量={}", orderType, associatedId, specId, quantity);
+                return true;
+            } else {
+                log.warn("活动规格库存回滚Lua失败,降级为incrby,associatedId={},specId={}", associatedId, specId);
+                redisCache.incr(specStockKey, Long.valueOf(quantity));
+                return true;
+            }
+        } catch (Exception e) {
+            log.error("活动库存回滚异常,orderType={}, associatedId={}, quantity={}", orderType, associatedId, quantity, e);
+            return false;
+        }
+    }
+
+    // ==================== 同步方法 ====================
+
+    public void syncFlashSaleStockToDb(Long flashSaleId, Long dbStock) {
+        syncActivityStockToDb(flashSaleId, dbStock);
+    }
+
+    public void syncDiscountStockToDb(Long discountId, Long dbStock) {
+        syncActivityStockToDb(discountId, dbStock);
+    }
+
+    private void syncActivityStockToDb(Long activityId, Long dbStock) {
+        try {
+            // 从活动info获取specId
+            String infoKey = ACTIVITY_INFO_KEY + activityId;
+            Map<String, Object> activityInfo = redisCache.getCacheMap(infoKey);
+            if (activityInfo == null || activityInfo.isEmpty()) {
+                log.warn("活动{} 信息不存在,无法同步规格库存到DB", activityId);
+                return;
+            }
+            Long specId = parseLong(activityInfo.get("specId"));
+            if (specId == null || specId <= 0) {
+                log.warn("活动{} 未配置规格ID,无法同步规格库存到DB", activityId);
+                return;
+            }
+
+            // 读取真实的规格库存
+            String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
+            Object stockObj = redisTemplate.opsForValue().get(specStockKey);
+            if (stockObj == null) {
+                log.warn("规格{} Redis库存不存在,跳过同步到DB", specId);
+                return;
+            }
+            long redisStock = parseLongFromObject(stockObj);
+
+            // 比较并同步
+            if (dbStock == null || redisStock != dbStock.longValue()) {
+                if (dataProvider != null) {
+                    dataProvider.updateProductSpecStockToDb(specId, redisStock);
+                    log.info("活动{} 规格{} 库存同步到DB成功,redisStock={}", activityId, specId, redisStock);
+                } else {
+                    log.warn("dataProvider未注入,无法同步活动{}规格{}库存到DB", activityId, specId);
+                }
+            } else {
+                log.debug("活动{} 规格{} 库存一致(={}),无需同步", activityId, specId, redisStock);
+            }
+        } catch (Exception e) {
+            log.error("活动{} 同步规格库存到DB异常", activityId, e);
+        }
+    }
+
+    /**
+     * 同步指定规格的Redis库存到数据库(活动过期时调用)
+     */
+    public void syncProductSpecStockToDbBySpecId(Long specId) {
+        if (specId == null || dataProvider == null) {
+            return;
+        }
+        try {
+            String specStockKey = PRODUCT_SPEC_STOCK_KEY + specId;
+            Object stockObj = redisTemplate.opsForValue().get(specStockKey);
+            if (stockObj != null) {
+                long redisStock = parseLongFromObject(stockObj);
+                dataProvider.updateProductSpecStockToDb(specId, redisStock);
+                log.info("活动过期同步规格库存到DB,specId={}, redisStock={}", specId, redisStock);
+            } else {
+                log.info("活动过期规格库存key不存在,跳过同步,specId={}", specId);
+            }
+        } catch (Exception e) {
+            log.error("同步规格库存到数据库异常,specId={}", specId, e);
+        }
+    }
+
+    /**
+     * 定时对账:将Redis规格库存同步到数据库(兤底,防止Redis回滚失败导致不一致)
+     * 活动中间表已无stock字段,只同步规格库存
+     */
+    public void syncAllStockToDb() {
+        if (dataProvider == null) {
+            log.warn("ActivityStockDataProvider 未设置,无法同步库存到数据库");
+            return;
+        }
+        try {
+            // 同步商品规格库存到数据库(核心对账逻辑)
+            syncProductSpecStockToDb();
+        } catch (Exception e) {
+            log.error("全量库存同步到数据库异常", e);
+        }
+    }
+
+    /**
+     * 同步Redis中的商品规格库存到数据库
+     */
+    private void syncProductSpecStockToDb() {
+        try {
+            Set<String> specKeys = scanKeys(PRODUCT_SPEC_STOCK_KEY + "*");
+            if (specKeys == null || specKeys.isEmpty()) {
+                return;
+            }
+            for (String specKey : specKeys) {
+                try {
+                    Long specId = Long.parseLong(specKey.substring(PRODUCT_SPEC_STOCK_KEY.length()));
+                    Object stockObj = redisTemplate.opsForValue().get(specKey);
+                    if (stockObj != null) {
+                        long redisStock = parseLongFromObject(stockObj);
+                        if (dataProvider != null) {
+                            dataProvider.updateProductSpecStockToDb(specId, redisStock);
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("商品规格库存同步异常,key={}", specKey, e);
+                }
+            }
+            log.info("商品规格库存同步到数据库完成,key数={}", specKeys.size());
+        } catch (Exception e) {
+            log.error("商品规格库存同步到数据库异常", e);
+        }
+    }
+
+    // ==================== 内部工具方法 ====================
+
+    private boolean loadActivityFromDb(Integer orderType, Long associatedId) {
+        if (dataProvider == null) return false;
+        try {
+            ActivityStockData data = dataProvider.loadActivityData(orderType, associatedId);
+            if (data == null) return false;
+            String infoKey = ACTIVITY_INFO_KEY + associatedId;
+            Map<String, Object> activityInfo = new HashMap<>();
+            activityInfo.put("status", data.getStatus());
+            activityInfo.put("startTime", data.getStartTime());
+            activityInfo.put("endTime", data.getEndTime());
+            activityInfo.put("productId", data.getProductId());
+            activityInfo.put("specId", data.getSpecId());
+            redisCache.setCacheMap(infoKey, activityInfo);
+            if (data.getEndTime() != null) {
+                long ttlMs = data.getEndTime() - System.currentTimeMillis() + 3600000;
+                if (ttlMs > 0) {
+                    redisTemplate.expire(infoKey, ttlMs, TimeUnit.MILLISECONDS);
+                }
+            }
+            // 初始化规格库存到Redis
+            if (data.getSpecId() != null) {
+                dataProvider.loadProductSpecStock(data.getSpecId());
+            }
+            log.info("从数据库加载活动信息到Redis成功,associatedId={}, specId={}", associatedId, data.getSpecId());
+            return true;
+        } catch (Exception e) {
+            log.error("从数据库加载活动信息异常,orderType={}, associatedId={}", orderType, associatedId, e);
+            return false;
+        }
+    }
+
+    private Set<String> scanKeys(String pattern) {
+        Set<String> keys = new HashSet<>();
+        try {
+            org.springframework.data.redis.core.ScanOptions options =
+                    org.springframework.data.redis.core.ScanOptions.scanOptions()
+                            .match(pattern)
+                            .count(100)
+                            .build();
+            redisTemplate.execute((org.springframework.data.redis.core.RedisCallback<Void>) connection -> {
+                org.springframework.data.redis.core.Cursor<byte[]> cursor = connection.scan(options);
+                try {
+                    while (cursor.hasNext()) {
+                        keys.add(new String(cursor.next(), java.nio.charset.StandardCharsets.UTF_8));
+                    }
+                } finally {
+                    try { cursor.close(); } catch (java.io.IOException e) { log.warn("关闭Redis cursor失败", e); }
+                }
+                return null;
+            });
+        } catch (Exception e) {
+            log.error("SCAN扫描key异常,pattern={}", pattern, e);
+        }
+        return keys;
+    }
+
+    private int parseStockValue(Object stockObj) {
+        if (stockObj == null) return 0;
+        if (stockObj instanceof Integer) return (Integer) stockObj;
+        if (stockObj instanceof Long) return ((Long) stockObj).intValue();
+        if (stockObj instanceof String) {
+            try { return Integer.parseInt((String) stockObj); }
+            catch (NumberFormatException e) { return 0; }
+        }
+        return 0;
+    }
+
+    private long parseLongFromObject(Object obj) {
+        if (obj instanceof Integer) return ((Integer) obj).longValue();
+        if (obj instanceof Long) return (Long) obj;
+        if (obj instanceof String) return Long.parseLong((String) obj);
+        return 0;
+    }
+
+    private Integer parseInteger(Object obj) {
+        if (obj instanceof Integer) return (Integer) obj;
+        if (obj instanceof String) {
+            try { return Integer.parseInt((String) obj); } catch (NumberFormatException e) { return null; }
+        }
+        return null;
+    }
+
+    private Long parseLong(Object obj) {
+        if (obj instanceof Long) return (Long) obj;
+        if (obj instanceof Integer) return ((Integer) obj).longValue();
+        if (obj instanceof String) {
+            try { return Long.parseLong((String) obj); } catch (NumberFormatException e) { return null; }
+        }
+        return null;
+    }
+
+    private String parseErrorCode(Long code) {
+        if (code == null) return "Lua脚本返回null";
+        switch (code.intValue()) {
+            case -1: return "活动库存不足";
+            case -2: return "活动库存Key不存在";
+            case -3: return "库存值非数字";
+            case -4: return "扣减数量无效";
+            case -5: return "商品规格库存Key不存在";
+            case -6: return "商品规格库存不足";
+            default: return "未知错误,错误码:" + code;
+        }
+    }
+
+    // ==================== 兼容旧接口 ====================
+
+    public CompletableFuture<Integer> getFlashSaleStockAsync(Long flashSaleId) {
+        return CompletableFuture.supplyAsync(() -> getFlashSaleStock(flashSaleId));
+    }
+
+    public CompletableFuture<Integer> getDiscountStockAsync(Long discountId) {
+        return CompletableFuture.supplyAsync(() -> getDiscountStock(discountId));
+    }
+}

+ 26 - 0
fs-common/src/main/java/com/fs/common/core/redis/service/ActivityValidateResult.java

@@ -0,0 +1,26 @@
+package com.fs.common.core.redis.service;
+
+import lombok.Data;
+
+/**
+ * 活动校验结果
+ */
+@Data
+public class ActivityValidateResult {
+
+    private boolean valid;
+    private String message;
+
+    private ActivityValidateResult(boolean valid, String message) {
+        this.valid = valid;
+        this.message = message;
+    }
+
+    public static ActivityValidateResult success() {
+        return new ActivityValidateResult(true, "校验通过");
+    }
+
+    public static ActivityValidateResult fail(String message) {
+        return new ActivityValidateResult(false, message);
+    }
+}

+ 80 - 8
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticController.java

@@ -25,22 +25,22 @@ import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.company.service.ICompanyVoiceRoboticService;
 import com.fs.company.service.ICompanyVoiceRoboticWxService;
-import com.fs.company.vo.CalleeRoboticCallOutCountVO;
-import com.fs.company.vo.CdrBodyVo;
-import com.fs.company.vo.CdrDetailVo;
-import com.fs.company.vo.WorkflowExecRecordVo;
+import com.fs.company.service.impl.CompanyUserServiceImpl;
+import com.fs.company.vo.*;
+import com.fs.crm.domain.CrmCustomerProperty;
+import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.sop.domain.QwSopTemp;
+import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -66,6 +66,11 @@ public class CompanyVoiceRoboticController extends BaseController
     private TokenService tokenService;
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+    @Autowired
+    private CompanyUserServiceImpl companyUserService;
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
 
     /**
      * 查询机器人外呼任务列表
@@ -77,8 +82,55 @@ public class CompanyVoiceRoboticController extends BaseController
         companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        fillUserInfo(list);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('system:companyVoiceRobotic:myList')")
+    @GetMapping("/myList")
+    public TableDataInfo myList(CompanyVoiceRobotic companyVoiceRobotic){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        companyVoiceRobotic.setCompanyId(loginUser.getCompany().getCompanyId());
+        companyVoiceRobotic.setCreateUser(loginUser.getUser().getUserId());
+        startPage();
+        List<CompanyVoiceRobotic> list = companyVoiceRoboticService.selectCompanyVoiceRoboticListCompany(companyVoiceRobotic);
+        fillUserInfo(list);
         return getDataTable(list);
     }
+
+    /**
+     * 填充创建人信息
+     */
+    private void fillUserInfo(List<CompanyVoiceRobotic> list) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+
+        Set<Long> userIds = list.stream()
+                .map(CompanyVoiceRobotic::getCreateUser)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        if (userIds.isEmpty()) {
+            return;
+        }
+
+        Map<Long, DocCompanyUserVO> userMap = companyUserService
+                .selectDocCompanyUserListByUserIds(userIds)
+                .stream()
+                .collect(Collectors.toMap(DocCompanyUserVO::getUserId, Function.identity()));
+
+        list.forEach(item -> {
+            if (item.getCreateUser() != null) {
+                DocCompanyUserVO user = userMap.get(item.getCreateUser());
+                if (user != null) {
+                    item.setCreateByName(user.getNickName());
+                    item.setCreateByDeptName(user.getDeptName());
+                }
+            }
+        });
+    }
+
     /**
      * 查询机器人外呼任务列表
      */
@@ -95,6 +147,22 @@ public class CompanyVoiceRoboticController extends BaseController
         startPage();
         List<CompanyVoiceRoboticCallees> list = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesListByRoboticId(id);
         if (list != null && !list.isEmpty() && id != null) {
+
+            List<Long> customerIds = list.stream()
+                    .map(CompanyVoiceRoboticCallees::getUserId)
+                    .filter(Objects::nonNull)
+                    .distinct()
+                    .collect(Collectors.toList());
+
+            Map<Long, List<CrmCustomerProperty>> propertyMap = new HashMap<>();
+            if (!customerIds.isEmpty()) {
+                List<CrmCustomerProperty> allProperties = crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerIds(customerIds);
+                propertyMap = allProperties.stream()
+                        .collect(Collectors.groupingBy(CrmCustomerProperty::getCustomerId));
+            }
+
+
+
             LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
             Long companyId = loginUser.getCompany().getCompanyId();
             List<Long> calleeIds = list.stream().map(CompanyVoiceRoboticCallees::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
@@ -104,6 +172,10 @@ public class CompanyVoiceRoboticController extends BaseController
                 for (CompanyVoiceRoboticCallees row : list) {
                     long n = row.getId() == null ? 0L : countMap.getOrDefault(row.getId(), 0L);
                     row.setRoboticCallOutCount((int) n);
+
+                    if (row.getUserId() != null) {
+                        row.setTagList(propertyMap.getOrDefault(row.getUserId(), new ArrayList<>()));
+                    }
                 }
             }
         }

+ 60 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java

@@ -8,11 +8,14 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyVoiceRobotic;
 import com.fs.company.domain.CompanyWorkflow;
 import com.fs.company.domain.CompanyWorkflowNodeType;
 import com.fs.company.param.CompanyWorkflowSaveParam;
 import com.fs.company.param.CompanyWorkflowUpdateBindWCParam;
 import com.fs.company.service.ICompanyWorkflowService;
+import com.fs.company.service.impl.CompanyUserServiceImpl;
+import com.fs.company.vo.DocCompanyUserVO;
 import com.fs.company.vo.OptionVO;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -20,6 +23,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * AI工作流Controller
@@ -36,6 +44,9 @@ public class CompanyWorkflowController extends BaseController {
     @Autowired
     private TokenService tokenService;
 
+    @Autowired
+    private CompanyUserServiceImpl companyUserService;
+
     /**
      * 查询AI工作流列表
      */
@@ -45,9 +56,58 @@ public class CompanyWorkflowController extends BaseController {
         fsAiWorkflow.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         List<CompanyWorkflow> list = companyWorkflowService.selectCompanyWorkflowList(fsAiWorkflow);
+        fillUserInfo(list);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询AI工作流列表
+     */
+    @GetMapping("/myList")
+    public TableDataInfo myList(CompanyWorkflow fsAiWorkflow) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsAiWorkflow.setCompanyId(loginUser.getCompany().getCompanyId());
+        fsAiWorkflow.setCompanyUserId(loginUser.getUser().getUserId());
+        startPage();
+        List<CompanyWorkflow> list = companyWorkflowService.selectCompanyWorkflowList(fsAiWorkflow);
+        fillUserInfo(list);
         return getDataTable(list);
     }
 
+
+    /**
+     * 填充创建人信息
+     */
+    private void fillUserInfo(List<CompanyWorkflow> list) {
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+
+        Set<Long> userIds = list.stream()
+                .map(CompanyWorkflow::getCompanyUserId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        if (userIds.isEmpty()) {
+            return;
+        }
+
+        Map<Long, DocCompanyUserVO> userMap = companyUserService
+                .selectDocCompanyUserListByUserIds(userIds)
+                .stream()
+                .collect(Collectors.toMap(DocCompanyUserVO::getUserId, Function.identity()));
+
+        list.forEach(item -> {
+            if (item.getCompanyUserId() != null) {
+                DocCompanyUserVO user = userMap.get(item.getCompanyUserId());
+                if (user != null) {
+                    item.setCreateByName(user.getNickName());
+                    item.setCreateByDeptName(user.getDeptName());
+                }
+            }
+        });
+    }
+
     /**
      * 导出AI工作流列表
      */

+ 5 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -112,6 +112,11 @@ public class CrmCustomerController extends BaseController
 
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        if (!CompanyUser.isAdmin(loginUser.getUser().getUserType())) {
+            crmCustomer.setCustomerUserId(loginUser.getUser().getUserId());
+        }
+
         PageHelper.startPage(1, 1000);
         if(!StringUtils.isEmpty(crmCustomer.getReceiveTimeRange())){
             crmCustomer.setReceiveTimeList(crmCustomer.getReceiveTimeRange().split("--"));

+ 45 - 0
fs-company/src/main/java/com/fs/company/controller/fastGpt/FastgptChatQuestionStatisticsController.java

@@ -1,6 +1,8 @@
 package com.fs.company.controller.fastGpt;
 
 import java.util.List;
+
+import com.fs.fastGpt.domain.FastgptChatQuestion;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -16,7 +18,9 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.enums.BusinessType;
 import com.fs.fastGpt.domain.FastgptChatQuestionStatistics;
+import com.fs.fastGpt.service.IFastgptChatQuestionService;
 import com.fs.fastGpt.service.IFastgptChatQuestionStatisticsService;
+import com.fs.fastGpt.vo.FastgptChatQuestionDetailVO;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
 
@@ -33,6 +37,9 @@ public class FastgptChatQuestionStatisticsController extends BaseController
     @Autowired
     private IFastgptChatQuestionStatisticsService fastgptChatQuestionStatisticsService;
 
+    @Autowired
+    private IFastgptChatQuestionService fastgptChatQuestionService;
+
     /**
      * 查询高频聊天问题统计列表
      */
@@ -100,4 +107,42 @@ public class FastgptChatQuestionStatisticsController extends BaseController
     {
         return toAjax(fastgptChatQuestionStatisticsService.deleteFastgptChatQuestionStatisticsByIds(ids));
     }
+
+    /**
+     * 查询高频聊天问题明细(分页)
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatQuestionStatistics:query')")
+    @GetMapping("/detail/list")
+    public TableDataInfo questionDetailList(Long questionStatisticsId)
+    {
+        startPage();
+        FastgptChatQuestion q = new FastgptChatQuestion();
+        q.setQuestionStatisticsId(questionStatisticsId);
+        List<FastgptChatQuestionDetailVO> list = fastgptChatQuestionService.selectFastgptChatQuestionDetailVOList(q);
+        return getDataTable(list);
+    }
+
+    /**
+     * 新增/修改高频聊天问题回复(实际更新明细表的销售回复内容)
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatQuestionStatistics:edit')")
+    @Log(title = "高频聊天问题回复", businessType = BusinessType.UPDATE)
+    @PutMapping("/question/reply")
+    public AjaxResult saveOrUpdateQuestionReply(@RequestBody FastgptChatQuestionDetailVO param)
+    {
+        com.fs.fastGpt.domain.FastgptChatQuestion q = new com.fs.fastGpt.domain.FastgptChatQuestion();
+        q.setId(param.getId());
+        q.setCompanyUserContent(param.getCompanyUserContent());
+        int rows = fastgptChatQuestionService.updateFastgptChatQuestion(q);
+
+        // 同步更新统计表:标记为已解决
+        if (param.getQuestionStatisticsId() != null) {
+            FastgptChatQuestionStatistics statistics = new FastgptChatQuestionStatistics();
+            statistics.setId(param.getQuestionStatisticsId());
+            statistics.setIsResolve(1);
+            rows += fastgptChatQuestionStatisticsService.updateFastgptChatQuestionStatistics(statistics);
+        }
+
+        return toAjax(rows);
+    }
 }

+ 13 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopController.java

@@ -14,11 +14,14 @@ 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.core.domain.R;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.params.SendWxSopMsgParam;
+import com.fs.wx.sop.service.IWxSopLogsService;
 import com.fs.wx.sop.service.IWxSopService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
@@ -37,6 +40,8 @@ public class WxSopController extends BaseController
     private IWxSopService wxSopService;
     @Autowired
     private TokenService tokenService;
+    @Autowired
+    private IWxSopLogsService wxSopLogsService;
 
     /**
      * 查询个微SOP列表
@@ -107,4 +112,12 @@ public class WxSopController extends BaseController
     {
         return toAjax(wxSopService.deleteWxSopByIds(ids));
     }
+
+    /**
+     * 个微SOP一键群发
+     */
+    @PostMapping("/sendMsg")
+    public R sendWxSopMsg(@RequestBody SendWxSopMsgParam param) {
+        return wxSopLogsService.sendWxSopMsg(param);
+    }
 }

+ 5 - 4
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopWxLogsTaskServiceImpl.java

@@ -12,6 +12,7 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.date.DateUtil;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyMiniapp;
 import com.fs.company.domain.CompanyUser;
@@ -754,7 +755,7 @@ public class SopWxLogsTaskServiceImpl implements SopWxLogsTaskService {
         WxSopLogs wxSopLogs = new WxSopLogs();
 
         // 基础信息
-        wxSopLogs.setSendTime(formattedSendTime);
+        wxSopLogs.setSendTime(DateUtil.stringToLocalDateTime(formattedSendTime));
         wxSopLogs.setAccountId(Long.valueOf(logVo.getCustomerId())); // 个微账号ID
         wxSopLogs.setType(logVo.getType());
         wxSopLogs.setFsUserId(fsUserId);
@@ -1032,7 +1033,7 @@ public class SopWxLogsTaskServiceImpl implements SopWxLogsTaskService {
                     try {
                         if (sopLogs.getFsUserId() != null && !Objects.equals(0L, sopLogs.getFsUserId())) {
                             sopLogs.setSendStatus(5);
-                            sopLogs.setReceivingStatus(0);
+//                            sopLogs.setReceivingStatus(0);
                             sopLogs.setRemark("已经注册过的客户不发送");
                         }
                         if (ObjectUtil.isNotEmpty(setting.getValue())) {
@@ -2392,7 +2393,7 @@ public class SopWxLogsTaskServiceImpl implements SopWxLogsTaskService {
         watchLog.setCourseId(courseId != null ? courseId.longValue() : null);
         watchLog.setCompanyUserId(companyUserId != null ? Long.valueOf(companyUserId) : null);
         watchLog.setCompanyId(companyId != null ? Long.valueOf(companyId) : null);
-        watchLog.setCreateTime(convertStringToDate(sopLogs.getSendTime(), "yyyy-MM-dd HH:mm:ss"));
+        watchLog.setCreateTime(DateUtil.stringToDate(DateUtil.formatLocalDateTime(sopLogs.getSendTime())));
         watchLog.setUpdateTime(new Date());
         watchLog.setLogType(3);
         watchLog.setUserId(sopLogs.getFsUserId());
@@ -3350,7 +3351,7 @@ public class SopWxLogsTaskServiceImpl implements SopWxLogsTaskService {
     private void setSopLogsStatus(WxSopLogs sopLogs, Integer sendStatus, Integer receivingStatus, String remark) {
         if (sopLogs != null) {
             sopLogs.setSendStatus(sendStatus);
-            sopLogs.setReceivingStatus(receivingStatus);
+//            sopLogs.setReceivingStatus(receivingStatus);
             sopLogs.setRemark(remark);
         }
     }

+ 6 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java

@@ -138,4 +138,10 @@ public class CompanyVoiceRobotic {
 
     /** 删除标志 0正常 1删除 */
     private Integer delFlag;
+
+    @TableField(exist = false)
+    private String createByName;
+
+    @TableField(exist = false)
+    private String createByDeptName;
 }

+ 6 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallees.java

@@ -4,8 +4,11 @@ import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
+import com.fs.crm.domain.CrmCustomerProperty;
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * 任务外呼电话对象 company_voice_robotic_callees
  *
@@ -71,4 +74,7 @@ public class CompanyVoiceRoboticCallees{
     /** 本任务下该 callee 的 AI 外呼呼出次数(非表字段,来自 call_log 统计) */
     @TableField(exist = false)
     private Integer roboticCallOutCount;
+
+    @TableField(exist = false)
+    private List<CrmCustomerProperty> tagList ;
 }

+ 8 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflow.java

@@ -1,6 +1,7 @@
 package com.fs.company.domain;
 
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
@@ -53,6 +54,13 @@ public class CompanyWorkflow extends BaseEntity {
     private String endNodeKey;
     private Long companyId;
 
+
+    @TableField(exist = false)
+    private String createByName;
+
+    @TableField(exist = false)
+    private String createByDeptName;
+
     public Boolean isStartNode(String nodeKey) {
         return nodeKey.equals(startNodeKey);
     }

+ 6 - 0
fs-service/src/main/java/com/fs/company/service/CompanyWorkflowEngine.java

@@ -1,5 +1,6 @@
 package com.fs.company.service;
 
+import com.alibaba.fastjson.JSONObject;
 import com.fs.company.domain.CompanyAiWorkflowExecLog;
 import com.fs.company.vo.ExecutionResult;
 
@@ -69,4 +70,9 @@ public interface CompanyWorkflowEngine {
      * @param workFlowId
      */
     Long createSipTask(Long roboticId,Long workFlowId);
+    /**
+     * 添加微信成功
+     * @param object
+     */
+    void addWxSuccess(JSONObject object);
 }

+ 5 - 5
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -846,11 +846,11 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     @Override
     @Async("cidWorkFlowExecutor")
     public void callerResult4EasyCall(CdrDetailVo result) {
-//        try {
-//            Thread.sleep(3000L);
-//        } catch (InterruptedException e) {
-//            throw new RuntimeException(e);
-//        }
+        try {
+            Thread.sleep(5000L);
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
 //        EASYCALL
         log.info("进入easyCall外呼结果回调:{}", JSON.toJSONString(result));
         if (result == null || StringUtils.isBlank(result.getUuid())) return;

+ 7 - 5
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -619,13 +619,15 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
 
     /**
      * 加微成功后流程唤醒操作
-     * @param workflowInstanceId
-     * @param nodeKey
-     * @param accountId
-     * @param remark
+     * @param object
      */
     @Async("cidWorkFlowExecutor")
-    public void addWxSuccess(String workflowInstanceId, String nodeKey,Long accountId,String remark){
+    public void addWxSuccess(JSONObject object){
+
+        String workflowInstanceId = object.getString("instanceId");
+        String nodeKey = object.getString("nodeKey");
+        Long accountId = object.getLong("accountId");
+        String remark = object.getString("remark");
         if(StringUtils.isBlank(remark) || StringUtils.isBlank(workflowInstanceId) || StringUtils.isBlank(nodeKey) || accountId == null){
             log.error("addWxSuccess: 参数错误,workflowInstanceId:{},nodeKey:{},accountId:{},remark:{}", workflowInstanceId, nodeKey, accountId, remark);
             return;

+ 15 - 15
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxServiceImpl.java

@@ -587,19 +587,19 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
     public void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
         try {
             // 先查老类型的等待中工作流实例
-            CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
-                    wxClientId,
-                    ExecutionStatusEnum.WAITING.getValue(),
-                    NodeTypeEnum.AI_ADD_WX_TASK.getValue());
-            boolean isNewNodeType = false;
-            // 老类型未找到,再查新类型
-            if (waitingExec == null) {
-                waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+//            CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+//                    wxClientId,
+//                    ExecutionStatusEnum.WAITING.getValue(),
+//                    NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+//            boolean isNewNodeType = false;
+//            // 老类型未找到,再查新类型
+//            if (waitingExec == null) {
+            CompanyAiWorkflowExec  waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
                         wxClientId,
                         ExecutionStatusEnum.WAITING.getValue(),
                         NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue());
-                isNewNodeType = true;
-            }
+//                isNewNodeType = true;
+//            }
 
             if (waitingExec == null) {
                 log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
@@ -609,7 +609,7 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
             //查询工作流加微执行日志是否未更新状态
             CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
             queryP.setWorkflowInstanceId(waitingExec.getWorkflowInstanceId());
-            queryP.setNodeType(isNewNodeType ? NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue() : NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+            queryP.setNodeType(NodeTypeEnum.AI_ADD_WX_TASK_NEW.getValue());
             queryP.setStatus(ExecutionStatusEnum.WAITING.getValue());
             List<CompanyAiWorkflowExecLog> companyAiWorkflowExecLogs = companyAiWorkflowExecLogMapper.selectCompanyAiWorkflowExecLogList(queryP);
             companyAiWorkflowExecLogs.forEach(log -> {
@@ -626,11 +626,11 @@ public class CompanyWxServiceImpl extends ServiceImpl<CompanyWxAccountMapper, Co
 
             // 互斥检查:根据节点类型使用对应的互斥方法
             boolean canExecute;
-            if (isNewNodeType) {
+//            if (isNewNodeType) {
                 canExecute = AiAddWxTaskNewNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
-            } else {
-                canExecute = AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
-            }
+//            } else {
+//                canExecute = AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId);
+//            }
             if (!canExecute) {
                 log.info("工作流已被其他路径执行,跳过 - workflowInstanceId: {}, wxClientId: {}",
                         workflowInstanceId, wxClientId);

+ 15 - 2
fs-service/src/main/java/com/fs/company/service/impl/call/node/AbstractWorkflowNode.java

@@ -205,7 +205,8 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             fExecLog.setStatus(status.getValue());
             fExecLog.setEndTime(new Date());
             long durationInMillis = fExecLog.getEndTime().getTime() - fExecLog.getStartTime().getTime();
-            fExecLog.setDuration(durationInMillis);
+            // 兜底防护:确保duration不为负数;当计算结果<=0时设为1毫秒以显得更加真实
+            fExecLog.setDuration(durationInMillis > 0 ? durationInMillis : 1);
             companyAiWorkflowExecLogMapper.updateById(fExecLog);
         }else{
             log.error("未更新到节点状态:context:{},findS:{},targetS:{}",context,findStatus,status);
@@ -364,14 +365,21 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             logEntry.setErrorMessage(result.getErrorMessage());
             logEntry.setCreatedTime(new Date());
             Long startTime = context.getVariable("node_start_time_" + nodeKey, Long.class);
+            Long endTime = context.getVariable("node_end_time_" + nodeKey, Long.class);
+            // 当startTime为null但endTime不为null时(如获取锁失败、preExecute未执行),
+            // 不能用new Date()作为startTime的fallback,因为此时new Date()可能晚于endTime,
+            // 导致duration计算为负数。应将startTime对齐到endTime,表示节点未能正常启动
             if (null != startTime) {
                 logEntry.setStartTime(new Date(startTime));
+            } else if (null != endTime) {
+                logEntry.setStartTime(new Date(endTime));
             } else {
                 logEntry.setStartTime(new Date());
             }
-            Long endTime = context.getVariable("node_end_time_" + nodeKey, Long.class);
             if (null != endTime) {
                 logEntry.setEndTime(new Date(endTime));
+            } else if (null != startTime) {
+                logEntry.setEndTime(new Date(startTime));
             } else {
                 logEntry.setEndTime(new Date());
             }
@@ -379,6 +387,11 @@ public abstract class AbstractWorkflowNode implements IWorkflowNode {
             if (null != startTime && null != endTime) {
                 duration = endTime - startTime;
             }
+            // 兜底防护:确保duration不为负数;当使用fallback值(startTime或endTime为null)时,
+            // 最小耗时设为1毫秒以显得更加真实
+            if (duration <= 0) {
+                duration = (null != startTime && null != endTime) ? 0 : 1;
+            }
             logEntry.setDuration(duration);
             return logEntry;
         } catch (JsonProcessingException e) {

+ 7 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java

@@ -1,5 +1,6 @@
 package com.fs.company.service.impl.call.node;
 
+import cn.hutool.core.util.RandomUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.redis.RedisCacheT;
@@ -94,6 +95,7 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             }
 
             WxContact wxQuery = companyAiWorkflowExecMapper.selectWxContectByWorkflowInstanceId(context.getWorkflowInstanceId());
+            wxQuery.setRemark(wxQuery.getRemark() + RandomUtil.randomNumbers(10));
             wxQuery.setNickName(wxQuery.getRemark());
             wxQuery.setFriends(0);
             wxContactMapper.insert(wxQuery);
@@ -313,6 +315,10 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
     @Override
     protected void postExecute(ExecutionContext context, ExecutionResult result) {
         super.postExecute(context, result);
-        doneAddwx(context.getWorkflowInstanceId());
+        // 仅当节点成功进入PAUSED状态时才执行doneAddwx,
+        // 避免在result为null(执行异常)或FAILURE(加微准备失败)时仍继续流转流程
+        if (result != null && result.getStatus() == ExecutionStatusEnum.PAUSED) {
+            doneAddwx(context.getWorkflowInstanceId());
+        }
     }
 }

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

@@ -54,4 +54,6 @@ public class FsCourseSendRewardUParam implements Serializable
 
     private String code;
 
+    private Long watchLogId;
+
 }

+ 2 - 2
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -288,7 +288,7 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
     R createAppFd(LuckyBagCollectRecord param);
 
     /**
-    * app发红包
+    * app发红包 1 自动发课 2 手动发课
     */
-    R withdrawal(FsCourseSendRewardUParam param);
+    R withdrawal(FsCourseSendRewardUParam param,Integer type);
 }

+ 30 - 7
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -1598,7 +1598,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
             //服务号授权的,缺mpOpenId的重新登录
-            if (config.getMiniAppAuthType() == 2 && StringUtil.strIsNullOrEmpty(user.getMpOpenId())) {
+            if (config.getMiniAppAuthType() == 2 && StringUtil.strIsNullOrEmpty(user.getMpOpenId()) && param.getSource() != 3) {
                 return R.error(401, "授权后可继续!");
             }
 
@@ -1607,7 +1607,13 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             switch (config.getRewardType()) {
                 // 红包奖励
                 case 1:
-                    return sendRedPacketReward(param, user, watchLog, video, config);
+                    if (param.getSource() == 3){
+                        param.setWatchLogId(watchLog.getLogId());
+                        return withdrawal(param,1);
+                    } else {
+                        return sendRedPacketReward(param, user, watchLog, video, config);
+                    }
+//                    return sendRedPacketReward(param, user, watchLog, video, config);
                 // 积分奖励
                 case 2:
                     return sendIntegralReward(param, user, watchLog, config);
@@ -1748,7 +1754,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
         // 判断来源是否是app,如是app,则发放积分奖励
         int sourceApp = 3;
-        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康")) {
+        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康", "鸿森堂")) {
             return sendIntegralReward(param, user, log, config);
         }
 
@@ -4891,7 +4897,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
      */
     @Override
     @Transactional
-    public R withdrawal(FsCourseSendRewardUParam param) {
+    public R withdrawal(FsCourseSendRewardUParam param,Integer type) {
         Long userId = param.getUserId();
         // 生成锁的key,基于用户ID和视频ID确保同一用户同一视频的请求被锁定
         String lockKey = "reward_red_lock:user:" + userId;
@@ -4906,7 +4912,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             }
 
             logger.info("成功获取锁,开始处理奖励发放,用户ID:{}", userId);
-            return executeWithdrawal(param);
+            return executeWithdrawal(param,type);
 
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
@@ -5028,7 +5034,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     }
 
 
-    private R executeWithdrawal(FsCourseSendRewardUParam param) {
+    private R executeWithdrawal(FsCourseSendRewardUParam param, Integer type) {
         log.info("进入用户判断");
         FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
         if (user == null) {
@@ -5097,8 +5103,12 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 }
                 packetParam.setOpenId(openId);
                 BeanUtils.copyProperties(param, packetParam);
+                if (type==1){
+                    return sendAppRedPacketAuto(packetParam, log,video, config);
+                }else if (type==2){
+                    return sendAppRedPacket(packetParam, log,video, config);
+                }
 
-                return sendAppRedPacket(packetParam, log,video, config);
             // 积分奖励
             case 2:
                 return sendIntegralReward(param, user, log, config);
@@ -5334,6 +5344,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     }
 
 
+    /**
+    * app手动看课的
+    */
     private R sendAppRedPacket(WxSendRedPacketParam packetParam,FsCourseWatchLog log,FsUserCourseVideo video,CourseConfig config) {
         FsUserCoursePeriodDays periodDays = new FsUserCoursePeriodDays();
         periodDays.setVideoId(log.getVideoId());
@@ -5347,6 +5360,14 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             return R.error(403, "已超过领取红包时间");
         }
 
+        // 手动的就多一个查询手动营期的时间 ,所以这里直接复用
+        return sendAppRedPacketAuto(packetParam,log,video,config);
+    }
+
+    /**
+    * app自动看课的
+    */
+    private R sendAppRedPacketAuto(WxSendRedPacketParam packetParam,FsCourseWatchLog log,FsUserCourseVideo video,CourseConfig config) {
 
         // 确定红包金额
         BigDecimal amount = BigDecimal.ZERO;
@@ -5568,6 +5589,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
     }
 
+
+
     /**
      * 获取用户openId
      */

+ 3 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -233,6 +233,9 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "<if test = 'maps.customerName != null and  maps.customerName !=\"\"    '> " +
             "and c.customer_name like CONCAT('%',#{maps.customerName},'%') " +
             "</if>" +
+            "<if test = 'maps.customerUserId != null and maps.customerUserId !=\"\"     '> " +
+            "and c.receive_user_id =#{maps.customerUserId} " +
+            "</if>" +
             "<if test = 'maps.companyUserNickName != null and  maps.companyUserNickName !=\"\"    '> " +
             "and cu.nick_name like CONCAT(#{maps.companyUserNickName},'%') " +
             "</if>" +

+ 1 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyMapper.java

@@ -13,6 +13,7 @@ public interface CrmCustomerPropertyMapper extends BaseMapper<CrmCustomerPropert
     List<CrmCustomerProperty> selectCrmCustomerPropertyList(CrmCustomerProperty crmCustomerProperty);
 
     List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId);
+    List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerIds(List<Long> customerIds);
 
     CrmCustomerProperty selectByCustomerIdAndPropertyId(@Param("customerId") Long customerId, @Param("propertyId") Long propertyId);
 

+ 3 - 2
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java

@@ -9,8 +9,8 @@ import java.util.Map;
 
 /**
  * 客户属性服务接口
- * @author 
- * @date 
+ * @author
+ * @date
  */
 public interface ICrmCustomerPropertyService extends IService<CrmCustomerProperty> {
 
@@ -62,6 +62,7 @@ public interface ICrmCustomerPropertyService extends IService<CrmCustomerPropert
      * @return 客户属性列表
      */
     List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId);
+    List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerIds(List<Long> customerIds);
 
     /**
      * 添加客户属性

+ 10 - 14
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java

@@ -5,8 +5,6 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -21,13 +19,11 @@ import com.fs.common.utils.spring.SpringUtils;
 import com.fs.crm.domain.*;
 import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
 import com.fs.crm.mapper.CrmCustomerAnalyzeMapper;
-import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.crm.param.PolishingScriptParam;
 import com.fs.crm.service.ICrmCustomerAnalyzeService;
 import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
 import com.fs.crm.utils.CrmCustomerAiTagUtil;
-import com.fs.crm.vo.CrmCustomerAiTagVo;
-import com.fs.crm.vo.QwCustomerAiTagVo;
+import com.fs.qw.vo.QwCustomerAiTagVo;
 import com.fs.qw.domain.QwExternalAiAnalyze;
 import com.fs.qw.mapper.QwCustomerPropertyMapper;
 import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
@@ -402,7 +398,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("userIntent", "");
         long startTime = System.currentTimeMillis();
         R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, Long.valueOf(param.getChatId()),OTHER_KEY);
-        System.out.println(aiResponse);
+//        System.out.println(aiResponse);
         String result = "";
         CrmCustomerChatMessage crmCustomerChatMessage = new CrmCustomerChatMessage();
         crmCustomerChatMessage.setContentType(1);
@@ -550,7 +546,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
 //        log.info("请求参数:{}", requestParam);
 
         R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, chatId,OTHER_KEY);
-        JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
+//        JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
 //        System.out.println(aiResponse);
 // 获取 data.responseData
         String result = "";
@@ -598,7 +594,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
 
     @Override
     public String aiGeneratedCustomerPortraitQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
-        Map<String, Object> stringObjectMap = buildRequestParamQw(qwExternalAiAnalyze, dataJson);
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
         stringObjectMap.put("modelType", "客户画像");
 //        log.info("请求参数:{}", stringObjectMap);
         R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
@@ -620,7 +616,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
 
     @Override
     public String aiCommunicationSummaryQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
-        Map<String, Object> stringObjectMap = buildRequestParamQw(qwExternalAiAnalyze, dataJson);
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
         stringObjectMap.put("modelType", "沟通总结");
 //        log.info("请求参数:{}", stringObjectMap);
         R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
@@ -639,7 +635,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
 
     @Override
     public String aiCommunicationAbstractQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
-        Map<String, Object> stringObjectMap = buildRequestParamQw(qwExternalAiAnalyze, dataJson);
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
         stringObjectMap.put("modelType", "沟通摘要");
         stringObjectMap.remove("userInfo");
         HashMap<String, String> map = MapUtil.of("沟通摘要", "");
@@ -660,7 +656,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
 
     @Override
     public Long aiAttritionLevelQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
-        Map<String, Object> stringObjectMap = buildRequestParamQw(qwExternalAiAnalyze, dataJson);
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
         stringObjectMap.put("modelType","流失风险等级");
         stringObjectMap.remove("userInfo");
         HashMap<String, String> map = MapUtil.of("流失风险等级", "");
@@ -682,7 +678,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
 
     @Override
     public String aiCustomerFocusQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
-        Map<String, Object> stringObjectMap = buildRequestParamQw(qwExternalAiAnalyze, dataJson);
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
         stringObjectMap.put("modelType","客户关注点");
         stringObjectMap.remove("userInfo");
         HashMap<String, String> map = MapUtil.of("客户关注点", "");
@@ -705,7 +701,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
 
     @Override
     public String aiIntentionDegreeQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
-        Map<String, Object> stringObjectMap = buildRequestParamQw(qwExternalAiAnalyze, dataJson);
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
         stringObjectMap.put("modelType","客户意向度");
         stringObjectMap.remove("userInfo");
         HashMap<String, String> map = MapUtil.of("客户意向度", "");
@@ -878,7 +874,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         return null;
     }
 
-    private Map<String, Object> buildRequestParamQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson) {
+    private Map<String, Object> buildRequestParam(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson) {
         Map<String, Object> requestParam = new HashMap<>();
 
         // 获取各类数据

+ 9 - 3
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java

@@ -23,9 +23,7 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -74,6 +72,14 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
         return baseMapper.selectCrmCustomerPropertyByCustomerId(customerId);
     }
 
+    @Override
+    public List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerIds(List<Long> customerIds) {
+        if (customerIds == null || customerIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return baseMapper.selectCrmCustomerPropertyByCustomerIds(customerIds);
+    }
+
     @Override
     public int addCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy) {
         CrmCustomerProperty property = new CrmCustomerProperty();

+ 15 - 7
fs-service/src/main/java/com/fs/fastGpt/mapper/FastgptChatQuestionMapper.java

@@ -3,18 +3,19 @@ package com.fs.fastGpt.mapper;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.fastGpt.domain.FastgptChatQuestion;
+import com.fs.fastGpt.vo.FastgptChatQuestionDetailVO;
 import org.apache.ibatis.annotations.Param;
 
 /**
  * 聊天问题收集Mapper接口
- * 
+ *
  * @author fs
  * @date 2026-04-20
  */
 public interface FastgptChatQuestionMapper extends BaseMapper<FastgptChatQuestion>{
     /**
      * 查询聊天问题收集
-     * 
+     *
      * @param id 聊天问题收集主键
      * @return 聊天问题收集
      */
@@ -22,15 +23,22 @@ public interface FastgptChatQuestionMapper extends BaseMapper<FastgptChatQuestio
 
     /**
      * 查询聊天问题收集列表
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 聊天问题收集集合
      */
     List<FastgptChatQuestion> selectFastgptChatQuestionList(FastgptChatQuestion fastgptChatQuestion);
 
+    /**
+     * 查询聊天问题明细
+     * @param fastgptChatQuestion 入参
+     * @return vo
+     */
+    List<FastgptChatQuestionDetailVO> selectFastgptChatQuestionDetailVOList(FastgptChatQuestion fastgptChatQuestion);
+
     /**
      * 新增聊天问题收集
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 结果
      */
@@ -38,7 +46,7 @@ public interface FastgptChatQuestionMapper extends BaseMapper<FastgptChatQuestio
 
     /**
      * 修改聊天问题收集
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 结果
      */
@@ -46,7 +54,7 @@ public interface FastgptChatQuestionMapper extends BaseMapper<FastgptChatQuestio
 
     /**
      * 删除聊天问题收集
-     * 
+     *
      * @param id 聊天问题收集主键
      * @return 结果
      */
@@ -54,7 +62,7 @@ public interface FastgptChatQuestionMapper extends BaseMapper<FastgptChatQuestio
 
     /**
      * 批量删除聊天问题收集
-     * 
+     *
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */

+ 9 - 0
fs-service/src/main/java/com/fs/fastGpt/mapper/FastgptChatQuestionStatisticsMapper.java

@@ -65,4 +65,13 @@ public interface FastgptChatQuestionStatisticsMapper extends BaseMapper<FastgptC
                                                          @Param("threshold") Integer threshold);
 
     int incrementFrequencyById(@Param("id") Long id, @Param("updateTime") Date updateTime);
+
+    FastgptChatQuestionStatistics selectFirstByQuestionCategory(@Param("questionCategory") Integer questionCategory);
+
+    /**
+     * 按 SimHash 候选列表(包含 dist 计算列,供 Java 侧二次筛选)
+     */
+    List<FastgptChatQuestionStatistics> selectCandidatesBySimhash(@Param("simhash") Long simhash,
+                                                                  @Param("threshold") Integer threshold,
+                                                                  @Param("limit") Integer limit);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/fastGpt/param/FastgptKnowledgeMissCollectParam.java

@@ -8,6 +8,9 @@ public class FastgptKnowledgeMissCollectParam {
     private String aiUserContent;
     private String contentEmj;
 
+    /** 调用 CrmCustomerAiTagUtil.callAiService 的 appKey,与 AI Hook 中 config.getAPPKey() 一致 */
+    private String appKey;
+
     private Long sessionId;
     private Long msgId;
     private String extId;

+ 15 - 7
fs-service/src/main/java/com/fs/fastGpt/service/IFastgptChatQuestionService.java

@@ -3,17 +3,18 @@ package com.fs.fastGpt.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.fastGpt.domain.FastgptChatQuestion;
+import com.fs.fastGpt.vo.FastgptChatQuestionDetailVO;
 
 /**
  * 聊天问题收集Service接口
- * 
+ *
  * @author fs
  * @date 2026-04-20
  */
 public interface IFastgptChatQuestionService extends IService<FastgptChatQuestion>{
     /**
      * 查询聊天问题收集
-     * 
+     *
      * @param id 聊天问题收集主键
      * @return 聊天问题收集
      */
@@ -21,15 +22,22 @@ public interface IFastgptChatQuestionService extends IService<FastgptChatQuestio
 
     /**
      * 查询聊天问题收集列表
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 聊天问题收集集合
      */
     List<FastgptChatQuestion> selectFastgptChatQuestionList(FastgptChatQuestion fastgptChatQuestion);
 
+    /**
+     * 查询聊天问题明细
+     * @param fastgptChatQuestion 参数
+     * @return vo
+     */
+    List<FastgptChatQuestionDetailVO> selectFastgptChatQuestionDetailVOList(FastgptChatQuestion fastgptChatQuestion);
+
     /**
      * 新增聊天问题收集
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 结果
      */
@@ -37,7 +45,7 @@ public interface IFastgptChatQuestionService extends IService<FastgptChatQuestio
 
     /**
      * 修改聊天问题收集
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 结果
      */
@@ -45,7 +53,7 @@ public interface IFastgptChatQuestionService extends IService<FastgptChatQuestio
 
     /**
      * 批量删除聊天问题收集
-     * 
+     *
      * @param ids 需要删除的聊天问题收集主键集合
      * @return 结果
      */
@@ -53,7 +61,7 @@ public interface IFastgptChatQuestionService extends IService<FastgptChatQuestio
 
     /**
      * 删除聊天问题收集信息
-     * 
+     *
      * @param id 聊天问题收集主键
      * @return 结果
      */

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

@@ -614,6 +614,7 @@ public class AiHookServiceImpl implements AiHookService {
                     p.setRoleId(role.getRoleId());
                     p.setNickName(qwExternalContacts.getName());
                     p.setUserType(fastGptChatSession.getUserType());
+                    p.setAppKey(config.getAPPKey());
                     fastgptChatQuestionCollectServiceImpl.collectAsync(p);
                 }
             }else{

+ 204 - 31
fs-service/src/main/java/com/fs/fastGpt/service/impl/FastgptChatQuestionCollectServiceImpl.java

@@ -1,7 +1,12 @@
 package com.fs.fastGpt.service.impl;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.BeanCopyUtils;
+import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
+import com.fs.crm.utils.CrmCustomerAiTagUtil;
 import com.fs.fastGpt.domain.FastgptChatQuestion;
 import com.fs.fastGpt.domain.FastgptChatQuestionStatistics;
 import com.fs.fastGpt.mapper.FastgptChatQuestionMapper;
@@ -9,6 +14,7 @@ import com.fs.fastGpt.mapper.FastgptChatQuestionStatisticsMapper;
 import com.fs.fastGpt.param.FastgptKnowledgeMissCollectParam;
 import com.fs.fastGpt.service.IFastgptChatQuestionService;
 import com.fs.fastGpt.util.FastgptQuestionNormalizeUtil;
+import cn.hutool.json.JSONUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -17,17 +23,33 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
- * 知识库未命中:异步写入明细,按 content_summary 归并统计并回填 question_statistics_id
+ * 知识库未命中:异步写入明细。
+ * 顺序:① SimHash+Jaccard 本地匹配已有统计;
+ * ② 未命中再调 AI 按 contentType(question_category)归并;③ 仍失败则新建统计行(含 SimHash 兜底插入)。</p>
  */
 @Slf4j
 @Service
 public class FastgptChatQuestionCollectServiceImpl {
 
-    private static final int SIMHASH_THRESHOLD = 14;
-    private static final double JACCARD_THRESHOLD = 0.55d;
+    /**
+     * 本地匹配阈值(越大越松:允许更大的 SimHash 汉明距离)
+     */
+    private static final int SIMHASH_THRESHOLD = 30;
 
+    /**
+     * 本地 Jaccard 阈值(越小越松)
+     */
+    private static final double JACCARD_THRESHOLD = 0.45d;
+
+    private static final int LOCAL_CANDIDATE_LIMIT = 30;
+
+    private static final String MODEL_TYPE_HIGH_FREQ = "高频问题类别";
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
 
     @Autowired
     private IFastgptChatQuestionService fastgptChatQuestionService;
@@ -63,37 +85,30 @@ public class FastgptChatQuestionCollectServiceImpl {
 
             Date now = DateUtils.getNowDate();
             long sh = FastgptQuestionNormalizeUtil.simhash64(display);
-            FastgptChatQuestionStatistics best = fastgptChatQuestionStatisticsMapper.selectBestMatchBySimhash(sh, SIMHASH_THRESHOLD);
-            Long statId;
-            if (best != null && best.getId() != null) {
-                double jac = FastgptQuestionNormalizeUtil.jaccard(
-                        FastgptQuestionNormalizeUtil.ngramTokens(display),
-                        FastgptQuestionNormalizeUtil.ngramTokens(best.getContentSummary())
-                );
-                if (jac < JACCARD_THRESHOLD) {
-                    best = null;
-                }
-            }
-            if (best != null && best.getId() != null) {
-                statId = best.getId();
+
+            // ① 先本地匹配:命中则频次+1,并且才会继续走 AI;未命中则不走 AI
+            LocalMatch localMatch = findBestLocalMatch(display, sh);
+            Long statId = null;
+            if (localMatch != null) {
+                statId = localMatch.statId;
                 fastgptChatQuestionStatisticsMapper.incrementFrequencyById(statId, now);
-            } else {
-                FastgptChatQuestionStatistics row = new FastgptChatQuestionStatistics();
-                row.setQuestionCategory(0);
-                row.setContentSummary(display.length() > 200 ? display.substring(0, 200) : display);
-                row.setSimhash(sh);
-                row.setIsResolve(0);
-                row.setQuestionId(detailId);
-                row.setFrequency(1);
-                row.setCreateTime(now);
-                row.setUpdateTime(now);
-                fastgptChatQuestionStatisticsMapper.insertFastgptChatQuestionStatistics(row);
-                statId = row.getId();
-                if (statId == null) {
-                    FastgptChatQuestionStatistics insertedBest = fastgptChatQuestionStatisticsMapper.selectBestMatchBySimhash(sh, SIMHASH_THRESHOLD);
-                    statId = insertedBest != null ? insertedBest.getId() : null;
+
+                // ② 只有本地命中时,才调用 AI 做类别归类(用于给统计行补齐/修正 question_category)
+                Integer aiCategory = getHighFreqCategoryByAi(param);
+                if (aiCategory != null) {
+                    FastgptChatQuestionStatistics upd = new FastgptChatQuestionStatistics();
+                    upd.setId(statId);
+                    upd.setQuestionCategory(aiCategory);
+                    upd.setUpdateTime(now);
+                    // 只更新类别(不覆盖其他字段)
+                    fastgptChatQuestionStatisticsMapper.updateFastgptChatQuestionStatistics(upd);
                 }
             }
+
+            if (statId == null) {
+                statId = mergeBySimhashFallback(display, sh, detailId, now);
+            }
+
             if (statId != null) {
                 fastgptChatQuestionMapper.updateQuestionStatisticsIdById(detailId, statId);
             }
@@ -102,6 +117,164 @@ public class FastgptChatQuestionCollectServiceImpl {
         }
     }
 
+    private Integer getHighFreqCategoryByAi(FastgptKnowledgeMissCollectParam param) {
+        if (param.getSessionId() == null || StringUtils.isBlank(param.getAppKey())) {
+            return null;
+        }
+        try {
+            Map<String, Object> requestParam = new HashMap<>();
+
+            requestParam.put("history", StringUtils.defaultString(param.getAiUserContent(), ""));
+            requestParam.put("aiContent", "");
+            requestParam.put("userContent", "");
+            requestParam.put("isRepository", "");
+            requestParam.put("contentType", "");
+            requestParam.put("modelType", MODEL_TYPE_HIGH_FREQ);
+
+            R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, param.getSessionId(), param.getAppKey());
+
+            // 处理返回结果
+            JsonNode userInfo = parseUserInfoFromAiResponse(aiResponse);
+            if (userInfo == null || userInfo.isMissingNode() || !userInfo.isObject()) {
+                return null;
+            }
+            if (userInfo.has("contentType") && !userInfo.get("contentType").isNull()) {
+                return parseIntNode(userInfo.get("contentType"));
+            }
+            if (userInfo.has(MODEL_TYPE_HIGH_FREQ) && !userInfo.get(MODEL_TYPE_HIGH_FREQ).isNull()) {
+                return parseIntNode(userInfo.get(MODEL_TYPE_HIGH_FREQ));
+            }
+        } catch (Exception e) {
+            log.warn("高频问题类别 AI 解析失败 sessionId={}", param.getSessionId(), e);
+        }
+        return null;
+    }
+
+    private static Integer parseIntNode(JsonNode n) {
+        if (n == null || n.isNull() || n.isMissingNode()) {
+            return null;
+        }
+        if (n.isIntegralNumber() || n.isNumber()) {
+            return n.intValue();
+        }
+        return parseIntLoose(n.asText());
+    }
+
+    // 文本转int
+    private static Integer parseIntLoose(String s) {
+        if (StringUtils.isBlank(s)) {
+            return null;
+        }
+        try {
+            return Integer.parseInt(s.trim());
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    private JsonNode parseUserInfoFromAiResponse(R aiResponse) throws JsonProcessingException {
+        if (aiResponse == null || !Integer.valueOf(200).equals(aiResponse.get("code"))) {
+            return null;
+        }
+        JsonNode rootS = objectMapper.readTree(JSONUtil.toJsonStr(aiResponse));
+        JsonNode choices = rootS.path("data").path("choices");
+        if (!choices.isArray() || choices.size() <= 0) {
+            return null;
+        }
+        JsonNode contentNode = choices.get(0).path("message").path("content");
+        if (!contentNode.isTextual()) {
+            return null;
+        }
+        String contentStr = contentNode.asText();
+        JsonNode contentArray = objectMapper.readTree(contentStr);
+        if (!contentArray.isArray() || contentArray.size() <= 1) {
+            return null;
+        }
+        JsonNode secondElement = contentArray.get(1);
+        JsonNode textNode = secondElement.path("text");
+        if (textNode.isMissingNode()) {
+            return null;
+        }
+        JsonNode contentInnerNode = textNode.path("content");
+        if (!contentInnerNode.isTextual()) {
+            return null;
+        }
+        String innerJsonStr = contentInnerNode.asText();
+        JsonNode innerJson = objectMapper.readTree(innerJsonStr);
+        return innerJson.path("userInfo");
+    }
+
+    /**
+     * 本地 SimHash + Jaccard 与已有统计行比对,命中则频次+1 并返回该统计 id;不插入新行。
+     */
+    private LocalMatch findBestLocalMatch(String display, long sh) {
+        // 取一批候选,Java 侧算 Jaccard 再挑最优
+        java.util.List<FastgptChatQuestionStatistics> candidates =
+                fastgptChatQuestionStatisticsMapper.selectCandidatesBySimhash(sh, SIMHASH_THRESHOLD, LOCAL_CANDIDATE_LIMIT);
+        if (candidates == null || candidates.isEmpty()) {
+            return null;
+        }
+        java.util.Set<String> a = FastgptQuestionNormalizeUtil.ngramTokens(display);
+        LocalMatch best = null;
+        for (FastgptChatQuestionStatistics c : candidates) {
+            if (c == null || c.getId() == null || StringUtils.isBlank(c.getContentSummary())) {
+                continue;
+            }
+            double jac = FastgptQuestionNormalizeUtil.jaccard(a, FastgptQuestionNormalizeUtil.ngramTokens(c.getContentSummary()));
+            if (jac < JACCARD_THRESHOLD) {
+                continue;
+            }
+            // dist 列未映射到实体,这里拿不到;所以只用 jac + id 做选择
+            if (best == null) {
+                best = new LocalMatch(c.getId(), jac);
+                continue;
+            }
+            // 选择“匹配值更大”的(这里用 Jaccard 作为匹配分数)
+            if (jac > best.jaccard) {
+                best = new LocalMatch(c.getId(), jac);
+            } else if (Double.compare(jac, best.jaccard) == 0 && c.getId() > best.statId) {
+                // 分数相同取最新(id 更大)
+                best = new LocalMatch(c.getId(), jac);
+            }
+        }
+        return best;
+    }
+
+    /** AI 也未归类时:再按 SimHash 找一遍(与本地逻辑一致),仍无则插入 question_category=0 的新统计行 */
+    private Long mergeBySimhashFallback(String display, long sh, Long detailId, Date now) {
+        LocalMatch localMatch = findBestLocalMatch(display, sh);
+        if (localMatch != null) {
+            fastgptChatQuestionStatisticsMapper.incrementFrequencyById(localMatch.statId, now);
+            return localMatch.statId;
+        }
+        FastgptChatQuestionStatistics row = new FastgptChatQuestionStatistics();
+        row.setQuestionCategory(0);
+        row.setContentSummary(display.length() > 200 ? display.substring(0, 200) : display);
+        row.setSimhash(sh);
+        row.setIsResolve(0);
+        row.setQuestionId(detailId);
+        row.setFrequency(1);
+        row.setCreateTime(now);
+        row.setUpdateTime(now);
+        fastgptChatQuestionStatisticsMapper.insertFastgptChatQuestionStatistics(row);
+        Long statId = row.getId();
+        if (statId == null) {
+            FastgptChatQuestionStatistics insertedBest = fastgptChatQuestionStatisticsMapper.selectBestMatchBySimhash(sh, SIMHASH_THRESHOLD);
+            statId = insertedBest != null ? insertedBest.getId() : null;
+        }
+        return statId;
+    }
+
+    private static class LocalMatch {
+        private final Long statId;
+        private final double jaccard;
+
+        private LocalMatch(Long statId, double jaccard) {
+            this.statId = statId;
+            this.jaccard = jaccard;
+        }
+    }
+
     private static FastgptChatQuestion buildQuestion(FastgptKnowledgeMissCollectParam param, String userContent) {
         FastgptChatQuestion q = new FastgptChatQuestion();
         BeanCopyUtils.copy(param, q);

+ 15 - 8
fs-service/src/main/java/com/fs/fastGpt/service/impl/FastgptChatQuestionServiceImpl.java

@@ -3,15 +3,15 @@ package com.fs.fastGpt.service.impl;
 import java.util.List;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.fastGpt.mapper.FastgptChatQuestionMapper;
 import com.fs.fastGpt.domain.FastgptChatQuestion;
 import com.fs.fastGpt.service.IFastgptChatQuestionService;
+import com.fs.fastGpt.vo.FastgptChatQuestionDetailVO;
 
 /**
  * 聊天问题收集Service业务层处理
- * 
+ *
  * @author fs
  * @date 2026-04-20
  */
@@ -20,7 +20,7 @@ public class FastgptChatQuestionServiceImpl extends ServiceImpl<FastgptChatQuest
 
     /**
      * 查询聊天问题收集
-     * 
+     *
      * @param id 聊天问题收集主键
      * @return 聊天问题收集
      */
@@ -32,7 +32,7 @@ public class FastgptChatQuestionServiceImpl extends ServiceImpl<FastgptChatQuest
 
     /**
      * 查询聊天问题收集列表
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 聊天问题收集
      */
@@ -42,9 +42,15 @@ public class FastgptChatQuestionServiceImpl extends ServiceImpl<FastgptChatQuest
         return baseMapper.selectFastgptChatQuestionList(fastgptChatQuestion);
     }
 
+    @Override
+    public List<FastgptChatQuestionDetailVO> selectFastgptChatQuestionDetailVOList(FastgptChatQuestion fastgptChatQuestion)
+    {
+        return baseMapper.selectFastgptChatQuestionDetailVOList(fastgptChatQuestion);
+    }
+
     /**
      * 新增聊天问题收集
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 结果
      */
@@ -57,19 +63,20 @@ public class FastgptChatQuestionServiceImpl extends ServiceImpl<FastgptChatQuest
 
     /**
      * 修改聊天问题收集
-     * 
+     *
      * @param fastgptChatQuestion 聊天问题收集
      * @return 结果
      */
     @Override
     public int updateFastgptChatQuestion(FastgptChatQuestion fastgptChatQuestion)
     {
+        fastgptChatQuestion.setUpdateTime(DateUtils.getNowDate());
         return baseMapper.updateFastgptChatQuestion(fastgptChatQuestion);
     }
 
     /**
      * 批量删除聊天问题收集
-     * 
+     *
      * @param ids 需要删除的聊天问题收集主键
      * @return 结果
      */
@@ -81,7 +88,7 @@ public class FastgptChatQuestionServiceImpl extends ServiceImpl<FastgptChatQuest
 
     /**
      * 删除聊天问题收集信息
-     * 
+     *
      * @param id 聊天问题收集主键
      * @return 结果
      */

+ 65 - 0
fs-service/src/main/java/com/fs/fastGpt/vo/FastgptChatQuestionDetailVO.java

@@ -0,0 +1,65 @@
+package com.fs.fastGpt.vo;
+
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 高频聊天问题明细 VO
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FastgptChatQuestionDetailVO extends BaseEntity {
+
+    /** 主键id */
+    private Long id;
+
+    /** 会话id */
+    private Long sessionId;
+
+    /** 消息id */
+    private Long msgId;
+
+    /** 外部ID */
+    private String extId;
+
+    /** 用户id */
+    private String userId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 公司名称 */
+    private String companyName;
+
+    /** 销售ID */
+    private Long companyUserId;
+
+    /** 销售昵称 */
+    private String companyUserNickName;
+
+    /** 角色ID */
+    private Long roleId;
+
+    /** 昵称 */
+    private String nickName;
+
+    /** 用户类型 1微信用户 2小程序用户 3销售用户 */
+    private Integer userType;
+
+    /** 客户内容 */
+    private String userContent;
+
+    /** 销售回复内容 */
+    private String companyUserContent;
+
+    /** 高频问题统计id,关联高频聊天问题统计表 */
+    private Long questionStatisticsId;
+
+    /** 外部联系人ID */
+    private Long externalContactId;
+
+    /** 外部联系人名称 */
+    private String externalContactName;
+}
+

+ 12 - 0
fs-service/src/main/java/com/fs/his/enums/AiSaleBehaviorAnalyzsEnum.java

@@ -0,0 +1,12 @@
+package com.fs.his.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum AiSaleBehaviorAnalyzsEnum {
+    SALE_ANALYZE_ADD("sale:behavior:analyze:order:add"),
+    SALE_ANALYZE_MINUS("sale:behavior:analyze:order:minus");
+    private String redisKey;
+}

+ 1 - 0
fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java

@@ -117,6 +117,7 @@ public interface IFsStorePaymentService
 
     String v3TransferNotify(String notifyData, HttpServletRequest request);
 
+    String v3TransferNotifyApp(String notifyData, HttpServletRequest request);
 
     String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
 

+ 4 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java

@@ -1146,6 +1146,8 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             } catch (Exception e) {
                 log.info("拆分订单错误:{}", order.getOrderId());
             }
+            //付款成功存入redis进行销售行为分析订单金额增减
+            redisCache.redisTemplate.opsForList().rightPush(AiSaleBehaviorAnalyzsEnum.SALE_ANALYZE_ADD.getRedisKey(),order.getCompanyUserId()+":"+order.getPayPrice());
         }
 
         return R.ok();
@@ -1753,6 +1755,8 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
                     userCouponService.updateFsUserCoupon(userCoupon);
                 }
             }
+            //付款成功存入redis进行销售行为分析订单金额增减
+            redisCache.redisTemplate.opsForList().rightPush(AiSaleBehaviorAnalyzsEnum.SALE_ANALYZE_ADD.getRedisKey(),order.getCompanyUserId()+":"+order.getPayPrice());
             return R.ok();
         } catch (Exception e) {
             log.info(payCode + "异常了" + e.getMessage());

+ 36 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -1154,6 +1154,42 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
     }
 
+    @Override
+    public String v3TransferNotifyApp(String notifyData, HttpServletRequest request) {
+        logger.info("zyp \n【app-收到转账回调V3】:{}",notifyData);
+        try {
+            String json = configService.selectConfigByKey("his.AppRedPacket");
+            RedPacketConfig config = JSONUtil.toBean(json, RedPacketConfig.class);
+            //创建微信订单
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(config,payConfig);
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+            SignatureHeader signatureHeader = new SignatureHeader();
+            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData,signatureHeader);
+            logger.info("app-到零钱回调:{}",result.getResult());
+            if (result.getResult().getState().equals("SUCCESS")) {
+                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(),result.getResult().getTransferBillNo());
+                logger.info("app,result:{}",r);
+                if (r.get("code").equals(200)){
+                    return WxPayNotifyResponse.success("处理成功");
+                }else {
+                    return WxPayNotifyResponse.fail("");
+                }
+            }else {
+                return WxPayNotifyResponse.fail("");
+            }
+        } catch (WxPayException e) {
+            e.printStackTrace();
+            logger.error("zyp \n【app-转账回调异常】:{}", e.getReturnMsg());
+            return WxPayNotifyResponse.fail(e.getMessage());
+        }
+    }
+
     @Override
     public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
         logger.info("分公司回调V3::companyId:{}",companyId);

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

@@ -392,4 +392,22 @@ public class FsStoreOrderScrm extends BaseEntity
 
     //虚拟手机号
     private String virtualPhone;
+
+    //关联id根据订单类型+associatedId唯一数据
+    private Long associatedId;
+
+    //是否同步库存 0-否 1-是
+    private Integer isSyncInventory;
+
+    @TableField(exist = false)
+    private String reasonValue1;
+
+    @TableField(exist = false)
+    private String reasonValue2;
+
+    @TableField(exist = false)
+    private String auditRemark;
+
+    @TableField(exist = false)
+    private String auditReasonName;
 }

+ 154 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductActivity.java

@@ -0,0 +1,154 @@
+package com.fs.hisStore.domain;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+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;
+
+/**
+ * 商品活动中间表对象 fs_store_product_activity
+ * 合并秒杀(6)和限时折扣(7),一个商品+一个规格=一条记录
+ *
+ * @author fs
+ * @date 2026-04-16
+ */
+@Data
+public class FsStoreProductActivity implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 商品ID */
+    @Excel(name = "商品ID")
+    private Long productId;
+
+    /** 活动类型:6=秒杀 7=限时折扣 */
+    @Excel(name = "活动类型", readConverterExp = "6=秒杀,7=限时折扣")
+    private Integer activityType;
+
+    /** 规格ID(fs_store_product_attr_value.id) */
+    @Excel(name = "规格ID")
+    private Long specId;
+
+    /** 原价 */
+    @Excel(name = "原价")
+    private BigDecimal originalPrice;
+
+    /** 秒杀价(activity_type=6时) */
+    @Excel(name = "秒杀价")
+    private BigDecimal flashPrice;
+
+    /** 折扣如0.8(activity_type=7时) */
+    @Excel(name = "折扣")
+    private BigDecimal discount;
+
+    /** 折扣价(activity_type=7时) */
+    @Excel(name = "折扣价")
+    private BigDecimal discountPrice;
+
+    /** 开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 状态:0=下架 1=上架 */
+    @Excel(name = "状态", readConverterExp = "0=下架,1=上架")
+    private Integer status;
+
+    /** 删除标志:0=正常 1=删除 */
+    private Integer delFlag;
+
+    /** 商品名称(关联查询) */
+    @Excel(name = "商品名称")
+    private String productName;
+
+    /** 商品图片(关联查询) */
+    private String productImage;
+
+    /** 商品售价(关联查询) */
+    private BigDecimal price;
+
+    /** 商品原价(关联查询) */
+    private BigDecimal otPrice;
+
+    /** 商品销量(关联查询) */
+    private Integer sales;
+
+    /** 商品库存(关联查询) */
+    private Integer productStock;
+
+    /** 商品分类名称(关联查询) */
+    private String cateName;
+
+    /** 商品编号(关联查询) */
+    private String barCode;
+
+    /** 商品详情(关联查询) */
+    @TableField(exist = false)
+    private String productInfo;
+
+    /** 商品轮播图(关联查询) */
+    @TableField(exist = false)
+    private String sliderImage;
+
+    /** 规格名称/SKU(关联查询) */
+    @TableField(exist = false)
+    private String specName;
+
+    /** 规格库存(关联查询) */
+    @TableField(exist = false)
+    private Integer specStock;
+
+    /** Redis实时库存(非数据库字段) */
+    @TableField(exist = false)
+    private Integer remainStock;
+
+    /** 图片 */
+    @TableField(exist = false)
+    private String image;
+
+    /** 活动状态(非数据库字段): not_started/ongoing/sold_out/ended */
+    @TableField(exist = false)
+    private String activityStatus;
+
+    /** 倒计时秒数(非数据库字段) */
+    @TableField(exist = false)
+    private Long countdown;
+
+    /** 是否正在进行中(非数据库字段,前端控制只读) */
+    @TableField(exist = false)
+    private Boolean isOngoing;
+
+    /** 创建者 */
+    @TableField(fill = FieldFill.INSERT)
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /** 更新者 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /** 备注 */
+    private String remark;
+}

+ 128 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductDiscount.java

@@ -0,0 +1,128 @@
+package com.fs.hisStore.domain;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+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;
+
+/**
+ * 限时折扣商品对象 fs_store_product_discount
+ *
+ * @author fs
+ * @date 2026-04-03
+ */
+@Data
+public class FsStoreProductDiscount implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 关联商品ID(fs_store_product_scrm.product_id) */
+    @Excel(name = "商品ID")
+    private Long productId;
+
+    /** 折扣库存 */
+    @Excel(name = "折扣库存")
+    private Long stock;
+
+    /** 折扣开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "折扣开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 折扣结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "折扣结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 原价 */
+    @Excel(name = "原价")
+    private BigDecimal originalPrice;
+
+    /** 折扣(如0.8表示8折) */
+    @Excel(name = "折扣")
+    private BigDecimal discount;
+
+    /** 折扣支付价格 */
+    @Excel(name = "折扣价格")
+    private BigDecimal discountPrice;
+
+    /** 状态(0:下架,1:上架) */
+    @Excel(name = "状态", readConverterExp = "0=下架,1=上架")
+    private Integer status;
+
+    /** 删除标志(0:正常,1:删除) */
+    private Integer delFlag;
+
+    /** 商品名称(关联查询) */
+    @Excel(name = "商品名称")
+    private String productName;
+
+    /** 商品图片(关联查询) */
+    private String productImage;
+
+    /** 商品售价(关联查询) */
+    private BigDecimal price;
+
+    /** 商品原价(关联查询) */
+    private BigDecimal otPrice;
+
+    /** 商品销量(关联查询) */
+    private Integer sales;
+
+    /** 商品库存(关联查询) */
+    private Integer productStock;
+
+    /** 商品分类名称(关联查询) */
+    private String cateName;
+
+    /** 商品编号(关联查询) */
+    private String barCode;
+
+    /** 商品简介(关联查询) */
+    private String productInfo;
+
+    /** 轮播图(关联查询) */
+    private String sliderImage;
+
+    /** Redis实时库存(非数据库字段) */
+    @TableField(exist = false)
+    private Integer remainStock;
+
+    /** 活动状态(非数据库字段): not_started/ongoing/sold_out/ended */
+    @TableField(exist = false)
+    private String activityStatus;
+
+    /** 倒计时秒数(非数据库字段) */
+    @TableField(exist = false)
+    private Long countdown;
+
+    /** 创建者 */
+    @TableField(fill = FieldFill.INSERT)
+    private String createBy;
+
+    /** 创建时间 */
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /** 更新者 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /** 备注 */
+    private String remark;
+}

+ 123 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductFlashSale.java

@@ -0,0 +1,123 @@
+package com.fs.hisStore.domain;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+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;
+
+/**
+ * 秒杀商品对象 fs_store_product_flash_sale
+ *
+ * @author fs
+ * @date 2026-04-08
+ */
+@Data
+public class FsStoreProductFlashSale implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 关联商品ID(fs_store_product_scrm.product_id) */
+    @Excel(name = "商品ID")
+    private Long productId;
+
+    /** 原价 */
+    @Excel(name = "原价")
+    private BigDecimal originalPrice;
+
+    /** 秒杀价 */
+    @Excel(name = "秒杀价")
+    private BigDecimal flashPrice;
+
+    /** 秒杀库存 */
+    @Excel(name = "库存")
+    private Long stock;
+
+    /** 秒杀开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 秒杀结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 状态(0:下架,1:上架) */
+    @Excel(name = "状态", readConverterExp = "0=下架,1=上架")
+    private Integer status;
+
+    /** 删除标志(0:正常,1:删除) */
+    private Integer delFlag;
+
+    /** 商品名称(关联查询) */
+    @Excel(name = "商品名称")
+    private String productName;
+
+    /** 商品图片(关联查询) */
+    private String productImage;
+
+    /** 商品售价(关联查询) */
+    private BigDecimal price;
+
+    /** 商品原价(关联查询) */
+    private BigDecimal otPrice;
+
+    /** 商品销量(关联查询) */
+    private Integer sales;
+
+    /** 商品库存(关联查询) */
+    private Integer productStock;
+
+    /** 商品分类名称(关联查询) */
+    private String cateName;
+
+    /** 商品编号(关联查询) */
+    private String barCode;
+
+    /** 商品简介(关联查询) */
+    private String productInfo;
+
+    /** 轮播图(关联查询) */
+    private String sliderImage;
+
+    /** Redis实时库存(非数据库字段) */
+    @TableField(exist = false)
+    private Integer remainStock;
+
+    /** 活动状态(非数据库字段): not_started/ongoing/sold_out/ended */
+    @TableField(exist = false)
+    private String activityStatus;
+
+    /** 倒计时秒数(非数据库字段) */
+    @TableField(exist = false)
+    private Long countdown;
+
+    /** 创建者 */
+    @TableField(fill = FieldFill.INSERT)
+    private String createBy;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /** 更新者 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateBy;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /** 备注 */
+    private String remark;
+}

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

@@ -354,6 +354,18 @@ public class FsStoreProductScrm extends BaseEntity
     @Excel(name = "单次购买数量")
     private Integer singlePurchaseLimit;
 
+    /** 活动类型:0=无 6=秒杀 7=限时折扣 */
+    @Excel(name = "活动类型", readConverterExp = "0=无,6=秒杀,7=限时折扣")
+    private Integer activityType;
+
+    /** 活动开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date activityStartTime;
+
+    /** 活动结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date activityEndTime;
+
     @TableField(exist = false)
     private String onShelfTime;
 

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

@@ -1526,4 +1526,12 @@ public interface FsStoreOrderScrmMapper
 
 
     List<FsMyStoreOrderListQueryVO> selectFsStoreOrderListBySidebarVO(@Param("maps") FsStoreOrderScrmSidebarVO param);
+
+    /**
+     * 查询超时未支付的SCRM订单(用于定时任务自动取消)
+     * @param unPayTime 超时时间(分钟)
+     * @return 超时未支付订单列表
+     */
+    @Select("SELECT * FROM fs_store_order_scrm WHERE status = 0 AND paid = 0 AND create_time < DATE_SUB(NOW(), INTERVAL #{unPayTime} MINUTE)")
+    List<FsStoreOrderScrm> selectUnpayTimeoutOrderList(@Param("unPayTime") Integer unPayTime);
 }

+ 131 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductActivityMapper.java

@@ -0,0 +1,131 @@
+package com.fs.hisStore.mapper;
+
+import java.util.List;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 商品活动中间表Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-16
+ */
+public interface FsStoreProductActivityMapper {
+
+    /**
+     * 查询活动记录
+     */
+    public FsStoreProductActivity selectFsStoreProductActivityById(Long id);
+
+    /**
+     * 查询活动列表
+     */
+    public List<FsStoreProductActivity> selectFsStoreProductActivityList(FsStoreProductActivity query);
+
+    /**
+     * 根据商品ID查询活动记录(未删除)
+     */
+    public List<FsStoreProductActivity> selectByProductId(@Param("productId") Long productId);
+
+    /**
+     * 根据商品ID和规格ID查询
+     */
+    public FsStoreProductActivity selectByProductIdAndSpecId(@Param("productId") Long productId, @Param("specId") Long specId);
+
+    /**
+     * 查询即将开始的活动(1小时内开始+未过期)
+     */
+    public List<FsStoreProductActivity> selectUpcomingList(@Param("activityType") Integer activityType);
+
+    /**
+     * 查询已过期的活动(status=1且end_time<now)
+     */
+    public List<FsStoreProductActivity> selectExpiredList();
+
+    /**
+     * 查询即将开始的活动(status=0且start_time-1h<=now且end_time>now且stock>0)
+     */
+    public List<FsStoreProductActivity> selectStartingList();
+
+    /**
+     * 新增活动记录
+     */
+    public int insertFsStoreProductActivity(FsStoreProductActivity activity);
+
+    /**
+     * 批量新增活动记录
+     */
+    public int batchInsertActivity(@Param("list") List<FsStoreProductActivity> list);
+
+    /**
+     * 修改活动记录
+     */
+    public int updateFsStoreProductActivity(FsStoreProductActivity activity);
+
+    /**
+     * 逻辑删除活动记录
+     */
+    public int deleteFsStoreProductActivityById(Long id);
+
+    /**
+     * 根据商品ID逻辑删除所有活动记录
+     */
+    public int deleteByProductId(@Param("productId") Long productId);
+
+    /**
+     * 更新状态
+     */
+    public int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+
+    /**
+     * 查询商品正在进行中的活动(校验用)
+     */
+    public FsStoreProductActivity selectOngoingActivity(@Param("productId") Long productId);
+
+    /**
+     * 更新商品表activity_type及活动时间
+     */
+    public int updateProductActivityType(@Param("productId") Long productId, @Param("activityType") Integer activityType,
+                                         @Param("activityStartTime") java.util.Date activityStartTime,
+                                         @Param("activityEndTime") java.util.Date activityEndTime);
+
+    // ===== 小程序端查询方法 =====
+
+    /**
+     * 查询秒杀活动列表(1小时内即将开抢+未过期)
+     */
+    List<FsStoreProductActivity> selectUpcomingFlashSaleActivityList();
+
+    /**
+     * 查询折扣活动列表(1小时内即将开抢+未过期)
+     */
+    List<FsStoreProductActivity> selectUpcomingDiscountActivityList();
+
+    /**
+     * 按活动ID查询详情(JOIN商品表获取完整信息)
+     */
+    FsStoreProductActivity selectActivityDetailById(@Param("id") Long id);
+
+    /**
+     * 按商品ID查询当前进行中的秒杀活动
+     */
+    List<FsStoreProductActivity> selectFlashSaleActivityByProductId(@Param("productId") Long productId);
+
+    /**
+     * 按商品ID查询当前进行中的折扣活动
+     */
+    List<FsStoreProductActivity> selectDiscountActivityByProductId(@Param("productId") Long productId);
+
+    /**
+     * 按商品ID和活动类型查询所有参与活动的规格(用于详情页返回规格数组)
+     */
+    List<FsStoreProductActivity> selectActivitySpecsByProductIdAndType(
+            @Param("productId") Long productId,
+            @Param("activityType") Integer activityType);
+    /**
+     * 按商品ID和规格ID查询活动记录
+     * @param productId 商品id
+     * @param specId 规格id
+     */
+    FsStoreProductActivity selectActivityByProductIdAndSpecId(@Param("productId") Long productId,@Param("specId") Long specId);
+}

+ 109 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductDiscountMapper.java

@@ -0,0 +1,109 @@
+package com.fs.hisStore.mapper;
+
+import java.util.List;
+
+import com.fs.hisStore.domain.FsStoreProductDiscount;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 限时折扣商品Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-03
+ */
+public interface FsStoreProductDiscountMapper
+{
+    /**
+     * 查询限时折扣商品
+     *
+     * @param id 限时折扣商品主键
+     * @return 限时折扣商品
+     */
+    public FsStoreProductDiscount selectFsStoreProductDiscountById(Long id);
+
+    /**
+     * 查询限时折扣商品列表
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 限时折扣商品集合
+     */
+    public List<FsStoreProductDiscount> selectFsStoreProductDiscountList(FsStoreProductDiscount fsStoreProductDiscount);
+
+    /**
+     * 新增限时折扣商品
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 结果
+     */
+    public int insertFsStoreProductDiscount(FsStoreProductDiscount fsStoreProductDiscount);
+
+    /**
+     * 修改限时折扣商品
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 结果
+     */
+    public int updateFsStoreProductDiscount(FsStoreProductDiscount fsStoreProductDiscount);
+
+    /**
+     * 删除限时折扣商品
+     *
+     * @param id 限时折扣商品主键
+     * @return 结果
+     */
+    public int deleteFsStoreProductDiscountById(Long id);
+
+    /**
+     * 批量删除限时折扣商品
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteFsStoreProductDiscountByIds(Long[] ids);
+
+    /**
+     * 查询当前有效的限时折扣商品列表(小程序端使用)
+     *
+     * @return 限时折扣商品集合
+     */
+    public List<FsStoreProductDiscount> selectActiveDiscountList();
+
+    /**
+     * 根据商品ID查询限时折扣信息
+     *
+     * @param productId 商品ID
+     * @return 限时折扣商品
+     */
+    public FsStoreProductDiscount selectDiscountByProductId(@Param("productId") Long productId);
+
+    public List<FsStoreProductDiscount> selectUpcomingDiscountList();
+
+    /**
+     * 获取则扣商品信息
+     * @param ids 折扣id
+     * @return list
+     * **/
+    public List<FsStoreProductDiscount> getProductDiscountInfoByIds(@Param("ids") List<Long> ids);
+
+    int batchUpdateStock(@Param("list") List<FsStoreProductDiscount> list);
+
+    /**
+     * 查询已过期的折扣商品(已上架且结束时间已过)
+     */
+    List<FsStoreProductDiscount> selectExpiredDiscountList();
+
+    /**
+     * 查询即将开始的折扣商品(开始时间在当前时间之前或等于当前时间)
+     */
+    List<FsStoreProductDiscount> selectStartingDiscountList();
+
+    /**
+     * 更新折扣商品状态
+     */
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+
+    /**
+     * 原子性增加库存(退款回滚用)
+     */
+    int increaseStock(@Param("id") Long id, @Param("num") Long num);
+}

+ 109 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductFlashSaleMapper.java

@@ -0,0 +1,109 @@
+package com.fs.hisStore.mapper;
+
+import java.util.List;
+
+import com.fs.hisStore.domain.FsStoreProductFlashSale;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 秒杀商品Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-08
+ */
+public interface FsStoreProductFlashSaleMapper
+{
+    /**
+     * 查询秒杀商品
+     *
+     * @param id 秒杀商品主键
+     * @return 秒杀商品
+     */
+    public FsStoreProductFlashSale selectFsStoreProductFlashSaleById(Long id);
+
+    /**
+     * 查询秒杀商品列表
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 秒杀商品集合
+     */
+    public List<FsStoreProductFlashSale> selectFsStoreProductFlashSaleList(FsStoreProductFlashSale fsStoreProductFlashSale);
+
+    /**
+     * 新增秒杀商品
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 结果
+     */
+    public int insertFsStoreProductFlashSale(FsStoreProductFlashSale fsStoreProductFlashSale);
+
+    /**
+     * 修改秒杀商品
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 结果
+     */
+    public int updateFsStoreProductFlashSale(FsStoreProductFlashSale fsStoreProductFlashSale);
+
+    /**
+     * 删除秒杀商品
+     *
+     * @param id 秒杀商品主键
+     * @return 结果
+     */
+    public int deleteFsStoreProductFlashSaleById(Long id);
+
+    /**
+     * 批量删除秒杀商品
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteFsStoreProductFlashSaleByIds(Long[] ids);
+
+    /**
+     * 查询当前有效的秒杀商品列表(小程序端使用)
+     *
+     * @return 秒杀商品集合
+     */
+    public List<FsStoreProductFlashSale> selectActiveFlashSaleList();
+
+    /**
+     * 根据商品ID查询秒杀信息
+     *
+     * @param productId 商品ID
+     * @return 秒杀商品
+     */
+    public FsStoreProductFlashSale selectFlashSaleByProductId(@Param("productId") Long productId);
+
+    public List<FsStoreProductFlashSale> selectUpcomingFlashSaleList();
+
+    /**
+     * 获取商品信息
+     * @param ids 秒杀id
+     * @return 秒杀数据
+     */
+    public List<FsStoreProductFlashSale> getProductFlashSaleInfoByIds(@Param("ids") List<Long> ids);
+
+    int batchUpdateStock(@Param("list") List<FsStoreProductFlashSale> list);
+
+    /**
+     * 查询已过期的秒杀商品(已上架且结束时间已过)
+     */
+    List<FsStoreProductFlashSale> selectExpiredFlashSaleList();
+
+    /**
+     * 查询即将开始的秒杀商品(开始时间在当前时间之前或等于当前时间)
+     */
+    List<FsStoreProductFlashSale> selectStartingFlashSaleList();
+
+    /**
+     * 更新秒杀商品状态
+     */
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+
+    /**
+     * 原子性增加库存(退款回滚用)
+     */
+    int increaseStock(@Param("id") Long id, @Param("num") Long num);
+}

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

@@ -256,6 +256,9 @@ public interface FsStoreProductScrmMapper
             "<if test = 'maps.newOrder != null and maps.newOrder==\"desc\" '> " +
             "and p.is_new =1 order by p.create_time desc  " +
             "</if>" +
+            "<if test = 'maps.activityType != null and maps.activityType != 0 '> " +
+            "and p.activity_type =#{maps.activityType}  " +
+            "</if>" +
             "</script>"})
     List<FsStoreProductListQueryVO> selectFsStoreProductListQuery(@Param("maps")FsStoreProductQueryParam param);
     @Select({"<script> " +

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

@@ -14,4 +14,7 @@ public class FsStoreConfirmOrderParam {
     @NotBlank(message = "购买类型不能为空")
     @ApiModelProperty(value = "buy cart")
     private String type;
+
+    @ApiModelProperty(value = "商品类型6秒杀/7折扣")
+    private Integer productType;
 }

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

@@ -65,4 +65,7 @@ public class FsStoreOrderCreateParam implements Serializable
     private Integer projectId;
     //营期ID
     private Integer periodId;
+
+    //关联Id
+    private Long associatedId;
 }

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

@@ -43,4 +43,7 @@ public class FsStoreProductQueryParam extends BaseQueryParam implements Serializ
 
     @ApiModelProperty(value = "当前的appid")
     private String appId;
+
+    @ApiModelProperty(value = "活动类型 0=普通 6=秒杀 7=限时折扣")
+    private Integer activityType;
 }

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

@@ -109,6 +109,11 @@ public interface IFsStoreOrderScrmService
 
     R createOrder(long userId, FsStoreOrderCreateParam param);
 
+    /**
+     * 创建秒杀/限时折扣活动订单(独立接口)
+     */
+    R createActivityOrder(long userId, FsStoreOrderCreateParam param);
+
     R createOrderByPrescribe(Long prescribeId);
 
     void cancelOrder(Long orderId);

+ 99 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductActivityService.java

@@ -0,0 +1,99 @@
+package com.fs.hisStore.service;
+
+import java.util.List;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+
+/**
+ * 商品活动中间表Service接口
+ *
+ * @author fs
+ * @date 2026-04-16
+ */
+public interface IFsStoreProductActivityService {
+
+    /**
+     * 查询活动记录
+     */
+    public FsStoreProductActivity selectFsStoreProductActivityById(Long id);
+
+    /**
+     * 查询活动列表
+     */
+    public List<FsStoreProductActivity> selectFsStoreProductActivityList(FsStoreProductActivity query);
+
+    /**
+     * 根据商品ID查询活动记录
+     */
+    public List<FsStoreProductActivity> selectByProductId(Long productId);
+
+    /**
+     * 保存活动设置(批量:先删旧再插新+更新商品表activity_type)
+     * @param productId 商品ID
+     * @param activityType 活动类型 0=无 6=秒杀 7=限时折扣
+     * @param activityList 规格活动列表(activityType!=0时)
+     * @return 结果
+     */
+    public int saveActivity(Long productId, Integer activityType, List<FsStoreProductActivity> activityList);
+
+    /**
+     * 查询商品正在进行中的活动
+     */
+    public FsStoreProductActivity selectOngoingActivity(Long productId);
+
+    /**
+     * 查询即将开始的活动
+     */
+    public List<FsStoreProductActivity> selectUpcomingList(Integer activityType);
+
+    /**
+     * 查询已过期的活动
+     */
+    public List<FsStoreProductActivity> selectExpiredList();
+
+    /**
+     * 查询即将开始需要上架的活动
+     */
+    public List<FsStoreProductActivity> selectStartingList();
+
+    /**
+     * 更新状态
+     */
+    public int updateStatus(Long id, Integer status);
+
+    /**
+     * 处理过期活动:同步Redis库存到DB + 重置商品activity_type=0
+     */
+    public void handleExpiredActivity(Long productId, List<FsStoreProductActivity> activityList);
+
+    // ===== 小程序端查询方法 =====
+
+    /**
+     * 查询秒杀活动列表(1小时内即将开抢+未过期)
+     */
+    List<FsStoreProductActivity> selectUpcomingFlashSaleActivityList();
+
+    /**
+     * 查询折扣活动列表(1小时内即将开抢+未过期)
+     */
+    List<FsStoreProductActivity> selectUpcomingDiscountActivityList();
+
+    /**
+     * 按活动ID查询详情(JOIN商品表获取完整信息)
+     */
+    FsStoreProductActivity selectActivityDetailById(Long id);
+
+    /**
+     * 按商品ID查询当前进行中的秒杀活动
+     */
+    List<FsStoreProductActivity> selectFlashSaleActivityByProductId(Long productId);
+
+    /**
+     * 按商品ID查询当前进行中的折扣活动
+     */
+    List<FsStoreProductActivity> selectDiscountActivityByProductId(Long productId);
+
+    /**
+     * 按商品ID和活动类型查询所有参与活动的规格(用于详情页返回规格数组)
+     */
+    List<FsStoreProductActivity> selectActivitySpecsByProductIdAndType(Long productId, Integer activityType);
+}

+ 94 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductDiscountService.java

@@ -0,0 +1,94 @@
+package com.fs.hisStore.service;
+
+import java.util.List;
+
+import com.fs.hisStore.domain.FsStoreProductDiscount;
+
+/**
+ * 限时折扣商品Service接口
+ *
+ * @author fs
+ * @date 2026-04-03
+ */
+public interface IFsStoreProductDiscountService
+{
+    /**
+     * 查询限时折扣商品
+     *
+     * @param id 限时折扣商品主键
+     * @return 限时折扣商品
+     */
+    public FsStoreProductDiscount selectFsStoreProductDiscountById(Long id);
+
+    /**
+     * 查询限时折扣商品列表
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 限时折扣商品集合
+     */
+    public List<FsStoreProductDiscount> selectFsStoreProductDiscountList(FsStoreProductDiscount fsStoreProductDiscount);
+
+    /**
+     * 新增限时折扣商品
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 结果
+     */
+    public int insertFsStoreProductDiscount(FsStoreProductDiscount fsStoreProductDiscount);
+
+    /**
+     * 修改限时折扣商品
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 结果
+     */
+    public int updateFsStoreProductDiscount(FsStoreProductDiscount fsStoreProductDiscount);
+
+    /**
+     * 批量删除限时折扣商品
+     *
+     * @param ids 需要删除的主键集合
+     * @return 结果
+     */
+    public int deleteFsStoreProductDiscountByIds(Long[] ids);
+
+    /**
+     * 删除限时折扣商品信息
+     *
+     * @param id 限时折扣商品主键
+     * @return 结果
+     */
+    public int deleteFsStoreProductDiscountById(Long id);
+
+    /**
+     * 查询当前有效的限时折扣商品列表(小程序端使用)
+     *
+     * @return 限时折扣商品集合
+     */
+    public List<FsStoreProductDiscount> selectActiveDiscountList();
+
+    /**
+     * 根据商品ID查询限时折扣信息
+     *
+     * @param productId 商品ID
+     * @return 限时折扣商品
+     */
+    public FsStoreProductDiscount selectDiscountByProductId(Long productId);
+
+    /**
+     * 查询即将开抢和未过期的折扣商品列表(小程序端使用)
+     * 展示1小时内即将开抢和当前进行中的商品
+     *
+     * @return 折扣商品集合
+     */
+    public List<FsStoreProductDiscount> selectUpcomingDiscountList();
+
+    /**
+     * 更新折扣商品库存
+     *
+     * @param id 折扣商品ID
+     * @param stock 库存数量
+     * @return 结果
+     */
+    public int updateDiscountStock(Long id, Long stock);
+}

+ 94 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductFlashSaleService.java

@@ -0,0 +1,94 @@
+package com.fs.hisStore.service;
+
+import java.util.List;
+
+import com.fs.hisStore.domain.FsStoreProductFlashSale;
+
+/**
+ * 秒杀商品Service接口
+ *
+ * @author fs
+ * @date 2026-04-08
+ */
+public interface IFsStoreProductFlashSaleService
+{
+    /**
+     * 查询秒杀商品
+     *
+     * @param id 秒杀商品主键
+     * @return 秒杀商品
+     */
+    public FsStoreProductFlashSale selectFsStoreProductFlashSaleById(Long id);
+
+    /**
+     * 查询秒杀商品列表
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 秒杀商品集合
+     */
+    public List<FsStoreProductFlashSale> selectFsStoreProductFlashSaleList(FsStoreProductFlashSale fsStoreProductFlashSale);
+
+    /**
+     * 新增秒杀商品
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 结果
+     */
+    public int insertFsStoreProductFlashSale(FsStoreProductFlashSale fsStoreProductFlashSale);
+
+    /**
+     * 修改秒杀商品
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 结果
+     */
+    public int updateFsStoreProductFlashSale(FsStoreProductFlashSale fsStoreProductFlashSale);
+
+    /**
+     * 批量删除秒杀商品
+     *
+     * @param ids 需要删除的主键集合
+     * @return 结果
+     */
+    public int deleteFsStoreProductFlashSaleByIds(Long[] ids);
+
+    /**
+     * 删除秒杀商品信息
+     *
+     * @param id 秒杀商品主键
+     * @return 结果
+     */
+    public int deleteFsStoreProductFlashSaleById(Long id);
+
+    /**
+     * 查询当前有效的秒杀商品列表(小程序端使用)
+     *
+     * @return 秒杀商品集合
+     */
+    public List<FsStoreProductFlashSale> selectActiveFlashSaleList();
+
+    /**
+     * 根据商品ID查询秒杀信息
+     *
+     * @param productId 商品ID
+     * @return 秒杀商品
+     */
+    public FsStoreProductFlashSale selectFlashSaleByProductId(Long productId);
+
+    /**
+     * 查询即将开抢和未过期的秒杀商品列表(小程序端使用)
+     * 展示1小时内即将开抢和当前进行中的商品
+     *
+     * @return 秒杀商品集合
+     */
+    public List<FsStoreProductFlashSale> selectUpcomingFlashSaleList();
+
+    /**
+     * 更新秒杀商品库存
+     *
+     * @param id 秒杀商品ID
+     * @param stock 库存数量
+     * @return 结果
+     */
+    public int updateFlashSaleStock(Long id, Long stock);
+}

+ 8 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java

@@ -36,6 +36,14 @@ public interface IFsStoreProductScrmService
      */
     public FsStoreProductScrm selectFsStoreProductById(Long productId);
 
+    /**
+     * 查询商品
+     *
+     * @param productId 商品ID
+     * @return 商品
+     */
+    public FsStoreProductScrm selectFsStoreRedisProductById(Long productId);
+
     /**
      * 查询商品列表
      *

+ 119 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/ActivityStockDataProviderImpl.java

@@ -0,0 +1,119 @@
+package com.fs.hisStore.service.impl;
+
+import com.fs.common.core.redis.service.ActivityStockData;
+import com.fs.common.core.redis.service.ActivityStockDataProvider;
+import com.fs.common.core.redis.service.ActivityStockService;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import com.fs.hisStore.mapper.FsStoreProductActivityMapper;
+import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
+import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Component
+public class ActivityStockDataProviderImpl implements ActivityStockDataProvider {
+
+    @Autowired
+    private FsStoreProductActivityMapper activityMapper;
+
+    @Autowired
+    private FsStoreProductAttrValueScrmMapper attrValueMapper;
+
+    @Autowired
+    private ActivityStockService activityStockService;
+
+    @PostConstruct
+    public void init() {
+        activityStockService.setDataProvider(this);
+        log.info("ActivityStockDataProvider 注册完成");
+    }
+
+    @Override
+    public ActivityStockData loadActivityData(Integer orderType, Long associatedId) {
+        FsStoreProductActivity activity = activityMapper.selectFsStoreProductActivityById(associatedId);
+        if (activity == null || activity.getDelFlag() == 1) {
+            return null;
+        }
+        ActivityStockData data = new ActivityStockData();
+        data.setId(activity.getId());
+        data.setProductId(activity.getProductId());
+        data.setSpecId(activity.getSpecId());
+        data.setStock(activity.getSpecStock() != null ? activity.getSpecStock().longValue() : null);
+        data.setStatus(activity.getStatus());
+        if (activity.getStartTime() != null) {
+            data.setStartTime(activity.getStartTime().getTime());
+        }
+        if (activity.getEndTime() != null) {
+            data.setEndTime(activity.getEndTime().getTime());
+        }
+        return data;
+    }
+
+    @Override
+    public void updateStockToDb(Integer orderType, Long associatedId, Long redisStock) {
+        // 活动中间表已无stock字段,库存复用规格库存,下单/退款实时同步DB,无需此操作
+        log.info("活动商品{}库存同步跳过,库存复用规格库存实时同步", associatedId);
+    }
+
+    @Override
+    public List<ActivityStockData> loadAllActiveActivities(Integer orderType) {
+        List<ActivityStockData> result = new ArrayList<>();
+        FsStoreProductActivity query = new FsStoreProductActivity();
+        query.setStatus(1);
+        if (orderType != null) {
+            query.setActivityType(orderType);
+        }
+        List<FsStoreProductActivity> list = activityMapper.selectFsStoreProductActivityList(query);
+        if (list != null) {
+            for (FsStoreProductActivity item : list) {
+                ActivityStockData data = new ActivityStockData();
+                data.setId(item.getId());
+                data.setProductId(item.getProductId());
+                data.setSpecId(item.getSpecId());
+                data.setStock(item.getSpecStock() != null ? item.getSpecStock().longValue() : null);
+                data.setStatus(item.getStatus());
+                if (item.getStartTime() != null) {
+                    data.setStartTime(item.getStartTime().getTime());
+                }
+                if (item.getEndTime() != null) {
+                    data.setEndTime(item.getEndTime().getTime());
+                }
+                result.add(data);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void loadProductSpecStock(Long specId) {
+        if (specId == null) return;
+        try {
+            FsStoreProductAttrValueScrm attrValue = attrValueMapper.selectFsStoreProductAttrValueById(specId);
+            if (attrValue != null && attrValue.getStock() != null) {
+                activityStockService.initProductSpecStock(specId, attrValue.getStock());
+            }
+        } catch (Exception e) {
+            log.error("加载商品规格{}库存到Redis失败", specId, e);
+        }
+    }
+
+    @Override
+    public void updateProductSpecStockToDb(Long specId, long redisStock) {
+        if (specId == null) return;
+        try {
+            FsStoreProductAttrValueScrm attrValue = new FsStoreProductAttrValueScrm();
+            attrValue.setId(specId);
+            attrValue.setStock((int) redisStock);
+            attrValueMapper.updateFsStoreProductAttrValue(attrValue);
+            log.info("商品规格{}库存同步到数据库,库存={}", specId, redisStock);
+        } catch (Exception e) {
+            log.error("商品规格{}库存同步到数据库失败", specId, e);
+        }
+    }
+}

+ 55 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java

@@ -222,6 +222,9 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
     @Autowired
     private FsRefundReasonMapper fsRefundReasonMapper;
 
+    @Autowired
+    private com.fs.common.core.redis.service.ActivityStockService activityStockService;
+
     /**
      * 查询售后记录
      *
@@ -908,12 +911,15 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         //获取订单下的商品
         List<FsStoreOrderItemVO> orderItemVOS=fsStoreOrderItemMapper.selectFsStoreOrderItemListByOrderId(order.getId());
 
+        // 活动订单:先回滚Redis规格库存(确保Redis和DB库存一致)
+        rollbackActivityStockIfNeeded(order, orderItemVOS);
+
         // 获取售后商品
         FsStoreAfterSalesItemScrm params = new FsStoreAfterSalesItemScrm();
         params.setStoreAfterSalesId(storeAfterSales.getId());
         List<FsStoreAfterSalesItemScrm> fsStoreAfterSalesItems = afterSalesItemService.selectFsStoreAfterSalesItemList(params);
 
-        // 退库存
+        // 退DB库存
         for (FsStoreAfterSalesItemScrm item : fsStoreAfterSalesItems) {
             FsStoreOrderItemVO itemVO = orderItemVOS.stream().filter(i -> i.getProductId().equals(item.getProductId())).findFirst().orElse(null);
             if(Objects.nonNull(itemVO) && itemVO.getIsAfterSales() == 1 && Objects.nonNull(item.getNum())){
@@ -1866,4 +1872,52 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
     public FsStoreAfterSalesScrm selectFsStoreAfterSalesByOrderCode(String orderCode) {
         return fsStoreAfterSalesMapper.selectFsStoreAfterSalesByOrderCode(orderCode);
     }
+
+    /**
+     * 判断是否为活动订单(秒杀 orderType=6 或限时折扣 orderType=7)
+     */
+    private boolean isActivityOrder(FsStoreOrderScrm order) {
+        return order != null
+                && order.getOrderType() != null
+                && (order.getOrderType() == 6 || order.getOrderType() == 7)
+                && order.getAssociatedId() != null
+                && order.getAssociatedId() > 0;
+    }
+
+    /**
+     * 活动订单退款时回滚Redis规格库存。
+     * 异常不中断退款主流程,但记录错误日志以便人工排查。
+     */
+    private void rollbackActivityStockIfNeeded(FsStoreOrderScrm order, List<FsStoreOrderItemVO> items) {
+        if (!isActivityOrder(order)) {
+            return;
+        }
+        try {
+            int totalRefundNum = 0;
+            if (items != null) {
+                for (FsStoreOrderItemVO vo : items) {
+                    if (vo.getNum() != null) {
+                        totalRefundNum += vo.getNum();
+                    }
+                }
+            }
+            if (totalRefundNum > 0) {
+                boolean success = activityStockService.rollbackStock(
+                        order.getOrderType(),
+                        order.getAssociatedId(),
+                        totalRefundNum);
+                if (success) {
+                    logger.info("售后退款Redis活动库存回滚成功,orderId={},orderType={},associatedId={},quantity={}",
+                            order.getId(), order.getOrderType(), order.getAssociatedId(), totalRefundNum);
+                } else {
+                    logger.error("售后退款Redis活动库存回滚失败!orderId={},orderType={},associatedId={},需人工处理",
+                            order.getId(), order.getOrderType(), order.getAssociatedId());
+                }
+            }
+        } catch (Exception e) {
+            logger.error("售后退款Redis活动库存回滚异常!orderId={},orderType={},associatedId={},需人工处理",
+                    order.getId(), order.getOrderType(), order.getAssociatedId(), e);
+            // 不中断退款流程
+        }
+    }
 }

+ 527 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -181,6 +181,7 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
@@ -247,6 +248,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private RedissonClient redissonClient;
 
+    @Autowired
+    private com.fs.common.core.redis.service.ActivityStockService activityStockService;
+
+    @Autowired
+    private IFsStoreProductActivityService activityService;
+
     @Autowired
     private IFsStoreCartScrmService cartService;
 
@@ -449,6 +456,15 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private FsUserCompanyPackageScrmMapper fsUserCompanyPackageScrmMapper;
 
+    @Autowired
+    private FsStoreProductDiscountMapper fsStoreProductDiscountMapper;
+
+    @Autowired
+    private FsStoreProductFlashSaleMapper fsStoreProductFlashSaleMapper;
+
+    @Autowired
+    private FsStoreProductActivityMapper activityMapper;
+
     @Value("${cloud_host.company_name}")
     private String companyName;
     @PostConstruct
@@ -784,6 +800,24 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 cartMapper.updateFsStoreCart(fsStoreCart);
             }
         }
+
+        if(cartParam.getProductType() != null && (cartParam.getProductType() == 6 || cartParam.getProductType() == 7) ){//更新金额
+            for (FsStoreCartQueryVO c : carts){
+                //获取对应商品金额
+                FsStoreProductActivity activity = activityMapper.selectActivityByProductIdAndSpecId(c.getProductId(), c.getProductAttrValueId());
+                if(activity == null){
+                    return R.error("操作失败,对应活动未找到或已过期!");
+                }
+                //更新购物车信息
+                FsStoreCartScrm cartScrm = new FsStoreCartScrm();
+                cartScrm.setId(c.getId());
+                BigDecimal price = cartParam.getProductType() == 7?activity.getDiscountPrice():activity.getFlashPrice();
+                cartScrm.setChangePrice(price);
+                c.setPrice(price);
+                c.setChangePrice(price);
+                cartMapper.updateFsStoreCart(cartScrm);
+            }
+        }
         String uuid = IdUtil.randomUUID();
         redisCache.setCacheObject("orderKey:" + uuid, cartParam.getCartIds(), 300, TimeUnit.SECONDS);
         redisCache.setCacheObject("orderCarts:" + uuid, carts, 300, TimeUnit.SECONDS);
@@ -936,6 +970,22 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         if (cartIds != null) {
             //获取购物车列表
             List<FsStoreCartQueryVO> carts = redisCache.getCacheObject("orderCarts:" + param.getOrderKey());
+
+            // 校验:购物车商品是否在活动期间,活动期间不允许走普通下单
+            if (carts != null) {
+                long now = System.currentTimeMillis();
+                for (FsStoreCartQueryVO cart : carts) {
+                    FsStoreProductScrm product = productService.selectFsStoreRedisProductById(cart.getProductId());
+                    if (product != null && product.getActivityType() != null && product.getActivityType() != 0
+                            && product.getActivityStartTime() != null && product.getActivityEndTime() != null) {
+                        if (now >= product.getActivityStartTime().getTime() && now <= product.getActivityEndTime().getTime()) {
+                            String typeName = product.getActivityType() == 6 ? "秒杀" : "限时折扣";
+                            return R.error("商品【" + product.getProductName() + "】正在" + typeName + "活动中,请从活动专区购买");
+                        }
+                    }
+                }
+            }
+
             //获取地址
             FsUserAddressScrm address = userAddressMapper.selectFsUserAddressById(param.getAddressId());
             //生成分布式唯一值
@@ -1239,6 +1289,422 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
     }
 
+    /**
+     * 创建秒杀/限时折扣活动订单(独立接口,流程与createOrder一致)
+     */
+    @Override
+    @Transactional
+    public R createActivityOrder(long userId, FsStoreOrderCreateParam param) {
+        Long associatedId = null;
+        Integer orderType = param.getOrderType();
+    
+        // 1. 活动参数校验
+        if (orderType == null || (orderType != 6 && orderType != 7)) {
+            return R.error("无效的活动类型");
+        }
+        if (param.getAssociatedId() == null || param.getAssociatedId() <= 0) {
+            return R.error("活动ID不能为空");
+        }
+    
+        // 2. 校验活动时间和状态
+        com.fs.common.core.redis.service.ActivityValidateResult validateResult =
+                activityStockService.validateActivityWithDetail(orderType, param.getAssociatedId());
+        if (!validateResult.isValid()) {
+            return R.error(validateResult.getMessage());
+        }
+    
+        // 3. 确保Redis活动信息和规格库存已初始化(活动无独立库存,用规格库存)
+        FsStoreProductActivity activityInfo = activityService.selectFsStoreProductActivityById(param.getAssociatedId());
+        if (activityInfo == null) {
+            return R.error("活动信息不存在");
+        }
+        activityStockService.initActivityInfo(
+                param.getAssociatedId(), 1,
+                activityInfo.getStartTime().getTime(), activityInfo.getEndTime().getTime(),
+                activityInfo.getProductId(), activityInfo.getSpecId(), null
+        );
+        if (activityInfo.getSpecId() != null && activityInfo.getSpecStock() != null) {
+            activityStockService.initProductSpecStock(activityInfo.getSpecId(), activityInfo.getSpecStock());
+        }
+
+        // 3.5 提前获取购物车,计算活动商品总件数,确保Lua扣减数量与DB扣减数量一致
+        String preCartIds = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
+        List<FsStoreCartQueryVO> preCarts = null;
+        int activityDeductNum = 1; // 默认扣减1件
+        if (preCartIds != null) {
+            preCarts = redisCache.getCacheObject("orderCarts:" + param.getOrderKey());
+            if (preCarts != null) {
+                activityDeductNum = 0;
+                for (FsStoreCartQueryVO cart : preCarts) {
+                    if (cart.getCartNum() != null) {
+                        activityDeductNum += cart.getCartNum();
+                    }
+                }
+                if (activityDeductNum <= 0) {
+                    activityDeductNum = 1;
+                }
+            }
+        }
+
+        // 4. Lua原子扣减活动库存(已移除 getStock() 预检查——先查再扣存在竞态窗口,Lua脚本本身会原子判断库存是否充足)
+        boolean deductSuccess = activityStockService.deductStock(orderType, param.getAssociatedId(), activityDeductNum);
+        if (!deductSuccess) {
+            return R.error("活动商品已售罄,请稍后重试");
+        }
+        associatedId = param.getAssociatedId();
+
+        try {
+            FsUserCompanyUser fsUserCompanyUser = fsUserCompanyUserMapper.selectFsUserCompanyUserByUserId(userId);
+            if (ObjectUtil.isNotEmpty(fsUserCompanyUser) && param.getVideoId()!=null && param.getCompanyId() == null){
+                param.setCompanyId(fsUserCompanyUser.getCompanyId());
+                param.setCompanyUserId(fsUserCompanyUser.getCompanyUserId());
+            }
+    
+            if (!CloudHostUtils.hasCloudHostName("鹤颜堂")){
+                if (ObjectUtil.isEmpty(param.getAddressId())){
+                    return R.error("地址不能为空!");
+                }
+            }
+    
+            FsStoreOrderComputedParam computedParam = new FsStoreOrderComputedParam();
+            BeanUtils.copyProperties(param, computedParam);
+            //计算金额
+            FsStoreOrderComputeDTO dto;
+            try {
+                dto = this.computedOrder(userId, computedParam);
+            } catch (ServiceException e) {
+                if ("偏远地区暂不可购买".equals(e.getMessage())) {
+                    return R.error("偏远地区暂不可购买");
+                }
+                throw e;
+            }
+            String cartIds = redisCache.getCacheObject("orderKey:" + param.getOrderKey());
+            Integer payType = redisCache.getCacheObject("createOrderPayType:" + param.getCreateOrderKey());
+            if (payType != null) {
+                param.setPayType(payType.toString());
+            }
+            BigDecimal integral = BigDecimal.ZERO;
+            if (cartIds != null) {
+                //获取购物车列表
+                List<FsStoreCartQueryVO> carts = redisCache.getCacheObject("orderCarts:" + param.getOrderKey());
+                //获取地址
+                FsUserAddressScrm address = userAddressMapper.selectFsUserAddressById(param.getAddressId());
+                //生成分布式唯一值
+                String orderSn = SnowflakeUtil.nextIdStr();
+                //是否使用积分
+                Boolean isIntegral = false;
+                //组合数据
+                FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+                storeOrder.setStoreHouseCode("CK01");
+                if(param.getCompanyId()!=null){
+                    storeOrder.setCompanyId(param.getCompanyId());
+                }
+                if(param.getCompanyUserId()!=null){
+                    storeOrder.setCompanyUserId(param.getCompanyUserId());
+                }
+                if ("北京卓美".equals(companyName) && param.getVideoId()!=null){
+                    storeOrder.setVideoId(param.getVideoId());
+                    storeOrder.setCourseId(param.getCourseId());
+                    storeOrder.setPeriodId(param.getPeriodId());
+                    storeOrder.setProjectId(param.getProjectId());
+                }
+                String json = configService.selectConfigByKey("store.config");
+                StoreConfig config= JSONUtil.toBean(json, StoreConfig.class);
+    
+                //绑定销售
+                if("北京卓美".equals(companyName) && param.getVideoId()!=null && storeOrder.getCompanyId() == null || !("北京卓美").equals(companyName)){
+                    FsUserScrm fsuser= userService.selectFsUserById(userId);
+                    if(ObjectUtil.isEmpty(config.getOrderAttribution())
+                            ||!config.getOrderAttribution().equals(1)){
+                        if(param.getCompanyUserId()!=null){
+                            if (ObjectUtil.isNotEmpty(fsuser.getCompanyUserId())&&!fsuser.getCompanyUserId().equals(param.getCompanyUserId())){
+                                CompanyUser companyUser=companyUserService.selectCompanyUserById(fsuser.getCompanyUserId());
+                                return R.error(String.format("请联系【%s】销售进行购买商品!",companyUser.getUserName()));
+                            }else {
+                                fsuser.setCompanyUserId(param.getCompanyUserId());
+                                userService.updateFsUser(fsuser);
+                            }
+                            CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+                            if(companyUser!=null){
+                                storeOrder.setDeptId(companyUser.getDeptId());
+                            }
+                        }else {
+                            storeOrder.setCompanyUserId(fsuser.getCompanyUserId());
+                        }
+                    }
+                }
+    
+                CompanyUserUser map=new CompanyUserUser();
+                map.setCompanyUserId(param.getCompanyUserId());
+                map.setUserId(userId);
+                List<CompanyUserUser> list= companyUserUserMapper.selectCompanyUserUserList(map);
+                if(list==null||list.size()==0){
+                    CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+                    if(companyUser!=null&&companyUser.getStatus().equals("0")){
+                        map.setCompanyId(companyUser.getCompanyId());
+                        companyUserUserMapper.insertCompanyUserUser(map);
+                    }
+                }
+    
+                storeOrder.setUserId(userId);
+                storeOrder.setOrderCode(orderSn);
+                if (ObjectUtil.isNotEmpty(address)){
+                    storeOrder.setRealName(address.getRealName());
+                    storeOrder.setUserPhone(address.getPhone());
+                    storeOrder.setUserAddress(address.getProvince() + " " + address.getCity() +
+                            " " + address.getDistrict() + " " + address.getDetail().trim());
+                }
+                storeOrder.setCartId(cartIds);
+                storeOrder.setTotalNum(Long.parseLong(String.valueOf(carts.size())));
+                storeOrder.setTotalPrice(dto.getTotalPrice());
+                storeOrder.setTotalPostage(dto.getPayPostage());
+    
+                //优惠券处理
+                if (param.getCouponUserId() != null) {
+                    FsStoreCouponUserScrm couponUser = couponUserService.selectFsStoreCouponUserById(param.getCouponUserId());
+                    if (couponUser != null && couponUser.getStatus() == 0) {
+                        storeOrder.setCouponId(couponUser.getId());
+                        storeOrder.setCouponPrice(couponUser.getCouponPrice());
+                        couponUser.setStatus(1);
+                        couponUser.setUseTime(new Date());
+                        couponUserService.updateFsStoreCouponUser(couponUser);
+                    }
+                }
+                //处理推荐人
+                FsUserScrm user = userService.selectFsUserById(storeOrder.getUserId());
+                if (user.getSpreadUserId() != null && user.getSpreadUserId() > 0) {
+                    storeOrder.setTuiUserId(user.getSpreadUserId());
+                } else {
+                    if (param.getTuiUserId() != null) {
+                        FsUserScrm tuiUser = userService.selectFsUserById(param.getTuiUserId());
+                        if (tuiUser != null && tuiUser.getIsPromoter() == 1) {
+                            storeOrder.setTuiUserId(param.getTuiUserId());
+                        }
+                    }
+                }
+                storeOrder.setPayPostage(dto.getPayPostage());
+                storeOrder.setDeductionPrice(dto.getDeductionPrice());
+                storeOrder.setPaid(0);
+                storeOrder.setPayType(param.getPayType());
+                if (isIntegral) {
+                    storeOrder.setPayIntegral(integral);
+                }
+                storeOrder.setUseIntegral(BigDecimal.valueOf(dto.getUsedIntegral()));
+                storeOrder.setBackIntegral(BigDecimal.ZERO);
+                storeOrder.setGainIntegral(BigDecimal.ZERO);
+                storeOrder.setMark(param.getMark());
+                //获取成本价
+                BigDecimal costPrice = this.getOrderSumPrice(carts, "costPrice");
+                storeOrder.setCost(costPrice);
+                storeOrder.setIsChannel(1);
+                storeOrder.setShippingType(1);
+                storeOrder.setCreateTime(new Date());
+    
+                if (config.getServiceFee() != null) {
+                    storeOrder.setServiceFee(config.getServiceFee());
+                }
+    
+                //后台制单处理
+                if (param.getPayPrice() != null && param.getPayPrice().compareTo(BigDecimal.ZERO) > 0) {
+                    if (param.getPayPrice().compareTo(dto.getTotalPrice()) > 0) {
+                        return R.error("改价价格不能大于商品总价");
+                    }
+                    storeOrder.setPayPrice(param.getPayPrice());
+                } else {
+                    storeOrder.setPayPrice(dto.getPayPrice());
+                }
+    
+                //付款方式
+                if (param.getPayType().equals("1") || param.getPayType().equals("99")) {
+                    storeOrder.setStatus(0);
+                    if("广州郑多燕".equals(cloudHostProper.getCompanyName())){
+                        BigDecimal amount = redisCache.getCacheObject("createOrderAmount:" + param.getCreateOrderKey());
+                        storeOrder.setPayDelivery(amount != null ? amount : BigDecimal.ZERO);
+                    }
+                }
+                else if (param.getPayType().equals("2")) {
+                    storeOrder.setStatus(1);
+                }
+                else if (param.getPayType().equals("3")) {
+                    BigDecimal amount = param.getAmount();
+                    if (amount != null && amount.compareTo(BigDecimal.ZERO) > 0) {
+                        storeOrder.setStatus(0);
+                        storeOrder.setPayMoney(amount);
+                        storeOrder.setPayDelivery(storeOrder.getPayPrice().subtract(amount));
+                    } else {
+                        storeOrder.setStatus(1);
+                    }
+                }
+                Boolean isPay = true;
+                if (param.getOrderCreateType() != null && param.getOrderCreateType() == 3 && param.getCompanyId() != null) {
+                    Company company = companyMapper.selectCompanyById(param.getCompanyId());
+                    if (company != null) {
+                        if (company.getIsPay() != null && company.getIsPay() == 0 && param.getIsUserApp()) {
+                            storeOrder.setStatus(1);
+                            isPay = false;
+                        }
+                    }
+                }
+                storeOrder.setOrderCreateType(param.getOrderCreateType());
+                Long prescribe = carts.stream().filter(item -> item.getProductType() != null && item.getProductType() == 2).count();
+                if (prescribe > 0) {
+                    storeOrder.setIsPrescribe(1);
+                } else {
+                    storeOrder.setIsPrescribe(0);
+                }
+                if (storeOrder.getCustomerId() == null) {
+                    storeOrder.setCustomerId(param.getCustomerId());
+                }
+                FsStoreOrderScrm tempOrder = redisCache.getCacheObject("orderInfo:" + param.getCreateOrderKey());
+                if (tempOrder != null) {
+                    storeOrder.setOrderType(tempOrder.getOrderType());
+                    storeOrder.setOrderMedium(tempOrder.getOrderMedium());
+                    redisCache.deleteObject("orderInfo:" + param.getCreateOrderKey());
+                } else {
+                    storeOrder.setOrderType(param.getOrderType());
+                    storeOrder.setOrderMedium(param.getOrderMedium());
+                }
+    
+                //活动商品只扣Redis(已在上文扣减),DB延后扣减
+                // storeOrder.setAssociatedId
+                storeOrder.setAssociatedId(associatedId);
+                Integer flag = fsStoreOrderMapper.insertFsStoreOrder(storeOrder);
+                if (flag == 0) {
+                    return R.error("订单创建失败");
+                }
+    
+                // 活动商品:订单创建成功后,同步扣减DB中的商品规格库存和销量(与普通下单一致)
+                this.deStockIncSale(carts);
+    
+                if (!isPay && storeOrder.getCompanyId() != null) {
+                    addOrderAudit(storeOrder);
+                }
+                //使用了积分扣积分
+                if (dto.getUsedIntegral() > 0) {
+                    this.decIntegral(userId, dto.getUsedIntegral(), dto.getDeductionPrice(), storeOrder.getId().toString());
+                }
+    
+                //保存OrderItem
+                List<FsStoreOrderItemScrm> listOrderItem = new ArrayList<>();
+                for (FsStoreCartQueryVO vo : carts) {
+                    checkAndRecordPurchaseLimit(userId, vo.getProductId(), vo.getCartNum());
+                    FsStoreCartDTO fsStoreCartDTO = new FsStoreCartDTO();
+                    fsStoreCartDTO.setProductId(vo.getProductId());
+                    fsStoreCartDTO.setPrice(vo.getPrice());
+                    fsStoreCartDTO.setSku(vo.getProductAttrName());
+                    fsStoreCartDTO.setProductName(vo.getProductName());
+                    fsStoreCartDTO.setNum(vo.getCartNum());
+                    fsStoreCartDTO.setBarCode(vo.getBarCode());
+                    fsStoreCartDTO.setGroupBarCode(vo.getGroupBarCode());
+                    fsStoreCartDTO.setBrokerage(vo.getBrokerage());
+                    fsStoreCartDTO.setBrokerageTwo(vo.getBrokerageTwo());
+                    fsStoreCartDTO.setBrokerageThree(vo.getBrokerageThree());
+                    if (StringUtils.isEmpty(vo.getProductAttrImage())) {
+                        fsStoreCartDTO.setImage(vo.getProductImage());
+                    } else {
+                        fsStoreCartDTO.setImage(vo.getProductAttrImage());
+                    }
+                    FsStoreOrderItemScrm item = new FsStoreOrderItemScrm();
+                    item.setOrderId(storeOrder.getId());
+                    item.setOrderCode(orderSn);
+                    item.setCartId(vo.getId());
+                    item.setProductId(vo.getProductId());
+                    item.setJsonInfo(JSONUtil.toJsonStr(fsStoreCartDTO));
+                    item.setNum(vo.getCartNum());
+                    item.setIsAfterSales(0);
+                    if (vo.getProductType().equals(2)) {
+                        item.setIsPrescribe(1);
+                    }
+                    fsStoreOrderItemMapper.insertFsStoreOrderItem(item);
+                    listOrderItem.add(item);
+                }
+                if (listOrderItem.size() > 0) {
+                    String itemJson = JSONUtil.toJsonStr(listOrderItem);
+                    storeOrder.setItemJson(itemJson);
+                    fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
+                }
+                //购物车状态修改
+                cartMapper.updateIsPay(cartIds);
+    
+                //删除缓存
+                redisCache.deleteObject("orderKey:" + param.getOrderKey());
+                if (config.getIsBrushOrders() == null || !(config.getIsBrushOrders() && param.getCompanyUserId() != null)) {
+                    redisCache.deleteObject("orderCarts:" + param.getOrderKey());
+                }
+    
+                //添加记录
+                orderStatusService.create(storeOrder.getId(), OrderLogEnum.CREATE_ORDER.getValue(),
+                        OrderLogEnum.CREATE_ORDER.getDesc());
+    
+                //加入redis,24小时自动取消
+                String redisKey = String.valueOf(StrUtil.format("{}{}",
+                        StoreConstants.REDIS_ORDER_OUTTIME_UNPAY, storeOrder.getId()));
+                if (config.getUnPayTime() != null && config.getUnPayTime() > 0) {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), config.getUnPayTime(), TimeUnit.MINUTES);
+                } else {
+                    redisCache.setCacheObject(redisKey, storeOrder.getId(), 30, TimeUnit.MINUTES);
+                }
+                //添加支付到期时间
+                Calendar calendar = Calendar.getInstance();
+                calendar.setTime(storeOrder.getCreateTime());
+                if (config.getUnPayTime() != null) {
+                    calendar.add(Calendar.MINUTE, config.getUnPayTime());
+                }
+                SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                String payLimitTime = format.format(calendar.getTime());
+                redisCache.setCacheObject("orderAmount:" + storeOrder.getId(), storeOrder.getPayMoney(), 24, TimeUnit.HOURS);
+                //删除推荐订单KEY
+                String createOrderKey = param.getCreateOrderKey();
+                if("鸿森堂".equals(cloudHostProper.getCompanyName())){
+                    BigDecimal amount = redisCache.getCacheObject("createOrderAmount:" + createOrderKey);
+                    redisCache.setCacheObject("orderAmount:" + storeOrder.getId(), amount, 24, TimeUnit.HOURS);
+                }else {
+                    if (StringUtils.isNotEmpty(createOrderKey)) {
+                        if (config.getIsBrushOrders() == null || !(config.getIsBrushOrders() && param.getCompanyUserId() != null)) {
+                            redisCache.deleteObject("createOrderKey:" + createOrderKey);
+                            redisCache.deleteObject("orderCarts:" + createOrderKey);
+                            redisCache.deleteObject("createOrderMoney:" + createOrderKey);
+                        }
+                        BigDecimal amount = redisCache.getCacheObject("createOrderAmount:" + createOrderKey);
+                        redisCache.deleteObject("createOrderAmount:" + createOrderKey);
+                    }
+                }
+                // 订单创建成功后,异步同步活动中间表库存到DB(最终一致性兜底)
+                final Long finalAssociatedId = param.getAssociatedId();
+                final Integer finalOrderType = orderType;
+                CompletableFuture.runAsync(() -> {
+                    try {
+                        if (finalOrderType == 6) {
+                            activityStockService.syncFlashSaleStockToDb(finalAssociatedId,
+                                    (long) activityStockService.getStock(finalOrderType, finalAssociatedId));
+                        } else if (finalOrderType == 7) {
+                            activityStockService.syncDiscountStockToDb(finalAssociatedId,
+                                    (long) activityStockService.getStock(finalOrderType, finalAssociatedId));
+                        }
+                    } catch (Exception e) {
+                        log.error("活动中间表库存异步同步失败,associatedId={}, orderType={}", finalAssociatedId, finalOrderType, e);
+                    }
+                });
+
+                return R.ok().put("order", storeOrder).put("payLimitTime", payLimitTime);
+            } else {
+                return R.error("订单已过期");
+            }
+        } catch (Exception e) {
+            // Redis已扣减成功,订单创建过程异常,必须回滚Redis库存
+            try {
+                activityStockService.rollbackStock(orderType, param.getAssociatedId(), activityDeductNum);
+                log.info("订单创建异常,Redis库存已回滚,associatedId={}, orderType={}, 回滚数量={}",
+                        param.getAssociatedId(), orderType, activityDeductNum);
+            } catch (Exception rollbackEx) {
+                log.error("订单创建异常后Redis库存回滚失败!associatedId={},orderType={},需要人工处理",
+                        param.getAssociatedId(), orderType, rollbackEx);
+            }
+            log.error("活动订单创建异常,orderType={}, associatedId={}", orderType, param.getAssociatedId(), e);
+            throw e;
+        }
+    }
+
     @Override
     public R createOrderByPrescribe(Long prescribeId) {
         FsPrescribe prescribe = prescribeMapper.selectFsPrescribeByPrescribeId(prescribeId);
@@ -1359,6 +1825,14 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
     }
 
+    /**
+     * @deprecated 已改用deStockIncSale统一扣减
+     */
+    @Deprecated
+    private void decActivityProductStock(List<FsStoreCartQueryVO> cartInfo) {
+        deStockIncSale(cartInfo);
+    }
+
     //未支付取消订单
     @Override
     public void cancelOrder(Long orderId) {
@@ -2649,6 +3123,12 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         //退库存
         //获取订单下的商品
         List<FsStoreOrderItemVO> orderItemVOS = fsStoreOrderItemMapper.selectFsStoreOrderItemListByOrderId(order.getId());
+
+        // 活动订单:先回滚Redis规格库存
+        if (isActivityOrder(order)) {
+            refundActivityStock(order, orderItemVOS);
+        }
+
         for (FsStoreOrderItemVO vo : orderItemVOS) {
             if (vo.getIsAfterSales() == 1) {
                 productService.incProductStock(vo.getNum(), vo.getProductId(), vo.getProductAttrValueId());
@@ -3467,6 +3947,13 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     private void refundStock(FsStoreOrderScrm order) {
         //获取订单下的商品
         List<FsStoreOrderItemVO> orderItemVOS = fsStoreOrderItemMapper.selectFsStoreOrderItemListByOrderId(order.getId());
+
+        // 活动订单:先回滚Redis规格库存
+        if (isActivityOrder(order)) {
+            refundActivityStock(order, orderItemVOS);
+        }
+
+        // DB规格库存回滚
         for (FsStoreOrderItemVO vo : orderItemVOS) {
             productService.incProductStock(vo.getNum(), vo.getProductId()
                     , vo.getProductAttrValueId());
@@ -3474,6 +3961,46 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
     }
 
+    /**
+     * 判断是否为活动订单(秒杀 orderType=6 或限时折扣 orderType=7)
+     */
+    private boolean isActivityOrder(FsStoreOrderScrm order) {
+        return order != null
+                && order.getOrderType() != null
+                && (order.getOrderType() == 6 || order.getOrderType() == 7)
+                && order.getAssociatedId() != null
+                && order.getAssociatedId() > 0;
+    }
+
+    /**
+     * 活动订单退款时回滚Redis规格库存(Lua原子操作)。
+     * 异常不中断退款主流程,但记录错误日志以便人工排查。
+     */
+    private void refundActivityStock(FsStoreOrderScrm order, List<FsStoreOrderItemVO> items) {
+        try {
+            int totalRefundNum = 0;
+            if (items != null) {
+                for (FsStoreOrderItemVO vo : items) {
+                    if (vo.getNum() != null) {
+                        totalRefundNum += vo.getNum();
+                    }
+                }
+            }
+            if (totalRefundNum > 0) {
+                activityStockService.rollbackStock(
+                        order.getOrderType(),
+                        order.getAssociatedId(),
+                        totalRefundNum);
+                log.info("活动订单退款Redis库存回滚成功,orderId={},orderType={},associatedId={},quantity={}",
+                        order.getId(), order.getOrderType(), order.getAssociatedId(), totalRefundNum);
+            }
+        } catch (Exception e) {
+            log.error("活动订单退款Redis库存回滚失败!orderId={},orderType={},associatedId={},需人工处理",
+                    order.getId(), order.getOrderType(), order.getAssociatedId(), e);
+            // 不中断退款流程
+        }
+    }
+
 
     /**
      * 获取订单价格
@@ -6434,7 +6961,6 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         FsStoreOrderPriceDTO priceGroup = this.getOrderPriceGroup(carts, userAddress);
         BigDecimal payPostage = priceGroup.getStorePostage();
         BigDecimal badCode = BigDecimal.valueOf(-1);
-        // 检查运费计算结果,如果是 -1 表示偏远地区不可购买
         if (payPostage.compareTo(badCode) == 0) {
             throw new ServiceException("偏远地区暂不可购买");
         }

+ 244 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductActivityServiceImpl.java

@@ -0,0 +1,244 @@
+package com.fs.hisStore.service.impl;
+
+import java.util.List;
+
+import com.fs.common.core.redis.RedisCache;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import com.fs.hisStore.mapper.FsStoreProductActivityMapper;
+import com.fs.hisStore.service.IFsStoreProductActivityService;
+import com.fs.common.core.redis.service.ActivityStockService;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 商品活动中间表Service实现
+ *
+ * @author fs
+ * @date 2026-04-16
+ */
+@Slf4j
+@Service
+public class FsStoreProductActivityServiceImpl implements IFsStoreProductActivityService {
+
+    @Autowired
+    private FsStoreProductActivityMapper activityMapper;
+
+    @Autowired
+    private ActivityStockService activityStockService;
+
+    @Autowired
+    public RedisCache redisCache;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    private static final String EXPIRED_LOCK_PREFIX = "lock:activity:expired:";
+
+
+    /** 统一活动信息key前缀 */
+    private static final String ACTIVITY_INFO_KEY = "activity:info:";
+    /** 原商品规格库存key前缀 */
+    private static final String PRODUCT_SPEC_STOCK_KEY = "product:spec:stock:";
+
+    @Override
+    public FsStoreProductActivity selectFsStoreProductActivityById(Long id) {
+        return activityMapper.selectFsStoreProductActivityById(id);
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectFsStoreProductActivityList(FsStoreProductActivity query) {
+        return activityMapper.selectFsStoreProductActivityList(query);
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectByProductId(Long productId) {
+        return activityMapper.selectByProductId(productId);
+    }
+
+    @Override
+    @Transactional
+    public int saveActivity(Long productId, Integer activityType, List<FsStoreProductActivity> activityList) {
+        //校验:如果活动正在进行中则不允许修改
+        if (activityType != null && activityType != 0) {
+            FsStoreProductActivity ongoing = activityMapper.selectOngoingActivity(productId);
+            if (ongoing != null) {
+                throw new RuntimeException("当前商品正在" + (ongoing.getActivityType() == 6 ? "秒杀" : "限时折扣") + "活动中,活动进行期间无法修改活动设置");
+            }
+        }
+
+        //逻辑删除该商品旧的活动记录
+        activityMapper.deleteByProductId(productId);
+        int result = 0;
+        java.util.Date activityStartTime = null;
+        java.util.Date activityEndTime = null;
+        if (activityType != null && activityType != 0 && activityList != null && !activityList.isEmpty()) {
+            //批量插入新的活动记录
+            for (FsStoreProductActivity item : activityList) {
+                item.setProductId(productId);
+                item.setActivityType(activityType);
+                item.setStatus(1); // 直接设为上架,依赖时间范围过滤控制展示
+                item.setDelFlag(0);
+                // 取活动时间(所有规格共用同一时间)
+                if (item.getStartTime() != null) activityStartTime = item.getStartTime();
+                if (item.getEndTime() != null) activityEndTime = item.getEndTime();
+            }
+            result = activityMapper.batchInsertActivity(activityList);
+        }
+
+        //同步更新商品表的activity_type及活动时间
+        activityMapper.updateProductActivityType(productId, activityType != null ? activityType : 0, activityStartTime, activityEndTime);
+
+        //初始化Redis活动信息和规格库存(确保小程序接口能立即查到库存)
+        if (activityType != null && activityType != 0 && activityList != null && !activityList.isEmpty()) {
+            for (FsStoreProductActivity item : activityList) {
+                //清理对应的redis数据
+                redisCache.deleteObject(PRODUCT_SPEC_STOCK_KEY + item.getSpecId());
+                redisCache.deleteObject(ACTIVITY_INFO_KEY + item.getId());
+                if (item.getId() != null) {
+                    Long st = item.getStartTime() != null ? item.getStartTime().getTime() : null;
+                    Long et = item.getEndTime() != null ? item.getEndTime().getTime() : null;
+                    activityStockService.initActivityInfo(
+                            item.getId(), item.getStatus(),
+                            st, et,
+                            item.getProductId(), item.getSpecId(), null
+                    );
+                    // specStock是关联查询字段,batchInsert后item中无此值,需从规格表读取
+                    if (item.getSpecId() != null) {
+                        if (item.getSpecStock() != null) {
+                            activityStockService.initProductSpecStock(item.getSpecId(), item.getSpecStock());
+                        } else {
+                            // 从活动中间表重新JOIN查询获取specStock
+                            FsStoreProductActivity dbItem = activityMapper.selectFsStoreProductActivityById(item.getId());
+                            if (dbItem != null && dbItem.getSpecStock() != null) {
+                                activityStockService.initProductSpecStock(item.getSpecId(), dbItem.getSpecStock());
+                            }
+                        }
+                    }
+                    log.info("保存活动后初始化Redis,activityId={}, specId={}",
+                            item.getId(), item.getSpecId());
+                }
+            }
+        }
+        redisCache.deleteObject("fs:product:id:" + productId);//删除缓存预防创建订单无法校验
+        log.info("保存商品{}活动设置完成,activityType={}", productId, activityType);
+        return result;
+    }
+
+    @Override
+    public FsStoreProductActivity selectOngoingActivity(Long productId) {
+        return activityMapper.selectOngoingActivity(productId);
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectUpcomingList(Integer activityType) {
+        return activityMapper.selectUpcomingList(activityType);
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectExpiredList() {
+        return activityMapper.selectExpiredList();
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectStartingList() {
+        return activityMapper.selectStartingList();
+    }
+
+    @Override
+    public int updateStatus(Long id, Integer status) {
+        return activityMapper.updateStatus(id, status);
+    }
+
+
+    @Override
+    public void handleExpiredActivity(Long productId, List<FsStoreProductActivity> activityList) {
+        // 分布式锁防并发:同一商品只允许一个请求执行同步+重置
+        String lockKey = EXPIRED_LOCK_PREFIX + productId;
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
+            if (!locked) {
+                log.info("活动过期处理锁竞争失败,其他请求正在处理, productId={}", productId);
+                return;
+            }
+            try {
+                // 更新活动状态为已结束,并同步Redis缓存
+                if (activityList != null) {
+                    for (FsStoreProductActivity item : activityList) {
+                        activityMapper.updateStatus(item.getId(), 0);
+                        // 更新Redis中的活动状态为已结束
+                        if (item.getStartTime() != null && item.getEndTime() != null) {
+                            activityStockService.initActivityInfo(
+                                    item.getId(), 0,
+                                    item.getStartTime().getTime(), item.getEndTime().getTime(),
+                                    item.getProductId(), item.getSpecId(), null
+                            );
+                        }
+                        // 活动过期时,同步Redis规格库存到DB,并清理Redis残留key
+                        // 避免新活动复用同一规格时使用残留的错误库存值
+                        if (item.getSpecId() != null) {
+                            try {
+                                activityStockService.syncProductSpecStockToDbBySpecId(item.getSpecId());
+                                // 清理Redis中的规格库存key,下次访问时会从DB重新加载
+                                redisCache.deleteObject(PRODUCT_SPEC_STOCK_KEY + item.getSpecId());
+                                log.info("活动过期清理Redis规格库存key,specId={}", item.getSpecId());
+                            } catch (Exception e) {
+                                log.error("活动过期同步/清理规格库存异常,specId={}", item.getSpecId(), e);
+                            }
+                        }
+                        // 清理Redis中的活动信息key
+                        redisCache.deleteObject(ACTIVITY_INFO_KEY + item.getId());
+                    }
+                }
+
+                activityMapper.updateProductActivityType(productId, 0, null, null);
+                log.info("活动过期即时处理完成, productId={}, activity_type已重置为0", productId);
+            } finally {
+                if (lock.isHeldByCurrentThread()) {
+                    lock.unlock();
+                }
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("活动过期处理获取锁中断, productId={}", productId, e);
+        }
+    }
+
+    // ===== 小程序端查询方法 =====
+
+    @Override
+    public List<FsStoreProductActivity> selectUpcomingFlashSaleActivityList() {
+        return activityMapper.selectUpcomingFlashSaleActivityList();
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectUpcomingDiscountActivityList() {
+        return activityMapper.selectUpcomingDiscountActivityList();
+    }
+
+    @Override
+    public FsStoreProductActivity selectActivityDetailById(Long id) {
+        return activityMapper.selectActivityDetailById(id);
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectFlashSaleActivityByProductId(Long productId) {
+        return activityMapper.selectFlashSaleActivityByProductId(productId);
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectDiscountActivityByProductId(Long productId) {
+        return activityMapper.selectDiscountActivityByProductId(productId);
+    }
+
+    @Override
+    public List<FsStoreProductActivity> selectActivitySpecsByProductIdAndType(Long productId, Integer activityType) {
+        return activityMapper.selectActivitySpecsByProductIdAndType(productId, activityType);
+    }
+}

+ 144 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductDiscountServiceImpl.java

@@ -0,0 +1,144 @@
+package com.fs.hisStore.service.impl;
+
+import java.util.List;
+
+import com.fs.hisStore.domain.FsStoreProductDiscount;
+import com.fs.hisStore.mapper.FsStoreProductDiscountMapper;
+import com.fs.hisStore.service.IFsStoreProductDiscountService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 限时折扣商品Service业务层处理
+ *
+ * @author fs
+ * @date 2026-04-03
+ */
+@Service
+public class FsStoreProductDiscountServiceImpl implements IFsStoreProductDiscountService
+{
+    @Autowired
+    private FsStoreProductDiscountMapper fsStoreProductDiscountMapper;
+
+    /**
+     * 查询限时折扣商品
+     *
+     * @param id 限时折扣商品主键
+     * @return 限时折扣商品
+     */
+    @Override
+    public FsStoreProductDiscount selectFsStoreProductDiscountById(Long id)
+    {
+        return fsStoreProductDiscountMapper.selectFsStoreProductDiscountById(id);
+    }
+
+    /**
+     * 查询限时折扣商品列表
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 限时折扣商品
+     */
+    @Override
+    public List<FsStoreProductDiscount> selectFsStoreProductDiscountList(FsStoreProductDiscount fsStoreProductDiscount)
+    {
+        return fsStoreProductDiscountMapper.selectFsStoreProductDiscountList(fsStoreProductDiscount);
+    }
+
+    /**
+     * 新增限时折扣商品
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 结果
+     */
+    @Override
+    public int insertFsStoreProductDiscount(FsStoreProductDiscount fsStoreProductDiscount)
+    {
+        return fsStoreProductDiscountMapper.insertFsStoreProductDiscount(fsStoreProductDiscount);
+    }
+
+    /**
+     * 修改限时折扣商品
+     *
+     * @param fsStoreProductDiscount 限时折扣商品
+     * @return 结果
+     */
+    @Override
+    public int updateFsStoreProductDiscount(FsStoreProductDiscount fsStoreProductDiscount)
+    {
+        return fsStoreProductDiscountMapper.updateFsStoreProductDiscount(fsStoreProductDiscount);
+    }
+
+    /**
+     * 批量删除限时折扣商品
+     *
+     * @param ids 需要删除的主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductDiscountByIds(Long[] ids)
+    {
+        return fsStoreProductDiscountMapper.deleteFsStoreProductDiscountByIds(ids);
+    }
+
+    /**
+     * 删除限时折扣商品信息
+     *
+     * @param id 限时折扣商品主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductDiscountById(Long id)
+    {
+        return fsStoreProductDiscountMapper.deleteFsStoreProductDiscountById(id);
+    }
+
+    /**
+     * 查询当前有效的限时折扣商品列表(小程序端使用)
+     *
+     * @return 限时折扣商品集合
+     */
+    @Override
+    public List<FsStoreProductDiscount> selectActiveDiscountList()
+    {
+        return fsStoreProductDiscountMapper.selectActiveDiscountList();
+    }
+
+    /**
+     * 根据商品ID查询限时折扣信息
+     *
+     * @param productId 商品ID
+     * @return 限时折扣商品
+     */
+    @Override
+    public FsStoreProductDiscount selectDiscountByProductId(Long productId)
+    {
+        return fsStoreProductDiscountMapper.selectDiscountByProductId(productId);
+    }
+
+    /**
+     * 查询即将开抢和未过期的折扣商品列表(小程序端使用)
+     *
+     * @return 折扣商品集合
+     */
+    @Override
+    public List<FsStoreProductDiscount> selectUpcomingDiscountList()
+    {
+        return fsStoreProductDiscountMapper.selectUpcomingDiscountList();
+    }
+
+    /**
+     * 更新折扣商品库存
+     *
+     * @param id 折扣商品ID
+     * @param stock 库存数量
+     * @return 结果
+     */
+    @Override
+    public int updateDiscountStock(Long id, Long stock)
+    {
+        FsStoreProductDiscount update = new FsStoreProductDiscount();
+        update.setId(id);
+        update.setStock(stock);
+        return fsStoreProductDiscountMapper.updateFsStoreProductDiscount(update);
+    }
+}

+ 144 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductFlashSaleServiceImpl.java

@@ -0,0 +1,144 @@
+package com.fs.hisStore.service.impl;
+
+import java.util.List;
+
+import com.fs.hisStore.domain.FsStoreProductFlashSale;
+import com.fs.hisStore.mapper.FsStoreProductFlashSaleMapper;
+import com.fs.hisStore.service.IFsStoreProductFlashSaleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 秒杀商品Service业务层处理
+ *
+ * @author fs
+ * @date 2026-04-08
+ */
+@Service
+public class FsStoreProductFlashSaleServiceImpl implements IFsStoreProductFlashSaleService
+{
+    @Autowired
+    private FsStoreProductFlashSaleMapper fsStoreProductFlashSaleMapper;
+
+    /**
+     * 查询秒杀商品
+     *
+     * @param id 秒杀商品主键
+     * @return 秒杀商品
+     */
+    @Override
+    public FsStoreProductFlashSale selectFsStoreProductFlashSaleById(Long id)
+    {
+        return fsStoreProductFlashSaleMapper.selectFsStoreProductFlashSaleById(id);
+    }
+
+    /**
+     * 查询秒杀商品列表
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 秒杀商品
+     */
+    @Override
+    public List<FsStoreProductFlashSale> selectFsStoreProductFlashSaleList(FsStoreProductFlashSale fsStoreProductFlashSale)
+    {
+        return fsStoreProductFlashSaleMapper.selectFsStoreProductFlashSaleList(fsStoreProductFlashSale);
+    }
+
+    /**
+     * 新增秒杀商品
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 结果
+     */
+    @Override
+    public int insertFsStoreProductFlashSale(FsStoreProductFlashSale fsStoreProductFlashSale)
+    {
+        return fsStoreProductFlashSaleMapper.insertFsStoreProductFlashSale(fsStoreProductFlashSale);
+    }
+
+    /**
+     * 修改秒杀商品
+     *
+     * @param fsStoreProductFlashSale 秒杀商品
+     * @return 结果
+     */
+    @Override
+    public int updateFsStoreProductFlashSale(FsStoreProductFlashSale fsStoreProductFlashSale)
+    {
+        return fsStoreProductFlashSaleMapper.updateFsStoreProductFlashSale(fsStoreProductFlashSale);
+    }
+
+    /**
+     * 批量删除秒杀商品
+     *
+     * @param ids 需要删除的主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductFlashSaleByIds(Long[] ids)
+    {
+        return fsStoreProductFlashSaleMapper.deleteFsStoreProductFlashSaleByIds(ids);
+    }
+
+    /**
+     * 删除秒杀商品信息
+     *
+     * @param id 秒杀商品主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsStoreProductFlashSaleById(Long id)
+    {
+        return fsStoreProductFlashSaleMapper.deleteFsStoreProductFlashSaleById(id);
+    }
+
+    /**
+     * 查询当前有效的秒杀商品列表(小程序端使用)
+     *
+     * @return 秒杀商品集合
+     */
+    @Override
+    public List<FsStoreProductFlashSale> selectActiveFlashSaleList()
+    {
+        return fsStoreProductFlashSaleMapper.selectActiveFlashSaleList();
+    }
+
+    /**
+     * 根据商品ID查询秒杀信息
+     *
+     * @param productId 商品ID
+     * @return 秒杀商品
+     */
+    @Override
+    public FsStoreProductFlashSale selectFlashSaleByProductId(Long productId)
+    {
+        return fsStoreProductFlashSaleMapper.selectFlashSaleByProductId(productId);
+    }
+
+    /**
+     * 查询即将开抢和未过期的秒杀商品列表(小程序端使用)
+     *
+     * @return 秒杀商品集合
+     */
+    @Override
+    public List<FsStoreProductFlashSale> selectUpcomingFlashSaleList()
+    {
+        return fsStoreProductFlashSaleMapper.selectUpcomingFlashSaleList();
+    }
+
+    /**
+     * 更新秒杀商品库存
+     *
+     * @param id 秒杀商品ID
+     * @param stock 库存数量
+     * @return 结果
+     */
+    @Override
+    public int updateFlashSaleStock(Long id, Long stock)
+    {
+        FsStoreProductFlashSale update = new FsStoreProductFlashSale();
+        update.setId(id);
+        update.setStock(stock);
+        return fsStoreProductFlashSaleMapper.updateFsStoreProductFlashSale(update);
+    }
+}

+ 74 - 26
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -3,6 +3,7 @@ package com.fs.hisStore.service.impl;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import cn.hutool.core.collection.ListUtil;
@@ -127,6 +128,9 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     @Autowired
     private FsStoreProductCategoryScrmMapper fsStoreProductCategoryScrmMapper;
 
+    @Autowired
+    private FsStoreProductActivityMapper activityMapper;
+
     @Autowired
     private FsStoreProductUserEndCategoryMapper fsStoreProductUserEndCategoryMapper;
     @Autowired
@@ -188,13 +192,47 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
      */
     @Override
     public FsStoreProductScrm selectFsStoreProductById(Long productId){
+        return fsStoreProductMapper.selectFsStoreProductById(productId);
+    }
+
+    /**
+     * 查询商品缓存
+     *
+     * @param productId 商品ID
+     * @return 商品
+     */
+    @Override
+    public FsStoreProductScrm selectFsStoreRedisProductById(Long productId){
         String key = "fs:product:id:" + productId;
         FsStoreProductScrm scrm = redisCacheT.getCacheObject(key);
         if(scrm != null){
+            // 活动已过期但缓存中还保留旧的活动时间,需要清除缓存重新从DB加载
+            if (scrm.getActivityEndTime() != null && scrm.getActivityEndTime().before(new Date())) {
+                log.info("商品{}的活动已过期但缓存未失效,清除缓存重新加载,activityEndTime={}", productId, scrm.getActivityEndTime());
+                redisCacheT.deleteObject(key);
+                // 重新从DB加载
+                scrm = fsStoreProductMapper.selectFsStoreProductById(productId);
+                if (scrm != null) {
+                    redisCacheT.setCacheObject(key, scrm, 60L, TimeUnit.SECONDS);
+                }
+                return scrm;
+            }
             return scrm;
         }
         scrm = fsStoreProductMapper.selectFsStoreProductById(productId);
-        redisCacheT.setCacheObject(key, scrm);
+
+        // 计算过期时间
+        long expireSeconds = 60L;
+        if (scrm != null && scrm.getActivityEndTime() != null) {
+            Date now = new Date();
+            Date activityEndTime = scrm.getActivityEndTime();
+            if (activityEndTime.after(now)) {
+                expireSeconds = (activityEndTime.getTime() - now.getTime()) / 1000;
+                expireSeconds = Math.max(expireSeconds, 1L);
+            }
+        }
+
+        redisCacheT.setCacheObject(key, scrm, expireSeconds, TimeUnit.SECONDS);
         return scrm;
     }
 
@@ -240,6 +278,13 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         if(1 == fsStoreProduct.getIsShow() && "1".equals(fsStoreProduct.getIsAudit())){
             fsStoreProduct.setIsAudit("0");
         }
+        // 活动进行中的商品不允许修改库存
+        if (fsStoreProduct.getProductId() != null && fsStoreProduct.getActivityType() != null && fsStoreProduct.getActivityType() != 0) {
+            FsStoreProductActivity ongoing = activityMapper.selectOngoingActivity(fsStoreProduct.getProductId());
+            if (ongoing != null) {
+                throw new CustomException("商品正在活动中,不允许修改,请先结束活动");
+            }
+        }
         int result = fsStoreProductMapper.updateFsStoreProduct(fsStoreProduct);
         // 清除缓存
         clearProductDetailCache(fsStoreProduct.getProductId());
@@ -705,34 +750,37 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         } else {
             product.setSinglePurchaseLimit(0);
         }
-        //校验店铺资质信息
-        if (!CompanyEnum.contains(cloudHostProper.getCompanyName())) {
-            //获取店铺
-            FsStoreScrm store = fsStoreScrmService.selectFsStoreByStoreId(product.getStoreId());
-            if(store == null || 1 != store.getStatus()){
-                return R.error("店铺不存在或未启用");
-            }else{
-                //验证资质
-                switch (product.getProductType()){
-                    case 1://非处方
-                        break;
-                    case 2://处方
+
+        if(product.getStoreId() != null){
+            //校验店铺资质信息
+            if (!CompanyEnum.contains(cloudHostProper.getCompanyName())) {
+                //获取店铺
+                FsStoreScrm store = fsStoreScrmService.selectFsStoreByStoreId(product.getStoreId());
+                if(store == null || 1 != store.getStatus()){
+                    return R.error("店铺不存在或未启用");
+                }else{
+                    //验证资质
+                    switch (product.getProductType()){
+                        case 1://非处方
+                            break;
+                        case 2://处方
 //                        if("".equals(store.getDrugLicense()) ||  LocalDate.now().isBefore(store.getDrugLicenseExpiryEnd())){
 //                            return R.error("店铺药品资质为空或已过期,请完善后再添加");
 //                        }
-                        break;
-                    case 3://食品
-                        if("".equals(store.getFoodLicense()) ||  LocalDate.now().isBefore(store.getFoodLicenseExpiryEnd())){
-                            return R.error("店铺食品资质为空或已过期,请完善后再添加");
-                        }
-                        break;
-                    case 4://器械
-                        if("".equals(store.getMedicalDevice3()) ||  LocalDate.now().isBefore(store.getMedicalDevice3ExpiryEnd())){
-                            return R.error("店铺器械资质为空或已过期,请完善后再添加");
-                        }
-                        break;
-                    default:
-                        return R.error("商品类型错误");
+                            break;
+                        case 3://食品
+                            if("".equals(store.getFoodLicense()) ||  LocalDate.now().isBefore(store.getFoodLicenseExpiryEnd())){
+                                return R.error("店铺食品资质为空或已过期,请完善后再添加");
+                            }
+                            break;
+                        case 4://器械
+                            if("".equals(store.getMedicalDevice3()) ||  LocalDate.now().isBefore(store.getMedicalDevice3ExpiryEnd())){
+                                return R.error("店铺器械资质为空或已过期,请完善后再添加");
+                            }
+                            break;
+                        default:
+                            return R.error("商品类型错误");
+                    }
                 }
             }
         }

+ 7 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java

@@ -71,6 +71,13 @@ public class FsStoreProductListQueryVO implements Serializable
 
     private List<FsStoreProductAttrValueScrm> attrValueList = new ArrayList<>();
 
+    /** 活动类型 0=普通 6=秒杀 7=限时折扣 */
+    private Integer activityType;
 
+    /** 活动开始时间 */
+    private java.util.Date activityStartTime;
+
+    /** 活动结束时间 */
+    private java.util.Date activityEndTime;
 
 }

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

@@ -4,6 +4,7 @@ import lombok.Data;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
+import java.util.Date;
 
 @Data
 public class FsStoreProductQueryVO implements Serializable
@@ -145,4 +146,13 @@ public class FsStoreProductQueryVO implements Serializable
     /** 单次购买数量上限 */
     private Integer singlePurchaseLimit;
 
+    /** 活动类型:0=无 6=秒杀 7=限时折扣 */
+    private Integer activityType;
+
+    /** 活动开始时间 */
+    private Date activityStartTime;
+
+    /** 活动结束时间 */
+    private Date activityEndTime;
+
 }

+ 20 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsUnsyncOrderVO.java

@@ -0,0 +1,20 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class FsUnsyncOrderVO implements Serializable {
+    //订单ID
+    private Long id;
+
+    //关联ID
+    private Long associatedId;
+
+    //订单商品总数
+    private Long totalNum;
+
+    //订单类型6:秒杀、7折扣
+    private Integer orderType;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/ipad/vo/WxBaseVo.java

@@ -9,6 +9,7 @@ public class WxBaseVo {
 
     private Long id;
     private Long serverId;
+    private String userRemark;
     private String remark;
 
 
@@ -16,5 +17,6 @@ public class WxBaseVo {
         this.id = vo.getId();
         this.serverId = vo.getServerId();
         this.remark = vo.getRemark();
+        this.userRemark = vo.getUserRemark();
     }
 }

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwCustomerPropertyMapper.java

@@ -1,7 +1,7 @@
 package com.fs.qw.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.crm.vo.QwCustomerAiTagVo;
+import com.fs.qw.vo.QwCustomerAiTagVo;
 import com.fs.qw.domain.QwCustomerProperty;
 import org.springframework.stereotype.Repository;
 

+ 11 - 3
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -1,8 +1,6 @@
 package com.fs.qw.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.common.annotation.DataSource;
-import com.fs.common.enums.DataSourceType;
 import com.fs.fastGpt.domain.FastgptChatArtificialWords;
 import com.fs.hisStore.vo.FsStoreOrderScrmSidebarVO;
 import com.fs.qw.domain.QwExternalContact;
@@ -15,7 +13,6 @@ import com.fs.qw.result.QwExternalContactVo;
 import com.fs.qw.vo.*;
 import com.fs.qw.vo.newvo.ExternalContactListVO;
 import com.fs.qw.vo.newvo.ExternalContactNumVO;
-import com.fs.qw.vo.sidebar.ExternalContactQwUserVO;
 import com.fs.qwApi.param.QwExternalContactHParam;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
@@ -241,6 +238,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
             "select c.id from qw_external_ai_analyze c " +
             "where c.qw_user_id = ec.user_id and c.external_user_id = ec.external_user_id and c.corp_id = ec.corp_id " +
             "order by c.create_time desc limit 1) " +
+            " <if test=\"isDownloadApp != null\"> left join fs_user fapp on fapp.user_id = ec.fs_user_id and fapp.is_del = 0 </if> " +
             "<where>  \n" +
             "            <if test=\"id != null  and id != ''\"> and ec.id   like concat( #{id}, '%') </if>\n" +
             "            <if test=\"userId != null  and userId != ''\"> and ec.user_id   like concat( #{userId}, '%') </if>\n" +
@@ -307,6 +305,16 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
             "<if test ='companyUser!=null'> " +
                 "and (cu.nick_name like concat('%', #{companyUser}, '%') or cu.phonenumber= #{companyUser})"+
             "</if> " +
+            "            <if test=\"isDownloadApp != null\"> \n" +
+            "                <choose> \n" +
+            "                    <when test=\"isDownloadApp == 1\"> \n" +
+            "                        and ec.fs_user_id is not null and fapp.user_id is not null and (fapp.source is not null or fapp.login_device is not null) \n" +
+            "                    </when> \n" +
+            "                    <when test=\"isDownloadApp == 0\"> \n" +
+            "                        and (ec.fs_user_id is null or fapp.user_id is null or (fapp.source is null and fapp.login_device is null)) \n" +
+            "                    </when> \n" +
+            "                </choose> \n" +
+            "            </if> \n" +
             "        </where>"+
             "order by ec.create_time desc,ec.id desc"+
             "</script>"})

+ 6 - 0
fs-service/src/main/java/com/fs/qw/param/QwExternalContactParam.java

@@ -155,4 +155,10 @@ public class QwExternalContactParam {
      */
     private Integer isReply;
 
+    /**
+     * 是否已下载/使用 app:1-是 0-否;
+     */
+    @TableField(exist = false)
+    private Integer isDownloadApp;
+
 }

+ 1 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwCustomerPropertyServiceImpl.java

@@ -14,8 +14,7 @@ import com.fs.crm.domain.CrmCustomerPropertyTemplate;
 import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
 import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
 import com.fs.crm.utils.CrmCustomerAiTagUtil;
-import com.fs.crm.vo.CrmCustomerAiTagVo;
-import com.fs.crm.vo.QwCustomerAiTagVo;
+import com.fs.qw.vo.QwCustomerAiTagVo;
 import com.fs.hisapi.util.MapUtil;
 import com.fs.qw.domain.QwCustomerProperty;
 import com.fs.qw.domain.QwExternalAiAnalyze;

+ 1 - 1
fs-service/src/main/java/com/fs/crm/vo/QwCustomerAiTagVo.java → fs-service/src/main/java/com/fs/qw/vo/QwCustomerAiTagVo.java

@@ -1,4 +1,4 @@
-package com.fs.crm.vo;
+package com.fs.qw.vo;
 
 import lombok.Data;
 import lombok.experimental.Accessors;

+ 1 - 3
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopLogs.java

@@ -38,12 +38,10 @@ public class WxSopLogs extends BaseEntityTow {
     @Excel(name = "发送类型", readConverterExp = "字=典-wx_send_type")
     private Integer sendType;
 
-    private String sendTime;
+    private LocalDateTime sendTime;
 
     private String contentJson;
 
-    private Integer receivingStatus;
-
     /** 生成类型(0自动1手动) */
     @Excel(name = "生成类型(0自动1手动)")
     private Integer generateType;

+ 8 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java

@@ -23,6 +23,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param id 个微发送记录主键
      * @return 个微发送记录
      */
+    @DataSource(DataSourceType.SOP)
     WxSopLogs selectWxSopLogsById(Long id);
 
     /**
@@ -31,6 +32,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param wxSopLogs 个微发送记录
      * @return 个微发送记录集合
      */
+    @DataSource(DataSourceType.SOP)
     List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
 
     /**
@@ -39,6 +41,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param param 查询参数
      * @return 执行记录集合
      */
+    @DataSource(DataSourceType.SOP)
     List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param);
 
     /**
@@ -47,6 +50,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param wxSopLogs 个微发送记录
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int insertWxSopLogs(WxSopLogs wxSopLogs);
 
     /**
@@ -55,6 +59,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param wxSopLogs 个微发送记录
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int updateWxSopLogs(WxSopLogs wxSopLogs);
 
     /**
@@ -63,6 +68,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param id 个微发送记录主键
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int deleteWxSopLogsById(Long id);
 
     /**
@@ -71,10 +77,12 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int deleteWxSopLogsByIds(Long[] ids);
 
     @DataSource(DataSourceType.SOP)
     void batchInsertWxSopLogs(List<WxSopLogs> logsToInsert);
 
+    @DataSource(DataSourceType.SOP)
     List<WxSopLogs> selectByWxId(@Param("id") Long id);
 }

+ 7 - 1
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java

@@ -12,7 +12,6 @@ import com.fs.wx.sop.domain.WxSopUserInfo;
  * @author 吴树波
  * @date 2026-02-24
  */
-@DataSource(DataSourceType.SOP)
 public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
     /**
      * 查询个微营期详情
@@ -20,6 +19,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param id 个微营期详情主键
      * @return 个微营期详情
      */
+    @DataSource(DataSourceType.SOP)
     WxSopUserInfo selectWxSopUserInfoById(Long id);
 
     /**
@@ -28,6 +28,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 个微营期详情集合
      */
+    @DataSource(DataSourceType.SOP)
     List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo);
 
     /**
@@ -36,6 +37,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
 
     /**
@@ -44,6 +46,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
 
     /**
@@ -52,6 +55,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param id 个微营期详情主键
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int deleteWxSopUserInfoById(Long id);
 
     /**
@@ -60,6 +64,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
+    @DataSource(DataSourceType.SOP)
     int deleteWxSopUserInfoByIds(Long[] ids);
 
     /**
@@ -68,5 +73,6 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 个微营期详情
      */
+    @DataSource(DataSourceType.SOP)
     WxSopUserInfo selectWxSopUserInfoByCondition(WxSopUserInfo wxSopUserInfo);
 }

+ 1 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java

@@ -28,6 +28,7 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
      * @param wxSopUser 个微营期
      * @return 个微营期集合
      */
+    @DataSource(DataSourceType.SOP)
     List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser);
 
     /**

+ 13 - 0
fs-service/src/main/java/com/fs/wx/sop/params/SendWxSopMsgParam.java

@@ -0,0 +1,13 @@
+package com.fs.wx.sop.params;
+
+import lombok.Data;
+
+@Data
+public class SendWxSopMsgParam {
+    /** 选中的SOP ID数组 */
+    private Long[] sopIds;
+    /** 消息内容JSON,格式: [{"contentType":"1","value":"文本内容"}] */
+    private String setting;
+    /** 发送时间 HH:mm 格式,不填默认立即发送 */
+    private String sendTime;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java

@@ -2,8 +2,12 @@ package com.fs.wx.sop.service;
 
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.common.core.domain.R;
+import com.fs.wx.sop.params.SendWxSopMsgParam;
 import com.fs.wx.sop.params.WxSopLogsParam;
 import com.fs.wx.sop.vo.WxSopLogsListVO;
 
@@ -71,4 +75,14 @@ public interface IWxSopLogsService extends IService<WxSopLogs>{
     int deleteWxSopLogsById(Long id);
 
     void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert);
+
+    /**
+     * 个微SOP一键群发
+     *
+     * @param param 群发参数
+     * @return 结果
+     */
+    R sendWxSopMsg(SendWxSopMsgParam param);
+
+    boolean updateMapper(WxSopLogs updateQwSop);
 }

+ 0 - 3
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java

@@ -333,8 +333,6 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
      * @param customerId 客户ID
      */
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    @DataSource(DataSourceType.SOP)
     public void processCustomerTagsChange(Long customerId) {
         try {
             log.info("====== 开始处理客户标签变更,客户ID: {} ======", customerId);
@@ -418,7 +416,6 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
      * @param customerId 客户ID
      * @return 营期成员记录列表
      */
-    @DataSource(DataSourceType.SOP)
     public List<WxSopUserInfo> querySopUserInfosByCustomer(Long sopId, Long customerId) {
         WxSopUserInfo queryParam = new WxSopUserInfo();
         queryParam.setSopId(sopId);

+ 202 - 12
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java

@@ -1,20 +1,34 @@
 package com.fs.wx.sop.service.impl;
 
-import java.util.List;
-
-import com.fs.common.annotation.DataScope;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.R;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.DateUtils;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.fs.sop.domain.QwSopLogs;
+import com.fs.common.utils.PubFun;
+import com.fs.common.utils.date.DateUtil;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.mapper.WxSopLogsMapper;
+import com.fs.wx.sop.mapper.WxSopUserInfoMapper;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.params.SendWxSopMsgParam;
 import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.service.IWxSopLogsService;
 import com.fs.wx.sop.vo.WxSopLogsListVO;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.WxContactMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import com.fs.wx.sop.mapper.WxSopLogsMapper;
-import com.fs.wx.sop.domain.WxSopLogs;
-import com.fs.wx.sop.service.IWxSopLogsService;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * 个微发送记录Service业务层处理
@@ -27,6 +41,18 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
     @Autowired
     private WxSopLogsMapper wxSopLogsMapper;
 
+    @Autowired
+    private WxSopUserMapper wxSopUserMapper;
+
+    @Autowired
+    private WxSopUserInfoMapper wxSopUserInfoMapper;
+    @Autowired
+    private CompanyWxAccountMapper companyWxAccountMapper;
+    @Autowired
+    private WxContactMapper wxContactMapper;
+    @Autowired
+    private CrmCustomerMapper crmCustomerMapper;
+
     /**
      * 查询个微发送记录
      *
@@ -114,14 +140,178 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
      * @return 执行记录集合
      */
     @Override
-    @DataSource(DataSourceType.SOP)
-    public List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param)
-    {
-        return baseMapper.selectWxSopLogsListBySopId(param);
+    public List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param){
+        List<WxSopLogsListVO> list = baseMapper.selectWxSopLogsListBySopId(param);
+        List<Long> longs = PubFun.listToNewList(list, WxSopLogsListVO::getAccountId);
+        List<CompanyWxAccount> companyWxAccounts = companyWxAccountMapper.selectBatchIds(longs);
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(companyWxAccounts, CompanyWxAccount::getId);
+        list.parallelStream().filter(e -> accountMap.containsKey(e.getAccountId())).forEach(e -> {
+           e.setAccountName(accountMap.get(e.getAccountId()).getWxNickName());
+        });
+        return list;
     }
 
+    @DataSource(DataSourceType.SOP)
     public void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert) {
         if(logsToInsert == null || logsToInsert.isEmpty()) return;
         wxSopLogsMapper.batchInsertWxSopLogs(logsToInsert);
     }
+
+    /**
+     * 个微SOP一键群发
+     * 注意:不加 @DataSource(DataSourceType.SOP),因为需要跨库查询
+     * SOP相关Mapper方法自带 @DataSource(DataSourceType.SOP) 注解
+     * wx_contact、crm_customer 在主库,使用默认数据源
+     */
+    @Override
+    public R sendWxSopMsg(SendWxSopMsgParam param) {
+        if (param.getSopIds() == null || param.getSopIds().length == 0) {
+            return R.error("请选择要群发的SOP");
+        }
+
+        // 1. 解析发送时间:前端选了就用前端的,没选就用当前时间
+        String sendTimeStr;
+        if (param.getSendTime() != null && !param.getSendTime().isEmpty()) {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            sendTimeStr = sdf.format(new Date()) + " " + param.getSendTime() + ":00";
+        } else {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            sendTimeStr = sdf.format(new Date());
+        }
+
+        List<WxSopLogs> logsToInsert = new ArrayList<>();
+        // 收集所有联系人ID和客户ID,用于批量查询
+        List<Long> allContactIds = new ArrayList<>();
+        // 临时存储:sopUser + userInfos 的关系
+        List<Object[]> sopUserAndInfos = new ArrayList<>();
+
+        // 2. 遍历每个SOP,从SOP数据库查询营期和成员
+        for (Long sopId : param.getSopIds()) {
+            WxSopUser query = new WxSopUser();
+            query.setSopId(sopId);
+            // wxSopUserMapper.selectWxSopUserList 自带 @DataSource(DataSourceType.SOP)
+            List<WxSopUser> sopUsers = wxSopUserMapper.selectWxSopUserList(query);
+
+            if (sopUsers == null || sopUsers.isEmpty()) {
+                continue;
+            }
+
+            for (WxSopUser sopUser : sopUsers) {
+                WxSopUserInfo userInfoQuery = new WxSopUserInfo();
+                userInfoQuery.setSopUserId(sopUser.getId());
+                // wxSopUserInfoMapper.selectWxSopUserInfoList 自带 @DataSource(DataSourceType.SOP)
+                List<WxSopUserInfo> userInfos = wxSopUserInfoMapper.selectWxSopUserInfoList(userInfoQuery);
+
+                if (userInfos == null || userInfos.isEmpty()) {
+                    continue;
+                }
+
+                sopUserAndInfos.add(new Object[]{sopId, sopUser, userInfos});
+
+                for (WxSopUserInfo info : userInfos) {
+                    if (info.getWxContactId() != null) {
+                        allContactIds.add(info.getWxContactId());
+                    }
+                }
+            }
+        }
+
+        if (sopUserAndInfos.isEmpty()) {
+            return R.error("未找到可群发的营期成员");
+        }
+
+        // 3. 从主库批量查询 wx_contact 获取联系人昵称
+        Map<Long, WxContact> contactMap = new HashMap<>();
+        Map<Long, CrmCustomer> customerMap = new HashMap<>();
+        if (!allContactIds.isEmpty()) {
+            List<Long> uniqueContactIds = allContactIds.stream().distinct().collect(Collectors.toList());
+            List<WxContact> contacts = wxContactMapper.selectBatchIds(uniqueContactIds);
+            if (contacts != null) {
+                for (WxContact c : contacts) {
+                    contactMap.put(c.getId(), c);
+                }
+                // 4. 收集customerIds,从主库查询 crm_customer 获取客户标签
+                List<Long> customerIds = contacts.stream()
+                        .filter(c -> c.getCustomerId() != null)
+                        .map(WxContact::getCustomerId)
+                        .distinct()
+                        .collect(Collectors.toList());
+                if (!customerIds.isEmpty()) {
+                    List<CrmCustomer> customers = crmCustomerMapper.selectBatchIds(customerIds);
+                    if (customers != null) {
+                        for (CrmCustomer cust : customers) {
+                            customerMap.put(cust.getCustomerId(), cust);
+                        }
+                    }
+                }
+            }
+        }
+
+        // 5. 遍历构建发送记录,设置联系人昵称
+        // 同时收集需要更新标签的 userInfo
+        List<WxSopUserInfo> userInfosToUpdateTag = new ArrayList<>();
+        for (Object[] arr : sopUserAndInfos) {
+            Long sopId = (Long) arr[0];
+            WxSopUser sopUser = (WxSopUser) arr[1];
+            @SuppressWarnings("unchecked")
+            List<WxSopUserInfo> userInfos = (List<WxSopUserInfo>) arr[2];
+
+            for (WxSopUserInfo userInfo : userInfos) {
+                WxSopLogs log = new WxSopLogs();
+                log.setType(0);
+                log.setSopId(sopId);
+                log.setSopUserId(sopUser.getId());
+                log.setGenerateType(1);
+                log.setAccountId(sopUser.getAccountId());
+                log.setWxContactId(userInfo.getWxContactId());
+                log.setFsUserId(userInfo.getFsUserId());
+                log.setSendStatus(0);
+                log.setSendSort(30000000);
+                log.setContentJson(param.getSetting());
+                log.setSendTime(DateUtil.stringToLocalDateTime(sendTimeStr));
+                log.setCreateTime(new Date());
+
+                // 设置联系人昵称
+                WxContact contact = contactMap.get(userInfo.getWxContactId());
+                if (contact != null) {
+                    log.setWxContactName(contact.getNickName());
+
+                    // 更新 wx_sop_user_info 的标签(如果为空)
+                    if ((userInfo.getTagNames() == null || userInfo.getTagNames().isEmpty())
+                            && contact.getCustomerId() != null) {
+                        CrmCustomer customer = customerMap.get(contact.getCustomerId());
+                        if (customer != null && customer.getTags() != null && !customer.getTags().isEmpty()) {
+                            userInfo.setTagNames(customer.getTags());
+                            userInfosToUpdateTag.add(userInfo);
+                        }
+                    }
+                }
+
+                logsToInsert.add(log);
+            }
+        }
+
+        if (logsToInsert.isEmpty()) {
+            return R.error("未找到可群发的营期成员");
+        }
+
+        // 6. 更新 wx_sop_user_info 的标签信息(SOP数据库)
+        for (WxSopUserInfo info : userInfosToUpdateTag) {
+            WxSopUserInfo updateInfo = new WxSopUserInfo();
+            updateInfo.setId(info.getId());
+            updateInfo.setTagNames(info.getTagNames());
+            wxSopUserInfoMapper.updateWxSopUserInfo(updateInfo);
+        }
+
+        // 7. 批量插入发送记录到SOP数据库
+        batchInsertQwSopLogs(logsToInsert);
+
+        return R.ok("一键群发成功,共发送 " + logsToInsert.size() + " 条消息");
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public boolean updateMapper(WxSopLogs updateQwSop) {
+        return updateById(updateQwSop);
+    }
 }

+ 20 - 4
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java

@@ -1,11 +1,20 @@
 package com.fs.wx.sop.service.impl;
 
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.BaseEntityTow;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.PubFun;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.service.ICompanyWxAccountService;
+import lombok.AllArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.wx.sop.mapper.WxSopUserMapper;
@@ -19,8 +28,10 @@ import com.fs.wx.sop.service.IWxSopUserService;
  * @date 2026-02-24
  */
 @Service
+@AllArgsConstructor
 public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser> implements IWxSopUserService {
 
+    private final CompanyWxAccountMapper companyWxAccountMapper;
     /**
      * 查询个微营期
      * 
@@ -41,10 +52,15 @@ public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser
      * @return 个微营期
      */
     @Override
-    @DataSource(DataSourceType.SOP)
-    public List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser)
-    {
-        return baseMapper.selectWxSopUserList(wxSopUser);
+    public List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser){
+        List<WxSopUser> wxSopUsers = baseMapper.selectWxSopUserList(wxSopUser);
+        List<CompanyWxAccount> companyWxAccounts = companyWxAccountMapper.selectList(new QueryWrapper<CompanyWxAccount>().in("id", PubFun.listToNewList(wxSopUsers, WxSopUser::getAccountId)));
+        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(companyWxAccounts, CompanyWxAccount::getId);
+        wxSopUsers.parallelStream().filter(e -> accountMap.containsKey(e.getAccountId())).forEach(e -> {
+            CompanyWxAccount companyWxAccount = accountMap.get(e.getAccountId());
+            e.setAccountName(companyWxAccount.getWxNickName());
+        });
+        return wxSopUsers;
     }
 
     /**

+ 8 - 0
fs-service/src/main/java/com/fs/wx/sop/vo/WxSopLogsListVO.java

@@ -5,6 +5,7 @@ import com.fs.common.annotation.Excel;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.time.LocalDateTime;
 import java.util.Date;
 
 /**
@@ -99,4 +100,11 @@ public class WxSopLogsListVO implements Serializable {
 
     /** 公司ID */
     private Long companyId;
+
+    /** 预计发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime sendTime;
+
+    /** 消息内容JSON */
+    private String contentJson;
 }

+ 11 - 0
fs-service/src/main/java/com/fs/wx/sop/vo/WxSopMsgVo.java

@@ -0,0 +1,11 @@
+package com.fs.wx.sop.vo;
+
+import lombok.Data;
+
+@Data
+public class WxSopMsgVo {
+    private Integer contentType;
+    private String value;
+    private Integer sendStatus;
+    private String sendRemarks;
+}

+ 7 - 1
fs-service/src/main/java/com/fs/wxwork/service/WxIpadService.java

@@ -8,6 +8,7 @@ import com.fs.company.param.AddWxActionParam;
 import com.fs.ipad.vo.WxTxtVo;
 import com.fs.wxcid.domain.CidIpadServer;
 import com.fs.wxcid.service.ICidIpadServerService;
+import com.fs.wxcid.vo.wxvo.WxSendMsgVo;
 import com.fs.wxwork.utils.WxHttpUtil;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -41,8 +42,13 @@ public class WxIpadService {
 
     public void sendTxt(WxTxtVo vo){
         String url = getUrl(vo.getServerId());
-        ResponseResult<Void> result = WxHttpUtil.postWithType(url+"/app/common/sendMsg", vo, new TypeReference<ResponseResult<Void>>() {
+        WxSendMsgVo msgVO = new WxSendMsgVo();
+        msgVO.setWxId(vo.getRemark());
+        msgVO.setRemark(vo.getUserRemark());
+        msgVO.setTxt(vo.getContent());
+        ResponseResult<Void> result = WxHttpUtil.postWithType(url+"/app/common/sendMsg", msgVO, new TypeReference<ResponseResult<Void>>() {
         }, vo.getServerId());
+        log.info("发送结果:{}", result);
     }
     public void addWx(AddWxActionParam vo){
         String url = getUrl(vo.getServerId());

+ 1 - 1
fs-service/src/main/resources/application-config-druid-hst.yml

@@ -92,7 +92,7 @@ cloud_host:
 headerImg:
   imgUrl:
 ipad:
-  ipadUrl: http://ipad.schstyl.cn
+  ipadUrl: http://159.75.170.85:8667
   aiApi: http://49.232.181.28:3000/api
   voiceApi: http://123.207.48.104:8009
   commonApi: http://123.207.48.104:7771

+ 2 - 2
fs-service/src/main/resources/application-config-druid-sxtb.yml

@@ -48,8 +48,8 @@ wx:
         aesKey: Eswa6VjwtVcw03qZy6Wllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
   # 开放平台app微信授权配置
   open:
-    app-id: wx9746858bdb5e0643
-    secret: 32dfaa2b2dcad9229935ff089c65d372
+    app-id: wxeca5fa66737f170b
+    secret: ca8376b262d611ca9a4cf79adcf90600
 aifabu:  #爱链接
   appKey: 7b471be905ab17ef358c610dd117601d008
 watch:

+ 4 - 0
fs-service/src/main/resources/application-druid-hst.yml

@@ -138,6 +138,10 @@ rocketmq:
     group: test-group
     access-key: ak1vjak37reb7b19a2b09d1 # 替换为实际的 accessKey
     secret-key: sk3987beb638e3414f # 替换为实际的 secretKey
+#看课授权时显示的头像
+headerImg:
+  imgUrl:
+  download_poster_url:
 openIM:
   secret: openIM123
   userID: imAdmin

+ 366 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

@@ -717,5 +717,371 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     </select>
 
+    <select id="selectTransferCustomerList" resultType="com.fs.crm.vo.CrmLineCustomerListQueryVO">
+        select c.*,u.nick_name as company_user_nick_name,u2.nick_name as create_user_name
+        from  crm_customer c
+        left join company_user u on u.user_id=c.receive_user_id
+        left join company_user u2 on u2.user_id=c.create_user_id
+        where c.is_del = 0 and (u.status = 1 or u.del_flag = 2)
+        <if test="maps.isLine != null">
+            and c.is_line = #{maps.isLine}
+        </if>
+        <if test="maps.customerCode != null and  maps.customerCode !='' ">
+            and c.customer_code like CONCAT('%',#{maps.customerCode},'%')
+        </if>
+        <if test="maps.customerName != null and  maps.customerName !='' ">
+            and c.customer_name like CONCAT('%',#{maps.customerName},'%')
+        </if>
+        <if test="maps.customerCompanyName != null and  maps.customerCompanyName !='' ">
+            and c.customer_company_name like CONCAT('%',#{maps.customerCompanyName},'%')
+        </if>
+        <if test="maps.companyUserNickName != null and  maps.companyUserNickName !=''">
+            and u.nick_name like CONCAT('%',#{maps.companyUserNickName},'%')
+        </if>
+        <if test="maps.createUserName != null and  maps.createUserName !='' ">
+            and u2.nick_name like CONCAT('%',#{maps.createUserName},'%')
+        </if>
+        <if test="maps.mobile != null and  maps.mobile !='' ">
+            and c.mobile like CONCAT('%',#{maps.mobile},'%')
+        </if>
+        <if test="maps.receiveUserId == -1">
+            and c.receive_user_id is NULL
+        </if>
+        <if test="maps.receiveUserId == 0">
+            and c.receive_user_id is NOT NULL
+        </if>
+        <if test="maps.receiveUserId != null and maps.receiveUserId != 0 and maps.receiveUserId != -1">
+            and c.receive_user_id = #{maps.receiveUserId}
+        </if>
+        <if test="maps.status != null and maps.status !='' ">
+            and c.status =#{maps.status}
+        </if>
+        <if test="maps.isReceive != null ">
+            and c.is_receive =#{maps.isReceive}
+        </if>
+        <if test="maps.customerType != null ">
+            and c.customer_type =#{maps.customerType}
+        </if>
+        <if test="maps.companyId != null ">
+            and c.company_id =#{maps.companyId}
+        </if>
+        <if test="maps.source != null and maps.source !='' ">
+            and c.source =#{maps.source}
+        </if>
+        <if test="maps.tags != null ">
+            and
+            <foreach collection="maps.tags.split(',')" item="tag" open="(" close=")" separator="OR">
+                find_in_set(#{tag},c.tags)
+            </foreach>
+        </if>
+        <if test="maps.isPool != null ">
+            and c.is_pool = #{maps.isPool}
+        </if>
+        <if test="maps.createTime != null ">
+            and  DATE_FORMAT(c.create_time, '%Y-%m-%d')  = DATE_FORMAT(#{maps.createTime}, '%Y-%m-%d')
+        </if>
+        <if test="maps.beginTime != null and maps.beginTime !='' ">
+            and date_format(c.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d')
+        </if>
+        <if test="maps.endTime != null and maps.endTime !='' ">
+            and date_format(c.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d')
+        </if>
+        <if test="maps.beginReceiveTime != null and maps.beginReceiveTime !='' ">
+            and date_format(c.receive_time,'%y%m%d') &gt;= date_format(#{maps.beginReceiveTime},'%y%m%d')
+        </if>
+        <if test="maps.endReceiveTime != null and maps.endReceiveTime !='' ">
+            and date_format(c.receive_time,'%y%m%d') &lt;= date_format(#{maps.endReceiveTime},'%y%m%d')
+        </if>
+
+
+        ${maps.params.dataScope}
+        <if test="maps.orderBy != null and maps.orderBy != ''">
+            order by c.next_time ${maps.orderBy} ,c.customer_id desc
+        </if>
+        <if test="maps.orderBy == null or maps.orderBy == ''">
+            order by c.customer_id desc
+        </if>
+    </select>
+
+    <select id="selectCrmCustomerListByUnPool" resultType="com.fs.crm.vo.CrmCustomerUnPoolListQueryVO">
+        WITH RankedCustomers AS (
+        select c.*,ROW_NUMBER() OVER (PARTITION BY c.customer_id ORDER BY c.create_time DESC) AS rn from (
+        <if test="param1 != null">
+            (SELECT
+            *,
+            '领取未跟进' AS pool_type,
+            (receive_time+ INTERVAL ${param1.limitDay} DAY) AS pooling_time,
+            TIMESTAMPDIFF(MINUTE, NOW(), (receive_time + INTERVAL ${param1.limitDay} DAY)) AS `time`
+            from crm_customer n
+            WHERE n.is_del = 0
+            AND n.is_pool = 0
+            AND n.is_receive = 1
+            <if test="param2 != null">
+                AND n.visit_status IS NULL
+                AND n.receive_user_id IS NOT NULL
+                AND n.customer_id NOT IN (SELECT v.customer_id FROM `crm_customer_visit` v GROUP BY v.customer_id HAVING v.customer_id is NOT NULL)
+            </if>
+
+            )
+
+        </if>
+        <if test="param2 != null">
+            UNION(
+            SELECT *,
+            '未继续跟进' AS pool_type,
+            (GREATEST(
+            IFNULL(visit_time, '1000-01-01'),
+            IFNULL(sys_visit_time, '1000-01-01')
+            ) + INTERVAL ${param2.limitDay} DAY) AS pooling_time,
+            TIMESTAMPDIFF(
+            MINUTE,
+            NOW(),
+            (GREATEST(
+            IFNULL(visit_time, '1000-01-01'),
+            IFNULL(sys_visit_time, '1000-01-01')
+            ) + INTERVAL ${param2.limitDay} DAY)
+            ) AS `time`
+            FROM crm_customer
+            WHERE is_del = 0
+            AND is_pool = 0
+            AND visit_status IS NOT NULL
+            AND visit_time IS NOT NULL
+            )
+        </if>
+        <if test="param3!=null">
+            UNION(SELECT c.*,
+            '未有商机' AS pool_type,
+            (c.create_time + INTERVAL ${param3.limitDay} DAY) AS pooling_time,
+            TIMESTAMPDIFF(MINUTE, NOW(), (c.create_time + INTERVAL ${param3.limitDay} DAY)) AS `time`
+            FROM `crm_customer` c
+            WHERE c.is_del = 0
+            AND c.is_pool = 0
+            AND c.customer_id NOT IN (
+            SELECT b.customer_id FROM crm_business b WHERE b.create_time > (NOW()-INTERVAL ${param3.limitDay} DAY)
+            AND b.customer_id
+            IS NOT NULL GROUP BY b.customer_id)
+            )
+        </if>
+
+        ) as c
+        where c.is_del = 0 and c.is_pool_rule = 1
+        <if test="param.customerCode != null  and param.customerCode != ''"> and c.customer_code = #{param.customerCode}</if>
+        <if test="param.customerName != null  and param.customerName !=''"> and c.customer_name like concat('%', #{param.customerName}, '%')</if>
+        <if test="param.customerCompanyName != null  and param.customerCompanyName !=''"> and c.customer_company_name like concat('%', #{param.customerCompanyName}, '%')</if>
+        <if test="param.mobile != null  and param.mobile != ''"> and c.mobile= #{param.mobile}</if>
+        <if test="param.sex != null "> and c.sex =#{param.sex}</if>
+        <if test="param.weixin != null  and param.weixin != ''"> and c.weixin= #{param.weixin}</if>
+        <if test="param.createUserId != null "> and c.create_user_id = #{param.createUserId}</if>
+        <if test="param.receiveUserId != null "> and c.receive_user_id = #{param.receiveUserId}</if>
+        <if test="param.customerUserId != null "> and c.customer_user_id = #{param.customerUserId}</if>
+        <if test="param.deptId != null "> and c.dept_id = #{deptId}</if>
+        <if test="param.customerType != null "> and c.customer_type = #{param.customerType}</if>
+        <if test="param.companyId != null "> and c.company_id = #{param.companyId}</if>
+        <if test="param.source != null and param.source != ''"> and c.source = #{param.source}</if>
+        <if test="param.tags != null  and param.tags != ''"> and c.tags = #{param.tags}</if>
+        <if test="param.beginTime != null and param.beginTime !='' ">
+            and date_format(c.create_time,'%y%m%d') &gt;= date_format(#{param.beginTime},'%y%m%d')
+        </if>
+        <if test="param.endTime != null and param.endTime !='' ">
+            and date_format(c.create_time,'%y%m%d') &lt;= date_format(#{param.endTime},'%y%m%d')
+        </if>
+        <if test="param.beginReceiveTime != null and param.beginReceiveTime !='' ">
+            and date_format(c.receive_time,'%y%m%d%H%i%s') &gt;= date_format(#{param.beginReceiveTime},'%y%m%d%H%i%s')
+        </if>
+        <if test="param.endReceiveTime != null and param.endReceiveTime !='' ">
+            and date_format(c.receive_time,'%y%m%d%H%i%s') &lt;= date_format(#{param.endReceiveTime},'%y%m%d%H%i%s')
+        </if>
+        <if test="param.companyUserNickName != null and  param.companyUserNickName !='' ">
+            and cu.nick_name like CONCAT('%',#{param.companyUserNickName},'%')
+        </if>
+        )
+        SELECT r.*,cu.nick_name as company_user_nick_name
+        FROM RankedCustomers r
+        left join company_user cu on cu.user_id=r.receive_user_id
+        WHERE r.rn = 1
+        <if test="param.orderBy != null and param.orderBy != ''">
+            order by r.next_time ${param.orderBy} ,r.`time` asc
+        </if>
+        <if test="param.orderBy == null or param.orderBy == ''">
+            order by r.`time` asc
+        </if>
+        limit #{param.start}, #{param.pageSize};
+    </select>
+
+    <select id="countCrmCustomerListByUnPool" resultType="long">
+        WITH RankedCustomers AS (
+        select c.*,ROW_NUMBER() OVER (PARTITION BY c.customer_id ORDER BY c.create_time DESC) AS rn from (
+        <if test="param1 != null">
+            (SELECT *,
+            (receive_time+ INTERVAL ${param1.limitDay} DAY) AS pooling_time,
+            TIMESTAMPDIFF(MINUTE, NOW(), (receive_time + INTERVAL ${param1.limitDay} DAY)) AS `time`
+            from crm_customer
+            WHERE is_del = 0
+            AND is_pool = 0
+            AND is_receive = 1
+            <if test="param2 != null">
+                AND visit_status IS NULL
+                AND receive_user_id IS NOT NULL
+                AND customer_id NOT IN (SELECT v.customer_id FROM `crm_customer_visit` v GROUP BY v.customer_id HAVING v.customer_id is NOT NULL)
+            </if>
+            )
+
+        </if>
+        <if test="param2 != null">
+            UNION(
+            SELECT *,
+            (GREATEST(
+            IFNULL(visit_time, '1000-01-01'),
+            IFNULL(sys_visit_time, '1000-01-01')
+            ) + INTERVAL ${param2.limitDay} DAY) AS pooling_time,
+            TIMESTAMPDIFF(
+            MINUTE,
+            NOW(),
+            (GREATEST(
+            IFNULL(visit_time, '1000-01-01'),
+            IFNULL(sys_visit_time, '1000-01-01')
+            ) + INTERVAL ${param2.limitDay} DAY)
+            ) AS `time`
+            FROM crm_customer
+            WHERE is_del = 0
+            AND is_pool = 0
+            AND visit_status IS NOT NULL
+            AND visit_time IS NOT NULL
+            )
+        </if>
+        <if test="param3!=null">
+            UNION(SELECT c.*,
+            (c.create_time + INTERVAL ${param3.limitDay} DAY) AS pooling_time,
+            TIMESTAMPDIFF(MINUTE, NOW(), (c.create_time + INTERVAL ${param3.limitDay} DAY)) AS `time`
+            FROM `crm_customer` c
+            WHERE c.is_del = 0
+            AND c.is_pool = 0
+            AND c.customer_id NOT IN (
+            SELECT b.customer_id FROM crm_business b WHERE b.create_time > (NOW()-INTERVAL ${param3.limitDay} DAY)
+            AND b.customer_id
+            IS NOT NULL GROUP BY b.customer_id)
+            )
+        </if>
+        ) as c
+        where c.is_del = 0 and c.is_pool_rule = 1
+        <if test="param.customerCode != null  and param.customerCode != ''"> and c.customer_code = #{param.customerCode}</if>
+        <if test="param.customerName != null  and param.customerName !=''"> and c.customer_name like concat('%', #{param.customerName}, '%')</if>
+        <if test="param.customerCompanyName != null  and param.customerCompanyName !=''"> and c.customer_company_name like concat('%', #{param.customerCompanyName}, '%')</if>
+        <if test="param.mobile != null  and param.mobile != ''"> and c.mobile= #{param.mobile}</if>
+        <if test="param.sex != null "> and c.sex =#{param.sex}</if>
+        <if test="param.weixin != null  and param.weixin != ''"> and c.weixin= #{param.weixin}</if>
+        <if test="param.createUserId != null "> and c.create_user_id = #{param.createUserId}</if>
+        <if test="param.receiveUserId != null "> and c.receive_user_id = #{param.receiveUserId}</if>
+        <if test="param.customerUserId != null "> and c.customer_user_id = #{param.customerUserId}</if>
+        <if test="param.deptId != null "> and c.dept_id = #{deptId}</if>
+        <if test="param.customerType != null "> and c.customer_type = #{param.customerType}</if>
+        <if test="param.companyId != null "> and c.company_id = #{param.companyId}</if>
+        <if test="param.source != null and param.source != ''"> and c.source = #{param.source}</if>
+        <if test="param.tags != null  and param.tags != ''"> and c.tags = #{param.tags}</if>
+        <if test="param.beginTime != null and param.beginTime !='' ">
+            and date_format(c.create_time,'%y%m%d') &gt;= date_format(#{param.beginTime},'%y%m%d')
+        </if>
+        <if test="param.endTime != null and param.endTime !='' ">
+            and date_format(c.create_time,'%y%m%d') &lt;= date_format(#{param.endTime},'%y%m%d')
+        </if>
+        <if test="param.beginReceiveTime != null and param.beginReceiveTime !='' ">
+            and date_format(c.receive_time,'%y%m%d%H%i%s') &gt;= date_format(#{param.beginReceiveTime},'%y%m%d%H%i%s')
+        </if>
+        <if test="param.endReceiveTime != null and param.endReceiveTime !='' ">
+            and date_format(c.receive_time,'%y%m%d%H%i%s') &lt;= date_format(#{param.endReceiveTime},'%y%m%d%H%i%s')
+        </if>
+        <if test="param.companyUserNickName != null and  param.companyUserNickName !='' ">
+            and cu.nick_name like CONCAT('%',#{param.companyUserNickName},'%')
+        </if>
+        )
+        SELECT count(1)
+        FROM RankedCustomers r
+        left join company_user cu on cu.user_id=r.receive_user_id
+        WHERE r.rn = 1
+
+        ;
+    </select>
+
+    <update id="recoverCrmLineCustomerList">
+        update crm_customer c set  c.is_pool = 1,c.pool_time = now()
+        <where>
+            <if test="maps.isLine != null">
+                and c.is_line = #{maps.isLine}
+            </if>
+            <if test="maps.customerCode != null and  maps.customerCode !='' ">
+                and c.customer_code like CONCAT('%',#{maps.customerCode},'%')
+            </if>
+            <if test="maps.customerName != null and  maps.customerName !='' ">
+                and c.customer_name like CONCAT('%',#{maps.customerName},'%')
+            </if>
+            <if test="maps.mobile != null and  maps.mobile !='' ">
+                and c.mobile like CONCAT('%',#{maps.mobile},'%')
+            </if>
+            <if test="maps.receiveUserId == -1">
+                and c.receive_user_id is NULL
+            </if>
+            <if test="maps.receiveUserId == 0">
+                and c.receive_user_id is NOT NULL
+            </if>
+            <if test="maps.receiveUserId != null and maps.receiveUserId != 0 and maps.receiveUserId != -1">
+                and c.receive_user_id = #{maps.receiveUserId}
+            </if>
+            <if test="maps.status != null and maps.status !='' ">
+                and c.status =#{maps.status}
+            </if>
+            <if test="maps.isReceive != null ">
+                and c.is_receive =#{maps.isReceive}
+            </if>
+            <if test="maps.customerType != null ">
+                and c.customer_type =#{maps.customerType}
+            </if>
+            <if test="maps.companyId != null ">
+                and c.company_id =#{maps.companyId}
+            </if>
+            <if test="maps.source != null and maps.source !='' ">
+                and c.source =#{maps.source}
+            </if>
+            <if test="maps.tags != null ">
+                and
+                <foreach collection="maps.tags.split(',')" item="tag" open="(" close=")" separator="OR">
+                    find_in_set(#{tag},c.tags)
+                </foreach>
+            </if>
+            <if test="maps.isPool != null ">
+                and c.is_pool = #{maps.isPool}
+            </if>
+            <if test="maps.createTime != null ">
+                and  DATE_FORMAT(c.create_time, '%Y-%m-%d')  = DATE_FORMAT(#{maps.createTime}, '%Y-%m-%d')
+            </if>
+            <if test="maps.beginTime != null and maps.beginTime !='' ">
+                and date_format(c.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d')
+            </if>
+            <if test="maps.endTime != null and maps.endTime !='' ">
+                and date_format(c.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d')
+            </if>
+        </where>
+    </update>
+
+    <select id="selectCrmCustomerByCondition" resultType="com.fs.crm.domain.CrmCustomer">
+        <include refid="selectCrmCustomerVo"/>
+        where is_del = 0
+        <if test="(customerCompanyName != null and  customerCompanyName !='') and (mobile != null and  mobile !='')">
+            and(
+            <if test="customerCompanyName != null and  customerCompanyName !=''">
+                customer_company_name like #{customerCompanyName}
+            </if>
+            <if test="mobile != null and  mobile !=''">
+                or mobile like #{mobile}
+            </if>
+            )
+        </if>
+        <if test="(customerCompanyName != null and  customerCompanyName !='') and (mobile == null or  mobile =='')">
+            and customer_company_name like #{customerCompanyName}
+        </if>
+        <if test="(customerCompanyName == null or  customerCompanyName =='') and (mobile != null and  mobile !='')">
+            and mobile like #{mobile}
+        </if>
+
+    </select>
 
 </mapper>

+ 11 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyMapper.xml

@@ -137,6 +137,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         limit 1
     </select>
 
+    <select id="selectCrmCustomerPropertyByCustomerIds" resultMap="CrmCustomerPropertyResult">
+        <include refid="selectCrmCustomerPropertyVo"/>
+        where customer_id in
+        <foreach item="customerId" collection="list" open="(" separator="," close=")">
+            #{customerId}
+        </foreach>
+        and deleted = 0
+        order by customer_id desc, id desc
+    </select>
+
+
     <delete id="deleteByCustomerIdAndPropertyId">
         delete from crm_customer_property where customer_id = #{customerId} and property_id = #{propertyId}
     </delete>

+ 36 - 1
fs-service/src/main/resources/mapper/fastGpt/FastgptChatQuestionMapper.xml

@@ -18,13 +18,45 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="userContent"    column="user_content"    />
         <result property="companyUserContent"    column="company_user_content"    />
         <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
         <result property="questionStatisticsId"    column="question_statistics_id"    />
     </resultMap>
 
     <sql id="selectFastgptChatQuestionVo">
-        select id, session_id, msg_id, ext_id, user_id, company_id, company_user_id, role_id, nick_name, user_type, user_content, company_user_content, create_time, question_statistics_id from fastgpt_chat_question
+        select id, session_id, msg_id, ext_id, user_id, company_id, company_user_id, role_id, nick_name, user_type, user_content, company_user_content, create_time, update_time, question_statistics_id from fastgpt_chat_question
     </sql>
 
+    <select id="selectFastgptChatQuestionDetailVOList" parameterType="FastgptChatQuestion" resultType="com.fs.fastGpt.vo.FastgptChatQuestionDetailVO">
+        select
+            q.id,
+            q.session_id,
+            q.msg_id,
+            q.ext_id,
+            q.user_id,
+            q.company_id,
+            q.company_user_id,
+            q.role_id,
+            q.nick_name,
+            q.user_type,
+            q.user_content,
+            q.company_user_content,
+            q.create_time,
+            q.update_time,
+            q.question_statistics_id,
+            c.company_name,
+            cu.nick_name as company_user_nick_name,
+            ec.id as external_contact_id,
+            ec.name as external_contact_name
+        from fastgpt_chat_question q
+        left join company c on c.company_id = q.company_id
+        left join company_user cu on cu.user_id = q.company_user_id
+        left join qw_external_contact ec on ec.id = q.ext_id
+        <where>
+            <if test="questionStatisticsId != null"> and q.question_statistics_id = #{questionStatisticsId}</if>
+        </where>
+        order by q.create_time desc, q.id desc
+    </select>
+
     <select id="selectFastgptChatQuestionList" parameterType="FastgptChatQuestion" resultMap="FastgptChatQuestionResult">
         <include refid="selectFastgptChatQuestionVo"/>
         <where>
@@ -63,6 +95,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="userContent != null">user_content,</if>
             <if test="companyUserContent != null">company_user_content,</if>
             <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
             <if test="questionStatisticsId != null">question_statistics_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
@@ -78,6 +111,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="userContent != null">#{userContent},</if>
             <if test="companyUserContent != null">#{companyUserContent},</if>
             <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
             <if test="questionStatisticsId != null">#{questionStatisticsId},</if>
          </trim>
     </insert>
@@ -97,6 +131,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="userContent != null">user_content = #{userContent},</if>
             <if test="companyUserContent != null">company_user_content = #{companyUserContent},</if>
             <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
             <if test="questionStatisticsId != null">question_statistics_id = #{questionStatisticsId},</if>
         </trim>
         where id = #{id}

+ 17 - 0
fs-service/src/main/resources/mapper/fastGpt/FastgptChatQuestionStatisticsMapper.xml

@@ -101,10 +101,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         LIMIT 1
     </select>
 
+    <select id="selectCandidatesBySimhash" resultMap="FastgptChatQuestionStatisticsResult">
+        SELECT s.*,
+               BIT_COUNT(s.simhash ^ #{simhash}) AS dist
+        FROM fastgpt_chat_question_statistics s
+        WHERE s.simhash IS NOT NULL
+        HAVING dist &lt;= #{threshold}
+        ORDER BY dist ASC, s.frequency DESC, s.id DESC
+        LIMIT #{limit}
+    </select>
+
     <update id="incrementFrequencyById">
         update fastgpt_chat_question_statistics
         set frequency = frequency + 1,
             update_time = #{updateTime}
         where id = #{id}
     </update>
+
+    <select id="selectFirstByQuestionCategory" resultMap="FastgptChatQuestionStatisticsResult">
+        <include refid="selectFastgptChatQuestionStatisticsVo"/>
+        where question_category = #{questionCategory}
+        order by id asc
+        limit 1
+    </select>
 </mapper>

+ 311 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductActivityMapper.xml

@@ -0,0 +1,311 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.hisStore.mapper.FsStoreProductActivityMapper">
+
+    <resultMap type="com.fs.hisStore.domain.FsStoreProductActivity" id="FsStoreProductActivityResult">
+        <result property="id" column="id"/>
+        <result property="productId" column="product_id"/>
+        <result property="activityType" column="activity_type"/>
+        <result property="specId" column="spec_id"/>
+        <result property="originalPrice" column="original_price"/>
+        <result property="flashPrice" column="flash_price"/>
+        <result property="discount" column="discount"/>
+        <result property="discountPrice" column="discount_price"/>
+        <result property="startTime" column="start_time"/>
+        <result property="endTime" column="end_time"/>
+        <result property="status" column="status"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+        <!-- 关联查询字段 -->
+        <result property="productName" column="product_name"/>
+        <result property="productImage" column="image"/>
+        <result property="price" column="price"/>
+        <result property="otPrice" column="ot_price"/>
+        <result property="sales" column="sales"/>
+        <result property="productStock" column="product_stock"/>
+        <result property="cateName" column="cate_name"/>
+        <result property="barCode" column="bar_code"/>
+        <result property="productInfo" column="product_info"/>
+        <result property="sliderImage" column="slider_image"/>
+        <result property="specName" column="spec_name"/>
+        <result property="image" column="image"/>
+        <result property="specStock" column="spec_stock"/>
+    </resultMap>
+
+    <sql id="selectActivityVo">
+        select a.id,
+               a.product_id,
+               a.activity_type,
+               a.spec_id,
+               a.original_price,
+               a.flash_price,
+               a.discount,
+               a.discount_price,
+               a.start_time,
+               a.end_time,
+               a.status,
+               a.del_flag,
+               a.create_by,
+               a.create_time,
+               a.update_by,
+               a.update_time,
+               a.remark,
+               p.product_name,
+               p.image,
+               p.price,
+               p.ot_price,
+               p.sales,
+               p.bar_code,
+               p.product_info,
+               p.slider_image,
+               v.image,
+               p.product_name     AS cateName,
+               ifnull(p.stock, 0) as product_stock,
+               v.sku              as spec_name,
+               ifnull(v.stock, 0) as spec_stock
+        from fs_store_product_activity a
+                 left join fs_store_product_scrm p on a.product_id = p.product_id
+                 left join fs_store_product_attr_value_scrm v on a.spec_id = v.id
+    </sql>
+
+    <select id="selectFsStoreProductActivityById" parameterType="Long" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.id = #{id} and a.del_flag = 0
+    </select>
+
+    <select id="selectFsStoreProductActivityList" parameterType="com.fs.hisStore.domain.FsStoreProductActivity"
+            resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        <where>
+            a.del_flag = 0
+            <if test="productId != null">AND a.product_id = #{productId}</if>
+            <if test="activityType != null">AND a.activity_type = #{activityType}</if>
+            <if test="status != null">AND a.status = #{status}</if>
+            <if test="productName != null and productName != ''">AND p.product_name like concat('%', #{productName},
+                '%')
+            </if>
+        </where>
+        order by a.create_time desc
+    </select>
+
+    <select id="selectByProductId" parameterType="Long" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.product_id = #{productId} and a.del_flag = 0
+        order by v.id asc
+    </select>
+
+    <select id="selectByProductIdAndSpecId" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.product_id = #{productId} and a.spec_id = #{specId} and a.del_flag = 0
+        limit 1
+    </select>
+
+    <select id="selectUpcomingList" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.del_flag = 0
+        and a.start_time &lt;= date_add(now(), interval 1 hour)
+        and a.end_time > now()
+        and p.is_show = 1
+        and p.is_del = 0
+        <if test="activityType != null">AND a.activity_type = #{activityType}</if>
+        order by a.start_time asc
+    </select>
+
+    <select id="selectExpiredList" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.del_flag = 0
+        and a.status = 1
+        and a.end_time &lt; now()
+    </select>
+
+    <select id="selectStartingList" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.del_flag = 0
+        and a.start_time - INTERVAL 1 HOUR &lt;= now()
+        and a.end_time > now()
+    </select>
+
+    <select id="selectOngoingActivity" parameterType="Long" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.product_id = #{productId}
+        and a.del_flag = 0
+        and a.start_time &lt;= now()
+        and a.end_time > now()
+        limit 1
+    </select>
+
+    <insert id="insertFsStoreProductActivity" parameterType="com.fs.hisStore.domain.FsStoreProductActivity"
+            useGeneratedKeys="true" keyProperty="id">
+        insert into fs_store_product_activity
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="productId != null">product_id,</if>
+            <if test="activityType != null">activity_type,</if>
+            <if test="specId != null">spec_id,</if>
+            <if test="originalPrice != null">original_price,</if>
+            <if test="flashPrice != null">flash_price,</if>
+            <if test="discount != null">discount,</if>
+            <if test="discountPrice != null">discount_price,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="endTime != null">end_time,</if>
+            <if test="status != null">status,</if>
+            <if test="delFlag != null">del_flag,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="productId != null">#{productId},</if>
+            <if test="activityType != null">#{activityType},</if>
+            <if test="specId != null">#{specId},</if>
+            <if test="originalPrice != null">#{originalPrice},</if>
+            <if test="flashPrice != null">#{flashPrice},</if>
+            <if test="discount != null">#{discount},</if>
+            <if test="discountPrice != null">#{discountPrice},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="endTime != null">#{endTime},</if>
+            <if test="status != null">#{status},</if>
+            <if test="delFlag != null">#{delFlag},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <insert id="batchInsertActivity" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_store_product_activity
+        (product_id, activity_type, spec_id, original_price, flash_price, discount, discount_price, start_time,
+        end_time, status, create_time)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.productId}, #{item.activityType}, #{item.specId}, #{item.originalPrice},
+            #{item.flashPrice}, #{item.discount}, #{item.discountPrice},
+            #{item.startTime}, #{item.endTime}, #{item.status}, now())
+        </foreach>
+    </insert>
+
+    <update id="updateFsStoreProductActivity" parameterType="com.fs.hisStore.domain.FsStoreProductActivity">
+        update fs_store_product_activity
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="productId != null">product_id = #{productId},</if>
+            <if test="activityType != null">activity_type = #{activityType},</if>
+            <if test="specId != null">spec_id = #{specId},</if>
+            <if test="originalPrice != null">original_price = #{originalPrice},</if>
+            <if test="flashPrice != null">flash_price = #{flashPrice},</if>
+            <if test="discount != null">discount = #{discount},</if>
+            <if test="discountPrice != null">discount_price = #{discountPrice},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="endTime != null">end_time = #{endTime},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="delFlag != null">del_flag = #{delFlag},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="deleteFsStoreProductActivityById" parameterType="Long">
+        update fs_store_product_activity
+        set del_flag = 1
+        where id = #{id}
+    </update>
+
+    <update id="deleteByProductId">
+        update fs_store_product_activity
+        set del_flag = 1
+        where product_id = #{productId}
+          and del_flag = 0
+    </update>
+
+    <update id="updateStatus">
+        UPDATE fs_store_product_activity
+        SET status      = #{status},
+            update_time = now()
+        WHERE id = #{id}
+    </update>
+
+    <update id="updateProductActivityType">
+        UPDATE fs_store_product_scrm
+        SET activity_type       = #{activityType},
+            activity_start_time = #{activityStartTime},
+            activity_end_time   = #{activityEndTime}
+        WHERE product_id = #{productId}
+    </update>
+
+    <!-- ===== 小程序端查询方法 ===== -->
+
+    <!-- 查询秒杀活动列表(1小时内即将开抢+未过期) -->
+    <select id="selectUpcomingFlashSaleActivityList" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.del_flag = 0
+        and a.activity_type = 6
+        and a.start_time &lt;= date_add(now(), interval 1 hour)
+        and a.end_time &gt; now()
+        and p.is_show = 1
+        and p.is_del = 0
+        order by a.start_time asc
+    </select>
+
+    <!-- 查询折扣活动列表(1小时内即将开抢+未过期) -->
+    <select id="selectUpcomingDiscountActivityList" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.del_flag = 0
+        and a.activity_type = 7
+        and a.start_time &lt;= date_add(now(), interval 1 hour)
+        and a.end_time &gt; now()
+        and p.is_show = 1
+        and p.is_del = 0
+        order by a.start_time asc
+    </select>
+
+    <!-- 按活动ID查询详情(JOIN商品表获取完整信息) -->
+    <select id="selectActivityDetailById" parameterType="Long" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.id = #{id}
+        and a.del_flag = 0
+    </select>
+
+    <!-- 按商品ID查询当前进行中的秒杀活动 -->
+    <select id="selectFlashSaleActivityByProductId" parameterType="Long" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.product_id = #{productId}
+        and a.del_flag = 0
+        and a.activity_type = 6
+        and now() between a.start_time and a.end_time
+    </select>
+
+    <!-- 按商品ID查询当前进行中的折扣活动 -->
+    <select id="selectDiscountActivityByProductId" parameterType="Long" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.product_id = #{productId}
+        and a.del_flag = 0
+        and a.activity_type = 7
+        and now() between a.start_time and a.end_time
+    </select>
+
+    <!-- 按商品ID和活动类型查询所有参与活动的规格(用于详情页返回规格数组) -->
+    <select id="selectActivitySpecsByProductIdAndType" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.product_id = #{productId}
+          and a.del_flag = 0
+          and a.activity_type = #{activityType}
+          and a.start_time &lt;= date_add(now(), interval 1 hour)
+          and a.end_time &gt; now()
+        order by a.spec_id asc
+    </select>
+
+    <select id="selectActivityByProductIdAndSpecId" resultMap="FsStoreProductActivityResult">
+        <include refid="selectActivityVo"/>
+        where a.product_id = #{productId}
+          and a.del_flag = 0
+          and a.spec_id = #{specId}
+    </select>
+</mapper>

+ 215 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductDiscountMapper.xml

@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.hisStore.mapper.FsStoreProductDiscountMapper">
+
+    <resultMap type="com.fs.hisStore.domain.FsStoreProductDiscount" id="FsStoreProductDiscountResult">
+        <result property="id" column="id"/>
+        <result property="productId" column="product_id"/>
+        <result property="stock" column="stock"/>
+        <result property="startTime" column="start_time"/>
+        <result property="endTime" column="end_time"/>
+        <result property="originalPrice" column="original_price"/>
+        <result property="discount" column="discount"/>
+        <result property="discountPrice" column="discount_price"/>
+        <result property="status" column="status"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+        <result property="productName" column="product_name"/>
+        <result property="productImage" column="image"/>
+        <result property="price" column="price"/>
+        <result property="otPrice" column="ot_price"/>
+        <result property="sales" column="sales"/>
+        <result property="productStock" column="product_stock"/>
+        <result property="cateName" column="cate_name"/>
+        <result property="barCode" column="bar_code"/>
+        <result property="productInfo" column="product_info"/>
+        <result property="sliderImage" column="slider_image"/>
+    </resultMap>
+
+    <sql id="selectFsStoreProductDiscountVo">
+        select d.id, d.product_id, d.stock, d.start_time, d.end_time, d.original_price, d.discount, d.discount_price,
+               d.status, d.del_flag, d.create_by, d.create_time, d.update_by, d.update_time, d.remark,
+               p.product_name, p.image, p.price, p.ot_price, p.sales, p.bar_code, p.product_info, p.slider_image,
+               ifnull(p.stock, 0) as product_stock
+        from fs_store_product_discount d
+        left join fs_store_product_scrm p on d.product_id = p.product_id
+    </sql>
+
+    <select id="selectFsStoreProductDiscountById" parameterType="Long" resultMap="FsStoreProductDiscountResult">
+        <include refid="selectFsStoreProductDiscountVo"/>
+        where d.id = #{id} and d.del_flag = 0
+    </select>
+
+    <select id="selectFsStoreProductDiscountList" parameterType="com.fs.hisStore.domain.FsStoreProductDiscount" resultMap="FsStoreProductDiscountResult">
+        <include refid="selectFsStoreProductDiscountVo"/>
+        <where>
+            d.del_flag = 0
+            <if test="productId != null">
+                AND d.product_id = #{productId}
+            </if>
+            <if test="status != null">
+                AND d.status = #{status}
+            </if>
+            <if test="productName != null and productName != ''">
+                AND p.product_name like concat('%', #{productName}, '%')
+            </if>
+            <if test="startTime != null">
+                AND date_format(d.start_time,'%y%m%d') &gt;= date_format(#{startTime},'%y%m%d')
+            </if>
+            <if test="endTime != null">
+                AND date_format(d.end_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
+            </if>
+        </where>
+        order by d.create_time desc
+    </select>
+
+    <select id="selectActiveDiscountList" resultMap="FsStoreProductDiscountResult">
+        <include refid="selectFsStoreProductDiscountVo"/>
+        where d.del_flag = 0
+          and d.status = 1
+          and d.stock > 0
+          and now() between d.start_time and d.end_time
+          and p.is_show = 1
+          and p.is_del = 0
+        order by d.create_time desc
+    </select>
+
+    <select id="selectDiscountByProductId" parameterType="Long" resultMap="FsStoreProductDiscountResult">
+        <include refid="selectFsStoreProductDiscountVo"/>
+        where d.product_id = #{productId}
+          and d.del_flag = 0
+          and d.status = 1
+          and now() between d.start_time and d.end_time
+        limit 1
+    </select>
+
+    <select id="selectUpcomingDiscountList" resultMap="FsStoreProductDiscountResult">
+        <include refid="selectFsStoreProductDiscountVo"/>
+        where d.del_flag = 0
+          and d.status = 1
+          and d.stock > 0
+          and d.start_time &lt;= date_add(now(), interval 1 hour)
+          and d.end_time &gt; now()
+          and p.is_show = 1
+          and p.is_del = 0
+        order by d.start_time asc
+    </select>
+
+    <insert id="insertFsStoreProductDiscount" parameterType="com.fs.hisStore.domain.FsStoreProductDiscount" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_store_product_discount
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="productId != null">product_id,</if>
+            <if test="stock != null">stock,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="endTime != null">end_time,</if>
+            <if test="originalPrice != null">original_price,</if>
+            <if test="discount != null">discount,</if>
+            <if test="discountPrice != null">discount_price,</if>
+            <if test="status != null">status,</if>
+            <if test="delFlag != null">del_flag,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="productId != null">#{productId},</if>
+            <if test="stock != null">#{stock},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="endTime != null">#{endTime},</if>
+            <if test="originalPrice != null">#{originalPrice},</if>
+            <if test="discount != null">#{discount},</if>
+            <if test="discountPrice != null">#{discountPrice},</if>
+            <if test="status != null">#{status},</if>
+            <if test="delFlag != null">#{delFlag},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsStoreProductDiscount" parameterType="com.fs.hisStore.domain.FsStoreProductDiscount">
+        update fs_store_product_discount
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="productId != null">product_id = #{productId},</if>
+            <if test="stock != null">stock = #{stock},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="endTime != null">end_time = #{endTime},</if>
+            <if test="originalPrice != null">original_price = #{originalPrice},</if>
+            <if test="discount != null">discount = #{discount},</if>
+            <if test="discountPrice != null">discount_price = #{discountPrice},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="delFlag != null">del_flag = #{delFlag},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="deleteFsStoreProductDiscountById" parameterType="Long">
+        update fs_store_product_discount set del_flag = 1 where id = #{id}
+    </update>
+
+    <update id="deleteFsStoreProductDiscountByIds" parameterType="String">
+        update fs_store_product_discount set del_flag = 1 where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
+    <select id="getProductDiscountInfoByIds" resultType="com.fs.hisStore.domain.FsStoreProductDiscount">
+        SELECT
+            id,
+            stock
+        FROM
+            fs_store_product_discount
+        WHERE
+            id
+        IN
+            <foreach item="id" collection="ids" index="index"  open="(" separator="," close=")">
+                #{id}
+            </foreach>
+    </select>
+
+    <update id="batchUpdateStock">
+        <foreach collection="list" item="item" separator=";">
+            UPDATE fs_store_product_discount SET stock = #{item.stock} WHERE id = #{item.id}
+        </foreach>
+    </update>
+
+    <select id="selectExpiredDiscountList" resultMap="FsStoreProductDiscountResult">
+        <include refid="selectFsStoreProductDiscountVo"/>
+        where d.del_flag = 0
+          and d.status = 1
+          and d.end_time &lt; now()
+    </select>
+
+    <select id="selectStartingDiscountList" resultMap="FsStoreProductDiscountResult">
+        <include refid="selectFsStoreProductDiscountVo"/>
+        where d.del_flag = 0
+          and d.status = 0
+          and d.start_time - INTERVAL 1 HOUR &lt;= now()
+          and d.end_time &gt; now()
+          and d.stock > 0
+    </select>
+
+    <update id="updateStatus">
+        UPDATE fs_store_product_discount 
+        SET status = #{status}, update_time = now() 
+        WHERE id = #{id}
+    </update>
+
+    <update id="increaseStock">
+        UPDATE fs_store_product_discount
+        SET stock = stock + #{num}, update_time = now()
+        WHERE id = #{id}
+    </update>
+</mapper>

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott