Explorar o código

feat: add Discount module (entity+mapper+service+controller)

Add discount rule management with admin CRUD controller.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg hai 2 meses
pai
achega
e43a76c6d4

+ 49 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminDiscountController.java

@@ -0,0 +1,49 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DiscountRule;
+import com.etotem.cfc.service.DiscountService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/shop/discount")
+public class AdminDiscountController {
+
+    @Resource
+    private DiscountService discountService;
+
+    @PostMapping("/create")
+    public Result<String> create(@RequestBody DiscountRule rule) {
+        return discountService.create(rule);
+    }
+
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody DiscountRule rule) {
+        return discountService.update(rule);
+    }
+
+    @PostMapping("/list")
+    public Result<List<DiscountRule>> list(@RequestBody Map<String, Object> body) {
+        Boolean enabled = body.containsKey("enabled") ? (Boolean) body.get("enabled") : null;
+        return discountService.list(enabled);
+    }
+
+    @PostMapping("/toggle")
+    public Result<String> toggle(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        return discountService.toggle(id);
+    }
+
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        return discountService.delete(id);
+    }
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DiscountRule.java

@@ -0,0 +1,29 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("shop_discount_rules")
+public class DiscountRule implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String name;
+    private String type;
+    private Integer value;
+    private Integer thresholdAmount;
+    private Long categoryId;
+    private Long distributionSystemId;
+    private String productIds;
+    private Date startTime;
+    private Date endTime;
+    private Integer maxUses;
+    private Integer usedCount;
+    private Boolean enabled;
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/DiscountRuleMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.DiscountRule;
+
+public interface DiscountRuleMapper extends BaseMapper<DiscountRule> {
+}

+ 146 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DiscountService.java

@@ -0,0 +1,146 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.DiscountRule;
+import com.etotem.cfc.mapper.DiscountRuleMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class DiscountService {
+
+    @Resource
+    private DiscountRuleMapper discountRuleMapper;
+
+    public Result<String> create(DiscountRule rule) {
+        rule.setUsedCount(0);
+        rule.setEnabled(true);
+        rule.setCreatedAt(new Date());
+        rule.setUpdatedAt(new Date());
+        discountRuleMapper.insert(rule);
+        return Result.success("创建成功");
+    }
+
+    public Result<String> update(DiscountRule rule) {
+        DiscountRule existing = discountRuleMapper.selectById(rule.getId());
+        if (existing == null) return Result.error("规则不存在");
+        rule.setUpdatedAt(new Date());
+        discountRuleMapper.updateById(rule);
+        return Result.success("更新成功");
+    }
+
+    public Result<String> toggle(Long id) {
+        DiscountRule rule = discountRuleMapper.selectById(id);
+        if (rule == null) return Result.error("规则不存在");
+        rule.setEnabled(!Boolean.TRUE.equals(rule.getEnabled()));
+        rule.setUpdatedAt(new Date());
+        discountRuleMapper.updateById(rule);
+        return Result.success(rule.getEnabled() ? "已启用" : "已禁用");
+    }
+
+    public Result<List<DiscountRule>> list(Boolean enabled) {
+        LambdaQueryWrapper<DiscountRule> wrapper = new LambdaQueryWrapper<>();
+        if (enabled != null) {
+            wrapper.eq(DiscountRule::getEnabled, enabled);
+        }
+        wrapper.orderByDesc(DiscountRule::getCreatedAt);
+        return Result.success(discountRuleMapper.selectList(wrapper));
+    }
+
+    public Result<String> delete(Long id) {
+        DiscountRule rule = discountRuleMapper.selectById(id);
+        if (rule == null) return Result.error("规则不存在");
+        discountRuleMapper.deleteById(id);
+        return Result.success("删除成功");
+    }
+
+    public Result<Integer> calculateBestDiscount(Long categoryId, Long distributionSystemId,
+                                                  List<Long> productIds, Integer orderAmount) {
+        if (orderAmount == null || orderAmount <= 0) {
+            return Result.success(0);
+        }
+
+        List<DiscountRule> allRules = discountRuleMapper.selectList(
+            new LambdaQueryWrapper<DiscountRule>()
+                .eq(DiscountRule::getEnabled, true)
+        );
+
+        Date now = new Date();
+        int bestDiscount = 0;
+
+        for (DiscountRule rule : allRules) {
+            if (!isTimeValid(rule, now)) continue;
+            if (!isCategoryMatch(rule, categoryId)) continue;
+            if (!isDistributionMatch(rule, distributionSystemId)) continue;
+            if (!isProductMatch(rule, productIds)) continue;
+            if (!isUsageValid(rule)) continue;
+            if ("THRESHOLD".equals(rule.getType())
+                && (rule.getThresholdAmount() == null || orderAmount < rule.getThresholdAmount())) {
+                continue;
+            }
+
+            int discount = calculateDiscount(rule, orderAmount);
+            if (discount > bestDiscount) {
+                bestDiscount = discount;
+            }
+        }
+
+        return Result.success(bestDiscount);
+    }
+
+    private boolean isTimeValid(DiscountRule rule, Date now) {
+        if (rule.getStartTime() != null && now.before(rule.getStartTime())) return false;
+        if (rule.getEndTime() != null && now.after(rule.getEndTime())) return false;
+        return true;
+    }
+
+    private boolean isCategoryMatch(DiscountRule rule, Long categoryId) {
+        if (rule.getCategoryId() == null) return true;
+        if (categoryId == null) return false;
+        return rule.getCategoryId().equals(categoryId);
+    }
+
+    private boolean isDistributionMatch(DiscountRule rule, Long distributionSystemId) {
+        if (rule.getDistributionSystemId() == null) return true;
+        if (distributionSystemId == null) return false;
+        return rule.getDistributionSystemId().equals(distributionSystemId);
+    }
+
+    private boolean isProductMatch(DiscountRule rule, List<Long> productIds) {
+        if (rule.getProductIds() == null || rule.getProductIds().isEmpty()) return true;
+        if (productIds == null || productIds.isEmpty()) return false;
+        String[] ruleIds = rule.getProductIds().split(",");
+        for (String pid : ruleIds) {
+            try {
+                Long id = Long.valueOf(pid.trim());
+                if (productIds.contains(id)) return true;
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return false;
+    }
+
+    private boolean isUsageValid(DiscountRule rule) {
+        if (rule.getMaxUses() == null || rule.getMaxUses() == 0) return true;
+        if (rule.getUsedCount() == null) return true;
+        return rule.getUsedCount() < rule.getMaxUses();
+    }
+
+    private int calculateDiscount(DiscountRule rule, int orderAmount) {
+        switch (rule.getType()) {
+            case "PERCENT":
+                return orderAmount * rule.getValue() / 100;
+            case "FIXED":
+                return rule.getValue() != null ? rule.getValue() : 0;
+            case "THRESHOLD":
+                return rule.getValue() != null ? rule.getValue() : 0;
+            default:
+                return 0;
+        }
+    }
+}