浏览代码

商城相关活动代码提交

yjwang 1 周之前
父节点
当前提交
11270a1065
共有 46 个文件被更改,包括 5550 次插入36 次删除
  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. 14 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockData.java
  7. 22 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockDataProvider.java
  8. 711 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityStockService.java
  9. 26 0
      fs-common/src/main/java/com/fs/common/core/redis/service/ActivityValidateResult.java
  10. 18 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  11. 154 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductActivity.java
  12. 128 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductDiscount.java
  13. 123 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductFlashSale.java
  14. 12 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  15. 8 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  16. 131 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductActivityMapper.java
  17. 109 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductDiscountMapper.java
  18. 109 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductFlashSaleMapper.java
  19. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  20. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreConfirmOrderParam.java
  21. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java
  22. 3 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductQueryParam.java
  23. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  24. 99 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductActivityService.java
  25. 94 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductDiscountService.java
  26. 94 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductFlashSaleService.java
  27. 8 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  28. 119 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/ActivityStockDataProviderImpl.java
  29. 55 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  30. 527 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  31. 244 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductActivityServiceImpl.java
  32. 144 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductDiscountServiceImpl.java
  33. 144 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductFlashSaleServiceImpl.java
  34. 74 26
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  35. 7 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java
  36. 10 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductQueryVO.java
  37. 20 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsUnsyncOrderVO.java
  38. 311 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductActivityMapper.xml
  39. 215 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductDiscountMapper.xml
  40. 210 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductFlashSaleMapper.xml
  41. 20 8
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  42. 303 0
      fs-user-app/src/main/java/com/fs/app/controller/store/FlashSaleConcurrentTestController.java
  43. 363 0
      fs-user-app/src/main/java/com/fs/app/controller/store/FsStoreProductDiscountScrmController.java
  44. 348 0
      fs-user-app/src/main/java/com/fs/app/controller/store/FsStoreProductFlashSaleScrmController.java
  45. 28 0
      fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java
  46. 9 0
      fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

+ 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()

+ 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);
+    }
+}

+ 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

@@ -108,6 +108,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

@@ -180,6 +180,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;
 
@@ -244,6 +245,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;
 
@@ -446,6 +453,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
@@ -781,6 +797,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);
@@ -933,6 +967,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());
             //生成分布式唯一值
@@ -1236,6 +1286,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);
@@ -1356,6 +1822,14 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
     }
 
+    /**
+     * @deprecated 已改用deStockIncSale统一扣减
+     */
+    @Deprecated
+    private void decActivityProductStock(List<FsStoreCartQueryVO> cartInfo) {
+        deStockIncSale(cartInfo);
+    }
+
     //未支付取消订单
     @Override
     public void cancelOrder(Long orderId) {
@@ -2646,6 +3120,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());
@@ -3464,6 +3944,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());
@@ -3471,6 +3958,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);
+            // 不中断退款流程
+        }
+    }
+
 
     /**
      * 获取订单价格
@@ -6431,7 +6958,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;
+}

+ 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>

+ 210 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductFlashSaleMapper.xml

@@ -0,0 +1,210 @@
+<?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.FsStoreProductFlashSaleMapper">
+
+    <resultMap type="com.fs.hisStore.domain.FsStoreProductFlashSale" id="FsStoreProductFlashSaleResult">
+        <result property="id" column="id"/>
+        <result property="productId" column="product_id"/>
+        <result property="originalPrice" column="original_price"/>
+        <result property="flashPrice" column="flash_price"/>
+        <result property="stock" column="stock"/>
+        <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"/>
+    </resultMap>
+
+    <sql id="selectFsStoreProductFlashSaleVo">
+        select f.id, f.product_id, f.original_price, f.flash_price, f.stock, f.start_time, f.end_time,
+               f.status, f.del_flag, f.create_by, f.create_time, f.update_by, f.update_time, f.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_flash_sale f
+        left join fs_store_product_scrm p on f.product_id = p.product_id
+    </sql>
+
+    <select id="selectFsStoreProductFlashSaleById" parameterType="Long" resultMap="FsStoreProductFlashSaleResult">
+        <include refid="selectFsStoreProductFlashSaleVo"/>
+        where f.id = #{id} and f.del_flag = 0
+    </select>
+
+    <select id="selectFsStoreProductFlashSaleList" parameterType="com.fs.hisStore.domain.FsStoreProductFlashSale" resultMap="FsStoreProductFlashSaleResult">
+        <include refid="selectFsStoreProductFlashSaleVo"/>
+        <where>
+            f.del_flag = 0
+            <if test="productId != null">
+                AND f.product_id = #{productId}
+            </if>
+            <if test="status != null">
+                AND f.status = #{status}
+            </if>
+            <if test="productName != null and productName != ''">
+                AND p.product_name like concat('%', #{productName}, '%')
+            </if>
+            <if test="startTime != null">
+                AND date_format(f.start_time,'%y%m%d') &gt;= date_format(#{startTime},'%y%m%d')
+            </if>
+            <if test="endTime != null">
+                AND date_format(f.end_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
+            </if>
+        </where>
+        order by f.create_time desc
+    </select>
+
+    <select id="selectActiveFlashSaleList" resultMap="FsStoreProductFlashSaleResult">
+        <include refid="selectFsStoreProductFlashSaleVo"/>
+        where f.del_flag = 0
+          and f.status = 1
+          and f.stock > 0
+          and now() between f.start_time and f.end_time
+          and p.is_show = 1
+          and p.is_del = 0
+        order by f.create_time desc
+    </select>
+
+    <select id="selectFlashSaleByProductId" parameterType="Long" resultMap="FsStoreProductFlashSaleResult">
+        <include refid="selectFsStoreProductFlashSaleVo"/>
+        where f.product_id = #{productId}
+          and f.del_flag = 0
+          and f.status = 1
+          and now() between f.start_time and f.end_time
+        limit 1
+    </select>
+
+    <select id="selectUpcomingFlashSaleList" resultMap="FsStoreProductFlashSaleResult">
+        <include refid="selectFsStoreProductFlashSaleVo"/>
+        where f.del_flag = 0
+          and f.status = 1
+          and f.stock > 0
+          and f.start_time &lt;= date_add(now(), interval 1 hour)
+          and f.end_time &gt; now()
+          and p.is_show = 1
+          and p.is_del = 0
+        order by f.start_time asc
+    </select>
+
+    <insert id="insertFsStoreProductFlashSale" parameterType="com.fs.hisStore.domain.FsStoreProductFlashSale" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_store_product_flash_sale
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="productId != null">product_id,</if>
+            <if test="originalPrice != null">original_price,</if>
+            <if test="flashPrice != null">flash_price,</if>
+            <if test="stock != null">stock,</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="originalPrice != null">#{originalPrice},</if>
+            <if test="flashPrice != null">#{flashPrice},</if>
+            <if test="stock != null">#{stock},</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>
+
+    <update id="updateFsStoreProductFlashSale" parameterType="com.fs.hisStore.domain.FsStoreProductFlashSale">
+        update fs_store_product_flash_sale
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="productId != null">product_id = #{productId},</if>
+            <if test="originalPrice != null">original_price = #{originalPrice},</if>
+            <if test="flashPrice != null">flash_price = #{flashPrice},</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="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="deleteFsStoreProductFlashSaleById" parameterType="Long">
+        update fs_store_product_flash_sale set del_flag = 1 where id = #{id}
+    </update>
+
+    <update id="deleteFsStoreProductFlashSaleByIds" parameterType="String">
+        update fs_store_product_flash_sale set del_flag = 1 where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
+    <select id="getProductFlashSaleInfoByIds" resultType="com.fs.hisStore.domain.FsStoreProductFlashSale">
+        SELECT
+            id,
+            stock
+        FROM
+            fs_store_product_flash_sale
+        WHERE
+            id IN
+        <foreach collection="ids" item="item" index="index" open="(" separator="," close=")">
+            #{item}
+        </foreach>
+    </select>
+
+    <update id="batchUpdateStock">
+        <foreach collection="list" item="item" separator=";">
+            UPDATE fs_store_product_flash_sale SET stock = #{item.stock} WHERE id = #{item.id}
+        </foreach>
+    </update>
+
+    <select id="selectExpiredFlashSaleList" resultMap="FsStoreProductFlashSaleResult">
+        <include refid="selectFsStoreProductFlashSaleVo"/>
+        where f.del_flag = 0
+          and f.status = 1
+          and f.end_time &lt; now()
+    </select>
+
+    <select id="selectStartingFlashSaleList" resultMap="FsStoreProductFlashSaleResult">
+        <include refid="selectFsStoreProductFlashSaleVo"/>
+        where f.del_flag = 0
+          and f.status = 0
+          and f.start_time - INTERVAL 1 HOUR &lt;= now()
+          and f.end_time &gt; now()
+          and f.stock > 0
+    </select>
+
+    <update id="updateStatus">
+        UPDATE fs_store_product_flash_sale 
+        SET status = #{status}, update_time = now() 
+        WHERE id = #{id}
+    </update>
+
+    <update id="increaseStock">
+        UPDATE fs_store_product_flash_sale
+        SET stock = stock + #{num}, update_time = now()
+        WHERE id = #{id}
+    </update>
+</mapper>

+ 20 - 8
fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml

@@ -78,6 +78,9 @@
         <result property="appIds"    column="app_ids"    />
         <result property="purchaseLimit"    column="purchase_limit"    />
         <result property="singlePurchaseLimit"    column="single_purchase_limit"    />
+        <result property="activityType"    column="activity_type"    />
+        <result property="activityStartTime"    column="activity_start_time"    />
+        <result property="activityEndTime"    column="activity_end_time"    />
     </resultMap>
 
     <sql id="selectFsStoreProductVo">
@@ -89,7 +92,7 @@
                is_display,tui_cate_id,company_ids,is_drug,drug_image,drug_reg_cert_no,common_name,dosage_form,
                unit_price,batch_number,mah,mah_address,manufacturer,manufacturer_address,indications,dosage,
                adverse_reactions,contraindications,precautions,is_audit,store_id,return_address,brand,food_production_license_code,
-               origin_place,net_content,shelf_life,domestic_imported,app_ids,purchase_limit,single_purchase_limit
+               origin_place,net_content,shelf_life,domestic_imported,app_ids,purchase_limit,single_purchase_limit,activity_type,activity_start_time,activity_end_time
         from fs_store_product_scrm
     </sql>
 
@@ -102,7 +105,7 @@
                p.is_display,p.tui_cate_id,p.company_ids,p.is_drug,p.drug_image,p.drug_reg_cert_no,p.common_name,p.dosage_form,
                p.unit_price,p.batch_number,p.mah,p.mah_address,p.manufacturer,p.manufacturer_address,p.indications,p.dosage,
                p.adverse_reactions,p.contraindications,p.precautions,p.is_audit,p.store_id,p.return_address,p.brand,p.food_production_license_code,
-               p.origin_place,p.net_content,p.shelf_life,p.domestic_imported,app_ids,p.purchase_limit,p.single_purchase_limit
+               p.origin_place,p.net_content,p.shelf_life,p.domestic_imported,app_ids,p.purchase_limit,p.single_purchase_limit,p.activity_type
         from fs_store_product_scrm p
     </sql>
 
@@ -282,6 +285,7 @@
             <if test="appIds != null and appIds != ''">app_ids, </if>
             <if test="purchaseLimit != null">purchase_limit,</if>
             <if test="singlePurchaseLimit != null">single_purchase_limit,</if>
+            <if test="activityType != null">activity_type,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="image != null and image != ''">#{image},</if>
@@ -356,6 +360,7 @@
             <if test="appIds != null and appIds != ''">#{appIds}, </if>
             <if test="purchaseLimit != null">#{purchaseLimit},</if>
             <if test="singlePurchaseLimit != null">#{singlePurchaseLimit},</if>
+            <if test="activityType != null">#{activityType},</if>
         </trim>
     </insert>
 
@@ -434,6 +439,7 @@
             <if test="appIds != null and appIds != ''">app_ids = #{appIds}, </if>
             <if test="purchaseLimit != null">purchase_limit = #{purchaseLimit},</if>
             <if test="singlePurchaseLimit != null">single_purchase_limit = #{singlePurchaseLimit},</if>
+            <if test="activityType != null">activity_type = #{activityType},</if>
         </trim>
         where product_id = #{productId}
     </update>
@@ -497,7 +503,8 @@
         <if test = 'param.appId != null and param.appId != ""'>
             and ((FIND_IN_SET(#{param.appId}, fsp.app_ids) > 0))
         </if>
-        and fsp.is_best=1 and fsp.is_display=1 order by fsp.sort desc,fsp.product_id desc
+        and fsp.is_best=1 and fsp.is_display=1
+        order by fsp.sort desc,fsp.product_id desc
     </select>
     <select id="bulkCopyFsStoreProductByIds" resultMap="FsStoreProductResult">
         <include refid="selectFsStoreProductVo"/>
@@ -522,7 +529,8 @@
         <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
-        and p.is_new=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC limit #{count}
+        and p.is_new=1 and p.is_display=1
+        order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC limit #{count}
     </select>
     <select id="selectFsStoreProductNewQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -537,7 +545,8 @@
         <if test='keyword != null and keyword != ""'>
             and p.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        and p.is_new=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC
+        and p.is_new=1 and p.is_display=1
+        order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC
     </select>
     <select id="selectFsStoreProductHotQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -549,7 +558,8 @@
         <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
-        and  p.is_hot=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC limit #{count}
+        and  p.is_hot=1 and p.is_display=1
+        order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC limit #{count}
     </select>
     <select id="selectFsStoreProductHotQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -564,7 +574,8 @@
         <if test='keyword != null and keyword != ""'>
             and p.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        and  p.is_hot=1 and p.is_display=1 order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC
+        and  p.is_hot=1 and p.is_display=1
+        order by CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 1 ELSE 0 END ASC, CASE WHEN p.sort IS NULL OR p.sort = 0 THEN 0 ELSE p.sort END DESC, p.create_time DESC, p.product_id DESC
     </select>
     <select id="selectFsStoreProductGoodListQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -578,7 +589,8 @@
         <if test = 'param.appId != null and param.appId != ""'>
             and ((FIND_IN_SET(#{param.appId}, p.app_ids) > 0))
         </if>
-        and  p.is_good=1 and p.is_display=1 order by p.sort desc
+        and  p.is_good=1 and p.is_display=1
+        order by p.sort desc
     </select>
 
     <select id="getStoreProductInProductIds" resultType="com.fs.hisStore.domain.FsStoreProductScrm">

+ 303 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/FlashSaleConcurrentTestController.java

@@ -0,0 +1,303 @@
+package com.fs.app.controller.store;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.service.ActivityStockService;
+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.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 秒杀并发压测接口(仅用于测试环境,上线前删除)
+ */
+@Slf4j
+@Api("秒杀并发压测(测试用)")
+@RestController
+@RequestMapping("/test/flashSale")
+public class FlashSaleConcurrentTestController {
+
+    @Autowired
+    private ActivityStockService activityStockService;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    /** 测试用的活动ID */
+    private static final Long TEST_ACTIVITY_ID = 99999L;
+    /** 测试用的规格ID */
+    private static final Long TEST_SPEC_ID = 88888L;
+    /** 规格库存Key */
+    private static final String PRODUCT_SPEC_STOCK_KEY = "product:spec:stock:";
+    /** 活动信息Key */
+    private static final String ACTIVITY_INFO_KEY = "activity:info:";
+
+    /**
+     * 初始化测试数据:10库存 + 活动信息写入Redis
+     * 调用后再调 concurrentGrab 发起抢购
+     */
+    @ApiOperation("初始化测试数据(10库存)")
+    @GetMapping("/init")
+    public R init(@RequestParam(value = "stock", defaultValue = "10") Integer stock) {
+        // 清除旧数据
+        redisTemplate.delete(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        redisTemplate.delete(ACTIVITY_INFO_KEY + TEST_ACTIVITY_ID);
+
+        // 写入规格库存
+        redisTemplate.opsForValue().set(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID, stock);
+
+        // 写入活动信息(status=1上架,时间设为当前前后1天)
+        long now = System.currentTimeMillis();
+        Map<String, Object> activityInfo = new HashMap<>();
+        activityInfo.put("status", 1);
+        activityInfo.put("startTime", now - 86400000L);
+        activityInfo.put("endTime", now + 86400000L);
+        activityInfo.put("productId", 77777L);
+        activityInfo.put("specId", TEST_SPEC_ID);
+        redisTemplate.opsForHash().putAll(ACTIVITY_INFO_KEY + TEST_ACTIVITY_ID, activityInfo);
+
+        log.info("===== 压测数据初始化完成: activityId={}, specId={}, stock={} =====", TEST_ACTIVITY_ID, TEST_SPEC_ID, stock);
+        return R.ok().put("msg", "初始化完成")
+                .put("activityId", TEST_ACTIVITY_ID)
+                .put("specId", TEST_SPEC_ID)
+                .put("stock", stock);
+    }
+
+    /**
+     * 并发抢购压测
+     * 用线程池模拟 concurrentUsers 个用户同时抢购,每人抢1件
+     */
+    @ApiOperation("并发抢购压测")
+    @GetMapping("/concurrentGrab")
+    public R concurrentGrab(
+            @RequestParam(value = "concurrentUsers", defaultValue = "1000") Integer concurrentUsers,
+            @RequestParam(value = "deductNum", defaultValue = "1") Integer deductNum) {
+
+        // 先读当前库存
+        Object stockObj = redisTemplate.opsForValue().get(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        int initialStock = 0;
+        if (stockObj != null) {
+            initialStock = Integer.parseInt(stockObj.toString());
+        }
+
+        log.info("===== 开始并发压测: {}人抢 {}库存,每人抢{}件 =====", concurrentUsers, initialStock, deductNum);
+
+        // 结果统计
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger failCount = new AtomicInteger(0);
+        AtomicInteger stockNotEnough = new AtomicInteger(0);
+        AtomicInteger keyNotExist = new AtomicInteger(0);
+        AtomicInteger validateFail = new AtomicInteger(0);
+        AtomicInteger otherFail = new AtomicInteger(0);
+
+        // 线程池
+        int poolSize = Math.min(concurrentUsers, 200);
+        ExecutorService executor = Executors.newFixedThreadPool(poolSize);
+        CountDownLatch startLatch = new CountDownLatch(1);  // 发令枪
+        CountDownLatch doneLatch = new CountDownLatch(concurrentUsers);  // 完成计数
+
+        for (int i = 0; i < concurrentUsers; i++) {
+            final int userId = i + 1;
+            executor.submit(() -> {
+                try {
+                    // 等待发令枪响,所有已启动的线程同时开始抢购
+                    startLatch.await();
+
+                    // 调用 deductStock(走完整校验 + Lua扣减流程)
+                    boolean result = activityStockService.deductStock(6, TEST_ACTIVITY_ID, deductNum);
+
+                    if (result) {
+                        successCount.incrementAndGet();
+                    } else {
+                        failCount.incrementAndGet();
+                        // 分析失败原因:读当前Redis库存判断
+                        Object curStock = redisTemplate.opsForValue().get(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+                        if (curStock == null) {
+                            keyNotExist.incrementAndGet();
+                        } else {
+                            int s = Integer.parseInt(curStock.toString());
+                            if (s < deductNum) {
+                                stockNotEnough.incrementAndGet();
+                            } else {
+                                validateFail.incrementAndGet();
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    otherFail.incrementAndGet();
+                    log.error("用户{}抢购异常", userId, e);
+                } finally {
+                    doneLatch.countDown();
+                }
+            });
+        }
+
+        // 等待所有任务提交到线程池,然后开枪
+        try { Thread.sleep(500); } catch (InterruptedException ignored) {}
+        long startTime = System.currentTimeMillis();
+        startLatch.countDown();  // 开枪!所有线程同时开始
+
+        // 等待所有任务完成
+        try {
+            doneLatch.await(60, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            log.error("等待任务完成被中断", e);
+        }
+        executor.shutdown();
+
+        long costTime = System.currentTimeMillis() - startTime;
+
+        // 最终库存
+        Object finalStockObj = redisTemplate.opsForValue().get(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        int finalStock = finalStockObj != null ? Integer.parseInt(finalStockObj.toString()) : -1;
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("initialStock", initialStock);
+        result.put("concurrentUsers", concurrentUsers);
+        result.put("deductNumPerUser", deductNum);
+        result.put("costTimeMs", costTime);
+        result.put("successCount", successCount.get());
+        result.put("failCount", failCount.get());
+        result.put("fail_stockNotEnough", stockNotEnough.get());
+        result.put("fail_keyNotExist", keyNotExist.get());
+        result.put("fail_validateFail", validateFail.get());
+        result.put("fail_other", otherFail.get());
+        result.put("finalStock", finalStock);
+        result.put("integrityCheck", (initialStock - finalStock) == successCount.get() * deductNum ? "PASS ✅" : "FAIL ❌");
+
+        log.info("===== 压测结果: 成功={}, 失败={}, 最终库存={}, 耗时={}ms =====",
+                successCount.get(), failCount.get(), finalStock, costTime);
+
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 直接调用Lua脚本压测(跳过活动校验,纯测Lua原子扣减)
+     */
+    @ApiOperation("纯Lua脚本压测(跳过活动校验)")
+    @GetMapping("/luaOnly")
+    public R luaOnlyTest(
+            @RequestParam(value = "concurrentUsers", defaultValue = "1000") Integer concurrentUsers,
+            @RequestParam(value = "stock", defaultValue = "10") Integer stock) {
+
+        // 初始化:直接写规格库存
+        redisTemplate.delete(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        redisTemplate.opsForValue().set(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID, stock);
+
+        log.info("===== 纯Lua压测: {}人抢 {}库存 =====", concurrentUsers, stock);
+
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger failStockNotEnough = new AtomicInteger(0);
+        AtomicInteger failKeyNotExist = new AtomicInteger(0);
+
+        // Lua脚本(与ActivityStockService一致)
+        String luaScript = "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);";
+
+        org.springframework.data.redis.core.script.DefaultRedisScript<Long> redisScript =
+                new org.springframework.data.redis.core.script.DefaultRedisScript<>();
+        redisScript.setScriptText(luaScript);
+        redisScript.setResultType(Long.class);
+
+        CountDownLatch startLatch = new CountDownLatch(1);  // 发令枪
+        CountDownLatch doneLatch = new CountDownLatch(concurrentUsers);  // 完成计数
+        ExecutorService executor = Executors.newFixedThreadPool(Math.min(concurrentUsers, 200));
+
+        for (int i = 0; i < concurrentUsers; i++) {
+            executor.submit(() -> {
+                try {
+                    startLatch.await();  // 等待发令枪
+
+                    Long result = redisTemplate.execute(
+                            redisScript,
+                            Collections.singletonList(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID),
+                            1  // 每人扣1
+                    );
+
+                    if (result != null && result >= 0) {
+                        successCount.incrementAndGet();
+                    } else if (result != null && result == -1) {
+                        failStockNotEnough.incrementAndGet();
+                    } else if (result != null && result == -2) {
+                        failKeyNotExist.incrementAndGet();
+                    }
+                } catch (Exception e) {
+                    log.error("Lua压测异常", e);
+                } finally {
+                    doneLatch.countDown();
+                }
+            });
+        }
+
+        // 等待所有任务提交到线程池,然后开枪
+        try { Thread.sleep(500); } catch (InterruptedException ignored) {}
+        long startTime = System.currentTimeMillis();
+        startLatch.countDown();  // 开枪!
+
+        try {
+            doneLatch.await(60, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            log.error("等待任务完成被中断", e);
+        }
+        executor.shutdown();
+
+        long costTime = System.currentTimeMillis() - startTime;
+
+        Object finalStockObj = redisTemplate.opsForValue().get(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        int finalStock = finalStockObj != null ? Integer.parseInt(finalStockObj.toString()) : -1;
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("mode", "纯Lua脚本");
+        result.put("initialStock", stock);
+        result.put("concurrentUsers", concurrentUsers);
+        result.put("costTimeMs", costTime);
+        result.put("successCount", successCount.get());
+        result.put("fail_stockNotEnough", failStockNotEnough.get());
+        result.put("fail_keyNotExist", failKeyNotExist.get());
+        result.put("finalStock", finalStock);
+        result.put("integrityCheck", (stock - finalStock) == successCount.get() ? "PASS ✅" : "FAIL ❌");
+
+        log.info("===== 纯Lua压测结果: 成功={}, 库存不足={}, 最终库存={}, 耗时={}ms =====",
+                successCount.get(), failStockNotEnough.get(), finalStock, costTime);
+
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 查看当前测试数据状态
+     */
+    @ApiOperation("查看测试数据状态")
+    @GetMapping("/status")
+    public R status() {
+        Object stockObj = redisTemplate.opsForValue().get(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        Map<Object, Object> infoMap = redisTemplate.opsForHash().entries(ACTIVITY_INFO_KEY + TEST_ACTIVITY_ID);
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("specStockKey", PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        result.put("specStockValue", stockObj);
+        result.put("activityInfoKey", ACTIVITY_INFO_KEY + TEST_ACTIVITY_ID);
+        result.put("activityInfo", infoMap.isEmpty() ? "不存在" : infoMap);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 清除测试数据
+     */
+    @ApiOperation("清除测试数据")
+    @DeleteMapping("/clean")
+    public R clean() {
+        redisTemplate.delete(PRODUCT_SPEC_STOCK_KEY + TEST_SPEC_ID);
+        redisTemplate.delete(ACTIVITY_INFO_KEY + TEST_ACTIVITY_ID);
+        return R.ok().put("msg", "测试数据已清除");
+    }
+}

+ 363 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/FsStoreProductDiscountScrmController.java

@@ -0,0 +1,363 @@
+package com.fs.app.controller.store;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.service.ActivityStockService;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import com.fs.hisStore.domain.FsStoreProductDiscount;
+import com.fs.hisStore.service.IFsStoreProductActivityService;
+import com.fs.hisStore.service.IFsStoreProductDiscountService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+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.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Api("小程序端-限时折扣商品")
+@RestController
+@RequestMapping("/store/productDiscount")
+public class FsStoreProductDiscountScrmController extends BaseController
+{
+    @Autowired
+    private IFsStoreProductDiscountService fsStoreProductDiscountService;
+
+    @Autowired
+    private IFsStoreProductActivityService activityService;
+
+    @Autowired
+    private ActivityStockService activityStockService;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    private static final String SYNC_LOCK_KEY_PREFIX = "activity:sync:stock:lock:";
+
+    /**
+     * 查询折扣商品列表(1小时内即将开抢 + 未过期的商品)
+     * 按商品聚合,每个商品只展示一条,价格取最低折扣价,库存取所有规格之和
+     */
+    @ApiOperation("查询折扣商品列表(1小时内即将开抢+未过期)")
+    @GetMapping("/activeList")
+    public R activeList()
+    {
+        List<FsStoreProductActivity> list = activityService.selectUpcomingDiscountActivityList();
+        if (list == null || list.isEmpty()) {
+            return R.ok().put("data", Collections.emptyList()).put("serverTimestamp", System.currentTimeMillis());
+        }
+
+        long now = System.currentTimeMillis();
+
+        // 批量获取库存,避免N次串行Redis调用
+        List<Long> activityIds = list.stream()
+                .map(FsStoreProductActivity::getId)
+                .collect(Collectors.toList());
+        Map<Long, Integer> stockMap = activityStockService.batchGetStock(7, activityIds);
+
+        // 按productId分组,保持顺序
+        Map<Long, List<FsStoreProductActivity>> grouped = list.stream()
+                .collect(Collectors.groupingBy(FsStoreProductActivity::getProductId, LinkedHashMap::new, Collectors.toList()));
+
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        for (Map.Entry<Long, List<FsStoreProductActivity>> entry : grouped.entrySet()) {
+            List<FsStoreProductActivity> specs = entry.getValue();
+            FsStoreProductActivity first = specs.get(0);
+            // 最低折扣价
+            java.math.BigDecimal minDiscountPrice = specs.stream()
+                    .map(FsStoreProductActivity::getDiscountPrice)
+                    .filter(Objects::nonNull)
+                    .min(java.math.BigDecimal::compareTo)
+                    .orElse(first.getDiscountPrice());
+            // 最大折扣力度(值最小 = 折扣力度最大)
+            java.math.BigDecimal minDiscount = specs.stream()
+                    .map(FsStoreProductActivity::getDiscount)
+                    .filter(Objects::nonNull)
+                    .min(java.math.BigDecimal::compareTo)
+                    .orElse(first.getDiscount());
+            // 所有规格Redis库存之和
+            int totalRemainStock = specs.stream()
+                    .mapToInt(s -> stockMap.getOrDefault(s.getId(), 0))
+                    .sum();
+            Map<String, Object> map = buildActivityMap(first, now, totalRemainStock);
+            map.put("discountPrice", minDiscountPrice);
+            map.put("discount", minDiscount);
+            map.put("remainStock", totalRemainStock);
+            resultList.add(map);
+        }
+
+        return R.ok().put("data", resultList).put("serverTimestamp", now);
+    }
+
+    /**
+     * 根据商品ID查询折扣信息(含活动状态和实时库存,返回所有规格数组)
+     */
+    @ApiOperation("根据商品ID查询折扣信息(含活动状态和实时库存)")
+    @GetMapping("/getByProductId/{productId}")
+    public R getByProductId(@PathVariable("productId") Long productId)
+    {
+        // 查出该商品所有参与折扣活动的规格
+        List<FsStoreProductActivity> specs = activityService.selectActivitySpecsByProductIdAndType(productId, 7);
+        if (specs == null || specs.isEmpty()) {
+            List<FsStoreProductActivity> activities = activityService.selectDiscountActivityByProductId(productId);
+            if (activities == null || activities.isEmpty()) {
+                return R.ok().put("data", null).put("serverTimestamp", System.currentTimeMillis());
+            }
+            specs = activities;
+        }
+
+        long now = System.currentTimeMillis();
+        FsStoreProductActivity first = specs.get(0);
+
+        // 批量获取所有规格Redis库存
+        List<Long> specIds = specs.stream().map(FsStoreProductActivity::getId).collect(Collectors.toList());
+        Map<Long, Integer> stockMap = activityStockService.batchGetStock(7, specIds);
+
+        // 构建规格数组
+        List<Map<String, Object>> specList = buildSpecList(specs, stockMap);
+
+        // 构建商品级返回结构
+        Map<String, Object> data = buildProductLevelMap(first, now, stockMap, specs);
+        data.put("specs", specList);
+
+        return R.ok().put("data", data).put("serverTimestamp", now);
+    }
+
+    /**
+     * 获取折扣商品详情(含活动状态、实时库存和关联商品信息,返回规格数组)
+     */
+    @ApiOperation("获取折扣商品详情(含活动状态、实时库存和关联商品信息)")
+    @GetMapping("/detail/{id}")
+    public AjaxResult getDetail(@PathVariable("id") Long id)
+    {
+        FsStoreProductActivity activity = activityService.selectActivityDetailById(id);
+        if (activity == null) {
+            return AjaxResult.error("折扣商品不存在");
+        }
+
+        long now = System.currentTimeMillis();
+
+        // 查出该商品所有参与折扣活动的规格
+        List<FsStoreProductActivity> specs = activityService.selectActivitySpecsByProductIdAndType(
+                activity.getProductId(), 7);
+        if (specs == null || specs.isEmpty()) {
+            specs = Collections.singletonList(activity);
+        }
+
+        // 批量获取所有规格Redis库存
+        List<Long> specIds = specs.stream().map(FsStoreProductActivity::getId).collect(Collectors.toList());
+        Map<Long, Integer> stockMap = activityStockService.batchGetStock(7, specIds);
+
+        // 构建规格数组
+        List<Map<String, Object>> specList = buildSpecList(specs, stockMap);
+
+        // 构建商品级返回结构
+        Map<String, Object> map = buildProductLevelMap(activity, now, stockMap, specs);
+        map.put("productInfo", activity.getProductInfo());
+        map.put("sliderImage", activity.getSliderImage());
+        map.put("productStock", activity.getProductStock());
+        map.put("cateName", activity.getCateName());
+        map.put("specs", specList);
+        map.put("serverTimestamp", now);
+        return AjaxResult.success(map);
+    }
+
+    /**
+     * 获取折扣商品详情(兼容旧接口)
+     */
+    @ApiOperation("获取折扣商品详情(兼容旧接口)")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        FsStoreProductActivity activity = activityService.selectActivityDetailById(id);
+        if (activity == null) {
+            return AjaxResult.success(null);
+        }
+
+        long now = System.currentTimeMillis();
+        int realStock = activityStockService.getStock(7, id);
+        Map<String, Object> map = buildActivityMap(activity, now, realStock);
+        map.put("serverTimestamp", now);
+        return AjaxResult.success(map);
+    }
+
+    /**
+     * 获取服务器时间(用于前端倒计时校准)
+     */
+    @ApiOperation("获取服务器时间(用于前端倒计时校准)")
+    @GetMapping("/serverTime")
+    public R getServerTime()
+    {
+        return R.ok().put("serverTimestamp", System.currentTimeMillis());
+    }
+
+    /**
+     * 构建商品级信息Map(活动状态基于总库存计算)
+     */
+    private Map<String, Object> buildProductLevelMap(FsStoreProductActivity item, long now,
+                                                      Map<Long, Integer> stockMap, List<FsStoreProductActivity> specs) {
+        int totalRemainStock = specs.stream().mapToInt(s -> stockMap.getOrDefault(s.getId(), 0)).sum();
+        java.math.BigDecimal minDiscountPrice = specs.stream()
+                .map(FsStoreProductActivity::getDiscountPrice)
+                .filter(Objects::nonNull)
+                .min(java.math.BigDecimal::compareTo)
+                .orElse(item.getDiscountPrice());
+        java.math.BigDecimal minDiscount = specs.stream()
+                .map(FsStoreProductActivity::getDiscount)
+                .filter(Objects::nonNull)
+                .min(java.math.BigDecimal::compareTo)
+                .orElse(item.getDiscount());
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("productId", item.getProductId());
+        map.put("productName", item.getProductName());
+        map.put("image", item.getProductImage());
+        map.put("productImage", item.getProductImage());
+        map.put("startTime", item.getStartTime());
+        map.put("endTime", item.getEndTime());
+        map.put("status", item.getStatus());
+        map.put("discountPrice", minDiscountPrice);
+        map.put("discount", minDiscount);
+        map.put("originalPrice", item.getOriginalPrice());
+        map.put("remainStock", totalRemainStock);
+        map.put("price", item.getPrice());
+        map.put("otPrice", item.getOtPrice());
+        map.put("sales", item.getSales());
+        map.put("barCode", item.getBarCode());
+
+        long startMs = item.getStartTime().getTime();
+        long endMs = item.getEndTime().getTime();
+        String activityStatus;
+        long countdown = 0;
+        if (now < startMs) {
+            activityStatus = "not_started";
+            countdown = (startMs - now) / 1000;
+        } else if (now > endMs) {
+            activityStatus = "ended";
+        } else {
+            if (totalRemainStock <= 0) {
+                activityStatus = "sold_out";
+            } else {
+                activityStatus = "ongoing";
+                countdown = (endMs - now) / 1000;
+            }
+        }
+        map.put("activityStatus", activityStatus);
+        map.put("countdown", countdown);
+        return map;
+    }
+
+    /**
+     * 构建规格数组
+     */
+    private List<Map<String, Object>> buildSpecList(List<FsStoreProductActivity> specs, Map<Long, Integer> stockMap) {
+        List<Map<String, Object>> specList = new ArrayList<>();
+        for (FsStoreProductActivity spec : specs) {
+            int remainStock = stockMap.getOrDefault(spec.getId(), 0);
+            Map<String, Object> s = new HashMap<>();
+            s.put("id", spec.getId());
+            s.put("specId", spec.getSpecId());
+            s.put("specName", spec.getSpecName());
+            s.put("originalPrice", spec.getOriginalPrice());
+            s.put("discount", spec.getDiscount());
+            s.put("discountPrice", spec.getDiscountPrice());
+            s.put("stock", spec.getSpecStock());
+            s.put("remainStock", remainStock);
+            specList.add(s);
+        }
+        return specList;
+    }
+
+    /**
+     * 构建折扣活动信息Map(含实时库存和活动状态)
+     */
+    private Map<String, Object> buildActivityMap(FsStoreProductActivity item, long now, int realStock) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("id", item.getId());
+        map.put("productId", item.getProductId());
+        map.put("originalPrice", item.getOriginalPrice());
+        map.put("discount", item.getDiscount());
+        map.put("discountPrice", item.getDiscountPrice());
+        map.put("stock", item.getSpecStock());
+        map.put("startTime", item.getStartTime());
+        map.put("endTime", item.getEndTime());
+        map.put("status", item.getStatus());
+        map.put("productName", item.getProductName());
+        map.put("image", item.getProductImage());
+        map.put("productImage", item.getProductImage());
+        map.put("price", item.getPrice());
+        map.put("otPrice", item.getOtPrice());
+        map.put("sales", item.getSales());
+        map.put("barCode", item.getBarCode());
+
+        long startMs = item.getStartTime().getTime();
+        long endMs = item.getEndTime().getTime();
+
+        String activityStatus;
+        long countdown = 0;
+
+        if (now < startMs) {
+            activityStatus = "not_started";
+            countdown = (startMs - now) / 1000;
+        } else if (now > endMs) {
+            activityStatus = "ended";
+        } else {
+            // 活动进行中,检查Redis实时库存
+            if (realStock <= 0) {
+                activityStatus = "sold_out";
+                // Redis库存为0,同步到数据库
+                syncStockToDbIfNeeded(item.getId(), realStock);
+            } else {
+                activityStatus = "ongoing";
+                countdown = (endMs - now) / 1000;
+            }
+        }
+
+        map.put("remainStock", realStock);
+        map.put("activityStatus", activityStatus);
+        map.put("countdown", countdown);
+        return map;
+    }
+
+    /**
+     * Redis库存为0时同步到数据库(带Redisson分布式锁,防并发重复写)
+     */
+    private void syncStockToDbIfNeeded(Long activityId, int redisStock) {
+        String lockKey = SYNC_LOCK_KEY_PREFIX + activityId;
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            boolean locked = lock.tryLock(0, 5, TimeUnit.SECONDS);
+            if (!locked) {
+                log.debug("折扣商品{}库存同步正在进行,跳过本次", activityId);
+                return;
+            }
+            try {
+                // 锁内重新检查Redis库存,避免重复写库
+                int currentStock = activityStockService.getStock(7, activityId);
+                if (currentStock > 0) {
+                    log.debug("折扣商品{}库存已回升到{},无需同步", activityId, currentStock);
+                    return;
+                }
+                log.info("折扣商品{} Redis库存为{},开始同步到数据库", activityId, currentStock);
+                activityStockService.syncDiscountStockToDb(activityId, Long.valueOf(currentStock));
+            } finally {
+                try {
+                    lock.unlock();
+                } catch (Exception e) {
+                    log.warn("释放同步锁失败,lockKey={}", lockKey, e);
+                }
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("折扣商品{} 获取同步锁被中断", activityId, e);
+        } catch (Exception e) {
+            log.error("折扣商品{} 库存同步到数据库失败", activityId, e);
+        }
+    }
+}

+ 348 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/FsStoreProductFlashSaleScrmController.java

@@ -0,0 +1,348 @@
+package com.fs.app.controller.store;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.service.ActivityStockService;
+import com.fs.hisStore.domain.FsStoreProductActivity;
+import com.fs.hisStore.service.IFsStoreProductActivityService;
+import com.fs.hisStore.service.IFsStoreProductFlashSaleService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+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.web.bind.annotation.*;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Api("小程序端-秒杀商品")
+@RestController
+@RequestMapping("/store/productFlashSale")
+public class FsStoreProductFlashSaleScrmController extends BaseController
+{
+    @Autowired
+    private IFsStoreProductFlashSaleService fsStoreProductFlashSaleService;
+
+    @Autowired
+    private IFsStoreProductActivityService activityService;
+
+    @Autowired
+    private ActivityStockService activityStockService;
+
+    @Autowired
+    private RedissonClient redissonClient;
+
+    private static final String SYNC_LOCK_KEY_PREFIX = "flashsale:sync:stock:lock:";
+
+    /**
+     * 查询秒杀商品列表(1小时内即将开抢 + 未过期的商品)
+     * 按商品聚合,每个商品只展示一条,价格取最低秒杀价,库存取所有规格之和
+     */
+    @ApiOperation("查询秒杀商品列表(1小时内即将开抢+未过期)")
+    @GetMapping("/activeList")
+    public R activeList()
+    {
+        List<FsStoreProductActivity> list = activityService.selectUpcomingFlashSaleActivityList();
+        if (list == null || list.isEmpty()) {
+            return R.ok().put("data", Collections.emptyList()).put("serverTimestamp", System.currentTimeMillis());
+        }
+
+        long now = System.currentTimeMillis();
+
+        // 批量获取所有规格Redis库存
+        List<Long> activityIds = list.stream().map(FsStoreProductActivity::getId).collect(Collectors.toList());
+        Map<Long, Integer> stockMap = activityStockService.batchGetStock(6, activityIds);
+
+        // 按productId分组
+        Map<Long, List<FsStoreProductActivity>> grouped = list.stream()
+                .collect(Collectors.groupingBy(FsStoreProductActivity::getProductId, LinkedHashMap::new, Collectors.toList()));
+
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        for (Map.Entry<Long, List<FsStoreProductActivity>> entry : grouped.entrySet()) {
+            List<FsStoreProductActivity> specs = entry.getValue();
+            // 取第一条作为商品基础信息
+            FsStoreProductActivity first = specs.get(0);
+            // 最低秒杀价
+            java.math.BigDecimal minFlashPrice = specs.stream()
+                    .map(FsStoreProductActivity::getFlashPrice)
+                    .filter(Objects::nonNull)
+                    .min(java.math.BigDecimal::compareTo)
+                    .orElse(first.getFlashPrice());
+            // 所有规格Redis库存之和
+            int totalRemainStock = specs.stream()
+                    .mapToInt(s -> stockMap.getOrDefault(s.getId(), 0))
+                    .sum();
+            Map<String, Object> map = buildActivityMap(first, now, totalRemainStock);
+            map.put("flashPrice", minFlashPrice);
+            map.put("remainStock", totalRemainStock);
+            resultList.add(map);
+        }
+
+        return R.ok().put("data", resultList).put("serverTimestamp", now);
+    }
+
+    /**
+     * 根据商品ID查询秒杀信息(含活动状态和实时库存,返回所有规格数组)
+     */
+    @ApiOperation("根据商品ID查询秒杀信息(含活动状态和实时库存)")
+    @GetMapping("/getByProductId/{productId}")
+    public R getByProductId(@PathVariable("productId") Long productId)
+    {
+        // 查出该商品所有参与秒杀活动的规格
+        List<FsStoreProductActivity> specs = activityService.selectActivitySpecsByProductIdAndType(productId, 6);
+        if (specs == null || specs.isEmpty()) {
+            // 兜底:用旧方法查进行中的活动
+            List<FsStoreProductActivity> activities = activityService.selectFlashSaleActivityByProductId(productId);
+            if (activities == null || activities.isEmpty()) {
+                return R.ok().put("data", null).put("serverTimestamp", System.currentTimeMillis());
+            }
+            specs = activities;
+        }
+
+        long now = System.currentTimeMillis();
+        FsStoreProductActivity first = specs.get(0);
+
+        // 批量获取所有规格Redis库存
+        List<Long> specIds = specs.stream().map(FsStoreProductActivity::getId).collect(Collectors.toList());
+        Map<Long, Integer> stockMap = activityStockService.batchGetStock(6, specIds);
+
+        // 构建规格数组
+        List<Map<String, Object>> specList = buildSpecList(specs, stockMap, now);
+
+        // 构建商品级返回结构
+        Map<String, Object> data = buildProductLevelMap(first, now, stockMap, specs);
+        data.put("specs", specList);
+
+        return R.ok().put("data", data).put("serverTimestamp", now);
+    }
+
+    /**
+     * 获取秒杀商品详情(含活动状态、实时库存和关联商品信息,返回规格数组)
+     */
+    @ApiOperation("获取秒杀商品详情(含活动状态、实时库存和关联商品信息)")
+    @GetMapping("/detail/{id}")
+    public AjaxResult getDetail(@PathVariable("id") Long id)
+    {
+        FsStoreProductActivity activity = activityService.selectActivityDetailById(id);
+        if (activity == null) {
+            return AjaxResult.error("秒杀商品不存在");
+        }
+
+        long now = System.currentTimeMillis();
+
+        // 查出该商品所有参与秒杀活动的规格
+        List<FsStoreProductActivity> specs = activityService.selectActivitySpecsByProductIdAndType(
+                activity.getProductId(), 6);
+        if (specs == null || specs.isEmpty()) {
+            specs = Collections.singletonList(activity);
+        }
+
+        // 批量获取所有规格Redis库存
+        List<Long> specIds = specs.stream().map(FsStoreProductActivity::getId).collect(Collectors.toList());
+        Map<Long, Integer> stockMap = activityStockService.batchGetStock(6, specIds);
+
+        // 构建规格数组
+        List<Map<String, Object>> specList = buildSpecList(specs, stockMap, now);
+
+        // 构建商品级返回结构(以当前活动记录为基准)
+        Map<String, Object> map = buildProductLevelMap(activity, now, stockMap, specs);
+        map.put("productInfo", activity.getProductInfo());
+        map.put("sliderImage", activity.getSliderImage());
+        map.put("price", activity.getPrice());
+        map.put("otPrice", activity.getOtPrice());
+        map.put("sales", activity.getSales());
+        map.put("productStock", activity.getProductStock());
+        map.put("cateName", activity.getCateName());
+        map.put("barCode", activity.getBarCode());
+        map.put("specs", specList);
+        map.put("serverTimestamp", now);
+        return AjaxResult.success(map);
+    }
+
+    /**
+     * 获取秒杀商品详情(兼容旧接口)
+     */
+    @ApiOperation("获取秒杀商品详情(兼容旧接口)")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        FsStoreProductActivity activity = activityService.selectActivityDetailById(id);
+        if (activity == null) {
+            return AjaxResult.success(null);
+        }
+
+        long now = System.currentTimeMillis();
+        int realStock = activityStockService.getStock(6, activity.getId());
+        Map<String, Object> map = buildActivityMap(activity, now, realStock);
+        map.put("serverTimestamp", now);
+        return AjaxResult.success(map);
+    }
+
+    /**
+     * 获取服务器时间(用于前端倒计时校准)
+     */
+    @ApiOperation("获取服务器时间(用于前端倒计时校准)")
+    @GetMapping("/serverTime")
+    public R getServerTime()
+    {
+        return R.ok().put("serverTimestamp", System.currentTimeMillis());
+    }
+
+    /**
+     * 构建商品级信息Map(活动状态基于总库存计算)
+     */
+    private Map<String, Object> buildProductLevelMap(FsStoreProductActivity item, long now,
+                                                      Map<Long, Integer> stockMap, List<FsStoreProductActivity> specs) {
+        int totalRemainStock = specs.stream().mapToInt(s -> stockMap.getOrDefault(s.getId(), 0)).sum();
+        java.math.BigDecimal minFlashPrice = specs.stream()
+                .map(FsStoreProductActivity::getFlashPrice)
+                .filter(Objects::nonNull)
+                .min(java.math.BigDecimal::compareTo)
+                .orElse(item.getFlashPrice());
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("productId", item.getProductId());
+        map.put("productName", item.getProductName());
+        map.put("image", item.getProductImage());
+        map.put("productImage", item.getProductImage());
+        map.put("startTime", item.getStartTime());
+        map.put("endTime", item.getEndTime());
+        map.put("status", item.getStatus());
+        map.put("flashPrice", minFlashPrice);
+        map.put("originalPrice", item.getOriginalPrice());
+        map.put("remainStock", totalRemainStock);
+
+        long startMs = item.getStartTime().getTime();
+        long endMs = item.getEndTime().getTime();
+        String activityStatus;
+        long countdown = 0;
+        if (now < startMs) {
+            activityStatus = "not_started";
+            countdown = (startMs - now) / 1000;
+        } else if (now > endMs) {
+            activityStatus = "ended";
+        } else {
+            if (totalRemainStock <= 0) {
+                activityStatus = "sold_out";
+            } else {
+                activityStatus = "ongoing";
+                countdown = (endMs - now) / 1000;
+            }
+        }
+        map.put("activityStatus", activityStatus);
+        map.put("countdown", countdown);
+        return map;
+    }
+
+    /**
+     * 构建规格数组
+     */
+    private List<Map<String, Object>> buildSpecList(List<FsStoreProductActivity> specs,
+                                                    Map<Long, Integer> stockMap, long now) {
+        List<Map<String, Object>> specList = new ArrayList<>();
+        for (FsStoreProductActivity spec : specs) {
+            int remainStock = stockMap.getOrDefault(spec.getId(), 0);
+            Map<String, Object> s = new HashMap<>();
+            s.put("id", spec.getId());
+            s.put("image", spec.getProductImage());
+            s.put("specId", spec.getSpecId());
+            s.put("specName", spec.getSpecName());
+            s.put("originalPrice", spec.getOriginalPrice());
+            s.put("flashPrice", spec.getFlashPrice());
+            s.put("stock", spec.getSpecStock());
+            s.put("remainStock", remainStock);
+            specList.add(s);
+        }
+        return specList;
+    }
+
+    private Map<String, Object> buildActivityMap(FsStoreProductActivity item, long now, int realStock) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("id", item.getId());
+        map.put("productId", item.getProductId());
+        map.put("originalPrice", item.getOriginalPrice());
+        map.put("flashPrice", item.getFlashPrice());
+        map.put("stock", item.getSpecStock());
+        map.put("startTime", item.getStartTime());
+        map.put("endTime", item.getEndTime());
+        map.put("status", item.getStatus());
+        map.put("productName", item.getProductName());
+        map.put("image", item.getProductImage());
+        map.put("productImage", item.getProductImage());
+        map.put("price", item.getPrice());
+        map.put("otPrice", item.getOtPrice());
+        map.put("sales", item.getSales());
+        map.put("barCode", item.getBarCode());
+        map.put("specId", item.getSpecId());
+        map.put("specName", item.getSpecName());
+
+        long startMs = item.getStartTime().getTime();
+        long endMs = item.getEndTime().getTime();
+
+        String activityStatus;
+        long countdown = 0;
+
+        if (now < startMs) {
+            activityStatus = "not_started";
+            countdown = (startMs - now) / 1000;
+        } else if (now > endMs) {
+            activityStatus = "ended";
+        } else {
+            // 活动进行中,检查Redis实时库存
+            if (realStock <= 0) {
+                activityStatus = "sold_out";
+                // Redis库存为0,同步到数据库
+                syncStockToDbIfNeeded(item.getId(), realStock);
+            } else {
+                activityStatus = "ongoing";
+                countdown = (endMs - now) / 1000;
+            }
+        }
+
+        map.put("remainStock", realStock);
+        map.put("activityStatus", activityStatus);
+        map.put("countdown", countdown);
+        return map;
+    }
+
+    /**
+     * Redis库存为0时同步到数据库(带Redisson分布式锁,防并发重复写)
+     */
+    private void syncStockToDbIfNeeded(Long activityId, int redisStock) {
+        String lockKey = SYNC_LOCK_KEY_PREFIX + activityId;
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            boolean locked = lock.tryLock(0, 5, TimeUnit.SECONDS);
+            if (!locked) {
+                log.debug("秒杀商品{}库存同步正在进行,跳过本次", activityId);
+                return;
+            }
+            try {
+                // 锁内重新检查Redis库存,避免重复写库
+                int currentStock = activityStockService.getStock(6, activityId);
+                if (currentStock > 0) {
+                    log.debug("秒杀商品{}库存已回升到{},无需同步", activityId, currentStock);
+                    return;
+                }
+                log.info("秒杀商品{} Redis库存为{},开始同步到数据库", activityId, currentStock);
+                activityStockService.syncFlashSaleStockToDb(activityId, Long.valueOf(currentStock));
+            } finally {
+                try {
+                    lock.unlock();
+                } catch (Exception e) {
+                    log.warn("释放同步锁失败,lockKey={}", lockKey, e);
+                }
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("秒杀商品{} 获取同步锁被中断", activityId, e);
+        } catch (Exception e) {
+            log.error("秒杀商品{} 库存同步到数据库失败", activityId, e);
+        }
+    }
+}

+ 28 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java

@@ -21,8 +21,10 @@ import java.util.Map;
 import java.util.HashMap;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import com.fs.common.core.redis.service.ActivityStockService;
 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.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -32,6 +34,7 @@ import java.util.Date;
 import java.util.List;
 
 
+@Slf4j
 @Api("商品中心")
 @RestController
 @RequestMapping(value="/store/app/product")
@@ -63,8 +66,14 @@ public class ProductScrmController extends AppBaseController {
     @Autowired
     private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
 
+    @Autowired
+    private IFsStoreProductActivityService activityService;
+
     @Autowired
     private IFsStoreOrderItemScrmService orderItemService;
+
+    @Autowired
+    private ActivityStockService activityStockService;
     /**
      * 获取用户信息
      * @param storeId
@@ -106,6 +115,8 @@ public class ProductScrmController extends AppBaseController {
         PageHelper.startPage(param.getPage(), param.getPageSize());
         param.setIsDisplay(1);
         List<FsStoreProductListQueryVO> productList=productService.selectFsStoreProductListQuery(param);
+        // 检查列表中活动商品是否已过期,过期则即时同步库存+重置
+        checkAndHandleExpiredActivities(productList);
         PageInfo<FsStoreProductListQueryVO> listPageInfo=new PageInfo<>(productList);
         return R.ok().put("data",listPageInfo);
     }
@@ -566,4 +577,21 @@ public class ProductScrmController extends AppBaseController {
         }
     }
 
+    /**
+     * 列表页只读判断:如果商品活动已过期,仅标记activityType=0返回前端,不触发写操作
+     * 使用商品表的活动时间字段判断,无需查中间表
+     */
+    private void checkAndHandleExpiredActivities(List<FsStoreProductListQueryVO> productList) {
+        if (productList == null || productList.isEmpty()) return;
+        long now = System.currentTimeMillis();
+        for (FsStoreProductListQueryVO product : productList) {
+            if (product.getActivityType() != null && product.getActivityType() != 0) {
+                // 直接用商品表的活动结束时间判断
+                if (product.getActivityEndTime() != null && now > product.getActivityEndTime().getTime()) {
+                    product.setActivityType(0);
+                }
+            }
+        }
+    }
+
 }

+ 9 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

@@ -217,6 +217,15 @@ public class StoreOrderScrmController extends AppBaseController {
         return orderService.createOrder(Long.parseLong(getUserId()),param);
     }
 
+    @Login
+    @ApiOperation("创建秒杀/限时折扣活动订单")
+    @PostMapping("/createActivityOrder")
+    public R createActivityOrder(@Validated @RequestBody FsStoreOrderCreateParam param, HttpServletRequest request){
+        log.info("开始创建活动订单,用户id:{},活动类型:{},活动ID:{}", getUserId(), param.getOrderType(), param.getAssociatedId());
+        param.setIsUserApp(false);
+        return orderService.createActivityOrder(Long.parseLong(getUserId()), param);
+    }
+
     @Login
     @ApiOperation("支付宝支付")
     @PostMapping("/zfbPayment")