Parcourir la source

商家订单列表添加mdId过滤

qmj il y a 3 semaines
Parent
commit
d651794087
29 fichiers modifiés avec 2596 ajouts et 0 suppressions
  1. 91 0
      ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionActivityController.java
  2. 89 0
      ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionCouponController.java
  3. 187 0
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivity.java
  4. 156 0
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivityRule.java
  5. 246 0
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponBatch.java
  6. 156 0
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponRule.java
  7. 177 0
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionUserCoupon.java
  8. 14 0
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityMapper.java
  9. 22 0
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityRuleMapper.java
  10. 14 0
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponBatchMapper.java
  11. 21 0
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponRuleMapper.java
  12. 14 0
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionUserCouponMapper.java
  13. 22 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityRuleService.java
  14. 40 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityService.java
  15. 39 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponBatchService.java
  16. 21 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponRuleService.java
  17. 32 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionActivityRuleServiceImpl.java
  18. 88 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionActivityServiceImpl.java
  19. 88 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionCouponBatchServiceImpl.java
  20. 31 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionCouponRuleServiceImpl.java
  21. 19 0
      ruoyi-system/src/main/resources/mapper/system/PromotionActivityMapper.xml
  22. 32 0
      ruoyi-system/src/main/resources/mapper/system/PromotionActivityRuleMapper.xml
  23. 23 0
      ruoyi-system/src/main/resources/mapper/system/PromotionCouponBatchMapper.xml
  24. 32 0
      ruoyi-system/src/main/resources/mapper/system/PromotionCouponRuleMapper.xml
  25. 19 0
      ruoyi-system/src/main/resources/mapper/system/PromotionUserCouponMapper.xml
  26. 223 0
      specs/008-promotion-coupon/plan.md
  27. 403 0
      specs/008-promotion-coupon/spec.md
  28. 193 0
      specs/008-promotion-coupon/tasks.md
  29. 104 0
      specs/test/plan.md

+ 91 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionActivityController.java

@@ -0,0 +1,91 @@
+package com.ruoyi.app.mendian;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.system.domain.PromotionActivity;
+import com.ruoyi.system.domain.PromotionActivityRule;
+import com.ruoyi.system.service.IPromotionActivityService;
+import com.ruoyi.system.utils.Auth;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 商家端促销活动管理
+ */
+@RestController
+@RequestMapping("/system/shPromotionActivity")
+public class ShPromotionActivityController extends BaseController
+{
+    @Autowired
+    private IPromotionActivityService promotionActivityService;
+
+    /**
+     * 查询商家促销活动列表
+     */
+    @Anonymous
+    @Auth
+    @GetMapping("/list")
+    public AjaxResult list(@RequestHeader String token,
+                           @RequestParam Integer page,
+                           @RequestParam Integer size,
+                           @RequestParam Long storeId,
+                           @RequestParam(required = false) Integer type,
+                           @RequestParam(required = false) Integer status)
+    {
+        IPage<PromotionActivity> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<PromotionActivity> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(PromotionActivity::getStoreId, storeId);
+        if (type != null)
+        {
+            queryWrapper.eq(PromotionActivity::getType, type);
+        }
+        if (status != null)
+        {
+            queryWrapper.eq(PromotionActivity::getStatus, status);
+        }
+        queryWrapper.orderByDesc(PromotionActivity::getCreateTime);
+        IPage<PromotionActivity> result = promotionActivityService.page(pageParam, queryWrapper);
+        return success(result);
+    }
+
+    /**
+     * 获取促销活动详细信息(含规则)
+     */
+    @Anonymous
+    @Auth
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@RequestHeader String token, @PathVariable("id") Long id)
+    {
+        PromotionActivity activity = promotionActivityService.selectActivityWithRules(id);
+        return success(activity);
+    }
+
+    /**
+     * 新增促销活动
+     */
+    @Anonymous
+    @Auth
+    @PostMapping
+    public AjaxResult add(@RequestHeader String token, @RequestBody PromotionActivity activity)
+    {
+        List<PromotionActivityRule> rules = activity.getRules();
+        return toAjax(promotionActivityService.createActivity(activity, rules));
+    }
+
+    /**
+     * 结束促销活动
+     */
+    @Anonymous
+    @Auth
+    @PutMapping("/{id}/end")
+    public AjaxResult end(@RequestHeader String token, @PathVariable("id") Long id)
+    {
+        return toAjax(promotionActivityService.endActivity(id));
+    }
+}

+ 89 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionCouponController.java

@@ -0,0 +1,89 @@
+package com.ruoyi.app.mendian;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.system.domain.PromotionCouponBatch;
+import com.ruoyi.system.domain.PromotionCouponRule;
+import com.ruoyi.system.service.IPromotionCouponBatchService;
+import com.ruoyi.system.utils.Auth;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 商家端优惠券管理
+ */
+@RestController
+@RequestMapping("/system/shPromotionCoupon")
+public class ShPromotionCouponController extends BaseController
+{
+    @Autowired
+    private IPromotionCouponBatchService promotionCouponBatchService;
+
+    /**
+     * 查询商家优惠券批次列表
+     */
+    @Anonymous
+    @Auth
+    @GetMapping("/list")
+    public AjaxResult list(@RequestHeader String token,
+                           @RequestParam Integer page,
+                           @RequestParam Integer size,
+                           @RequestParam Long storeId,
+                           @RequestParam(required = false) Integer couponType,
+                           @RequestParam(required = false) Integer status)
+    {
+        IPage<PromotionCouponBatch> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<PromotionCouponBatch> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(PromotionCouponBatch::getStoreId, storeId);
+        if (couponType != null)
+        {
+            queryWrapper.eq(PromotionCouponBatch::getCouponType, couponType);
+        }
+        if (status != null)
+        {
+            queryWrapper.eq(PromotionCouponBatch::getStatus, status);
+        }
+        queryWrapper.orderByDesc(PromotionCouponBatch::getCreateTime);
+        IPage<PromotionCouponBatch> result = promotionCouponBatchService.page(pageParam, queryWrapper);
+        return success(result);
+    }
+
+    /**
+     * 获取优惠券批次详细信息(含规则)
+     */
+    @Anonymous
+    @Auth
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@RequestHeader String token, @PathVariable("id") Long id)
+    {
+        PromotionCouponBatch batch = promotionCouponBatchService.selectBatchWithRule(id);
+        return success(batch);
+    }
+
+    /**
+     * 新增优惠券批次
+     */
+    @Anonymous
+    @Auth
+    @PostMapping
+    public AjaxResult add(@RequestHeader String token, @RequestBody PromotionCouponBatch batch)
+    {
+        PromotionCouponRule rule = batch.getRule();
+        return toAjax(promotionCouponBatchService.createBatch(batch, rule));
+    }
+
+    /**
+     * 下架优惠券批次
+     */
+    @Anonymous
+    @Auth
+    @PutMapping("/{id}/offShelf")
+    public AjaxResult offShelf(@RequestHeader String token, @PathVariable("id") Long id)
+    {
+        return toAjax(promotionCouponBatchService.offShelfBatch(id));
+    }
+}

+ 187 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivity.java

@@ -0,0 +1,187 @@
+package com.ruoyi.system.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.persistence.GeneratedValue;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * PromotionActivity对象 promotion_activity
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Data
+@TableName(value = "promotion_activity")
+@EqualsAndHashCode(callSuper = false)
+public class PromotionActivity
+{
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    @GeneratedValue
+    /** id */
+    private Long id;
+
+    /** 门店ID */
+    @Excel(name = "门店ID")
+    private Long storeId;
+
+    /** 类型: 1=满减 2=折扣 3=第二份半价 4=新客立减 */
+    @Excel(name = "类型")
+    private Integer type;
+
+    /** 活动名称 */
+    @Excel(name = "活动名称")
+    private String name;
+
+    /** 0=未开始 1=进行中 2=已结束 */
+    @Excel(name = "状态")
+    private Integer status;
+
+    /** 开始时间 */
+    @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;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /** 活动规则 */
+    @TableField(exist = false)
+    private List<PromotionActivityRule> rules;
+
+    /** 是否可编辑 */
+    @TableField(exist = false)
+    private boolean editable;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setStoreId(Long storeId)
+    {
+        this.storeId = storeId;
+    }
+
+    public Long getStoreId()
+    {
+        return storeId;
+    }
+
+    public void setType(Integer type)
+    {
+        this.type = type;
+    }
+
+    public Integer getType()
+    {
+        return type;
+    }
+
+    public void setName(String name)
+    {
+        this.name = name;
+    }
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public void setStatus(Integer status)
+    {
+        this.status = status;
+    }
+
+    public Integer getStatus()
+    {
+        return status;
+    }
+
+    public void setStartTime(Date startTime)
+    {
+        this.startTime = startTime;
+    }
+
+    public Date getStartTime()
+    {
+        return startTime;
+    }
+
+    public void setEndTime(Date endTime)
+    {
+        this.endTime = endTime;
+    }
+
+    public Date getEndTime()
+    {
+        return endTime;
+    }
+
+    public void setCreateTime(Date createTime)
+    {
+        this.createTime = createTime;
+    }
+
+    public Date getCreateTime()
+    {
+        return createTime;
+    }
+
+    public void setUpdateTime(Date updateTime)
+    {
+        this.updateTime = updateTime;
+    }
+
+    public Date getUpdateTime()
+    {
+        return updateTime;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("storeId", getStoreId())
+            .append("type", getType())
+            .append("name", getName())
+            .append("status", getStatus())
+            .append("startTime", getStartTime())
+            .append("endTime", getEndTime())
+            .append("createTime", getCreateTime())
+            .append("updateTime", getUpdateTime())
+            .toString();
+    }
+}

+ 156 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivityRule.java

@@ -0,0 +1,156 @@
+package com.ruoyi.system.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.persistence.GeneratedValue;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * PromotionActivityRule对象 promotion_activity_rule
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Data
+@TableName(value = "promotion_activity_rule")
+@EqualsAndHashCode(callSuper = false)
+public class PromotionActivityRule
+{
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    @GeneratedValue
+    /** id */
+    private Long id;
+
+    /** 关联促销活动 */
+    @Excel(name = "关联促销活动")
+    private Long activityId;
+
+    /** 商品ID */
+    @Excel(name = "商品ID")
+    private Long productId;
+
+    /** 满减门槛 */
+    @Excel(name = "满减门槛")
+    private BigDecimal threshold;
+
+    /** 减免金额 */
+    @Excel(name = "减免金额")
+    private BigDecimal reduceAmount;
+
+    /** 折扣率 */
+    @Excel(name = "折扣率")
+    private BigDecimal discountRate;
+
+    /** 最低数量 */
+    @Excel(name = "最低数量")
+    private Integer minQuantity;
+
+    /** 商品名称 */
+    @TableField(exist = false)
+    private String productName;
+
+    /** 商品图片 */
+    @TableField(exist = false)
+    private String productImage;
+
+    /** 商品价格 */
+    @TableField(exist = false)
+    private BigDecimal productPrice;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setActivityId(Long activityId)
+    {
+        this.activityId = activityId;
+    }
+
+    public Long getActivityId()
+    {
+        return activityId;
+    }
+
+    public void setProductId(Long productId)
+    {
+        this.productId = productId;
+    }
+
+    public Long getProductId()
+    {
+        return productId;
+    }
+
+    public void setThreshold(BigDecimal threshold)
+    {
+        this.threshold = threshold;
+    }
+
+    public BigDecimal getThreshold()
+    {
+        return threshold;
+    }
+
+    public void setReduceAmount(BigDecimal reduceAmount)
+    {
+        this.reduceAmount = reduceAmount;
+    }
+
+    public BigDecimal getReduceAmount()
+    {
+        return reduceAmount;
+    }
+
+    public void setDiscountRate(BigDecimal discountRate)
+    {
+        this.discountRate = discountRate;
+    }
+
+    public BigDecimal getDiscountRate()
+    {
+        return discountRate;
+    }
+
+    public void setMinQuantity(Integer minQuantity)
+    {
+        this.minQuantity = minQuantity;
+    }
+
+    public Integer getMinQuantity()
+    {
+        return minQuantity;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("activityId", getActivityId())
+            .append("productId", getProductId())
+            .append("threshold", getThreshold())
+            .append("reduceAmount", getReduceAmount())
+            .append("discountRate", getDiscountRate())
+            .append("minQuantity", getMinQuantity())
+            .toString();
+    }
+}

+ 246 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponBatch.java

@@ -0,0 +1,246 @@
+package com.ruoyi.system.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.persistence.GeneratedValue;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * PromotionCouponBatch对象 promotion_coupon_batch
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Data
+@TableName(value = "promotion_coupon_batch")
+@EqualsAndHashCode(callSuper = false)
+public class PromotionCouponBatch
+{
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    @GeneratedValue
+    /** id */
+    private Long id;
+
+    /** 门店ID */
+    @Excel(name = "门店ID")
+    private Long storeId;
+
+    /** 券名称 */
+    @Excel(name = "券名称")
+    private String name;
+
+    /** 1=满减券 2=商品券 */
+    @Excel(name = "券类型")
+    private Integer couponType;
+
+    /** 发放总量 */
+    @Excel(name = "发放总量")
+    private Integer totalCount;
+
+    /** 剩余数量 */
+    @Excel(name = "剩余数量")
+    private Integer remainCount;
+
+    /** 已领取数量 */
+    @Excel(name = "已领取数量")
+    private Integer receivedCount;
+
+    /** 0=未开始 1=进行中 2=已结束 3=已下架 */
+    @Excel(name = "状态")
+    private Integer status;
+
+    /** 开始时间 */
+    @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 Integer validDays;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /** 券规则 */
+    @TableField(exist = false)
+    private PromotionCouponRule rule;
+
+    /** 是否可编辑 */
+    @TableField(exist = false)
+    private boolean editable;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setStoreId(Long storeId)
+    {
+        this.storeId = storeId;
+    }
+
+    public Long getStoreId()
+    {
+        return storeId;
+    }
+
+    public void setName(String name)
+    {
+        this.name = name;
+    }
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public void setCouponType(Integer couponType)
+    {
+        this.couponType = couponType;
+    }
+
+    public Integer getCouponType()
+    {
+        return couponType;
+    }
+
+    public void setTotalCount(Integer totalCount)
+    {
+        this.totalCount = totalCount;
+    }
+
+    public Integer getTotalCount()
+    {
+        return totalCount;
+    }
+
+    public void setRemainCount(Integer remainCount)
+    {
+        this.remainCount = remainCount;
+    }
+
+    public Integer getRemainCount()
+    {
+        return remainCount;
+    }
+
+    public void setReceivedCount(Integer receivedCount)
+    {
+        this.receivedCount = receivedCount;
+    }
+
+    public Integer getReceivedCount()
+    {
+        return receivedCount;
+    }
+
+    public void setStatus(Integer status)
+    {
+        this.status = status;
+    }
+
+    public Integer getStatus()
+    {
+        return status;
+    }
+
+    public void setStartTime(Date startTime)
+    {
+        this.startTime = startTime;
+    }
+
+    public Date getStartTime()
+    {
+        return startTime;
+    }
+
+    public void setEndTime(Date endTime)
+    {
+        this.endTime = endTime;
+    }
+
+    public Date getEndTime()
+    {
+        return endTime;
+    }
+
+    public void setValidDays(Integer validDays)
+    {
+        this.validDays = validDays;
+    }
+
+    public Integer getValidDays()
+    {
+        return validDays;
+    }
+
+    public void setCreateTime(Date createTime)
+    {
+        this.createTime = createTime;
+    }
+
+    public Date getCreateTime()
+    {
+        return createTime;
+    }
+
+    public void setUpdateTime(Date updateTime)
+    {
+        this.updateTime = updateTime;
+    }
+
+    public Date getUpdateTime()
+    {
+        return updateTime;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("storeId", getStoreId())
+            .append("name", getName())
+            .append("couponType", getCouponType())
+            .append("totalCount", getTotalCount())
+            .append("remainCount", getRemainCount())
+            .append("receivedCount", getReceivedCount())
+            .append("status", getStatus())
+            .append("startTime", getStartTime())
+            .append("endTime", getEndTime())
+            .append("validDays", getValidDays())
+            .append("createTime", getCreateTime())
+            .append("updateTime", getUpdateTime())
+            .toString();
+    }
+}

+ 156 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponRule.java

@@ -0,0 +1,156 @@
+package com.ruoyi.system.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.persistence.GeneratedValue;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * PromotionCouponRule对象 promotion_coupon_rule
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Data
+@TableName(value = "promotion_coupon_rule")
+@EqualsAndHashCode(callSuper = false)
+public class PromotionCouponRule
+{
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    @GeneratedValue
+    /** id */
+    private Long id;
+
+    /** 批次ID */
+    @Excel(name = "批次ID")
+    private Long batchId;
+
+    /** 商品ID */
+    @Excel(name = "商品ID")
+    private Long productId;
+
+    /** 0=同享 1=互斥 */
+    @Excel(name = "互斥状态")
+    private Integer isMutex;
+
+    /** 门槛金额 */
+    @Excel(name = "门槛金额")
+    private BigDecimal threshold;
+
+    /** 券面额 */
+    @Excel(name = "券面额")
+    private BigDecimal amount;
+
+    /** 折扣率 */
+    @Excel(name = "折扣率")
+    private BigDecimal discountRate;
+
+    /** 商品名称 */
+    @TableField(exist = false)
+    private String productName;
+
+    /** 商品图片 */
+    @TableField(exist = false)
+    private String productImage;
+
+    /** 商品价格 */
+    @TableField(exist = false)
+    private BigDecimal productPrice;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setBatchId(Long batchId)
+    {
+        this.batchId = batchId;
+    }
+
+    public Long getBatchId()
+    {
+        return batchId;
+    }
+
+    public void setProductId(Long productId)
+    {
+        this.productId = productId;
+    }
+
+    public Long getProductId()
+    {
+        return productId;
+    }
+
+    public void setIsMutex(Integer isMutex)
+    {
+        this.isMutex = isMutex;
+    }
+
+    public Integer getIsMutex()
+    {
+        return isMutex;
+    }
+
+    public void setThreshold(BigDecimal threshold)
+    {
+        this.threshold = threshold;
+    }
+
+    public BigDecimal getThreshold()
+    {
+        return threshold;
+    }
+
+    public void setAmount(BigDecimal amount)
+    {
+        this.amount = amount;
+    }
+
+    public BigDecimal getAmount()
+    {
+        return amount;
+    }
+
+    public void setDiscountRate(BigDecimal discountRate)
+    {
+        this.discountRate = discountRate;
+    }
+
+    public BigDecimal getDiscountRate()
+    {
+        return discountRate;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("batchId", getBatchId())
+            .append("productId", getProductId())
+            .append("isMutex", getIsMutex())
+            .append("threshold", getThreshold())
+            .append("amount", getAmount())
+            .append("discountRate", getDiscountRate())
+            .toString();
+    }
+}

+ 177 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionUserCoupon.java

@@ -0,0 +1,177 @@
+package com.ruoyi.system.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.persistence.GeneratedValue;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * PromotionUserCoupon对象 promotion_user_coupon
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Data
+@TableName(value = "promotion_user_coupon")
+@EqualsAndHashCode(callSuper = false)
+public class PromotionUserCoupon
+{
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    @GeneratedValue
+    /** id */
+    private Long id;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    /** 批次ID */
+    @Excel(name = "批次ID")
+    private Long batchId;
+
+    /** 门店ID */
+    @Excel(name = "门店ID")
+    private Long storeId;
+
+    /** 0=未使用 1=已使用 2=已过期 3=冻结 */
+    @Excel(name = "状态")
+    private Integer status;
+
+    /** 订单ID */
+    @Excel(name = "订单ID")
+    private Long orderId;
+
+    /** 领取时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "领取时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime;
+
+    /** 使用时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "使用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date useTime;
+
+    /** 过期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "过期时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date expireTime;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+
+    public void setUserId(Long userId)
+    {
+        this.userId = userId;
+    }
+
+    public Long getUserId()
+    {
+        return userId;
+    }
+
+    public void setBatchId(Long batchId)
+    {
+        this.batchId = batchId;
+    }
+
+    public Long getBatchId()
+    {
+        return batchId;
+    }
+
+    public void setStoreId(Long storeId)
+    {
+        this.storeId = storeId;
+    }
+
+    public Long getStoreId()
+    {
+        return storeId;
+    }
+
+    public void setStatus(Integer status)
+    {
+        this.status = status;
+    }
+
+    public Integer getStatus()
+    {
+        return status;
+    }
+
+    public void setOrderId(Long orderId)
+    {
+        this.orderId = orderId;
+    }
+
+    public Long getOrderId()
+    {
+        return orderId;
+    }
+
+    public void setReceiveTime(Date receiveTime)
+    {
+        this.receiveTime = receiveTime;
+    }
+
+    public Date getReceiveTime()
+    {
+        return receiveTime;
+    }
+
+    public void setUseTime(Date useTime)
+    {
+        this.useTime = useTime;
+    }
+
+    public Date getUseTime()
+    {
+        return useTime;
+    }
+
+    public void setExpireTime(Date expireTime)
+    {
+        this.expireTime = expireTime;
+    }
+
+    public Date getExpireTime()
+    {
+        return expireTime;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("userId", getUserId())
+            .append("batchId", getBatchId())
+            .append("storeId", getStoreId())
+            .append("status", getStatus())
+            .append("orderId", getOrderId())
+            .append("receiveTime", getReceiveTime())
+            .append("useTime", getUseTime())
+            .append("expireTime", getExpireTime())
+            .toString();
+    }
+}

+ 14 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityMapper.java

@@ -0,0 +1,14 @@
+package com.ruoyi.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.domain.PromotionActivity;
+
+/**
+ * PromotionActivityMapper接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface PromotionActivityMapper extends BaseMapper<PromotionActivity>
+{
+}

+ 22 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityRuleMapper.java

@@ -0,0 +1,22 @@
+package com.ruoyi.system.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.domain.PromotionActivityRule;
+
+/**
+ * PromotionActivityRuleMapper接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface PromotionActivityRuleMapper extends BaseMapper<PromotionActivityRule>
+{
+    /**
+     * 根据活动ID查询规则列表
+     *
+     * @param activityId 活动ID
+     * @return 规则列表
+     */
+    List<PromotionActivityRule> selectRulesByActivityId(Long activityId);
+}

+ 14 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponBatchMapper.java

@@ -0,0 +1,14 @@
+package com.ruoyi.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.domain.PromotionCouponBatch;
+
+/**
+ * PromotionCouponBatchMapper接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface PromotionCouponBatchMapper extends BaseMapper<PromotionCouponBatch>
+{
+}

+ 21 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponRuleMapper.java

@@ -0,0 +1,21 @@
+package com.ruoyi.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.domain.PromotionCouponRule;
+
+/**
+ * PromotionCouponRuleMapper接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface PromotionCouponRuleMapper extends BaseMapper<PromotionCouponRule>
+{
+    /**
+     * 根据批次ID查询券规则
+     *
+     * @param batchId 批次ID
+     * @return 券规则
+     */
+    PromotionCouponRule selectRuleByBatchId(Long batchId);
+}

+ 14 - 0
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionUserCouponMapper.java

@@ -0,0 +1,14 @@
+package com.ruoyi.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.domain.PromotionUserCoupon;
+
+/**
+ * PromotionUserCouponMapper接口
+ *
+ * @author ruoyi
+ * @date 2026-05-29
+ */
+public interface PromotionUserCouponMapper extends BaseMapper<PromotionUserCoupon>
+{
+}

+ 22 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityRuleService.java

@@ -0,0 +1,22 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.system.domain.PromotionActivityRule;
+
+/**
+ * 促销活动规则Service接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface IPromotionActivityRuleService extends IService<PromotionActivityRule>
+{
+    /**
+     * 根据活动ID查询规则列表
+     *
+     * @param activityId 活动ID
+     * @return 规则列表
+     */
+    List<PromotionActivityRule> selectRulesByActivityId(Long activityId);
+}

+ 40 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityService.java

@@ -0,0 +1,40 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.system.domain.PromotionActivity;
+import com.ruoyi.system.domain.PromotionActivityRule;
+
+/**
+ * 促销活动Service接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface IPromotionActivityService extends IService<PromotionActivity>
+{
+    /**
+     * 创建促销活动(含规则)
+     *
+     * @param activity 活动主体
+     * @param rules 活动规则列表
+     * @return 是否成功
+     */
+    boolean createActivity(PromotionActivity activity, List<PromotionActivityRule> rules);
+
+    /**
+     * 查询活动详情(含规则)
+     *
+     * @param id 活动ID
+     * @return 活动详情
+     */
+    PromotionActivity selectActivityWithRules(Long id);
+
+    /**
+     * 结束活动
+     *
+     * @param id 活动ID
+     * @return 是否成功
+     */
+    boolean endActivity(Long id);
+}

+ 39 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponBatchService.java

@@ -0,0 +1,39 @@
+package com.ruoyi.system.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.system.domain.PromotionCouponBatch;
+import com.ruoyi.system.domain.PromotionCouponRule;
+
+/**
+ * 优惠券批次Service接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface IPromotionCouponBatchService extends IService<PromotionCouponBatch>
+{
+    /**
+     * 创建优惠券批次(含规则)
+     *
+     * @param batch 批次主体
+     * @param rule 券规则
+     * @return 是否成功
+     */
+    boolean createBatch(PromotionCouponBatch batch, PromotionCouponRule rule);
+
+    /**
+     * 查询批次详情(含规则)
+     *
+     * @param id 批次ID
+     * @return 批次详情
+     */
+    PromotionCouponBatch selectBatchWithRule(Long id);
+
+    /**
+     * 下架批次
+     *
+     * @param id 批次ID
+     * @return 是否成功
+     */
+    boolean offShelfBatch(Long id);
+}

+ 21 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponRuleService.java

@@ -0,0 +1,21 @@
+package com.ruoyi.system.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.system.domain.PromotionCouponRule;
+
+/**
+ * 优惠券规则Service接口
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+public interface IPromotionCouponRuleService extends IService<PromotionCouponRule>
+{
+    /**
+     * 根据批次ID查询券规则
+     *
+     * @param batchId 批次ID
+     * @return 券规则
+     */
+    PromotionCouponRule selectRuleByBatchId(Long batchId);
+}

+ 32 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionActivityRuleServiceImpl.java

@@ -0,0 +1,32 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.mapper.PromotionActivityRuleMapper;
+import com.ruoyi.system.domain.PromotionActivityRule;
+import com.ruoyi.system.service.IPromotionActivityRuleService;
+
+/**
+ * 促销活动规则Service业务层处理
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Service
+public class PromotionActivityRuleServiceImpl extends ServiceImpl<BaseMapper<PromotionActivityRule>, PromotionActivityRule> implements IPromotionActivityRuleService
+{
+    @Autowired
+    private PromotionActivityRuleMapper ruleMapper;
+
+    /**
+     * 根据活动ID查询规则列表
+     */
+    @Override
+    public List<PromotionActivityRule> selectRulesByActivityId(Long activityId)
+    {
+        return ruleMapper.selectRulesByActivityId(activityId);
+    }
+}

+ 88 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionActivityServiceImpl.java

@@ -0,0 +1,88 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.Date;
+import java.util.List;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.mapper.PromotionActivityMapper;
+import com.ruoyi.system.mapper.PromotionActivityRuleMapper;
+import com.ruoyi.system.domain.PromotionActivity;
+import com.ruoyi.system.domain.PromotionActivityRule;
+import com.ruoyi.system.service.IPromotionActivityService;
+
+/**
+ * 促销活动Service业务层处理
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Service
+public class PromotionActivityServiceImpl extends ServiceImpl<BaseMapper<PromotionActivity>, PromotionActivity> implements IPromotionActivityService
+{
+    @Autowired
+    private PromotionActivityMapper promotionActivityMapper;
+
+    @Autowired
+    private PromotionActivityRuleMapper ruleMapper;
+
+    /**
+     * 创建促销活动(含规则)
+     */
+    @Override
+    @Transactional
+    public boolean createActivity(PromotionActivity activity, List<PromotionActivityRule> rules)
+    {
+        Date now = new Date();
+        activity.setCreateTime(now);
+        activity.setUpdateTime(now);
+        // 根据开始时间设置状态:开始时间早于当前→进行中(1),否则→未开始(0)
+        if (activity.getStartTime() != null && activity.getStartTime().before(now))
+        {
+            activity.setStatus(1);
+        }
+        else
+        {
+            activity.setStatus(0);
+        }
+        int rows = promotionActivityMapper.insert(activity);
+        Long activityId = activity.getId();
+        // 逐条插入规则
+        for (PromotionActivityRule rule : rules)
+        {
+            rule.setActivityId(activityId);
+            ruleMapper.insert(rule);
+        }
+        return rows > 0;
+    }
+
+    /**
+     * 查询活动详情(含规则)
+     */
+    @Override
+    public PromotionActivity selectActivityWithRules(Long id)
+    {
+        PromotionActivity activity = promotionActivityMapper.selectById(id);
+        if (activity != null)
+        {
+            List<PromotionActivityRule> rules = ruleMapper.selectRulesByActivityId(id);
+            activity.setRules(rules);
+        }
+        return activity;
+    }
+
+    /**
+     * 结束活动
+     */
+    @Override
+    public boolean endActivity(Long id)
+    {
+        PromotionActivity activity = new PromotionActivity();
+        activity.setId(id);
+        activity.setStatus(2);
+        activity.setUpdateTime(new Date());
+        return promotionActivityMapper.updateById(activity) > 0;
+    }
+}

+ 88 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionCouponBatchServiceImpl.java

@@ -0,0 +1,88 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.Date;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.mapper.PromotionCouponBatchMapper;
+import com.ruoyi.system.mapper.PromotionCouponRuleMapper;
+import com.ruoyi.system.domain.PromotionCouponBatch;
+import com.ruoyi.system.domain.PromotionCouponRule;
+import com.ruoyi.system.service.IPromotionCouponBatchService;
+
+/**
+ * 优惠券批次Service业务层处理
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Service
+public class PromotionCouponBatchServiceImpl extends ServiceImpl<BaseMapper<PromotionCouponBatch>, PromotionCouponBatch> implements IPromotionCouponBatchService
+{
+    @Autowired
+    private PromotionCouponBatchMapper promotionCouponBatchMapper;
+
+    @Autowired
+    private PromotionCouponRuleMapper ruleMapper;
+
+    /**
+     * 创建优惠券批次(含规则)
+     */
+    @Override
+    @Transactional
+    public boolean createBatch(PromotionCouponBatch batch, PromotionCouponRule rule)
+    {
+        Date now = new Date();
+        batch.setRemainCount(batch.getTotalCount());
+        batch.setReceivedCount(0);
+        batch.setCreateTime(now);
+        batch.setUpdateTime(now);
+        // 根据开始时间设置状态
+        if (batch.getStartTime() != null && batch.getStartTime().before(now))
+        {
+            batch.setStatus(1);
+        }
+        else
+        {
+            batch.setStatus(0);
+        }
+        int rows = promotionCouponBatchMapper.insert(batch);
+        // 设置规则的批次ID并插入
+        if (rule != null)
+        {
+            rule.setBatchId(batch.getId());
+            ruleMapper.insert(rule);
+        }
+        return rows > 0;
+    }
+
+    /**
+     * 查询批次详情(含规则)
+     */
+    @Override
+    public PromotionCouponBatch selectBatchWithRule(Long id)
+    {
+        PromotionCouponBatch batch = promotionCouponBatchMapper.selectById(id);
+        if (batch != null)
+        {
+            PromotionCouponRule rule = ruleMapper.selectRuleByBatchId(id);
+            batch.setRule(rule);
+        }
+        return batch;
+    }
+
+    /**
+     * 下架批次
+     */
+    @Override
+    public boolean offShelfBatch(Long id)
+    {
+        PromotionCouponBatch batch = new PromotionCouponBatch();
+        batch.setId(id);
+        batch.setStatus(3);
+        batch.setUpdateTime(new Date());
+        return promotionCouponBatchMapper.updateById(batch) > 0;
+    }
+}

+ 31 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionCouponRuleServiceImpl.java

@@ -0,0 +1,31 @@
+package com.ruoyi.system.service.impl;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.system.mapper.PromotionCouponRuleMapper;
+import com.ruoyi.system.domain.PromotionCouponRule;
+import com.ruoyi.system.service.IPromotionCouponRuleService;
+
+/**
+ * 优惠券规则Service业务层处理
+ *
+ * @author ruoyi
+ * @date 2024-05-30
+ */
+@Service
+public class PromotionCouponRuleServiceImpl extends ServiceImpl<BaseMapper<PromotionCouponRule>, PromotionCouponRule> implements IPromotionCouponRuleService
+{
+    @Autowired
+    private PromotionCouponRuleMapper ruleMapper;
+
+    /**
+     * 根据批次ID查询券规则
+     */
+    @Override
+    public PromotionCouponRule selectRuleByBatchId(Long batchId)
+    {
+        return ruleMapper.selectRuleByBatchId(batchId);
+    }
+}

+ 19 - 0
ruoyi-system/src/main/resources/mapper/system/PromotionActivityMapper.xml

@@ -0,0 +1,19 @@
+<?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.ruoyi.system.mapper.PromotionActivityMapper">
+
+    <resultMap type="PromotionActivity" id="PromotionActivityResult">
+        <result property="id"    column="id"    />
+        <result property="storeId"    column="store_id"    />
+        <result property="type"    column="type"    />
+        <result property="name"    column="name"    />
+        <result property="status"    column="status"    />
+        <result property="startTime"    column="start_time"    />
+        <result property="endTime"    column="end_time"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+</mapper>

+ 32 - 0
ruoyi-system/src/main/resources/mapper/system/PromotionActivityRuleMapper.xml

@@ -0,0 +1,32 @@
+<?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.ruoyi.system.mapper.PromotionActivityRuleMapper">
+
+    <resultMap type="PromotionActivityRule" id="PromotionActivityRuleResult">
+        <result property="id"    column="id"    />
+        <result property="activityId"    column="activity_id"    />
+        <result property="productId"    column="product_id"    />
+        <result property="threshold"    column="threshold"    />
+        <result property="reduceAmount"    column="reduce_amount"    />
+        <result property="discountRate"    column="discount_rate"    />
+        <result property="minQuantity"    column="min_quantity"    />
+        <result property="productName"    column="product_name"    />
+        <result property="productImage"    column="product_image"    />
+        <result property="productPrice"    column="product_price"    />
+    </resultMap>
+
+    <select id="selectRulesByActivityId" parameterType="Long" resultMap="PromotionActivityRuleResult">
+        SELECT
+            r.*,
+            f.name AS product_name,
+            f.image AS product_image,
+            f.price AS product_price
+        FROM promotion_activity_rule r
+        LEFT JOIN pos_food f ON r.product_id = f.id
+        WHERE r.activity_id = #{activityId}
+        ORDER BY r.id ASC
+    </select>
+
+</mapper>

+ 23 - 0
ruoyi-system/src/main/resources/mapper/system/PromotionCouponBatchMapper.xml

@@ -0,0 +1,23 @@
+<?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.ruoyi.system.mapper.PromotionCouponBatchMapper">
+
+    <resultMap type="PromotionCouponBatch" id="PromotionCouponBatchResult">
+        <result property="id"    column="id"    />
+        <result property="storeId"    column="store_id"    />
+        <result property="name"    column="name"    />
+        <result property="couponType"    column="coupon_type"    />
+        <result property="totalCount"    column="total_count"    />
+        <result property="remainCount"    column="remain_count"    />
+        <result property="receivedCount"    column="received_count"    />
+        <result property="status"    column="status"    />
+        <result property="startTime"    column="start_time"    />
+        <result property="endTime"    column="end_time"    />
+        <result property="validDays"    column="valid_days"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+</mapper>

+ 32 - 0
ruoyi-system/src/main/resources/mapper/system/PromotionCouponRuleMapper.xml

@@ -0,0 +1,32 @@
+<?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.ruoyi.system.mapper.PromotionCouponRuleMapper">
+
+    <resultMap type="PromotionCouponRule" id="PromotionCouponRuleResult">
+        <result property="id"    column="id"    />
+        <result property="batchId"    column="batch_id"    />
+        <result property="productId"    column="product_id"    />
+        <result property="isMutex"    column="is_mutex"    />
+        <result property="threshold"    column="threshold"    />
+        <result property="amount"    column="amount"    />
+        <result property="discountRate"    column="discount_rate"    />
+        <result property="productName"    column="product_name"    />
+        <result property="productImage"    column="product_image"    />
+        <result property="productPrice"    column="product_price"    />
+    </resultMap>
+
+    <select id="selectRuleByBatchId" parameterType="Long" resultMap="PromotionCouponRuleResult">
+        SELECT
+            r.*,
+            f.name AS product_name,
+            f.image AS product_image,
+            f.price AS product_price
+        FROM promotion_coupon_rule r
+        LEFT JOIN pos_food f ON r.product_id = f.id
+        WHERE r.batch_id = #{batchId}
+        LIMIT 1
+    </select>
+
+</mapper>

+ 19 - 0
ruoyi-system/src/main/resources/mapper/system/PromotionUserCouponMapper.xml

@@ -0,0 +1,19 @@
+<?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.ruoyi.system.mapper.PromotionUserCouponMapper">
+
+    <resultMap type="PromotionUserCoupon" id="PromotionUserCouponResult">
+        <result property="id"    column="id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="batchId"    column="batch_id"    />
+        <result property="storeId"    column="store_id"    />
+        <result property="status"    column="status"    />
+        <result property="orderId"    column="order_id"    />
+        <result property="receiveTime"    column="receive_time"    />
+        <result property="useTime"    column="use_time"    />
+        <result property="expireTime"    column="expire_time"    />
+    </resultMap>
+
+</mapper>

+ 223 - 0
specs/008-promotion-coupon/plan.md

@@ -0,0 +1,223 @@
+# Implementation Plan: 促销 + 优惠券系统
+
+**Branch**: `008-promotion-coupon` | **Date**: 2026-05-29 | **Spec**: [spec.md](./spec.md)
+**Input**: Feature specification from `/specs/008-promotion-coupon/spec.md`
+
+## Summary
+
+为 foodie 商家端新增促销活动管理(满减/折扣商品/第二份半价/新客立减 4 种类型)和优惠券管理(满减券/商品券),商家可在后台创建和管理促销活动与优惠券。旧促销/优惠券代码(SalesPromotion、VipQuanyi)不动,全部新建文件、新表(promotion_ 前缀)、新接口。前端在商家端侧边栏新增独立的「营销管理」菜单。用户端(小程序)的领券、下单优惠计算不在本范围。
+
+**技术方案**:后端跟随现有 CRUD 分层(Entity + MyBatis XML Mapper + Service + Controller),5 张新表 + 2 个商家端 Controller。前端新增 2 个 Vue 页面,促销活动创建页面用 el-tabs 切换 4 种活动类型表单,每种类型有不同的表单字段和数据提交结构。折扣类型的"分组"概念纯前端实现,提交时展开为扁平的 rules 列表。
+
+## Technical Context
+
+**Language/Version**: Java 17 (Spring Boot 3.x, MyBatis-Plus)
+**Primary Dependencies**: Spring Boot, MyBatis-Plus, Vue.js 2.6, Element UI 2.15, vue-i18n
+**Storage**: MySQL (5 张新表: promotion_activity, promotion_activity_rule, promotion_coupon_batch, promotion_coupon_rule, promotion_user_coupon)
+**Testing**: 手动测试 (Postman + 前端页面验证)
+**Target Platform**: 商家 PC 端管理后台 (foodie-store)
+**Project Type**: Web 应用 (Java 后端 + Vue 前端)
+**Performance Goals**: 无特殊性能要求,标准 CRUD 操作
+**Constraints**: 旧代码不动,新表用 promotion_ 前缀,SQL 写入 updatesql/sql.md 手动执行,前端文件 CRLF 换行
+**Scale/Scope**: 商家端 2 个新页面 + 后端 5 个实体 + 2 个 Controller
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+本项目 constitution 未配置具体规则(模板状态),跳过此检查。
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/008-promotion-coupon/
+├── spec.md              # 功能规格(已完成)
+├── plan.md              # 本文件 — 实施计划
+└── tasks.md             # 任务列表(后续生成)
+```
+
+### Source Code (repository root)
+
+```text
+# 后端 (foodie_server)
+ruoyi-system/src/main/java/com/ruoyi/system/
+├── domain/
+│   ├── PromotionActivity.java         # 促销活动实体
+│   ├── PromotionActivityRule.java     # 促销规则实体
+│   ├── PromotionCouponBatch.java      # 券批次实体
+│   ├── PromotionCouponRule.java       # 券规则实体
+│   └── PromotionUserCoupon.java       # 用户券实体
+├── mapper/
+│   ├── PromotionActivityMapper.java
+│   ├── PromotionActivityRuleMapper.java
+│   ├── PromotionCouponBatchMapper.java
+│   ├── PromotionCouponRuleMapper.java
+│   └── PromotionUserCouponMapper.java
+└── service/
+    ├── IPromotionActivityService.java
+    ├── IPromotionActivityRuleService.java
+    ├── IPromotionCouponBatchService.java
+    ├── IPromotionCouponRuleService.java
+    └── impl/
+        ├── PromotionActivityServiceImpl.java
+        ├── PromotionActivityRuleServiceImpl.java
+        ├── PromotionCouponBatchServiceImpl.java
+        └── PromotionCouponRuleServiceImpl.java
+
+ruoyi-system/src/main/resources/mapper/system/
+├── PromotionActivityMapper.xml
+├── PromotionActivityRuleMapper.xml
+├── PromotionCouponBatchMapper.xml
+├── PromotionCouponRuleMapper.xml
+└── PromotionUserCouponMapper.xml
+
+ruoyi-admin/src/main/java/com/ruoyi/app/mendian/
+├── ShPromotionActivityController.java    # 商家促销活动 API
+└── ShPromotionCouponController.java      # 商家优惠券 API
+
+updatesql/
+└── sql.md                                # SQL 迁移脚本(追加建表语句)
+
+# 前端 (foodie-store)
+src/
+├── api/
+│   ├── promotionActivity.js             # 促销活动 API
+│   └── promotionCoupon.js               # 优惠券 API
+├── views/
+│   ├── PromotionActivity.vue            # 促销活动管理页面
+│   └── CouponBatch.vue                  # 优惠券管理页面
+├── components/
+│   └── Aside.vue                        # 侧边栏(新增菜单项)
+├── router/
+│   └── index.js                         # 路由(新增2条路由)
+└── lang/
+    ├── zh.js                            # 中文 i18n(新增 promoMenu/promoActivity/couponBatch)
+    ├── tw.js                            # 繁体中文
+    ├── en.js                            # 英文
+    └── vi.js                            # 越南语
+```
+
+**Structure Decision**: 后端遵循现有分层架构(domain/mapper/service/controller),前端页面放在 views/ 目录,API 放在 api/ 目录。不新建子目录,与现有文件组织方式一致。
+
+## Key Design Decisions
+
+### 1. 折扣类型的"分组"概念
+
+折扣商品(type=2)在前端用"折扣区"的分组概念交互(如"4折区"、"7折区"),但后端 `promotion_activity_rule` 表不存储分组信息——每条规则直接记录 `(productId, discountRate)`。
+
+前端提交时将分组展开为扁平的 rules 列表:
+```
+折扣组 "4折区" (rate=0.4) 包含 [可乐, 雪碧]
+折扣组 "7折区" (rate=0.7) 包含 [宫保鸡丁]
+→ 提交为 rules: [
+    {productId: 可乐ID, discountRate: 0.4},
+    {productId: 雪碧ID, discountRate: 0.4},
+    {productId: 宫保鸡丁ID, discountRate: 0.7}
+  ]
+```
+
+### 2. 活动状态管理
+
+活动状态使用显式字段(0=未开始, 1=进行中, 2=已结束),在创建时根据 startTime 自动设置。不使用定时任务自动更新状态(简化实现),列表查询时按 status 字段过滤即可。
+
+### 3. 商品选择弹窗复用
+
+现有的 `Quanyi.vue` 中已有商品选择弹窗模式(分类下拉 + 搜索 + 商品表格 + 分页)。PromotionActivity.vue 和 CouponBatch.vue 复用相同的交互模式和 API 调用方式,不抽取为公共组件(遵循项目现有风格)。
+
+### 4. 前端提交数据结构
+
+促销活动创建:前端发送完整的 `{ storeId, type, name, startTime, endTime, rules: [...] }` JSON,后端在一个事务中插入 activity + rules。
+
+优惠券创建:前端发送 `{ storeId, name, couponType, totalCount, validDays, startTime, endTime, rule: {...} }` JSON,后端在一个事务中插入 batch + rule。
+
+## API Contracts
+
+### ShPromotionActivityController
+
+| Method | Path | Description | Request | Response |
+|--------|------|-------------|---------|----------|
+| GET | `/system/shPromotionActivity/list` | 分页列表 | params: page, size, storeId, type?, status? | `{ code:200, data: { records:[], total, current, size } }` |
+| GET | `/system/shPromotionActivity/{id}` | 详情(含规则) | path: id | `{ code:200, data: { id, storeId, type, name, status, startTime, endTime, rules: [...] } }` |
+| POST | `/system/shPromotionActivity` | 创建活动 | body: `{ storeId, type, name, startTime, endTime, rules: [...] }` | `{ code:200, msg:"操作成功" }` |
+| PUT | `/system/shPromotionActivity/{id}/end` | 结束活动 | path: id | `{ code:200, msg:"操作成功" }` |
+
+### ShPromotionCouponController
+
+| Method | Path | Description | Request | Response |
+|--------|------|-------------|---------|----------|
+| GET | `/system/shPromotionCoupon/list` | 分页列表 | params: page, size, storeId, couponType?, status? | `{ code:200, data: { records:[], total } }` |
+| GET | `/system/shPromotionCoupon/{id}` | 详情(含规则) | path: id | `{ code:200, data: { id, storeId, name, couponType, totalCount, ..., rule: {...} } }` |
+| POST | `/system/shPromotionCoupon` | 创建券 | body: `{ storeId, name, couponType, totalCount, validDays, startTime, endTime, rule: {...} }` | `{ code:200, msg:"操作成功" }` |
+| PUT | `/system/shPromotionCoupon/{id}/offShelf` | 下架券 | path: id | `{ code:200, msg:"操作成功" }` |
+
+### Request Body Examples
+
+**创建满减活动**:
+```json
+{
+  "storeId": 1,
+  "type": 1,
+  "name": "午市满减",
+  "startTime": "2026-06-01 10:00:00",
+  "endTime": "2026-06-30 22:00:00",
+  "rules": [
+    { "threshold": 20.00, "reduceAmount": 5.00 },
+    { "threshold": 40.00, "reduceAmount": 12.00 },
+    { "threshold": 60.00, "reduceAmount": 20.00 }
+  ]
+}
+```
+
+**创建折扣活动**:
+```json
+{
+  "storeId": 1,
+  "type": 2,
+  "name": "夏季折扣",
+  "startTime": "2026-06-01 00:00:00",
+  "endTime": "2026-06-30 23:59:59",
+  "rules": [
+    { "discountRate": 0.40, "productId": 101 },
+    { "discountRate": 0.40, "productId": 102 },
+    { "discountRate": 0.70, "productId": 201 }
+  ]
+}
+```
+
+**创建满减券**:
+```json
+{
+  "storeId": 1,
+  "name": "满30减5",
+  "couponType": 1,
+  "totalCount": 100,
+  "validDays": 7,
+  "startTime": "2026-06-01 00:00:00",
+  "endTime": "2026-06-30 23:59:59",
+  "rule": {
+    "isMutex": 0,
+    "threshold": 30.00,
+    "amount": 5.00
+  }
+}
+```
+
+## Data Model
+
+参见 spec.md 的 Data Model 章节,5 张表完整 DDL 已在 spec 中定义。
+
+表关系:
+```
+promotion_activity 1 → N promotion_activity_rule
+promotion_coupon_batch 1 → 1 promotion_coupon_rule
+promotion_coupon_batch 1 → N promotion_user_coupon
+```
+
+## Complexity Tracking
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| 折扣类型前端分组概念 | 商家需要按折扣率分组管理商品,类似美团交互 | 直接输入每条规则太繁琐,不符合商家操作习惯 |
+| 4种活动类型共用一个创建对话框 | 商家从同一个入口创建不同类型的活动 | 每种类型独立页面会导致页面冗余 |

+ 403 - 0
specs/008-promotion-coupon/spec.md

@@ -0,0 +1,403 @@
+# Feature Specification: 商家促销 + 优惠券系统
+
+**Feature Branch**: `008-promotion-coupon`  
+**Created**: 2026-05-29  
+**Status**: Draft  
+**Input**: 参考美团外卖商家端营销活动机制,为 foodie 系统实现促销和优惠券功能
+
+## 背景
+
+foodie 系统目前缺少商家端的营销工具。商家无法设置满减、折扣等促销活动,也无法发放优惠券。需要参考美团外卖的机制,实现一套适合 foodie 的促销 + 优惠券系统。
+
+### 旧代码处理
+
+系统中已有旧的促销/优惠券代码,**全部废弃不使用,不修改**:
+
+| 旧代码 | 位置 | 说明 |
+|--------|------|------|
+| `SalesPromotion.java` | ruoyi-system/domain | 旧促销活动实体,表 `sales_promotion` |
+| `VipQuanyi.java` | ruoyi-system/domain | 旧优惠券模板,表 `vip_quanyi` |
+| `VipUserQuanyi.java` | ruoyi-system/domain | 旧用户券,表 `vip_user_quanyi` |
+| `SalesPromotionController` | ruoyi-admin/app/promotion | 平台管理端促销接口 |
+| `ShSalesPromotionController` | ruoyi-admin/app/mendian | 商家端促销接口 |
+| `VipUserQuanyiController` | ruoyi-admin/app/user | 用户端优惠券接口 |
+
+**原则:所有新功能新建文件、新表、新接口,不动旧代码。**
+
+### 新表命名规则
+
+所有新建数据库表使用 `promotion_` 前缀,与旧表区分:
+
+| 新表名 | 说明 |
+|--------|------|
+| `promotion_activity` | 促销活动表(替代旧 `sales_promotion`) |
+| `promotion_activity_rule` | 促销规则明细表 |
+| `promotion_coupon_batch` | 券批次表(替代旧 `vip_quanyi`) |
+| `promotion_coupon_rule` | 券规则表 |
+| `promotion_user_coupon` | 用户券表(替代旧 `vip_user_quanyi`) |
+
+### 工作范围
+
+只有两部分:
+1. **后端 API**(Java/Spring Boot)— 新建 Entity、Mapper、Service、Controller
+2. **商家 PC 端前端**(foodie-store Vue.js)— 促销管理页面 + 优惠券管理页面
+
+用户端(小程序)的领券、下单优惠计算不在本 spec 范围内,后续单独做。
+
+### 美团商家营销活动分类
+
+美团外卖商家端有16种营销活动,分为三类:
+- **促销类**(直接影响价格):满减、折扣商品、第二份半价、新客立减、减配送费、满减运费、爆品
+- **优惠券类**(用户领券后使用):店内领券、店外发券、下单返券、集点返券、收藏有礼、售卖代金券
+- **其他营销**:满赠、买赠、超值换购、好友助力、美团会员红包
+
+### foodie 范围裁剪
+
+本需求只实现以下功能:
+
+**促销(4种)**:满减、折扣商品(分组折扣)、第二份半价、新客立减
+
+**不做**:爆品、买赠、减配送费、满减运费、好友助力等
+
+**优惠券**:只做"店内领券"一种发放方式
+
+**不做**:店外发券、下单返券、集点返券、收藏有礼、售卖代金券
+
+### 促销的作用对象
+
+促销不是都作用于同一个价格层级:
+
+| 层级 | 作用对象 | 包含的促销类型 |
+|------|---------|-------------|
+| 订单级 | 订单总价 | 满减、新客立减 |
+| 商品级 | 指定商品单价 | 折扣商品、第二份半价 |
+
+### 互斥规则
+
+参考美团做法,用户下单时系统自动计算两条路径,选最优惠的:
+
+- **满减、折扣商品、第二份半价 → 三选一**,系统算两遍自动选最优
+  - 路径A(走折扣):折扣商品按折扣价,非折扣商品原价
+  - 路径B(走满减):所有商品恢复原价,再减满减金额
+- **新客立减** → 可与以上任意一种叠加
+- **互斥在下单时判断**,不限制商家创建活动
+
+### 叠加计算顺序
+
+```
+商品原价 → 促销(满减/折扣/第二份半价,三选一)→ 商家满减券 → 平台券 = 实付金额
+商品券单独作用在指定商品上,不与满减券叠加
+一个订单最多:1个促销 + 1张满减券 + 1张平台券
+```
+
+## User Scenarios & Testing
+
+### User Story 1 - 商家创建促销活动 (Priority: P1)
+
+商家在后台创建促销活动(满减/折扣/第二份半价/新客立减),活动生效后用户下单时自动享受优惠。
+
+**Why this priority**: 促销是商家最核心的营销需求,没有促销就没有优惠,后续优惠券也无法叠加使用。
+
+**Independent Test**: 商家创建一个满减活动(满20减5),用户下单满20元后自动减5元,订单金额正确。
+
+**Acceptance Scenarios**:
+
+1. **Given** 商家未创建任何促销活动, **When** 商家创建满减活动(满20减5 / 满40减12), **Then** 活动列表显示进行中,用户端店铺显示满减标签
+2. **Given** 商家已创建折扣活动(宫保鸡丁7折、麻婆豆腐8折), **When** 用户下单宫保鸡丁(¥20) + 麻婆豆腐(¥18), **Then** 折扣价 ¥14 + ¥14.4 = ¥28.4
+3. **Given** 商家同时有满减(满40减12)和折扣(宫保鸡丁7折¥14), **When** 用户下单宫保鸡丁(¥20) + 鸡腿饭(¥25) = ¥45, **Then** 系统算两遍:走折扣¥39 vs 走满减¥33,自动选满减
+4. **Given** 商家创建新客立减3元 + 满减(满40减12), **When** 新用户下单¥45, **Then** 先减满减12 = ¥33,再减新客立减3 = ¥30
+5. **Given** 折扣活动采用分组折扣, **When** 商家先建"4折区"和"7折区",把可乐拖入4折区、宫保鸡丁拖入7折区, **Then** 可乐¥6×0.4=¥2.4,宫保鸡丁¥20×0.7=¥14
+
+---
+
+### User Story 2 - 商家创建优惠券 (Priority: P2)
+
+商家创建优惠券(满减券/商品券),设置库存和有效期。用户进店时看到领券区,领取后下单使用。
+
+**Why this priority**: 优惠券在促销基础上进一步促进复购和转化,但依赖促销系统先完成。
+
+**Independent Test**: 商家创建"满30减5"优惠券100张,用户进店领取,下单满30元时选择使用,实付减5元。
+
+**Acceptance Scenarios**:
+
+1. **Given** 商家创建满减券(同享券,满30减5,100张), **When** 用户进店看到领券区并领取, **Then** 用户"我的优惠券"列表显示该券,状态为未使用
+2. **Given** 用户已领取满减券(满30减5)且商家有满减活动(满40减12), **When** 用户下单¥45, **Then** 先减满减12=¥33,再减券5=¥28(同享券可叠加)
+3. **Given** 用户已领取互斥券(满30减5), **When** 用户下单且订单有满减活动, **Then** 互斥券不可与满减叠加,用户只能二选一
+4. **Given** 商家创建商品折扣券(宫保鸡丁5折), **When** 用户下单宫保鸡丁(¥20)并使用该券, **Then** 宫保鸡丁按¥10计算
+5. **Given** 优惠券库存为100张,已领取100张, **When** 新用户进店, **Then** 领券区显示"已领完"
+
+---
+
+### User Story 3 - 用户下单时优惠计算和展示 (Priority: P3)
+
+用户在结算页看到优惠明细,系统自动选择最优方案,用户可手动切换。
+
+**Why this priority**: 结算页的优惠展示和计算是用户最终看到的结果,需要在促销和优惠券都完成后实现。
+
+**Independent Test**: 用户选好商品进入结算页,页面展示优惠明细(满减/折扣自动选最优 + 可用券列表),选择券后实付金额实时更新。
+
+**Acceptance Scenarios**:
+
+1. **Given** 商家有满减(满40减12)和折扣(宫保鸡丁7折), **When** 用户进入结算页, **Then** 系统展示两条路径对比,默认选最优
+2. **Given** 用户有3张可用券, **When** 用户点击优惠券行, **Then** 弹出券列表,可用券和不可用券分Tab展示,选中券后金额重算
+3. **Given** 用户选了互斥券, **When** 订单同时有满减活动, **Then** 满减自动取消,金额重新计算
+
+---
+
+### Edge Cases
+
+- 商家创建了满减活动后又创建折扣活动,两个同时进行中,用户下单时怎么处理?→ 系统自动算两遍选最优,不限制商家创建
+- 用户领了券但下单时未达到门槛(券要求满30但订单只有25)→ 券在"不可用"Tab显示,提示"未达到¥30"
+- 用户领券后商家修改/下架了该券批次 → 已领取的券仍可使用(按领取时的规则),但不能再领取新券
+- 满减活动设了3个档位,订单金额正好在两个档位之间 → 命中低档位(如¥39命中满20减5而非满40减12)
+- 第二份半价商品用户只买了1件 → 不享受优惠,按原价
+- 商家结束促销活动时,正在进行中的订单 → 不影响已下单的订单,只影响新订单
+- 优惠券过期 → 过期券自动标记为已过期,不可使用
+
+## Requirements
+
+### Functional Requirements
+
+#### 促销活动管理(商家端)
+
+- **FR-001**: 商家 MUST 能创建满减活动,支持设置多个档位(如满20减5 / 满40减12 / 满60减20)
+- **FR-002**: 商家 MUST 能创建折扣商品活动,采用分组折扣方式:先建折扣档位(如4折区/7折区/8折区),再为每个档位分配商品
+- **FR-003**: 商家 MUST 能创建第二份半价活动,从商品列表勾选参加的商品
+- **FR-004**: 商家 MUST 能创建新客立减活动,设置固定减免金额
+- **FR-005**: 商家 MUST 能查看活动列表(进行中/未开始/已结束),能结束进行中的活动
+- **FR-006**: 系统 MUST 展示互斥规则提示:满减、折扣、第二份半价三选一,新客立减可叠加
+
+#### 优惠券管理(商家端)
+
+- **FR-007**: 商家 MUST 能创建满减券,设置同享/互斥属性、使用门槛、减免金额、发放总量、有效天数
+- **FR-008**: 商家 MUST 能创建商品券(折扣券/抵用券),关联指定商品,设置折扣率
+- **FR-009**: 商家 MUST 能查看优惠券列表,展示领取/使用情况、库存
+- **FR-010**: 优惠券发放方式只有一种:店内领券(用户进店时在领券区领取)
+
+#### 用户端
+
+- **FR-011**: 用户 MUST 能在店铺首页看到领券区,展示可领取的优惠券
+- **FR-012**: 用户 MUST 能领取优惠券,每人每批次限领1张
+- **FR-013**: 用户 MUST 能在"我的优惠券"中查看已领取的券(未使用/已使用/已过期)
+- **FR-014**: 用户 MUST 能在结算页看到促销优惠明细(自动选最优方案)
+- **FR-015**: 用户 MUST 能在结算页选择使用/不使用优惠券
+- **FR-016**: 系统 MUST 在结算页自动计算满减和折扣两条路径,默认选最优
+
+#### 算价逻辑
+
+- **FR-017**: 下单时系统 MUST 同时计算两条路径:
+  - 路径A(走折扣):折扣商品按折扣价,非折扣商品原价,再扣券
+  - 路径B(走满减):所有商品恢复原价,扣满减,再扣券
+- **FR-018**: 系统 MUST 默认选择实付金额更低的路径
+- **FR-019**: 用户 MUST 能在结算页手动切换路径
+- **FR-020**: 同享券 MUST 能与满减/折扣叠加使用
+- **FR-021**: 互斥券 MUST 不能与满减/折扣/第二份半价叠加
+- **FR-022**: 一个订单最多使用 1个促销 + 1张满减券 + 1张平台券
+
+### Key Entities
+
+- **促销活动 (store_promotion)**: 商家创建的促销规则,包含类型(满减/折扣/第二份半价/新客立减)、状态、时间范围
+- **促销规则 (store_promotion_rule)**: 促销活动的具体规则数据,一个活动对应多条规则(满减档位/折扣商品/第二份半价商品)
+- **券批次 (store_coupon_batch)**: 优惠券模板,包含券名称、类型、库存、有效期
+- **券规则 (store_coupon_rule)**: 优惠券的使用规则,包含门槛、金额、折扣率、同享/互斥属性
+- **用户券 (user_coupon)**: 用户领取的券实例,绑定用户和门店,记录状态和使用情况
+
+## Data Model
+
+所有新建表使用 `promotion_` 前缀,与旧表(`sales_promotion`、`vip_quanyi`、`vip_user_quanyi`)完全独立。
+
+### promotion_activity — 促销活动表
+
+```sql
+CREATE TABLE promotion_activity (
+  id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+  store_id        BIGINT       NOT NULL COMMENT '门店ID',
+  type            TINYINT      NOT NULL COMMENT '类型: 1=满减 2=折扣 3=第二份半价 4=新客立减',
+  name            VARCHAR(100) NOT NULL COMMENT '活动名称',
+  status          TINYINT      DEFAULT 0 COMMENT '0=未开始 1=进行中 2=已结束',
+  start_time      DATETIME     NOT NULL COMMENT '开始时间',
+  end_time        DATETIME     NOT NULL COMMENT '结束时间',
+  create_time     DATETIME     DEFAULT CURRENT_TIMESTAMP,
+  update_time     DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX idx_store_type (store_id, type)
+) COMMENT '促销活动表';
+```
+
+### promotion_activity_rule — 促销规则明细表
+
+```sql
+CREATE TABLE promotion_activity_rule (
+  id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+  activity_id     BIGINT   NOT NULL COMMENT '关联促销活动',
+  product_id      BIGINT   DEFAULT NULL COMMENT '商品ID(折扣/第二份半价用)',
+  threshold       DECIMAL(10,2) DEFAULT NULL COMMENT '满减门槛(满X元)',
+  reduce_amount   DECIMAL(10,2) DEFAULT NULL COMMENT '减免金额(满减/新客立减)',
+  discount_rate   DECIMAL(3,2)  DEFAULT NULL COMMENT '折扣率(0.70=7折 / 0.50=半价)',
+  min_quantity    INT       DEFAULT NULL COMMENT '最低数量(第二份半价=2)',
+  INDEX idx_activity (activity_id)
+) COMMENT '促销规则明细';
+```
+
+### promotion_coupon_batch — 券批次表(模板)
+
+```sql
+CREATE TABLE promotion_coupon_batch (
+  id                BIGINT AUTO_INCREMENT PRIMARY KEY,
+  store_id          BIGINT       NOT NULL COMMENT '门店ID',
+  name              VARCHAR(100) NOT NULL COMMENT '券名称',
+  coupon_type       TINYINT      NOT NULL COMMENT '1=满减券 2=商品券',
+  total_count       INT          NOT NULL COMMENT '发放总量',
+  remain_count      INT          NOT NULL COMMENT '剩余数量',
+  received_count    INT          DEFAULT 0 COMMENT '已领取数量',
+  status            TINYINT      DEFAULT 0 COMMENT '0=未开始 1=进行中 2=已结束 3=已下架',
+  start_time        DATETIME     NOT NULL COMMENT '领取开始时间',
+  end_time          DATETIME     NOT NULL COMMENT '领取结束时间',
+  valid_days        INT          NOT NULL COMMENT '领取后有效天数',
+  create_time       DATETIME     DEFAULT CURRENT_TIMESTAMP,
+  update_time       DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX idx_store_status (store_id, status)
+) COMMENT '券批次表(模板)';
+```
+
+### promotion_coupon_rule — 券规则表
+
+```sql
+CREATE TABLE promotion_coupon_rule (
+  id                BIGINT AUTO_INCREMENT PRIMARY KEY,
+  batch_id          BIGINT   NOT NULL COMMENT '关联券批次',
+  product_id        BIGINT   DEFAULT NULL COMMENT '商品ID(商品券用,满减券=NULL)',
+  is_mutex          TINYINT  DEFAULT 0 COMMENT '0=同享券 1=互斥券(满减券用)',
+  threshold         DECIMAL(10,2) DEFAULT NULL COMMENT '使用门槛(满X元可用)',
+  amount            DECIMAL(10,2) DEFAULT NULL COMMENT '减免金额(满减券/抵用券)',
+  discount_rate     DECIMAL(3,2)  DEFAULT NULL COMMENT '折扣率(商品折扣券用)',
+  INDEX idx_batch (batch_id)
+) COMMENT '券规则';
+```
+
+### promotion_user_coupon — 用户券表(实例)
+
+```sql
+CREATE TABLE promotion_user_coupon (
+  id                BIGINT AUTO_INCREMENT PRIMARY KEY,
+  user_id           BIGINT   NOT NULL COMMENT '用户ID',
+  batch_id          BIGINT   NOT NULL COMMENT '券批次ID',
+  store_id          BIGINT   NOT NULL COMMENT '门店ID',
+  status            TINYINT  DEFAULT 0 COMMENT '0=未使用 1=已使用 2=已过期 3=冻结',
+  order_id          BIGINT   DEFAULT NULL COMMENT '使用的订单ID',
+  receive_time      DATETIME NOT NULL COMMENT '领取时间',
+  use_time          DATETIME DEFAULT NULL COMMENT '使用时间',
+  expire_time       DATETIME NOT NULL COMMENT '过期时间',
+  INDEX idx_user_status (user_id, status),
+  INDEX idx_store (store_id)
+) COMMENT '用户领券记录';
+```
+
+### pos_order_promotion — 订单优惠明细表
+
+下单时快照记录每个订单命中的优惠信息,优惠类型级别(每条记录 = 一种优惠),不做 SKU 级分摊。
+
+```sql
+CREATE TABLE pos_order_promotion (
+  id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+  order_id        BIGINT       NOT NULL COMMENT '关联订单ID',
+  promo_type      TINYINT      NOT NULL COMMENT '优惠类型: 1=促销活动 2=商家优惠券',
+  promo_sub_type  TINYINT      DEFAULT NULL COMMENT '促销子类型: 1=满减 2=折扣 3=第二份半价 4=新客立减 (promo_type=1时有效)',
+  promo_id        BIGINT       DEFAULT NULL COMMENT '促销活动ID(promo_type=1) 或 券批次ID(promo_type=2)',
+  user_coupon_id  BIGINT       DEFAULT NULL COMMENT '用户券ID (仅优惠券 promo_type=2 时有值, 关联 promotion_user_coupon.id)',
+  promo_name      VARCHAR(200) NOT NULL COMMENT '快照名称: 如"午市满减(满40减12)"',
+  promo_detail    VARCHAR(500) DEFAULT NULL COMMENT '快照详情JSON: 如{"threshold":40,"reduce":12}',
+  reduce_amount   DECIMAL(10,2) NOT NULL COMMENT '减免金额',
+  path_summary    VARCHAR(500) DEFAULT NULL COMMENT '路径对比摘要, 仅第一条记录有值, 如"满减路径¥33 vs 折扣路径¥39, 选择满减"',
+  create_time     DATETIME     DEFAULT CURRENT_TIMESTAMP,
+  INDEX idx_order (order_id)
+) COMMENT '订单优惠明细';
+```
+
+#### 字段说明
+
+| 字段 | 作用 | 示例 |
+|------|------|------|
+| `promo_type` | 区分是促销还是优惠券 | 1=促销, 2=优惠券 |
+| `promo_sub_type` | 促销的具体子类型 | 1=满减, 2=折扣, 3=第二份半价, 4=新客立减;优惠券时为 null |
+| `promo_id` | 关联原始活动或券批次 | promo_type=1 时为 promotion_activity.id;promo_type=2 时为 promotion_coupon_batch.id |
+| `user_coupon_id` | 关联用户领取的券实例 | promo_type=2 时为 promotion_user_coupon.id;促销时为 null |
+| `promo_name` | 下单时的快照名称 | "午市满减(满40减12)" |
+| `promo_detail` | 规则快照 JSON | `{"type":"manjian","threshold":40,"reduce":12}` |
+| `reduce_amount` | 实际减免金额 | 12.00 |
+| `path_summary` | 路径对比摘要 | "满减路径¥33 vs 折扣路径¥39, 选择满减",仅第一条记录有值 |
+
+#### 示例数据
+
+订单:宫保鸡丁¥20 + 鸡腿饭¥25 = ¥45,商家有满减(满40减12)和折扣(宫保鸡丁7折),用户有满30减5同享券,新用户
+
+| promo_type | promo_sub_type | promo_name | reduce_amount | path_summary |
+|---|---|---|---|---|
+| 1(促销) | 1(满减) | 午市满减(满40减12) | 12.00 | 满减路径¥33 vs 折扣路径¥39, 选择满减 |
+| 2(优惠券) | null | 满30减5(同享券) | 5.00 | null |
+| 1(促销) | 4(新客立减) | 新客立减 | 3.00 | null |
+
+#### 与 pos_order 现有字段的关系
+
+下单时双写:
+- `pos_order` 的 `mdSalesReduction`、`mdDiscountAmount` 等汇总字段照常写入
+- `pos_order_promotion` 写入每条优惠明细(快照)
+
+查询场景:
+- 订单列表:只看 `pos_order` 汇总字段
+- 订单详情/退款:查 `pos_order_promotion` 获取明细
+
+#### 设计决策
+
+- **单表扁平设计**:一张表存储所有优惠明细,每条记录代表一种优惠,一个订单通常 3-5 条记录
+- **优惠类型级别**:不做 SKU 级优惠分摊,所有优惠都是商家级别
+- **快照机制**:下单时将促销名称、规则、金额拍快照存入,后续促销活动修改不影响已下单的订单
+- **路径对比摘要**:`path_summary` 字段记录系统计算的路径对比结果(如满减 vs 折扣),仅存在该订单的第一条明细记录上,其余记录该字段为 null
+- **pos_order 现有字段保留**:不删除现有的 `mdSalesReduction`、`mdDiscountAmount` 等字段,新表明细作为补充信息
+
+### 表关系
+
+```
+促销模块:
+  promotion_activity 1 → N promotion_activity_rule
+
+优惠券模块:
+  promotion_coupon_batch 1 → 1 promotion_coupon_rule
+  promotion_coupon_batch 1 → N promotion_user_coupon
+
+订单优惠:
+  pos_order 1 → N pos_order_promotion
+  pos_order_promotion N → 1 promotion_activity (promo_type=1 时)
+  pos_order_promotion N → 1 promotion_coupon_batch (promo_type=2 时)
+  pos_order_promotion N → 1 promotion_user_coupon (promo_type=2 时)
+```
+
+## Success Criteria
+
+### Measurable Outcomes
+
+- **SC-001**: 商家能在3分钟内完成一个满减活动的创建(包括设置3个档位)
+- **SC-002**: 商家能在3分钟内完成一个分组折扣活动的创建(建2个档位 + 分配5个商品)
+- **SC-003**: 商家能在2分钟内完成一张优惠券的创建
+- **SC-004**: 用户下单时优惠计算正确,结算页实时展示优惠明细
+- **SC-005**: 满100%覆盖互斥规则:满减/折扣/第二份半价三选一,新客立减可叠加
+
+## Assumptions
+
+- **旧代码不动**:`SalesPromotion`、`VipQuanyi`、`VipUserQuanyi` 等旧实体和旧接口全部废弃,新功能完全新建文件
+- **新表前缀 `promotion_`**:与旧表(`sales_promotion`、`vip_quanyi`)完全独立
+- **工作范围**:只有后端 API + 商家 PC 端前端(foodie-store)
+- **用户端不在本范围**:小程序端的领券、下单优惠计算、结算页展示后续单独做
+- 促销和优惠券都是门店级别(storeId),不是跨店通用
+- `pos_order_promotion`(订单优惠明细表):下单时快照记录优惠明细,单表扁平设计,优惠类型级别,不做 SKU 分摊。详见 Data Model 章节
+- 现有 `pos_order` 表的优惠汇总字段(`mdSalesReduction`、`mdDiscountAmount` 等)保留不动,新表明细作为补充
+- 平台券不在本需求范围内,只做商家级促销和商家券
+- SQL 变更写入 `updatesql/sql.md`,由开发者手动执行
+
+## 参考资源
+
+- 美团外卖商家活动创建攻略(知乎)
+- 美团外卖优惠规则(官方)
+- 美团外卖平台市场营销规则(rules-center.meituan.com)
+- 优惠券系统设计(知乎)
+- 大厂优惠券系统设计(掘金)
+- Demo文件:`meituan-promotion-types-demo.html`、`merchant-promotion-ui-demo.html`、`meituan-checkout-demo.html`

+ 193 - 0
specs/008-promotion-coupon/tasks.md

@@ -0,0 +1,193 @@
+# Tasks: 促销 + 优惠券系统
+
+**Input**: Design documents from `/specs/008-promotion-coupon/`
+**Prerequisites**: plan.md (required), spec.md (required for user stories)
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (US1=促销活动, US2=优惠券)
+- Include exact file paths in descriptions
+
+---
+
+## Phase 1: Setup (数据库 + 基础设施)
+
+**Purpose**: 创建数据库表,为后续所有任务提供基础
+
+- [ ] T001 追加 5 张新表 DDL 到 `foodie_server/updatesql/sql.md`(promotion_activity, promotion_activity_rule, promotion_coupon_batch, promotion_coupon_rule, promotion_user_coupon),DDL 来自 spec.md Data Model 章节
+- [ ] T002 手动执行 SQL 建表(提醒开发者操作)
+
+---
+
+## Phase 2: Foundational (后端实体 + 数据层)
+
+**Purpose**: 后端核心数据层,所有 Controller 和前端页面依赖此阶段完成
+
+### 实体类 (5个文件,全部可并行)
+
+- [ ] T003 [P] 创建 PromotionActivity 实体 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivity.java` — 字段: id, storeId, type, name, status, startTime, endTime, createTime, updateTime;非持久化: List<PromotionActivityRule> rules, boolean editable。使用 @Data @TableName @TableId(type=IdType.AUTO) @JsonFormat 注解,参考 SalesPromotion.java
+- [ ] T004 [P] 创建 PromotionActivityRule 实体 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivityRule.java` — 字段: id, activityId, productId, threshold(BigDecimal), reduceAmount(BigDecimal), discountRate(BigDecimal), minQuantity(Integer);非持久化: productName, productImage, productPrice
+- [ ] T005 [P] 创建 PromotionCouponBatch 实体 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponBatch.java` — 字段: id, storeId, name, couponType, totalCount, remainCount, receivedCount, status, startTime, endTime, validDays, createTime, updateTime;非持久化: PromotionCouponRule rule, boolean editable
+- [ ] T006 [P] 创建 PromotionCouponRule 实体 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponRule.java` — 字段: id, batchId, productId, isMutex(Integer), threshold(BigDecimal), amount(BigDecimal), discountRate(BigDecimal);非持久化: productName, productImage, productPrice
+- [ ] T007 [P] 创建 PromotionUserCoupon 实体 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionUserCoupon.java` — 字段: id, userId, batchId, storeId, status, orderId, receiveTime, useTime, expireTime
+
+### Mapper 接口 (5个文件,全部可并行)
+
+- [ ] T008 [P] 创建 PromotionActivityMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityMapper.java` — extends BaseMapper<PromotionActivity>
+- [ ] T009 [P] 创建 PromotionActivityRuleMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityRuleMapper.java` — extends BaseMapper<PromotionActivityRule>,额外方法 List<PromotionActivityRule> selectRulesByActivityId(Long activityId)
+- [ ] T010 [P] 创建 PromotionCouponBatchMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponBatchMapper.java` — extends BaseMapper<PromotionCouponBatch>
+- [ ] T011 [P] 创建 PromotionCouponRuleMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponRuleMapper.java` — extends BaseMapper<PromotionCouponRule>,额外方法 PromotionCouponRule selectRuleByBatchId(Long batchId)
+- [ ] T012 [P] 创建 PromotionUserCouponMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionUserCouponMapper.java` — extends BaseMapper<PromotionUserCoupon>
+
+### Mapper XML (5个文件,全部可并行)
+
+- [ ] T013 [P] 创建 PromotionActivityMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionActivityMapper.xml` — resultMap 映射全部字段
+- [ ] T014 [P] 创建 PromotionActivityRuleMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionActivityRuleMapper.xml` — resultMap + selectRulesByActivityId (LEFT JOIN pos_food 获取 productName/productImage/productPrice)
+- [ ] T015 [P] 创建 PromotionCouponBatchMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionCouponBatchMapper.xml` — resultMap 映射全部字段
+- [ ] T016 [P] 创建 PromotionCouponRuleMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionCouponRuleMapper.xml` — resultMap + selectRuleByBatchId (LEFT JOIN pos_food)
+- [ ] T017 [P] 创建 PromotionUserCouponMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionUserCouponMapper.xml` — resultMap 映射全部字段
+
+**Checkpoint**: 数据层完成,可进入 Service 和 Controller
+
+---
+
+## Phase 3: User Story 1 - 商家创建促销活动 (Priority: P1) 🎯 MVP
+
+**Goal**: 商家能创建满减/折扣/第二份半价/新客立减活动,查看活动列表,结束进行中的活动
+
+**Independent Test**: 商家创建一个满减活动(满20减5/满40减12),列表显示进行中,能查看详情,能结束活动
+
+### Service 层 (US1)
+
+- [ ] T018 [US1] 创建 IPromotionActivityService `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityService.java` — extends IService<PromotionActivity>,方法: createActivity(activity, rules), selectActivityWithRules(id), endActivity(id)
+- [ ] T019 [US1] 创建 PromotionActivityServiceImpl `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionActivityServiceImpl.java` — 实现 createActivity (@Transactional: 插入activity+批量插入rules, 根据 startTime 设置初始 status), selectActivityWithRules (查activity+查rules), endActivity (status→2)
+- [ ] T020 [US1] 创建 IPromotionActivityRuleService + PromotionActivityRuleServiceImpl `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityRuleService.java` 和 `impl/PromotionActivityRuleServiceImpl.java` — selectRulesByActivityId 委托给 mapper
+
+### Controller (US1)
+
+- [ ] T021 [US1] 创建 ShPromotionActivityController `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionActivityController.java` — @RestController @RequestMapping("/system/shPromotionActivity"),参考 ShSalesPromotionController。端点: GET /list (page,size,storeId,type?,status? → LambdaQueryWrapper 分页), GET /{id} (selectActivityWithRules), POST / (RequestBody含rules→createActivity), PUT /{id}/end (endActivity)
+
+### 前端 API (US1)
+
+- [ ] T022 [US1] 创建 promotionActivity.js `foodie-store/src/api/promotionActivity.js` — 导出 listPromotionActivity(query), getPromotionActivity(id), addPromotionActivity(data), endPromotionActivity(id),参考 src/api/promotion.js 模式
+
+### 前端页面 (US1)
+
+- [ ] T023 [US1] 创建 PromotionActivity.vue `foodie-store/src/views/PromotionActivity.vue` — 促销活动管理页面:
+  - **列表区域**: 面包屑 + 门店选择(复用xuanzhemd模式) + 状态筛选radio(全部/未开始/进行中/已结束) + el-table(ID/名称/类型el-tag/状态el-tag/时间/操作-结束按钮) + el-pagination
+  - **创建对话框** el-dialog width=750px + el-tabs v-model=activeTab:
+    - Tab1 满减(type=1): 活动名称 + 时间范围 + 动态档位行(v-for, 每行两个el-input-number:满X减Y) + 添加/删除档位按钮
+    - Tab2 折扣(type=2): 活动名称 + 时间范围 + 折扣组列表(v-for, 每组含折扣率+已选商品table+添加商品按钮) + 添加/删除折扣区按钮
+    - Tab3 第二份半价(type=3): 活动名称 + 时间范围 + 已选商品table + 选择商品按钮
+    - Tab4 新客立减(type=4): 活动名称 + 时间范围 + 减免金额el-input-number
+  - **商品选择弹窗**: 分类el-select + 搜索el-input + 商品el-table + el-pagination (复用Quanyi.vue商品加载API模式)
+  - **提交逻辑** tijiaobaocun(): 根据activeTab转换数据为 {storeId,type,name,startTime,endTime,rules:[...]},折扣组展开为扁平rules,调用addPromotionActivity
+  - **i18n**: 所有文字用 $t('promoActivity.xxx')
+
+**Checkpoint**: 促销活动完整功能可用——创建4种类型活动、查看列表、查看详情、结束活动
+
+---
+
+## Phase 4: User Story 2 - 商家创建优惠券 (Priority: P2)
+
+**Goal**: 商家能创建满减券/商品券,设置同享/互斥属性和库存,查看列表,下架券
+
+**Independent Test**: 商家创建"满30减5"满减券100张,列表显示,能查看详情,能下架
+
+### Service 层 (US2)
+
+- [ ] T024 [P] [US2] 创建 IPromotionCouponBatchService `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponBatchService.java` — 方法: createBatch(batch, rule), selectBatchWithRule(id), offShelfBatch(id)
+- [ ] T025 [P] [US2] 创建 PromotionCouponBatchServiceImpl `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionCouponBatchServiceImpl.java` — 实现 createBatch (@Transactional: remainCount=totalCount, 插入batch+rule), selectBatchWithRule (查batch+查rule), offShelfBatch (status→3)
+- [ ] T026 [P] [US2] 创建 IPromotionCouponRuleService + PromotionCouponRuleServiceImpl `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponRuleService.java` 和 `impl/PromotionCouponRuleServiceImpl.java` — selectRuleByBatchId 委托给 mapper
+
+### Controller (US2)
+
+- [ ] T027 [US2] 创建 ShPromotionCouponController `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionCouponController.java` — @RestController @RequestMapping("/system/shPromotionCoupon")。端点: GET /list (page,size,storeId,couponType?,status?), GET /{id} (selectBatchWithRule), POST / (RequestBody含rule→createBatch), PUT /{id}/offShelf
+
+### 前端 API (US2)
+
+- [ ] T028 [P] [US2] 创建 promotionCoupon.js `foodie-store/src/api/promotionCoupon.js` — 导出 listPromotionCoupon(query), getPromotionCoupon(id), addPromotionCoupon(data), offShelfPromotionCoupon(id)
+
+### 前端页面 (US2)
+
+- [ ] T029 [US2] 创建 CouponBatch.vue `foodie-store/src/views/CouponBatch.vue` — 优惠券管理页面:
+  - **列表区域**: 面包屑 + 门店选择 + 状态筛选(全部/未开始/进行中/已结束/已下架) + el-table(ID/名称/类型/总库存/剩余/已领取/状态/时间/有效期/操作-下架按钮) + el-pagination
+  - **创建对话框** el-dialog width=600px:
+    - 券类型 el-radio-group (满减券/商品券)
+    - 通用字段: 名称el-input, 门店el-select, 总量el-input-number, 有效天数el-input-number, 时间范围el-date-picker
+    - 满减券(v-if couponType===1): 叠加属性el-radio(同享/互斥), 使用门槛el-input-number, 减免金额el-input-number
+    - 商品券(v-if couponType===2): 选择商品按钮(复用商品弹窗), 折扣率el-input-number
+  - **提交逻辑** tijiaobaocun(): 构建 {storeId,name,couponType,totalCount,validDays,startTime,endTime,rule:{isMutex,threshold,amount,...}},调用addPromotionCoupon
+  - **i18n**: 所有文字用 $t('couponBatch.xxx')
+
+**Checkpoint**: 优惠券完整功能可用——创建满减券/商品券、查看列表、查看详情、下架券
+
+---
+
+## Phase 5: Menu + Router + i18n (跨 US)
+
+**Purpose**: 前端菜单、路由、多语言——连接所有页面的基础设施
+
+> **注意**: 前端文件使用 CRLF 换行,必须用 Python 脚本编辑,不能用 Edit 工具。
+
+- [ ] T030 在 `foodie-store/src/router/index.js` 的 /manage children 中添加路由 promotion-activity → PromotionActivity.vue 和 coupon-batch → CouponBatch.vue
+- [ ] T031 在 `foodie-store/src/components/Aside.vue` 末尾(el-submenu index="6" 之后)添加新的 el-submenu index="7"「营销管理」,含两个 menu-item: 促销活动(/manage/promotion-activity) + 优惠券管理(/manage/coupon-batch)
+- [ ] T032 [P] 在 `foodie-store/src/lang/zh.js` 末尾追加 promoMenu(营销管理/促销活动/优惠券管理)、promoActivity(~40个key覆盖页面全部文案)、couponBatch(~30个key)三个 i18n section
+- [ ] T033 [P] 在 `foodie-store/src/lang/tw.js` 追加同结构繁体中文翻译
+- [ ] T034 [P] 在 `foodie-store/src/lang/en.js` 追加同结构英文翻译
+- [ ] T035 [P] 在 `foodie-store/src/lang/vi.js` 追加同结构越南语翻译
+
+**Checkpoint**: 侧边栏显示新菜单,页面可访问,多语言正常
+
+---
+
+## Phase 6: Polish & 验证
+
+**Purpose**: 端到端验证
+
+- [ ] T036 后端启动验证:用 Postman 测试 4 个促销 API + 4 个优惠券 API 的完整 CRUD 流程
+- [ ] T037 前端启动验证:npm run dev → 侧边栏新菜单 → 创建满减活动(3档位) → 创建折扣活动(2组+商品) → 创建第二份半价 → 创建新客立减 → 创建满减券 → 创建商品券 → 列表筛选 → 结束活动 → 下架券
+- [ ] T038 [P] 切换4种语言验证所有文案显示正确
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Phase 1 (Setup)**: 无依赖,立即开始
+- **Phase 2 (Foundational)**: 依赖 Phase 1 的表已创建
+- **Phase 3 (US1 促销)**: 依赖 Phase 2 的实体和 Mapper
+- **Phase 4 (US2 优惠券)**: 依赖 Phase 2,可与 Phase 3 并行
+- **Phase 5 (Menu/i18n)**: 依赖 Phase 3 + Phase 4 的页面文件存在
+- **Phase 6 (验证)**: 依赖全部完成
+
+### Parallel Opportunities
+
+- T003-T007 (5个实体) 可并行
+- T008-T012 (5个Mapper接口) 可并行
+- T013-T017 (5个Mapper XML) 可并行
+- Phase 3 和 Phase 4 可并行(不同文件,不同 Controller)
+- T032-T035 (4个语言文件) 可并行
+
+### Recommended Execution Order
+
+```
+T001 → T002(手动)
+  → T003-T017 (Phase 2, 大量并行)
+  → T018-T023 (Phase 3, 促销活动) + T024-T029 (Phase 4, 优惠券) [可并行]
+  → T030-T035 (Phase 5, 菜单路由i18n)
+  → T036-T038 (Phase 6, 验证)
+```
+
+---
+
+## Notes
+
+- [P] tasks = 不同文件,无依赖,可并行
+- [Story] label 映射到 spec 中的 User Story (US1=促销, US2=优惠券)
+- 每个任务完成后 commit
+- 前端文件 CRLF 换行,用 Python 脚本编辑
+- 旧代码(SalesPromotion, VipQuanyi)不动
+- PromotionUserCoupon 实体和 Mapper 在 Phase 2 创建但无 Service/Controller(用户端功能不在本范围)

+ 104 - 0
specs/test/plan.md

@@ -0,0 +1,104 @@
+# Implementation Plan: [FEATURE]
+
+**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
+**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
+
+**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
+
+## Summary
+
+[Extract from feature spec: primary requirement + technical approach from research]
+
+## Technical Context
+
+<!--
+  ACTION REQUIRED: Replace the content in this section with the technical details
+  for the project. The structure here is presented in advisory capacity to guide
+  the iteration process.
+-->
+
+**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]  
+**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]  
+**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]  
+**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]  
+**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
+**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]  
+**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]  
+**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]  
+**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+[Gates determined based on constitution file]
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/[###-feature]/
+├── plan.md              # This file (/speckit.plan command output)
+├── research.md          # Phase 0 output (/speckit.plan command)
+├── data-model.md        # Phase 1 output (/speckit.plan command)
+├── quickstart.md        # Phase 1 output (/speckit.plan command)
+├── contracts/           # Phase 1 output (/speckit.plan command)
+└── tasks.md             # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
+```
+
+### Source Code (repository root)
+<!--
+  ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
+  for this feature. Delete unused options and expand the chosen structure with
+  real paths (e.g., apps/admin, packages/something). The delivered plan must
+  not include Option labels.
+-->
+
+```text
+# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
+src/
+├── models/
+├── services/
+├── cli/
+└── lib/
+
+tests/
+├── contract/
+├── integration/
+└── unit/
+
+# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
+backend/
+├── src/
+│   ├── models/
+│   ├── services/
+│   └── api/
+└── tests/
+
+frontend/
+├── src/
+│   ├── components/
+│   ├── pages/
+│   └── services/
+└── tests/
+
+# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
+api/
+└── [same as backend above]
+
+ios/ or android/
+└── [platform-specific structure: feature modules, UI flows, platform tests]
+```
+
+**Structure Decision**: [Document the selected structure and reference the real
+directories captured above]
+
+## Complexity Tracking
+
+> **Fill ONLY if Constitution Check has violations that must be justified**
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
+| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |