|
@@ -0,0 +1,153 @@
|
|
|
|
|
+package com.etotem.cfc.service;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.entity.SpecialPriceDeal;
|
|
|
|
|
+import com.etotem.cfc.entity.Product;
|
|
|
|
|
+import com.etotem.cfc.mapper.SpecialPriceDealMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.ProductMapper;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+@Service
|
|
|
|
|
+public class SpecialPriceService {
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private SpecialPriceDealMapper specialPriceDealMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private ProductMapper productMapper;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 状态机:从存储状态 + 时间窗 + 已售量推导出运行状态(纯逻辑,不访问 DB)。
|
|
|
|
|
+ * 用于详情与列表接口,避免每次跑 SQL。
|
|
|
|
|
+ */
|
|
|
|
|
+ public static String computeRunStatus(String status,
|
|
|
|
|
+ java.util.Date startTime,
|
|
|
|
|
+ java.util.Date endTime,
|
|
|
|
|
+ int soldQuantity,
|
|
|
|
|
+ int totalQuantity) {
|
|
|
|
|
+ if (status == null || status.isEmpty()) return "DRAFT";
|
|
|
|
|
+ switch (status) {
|
|
|
|
|
+ case "draft": return "DRAFT";
|
|
|
|
|
+ case "disabled": return "DISABLED";
|
|
|
|
|
+ case "active": {
|
|
|
|
|
+ java.util.Date now = new java.util.Date();
|
|
|
|
|
+ if (startTime != null && now.before(startTime)) return "NOT_STARTED";
|
|
|
|
|
+ if (endTime != null && now.after(endTime)) return "ENDED";
|
|
|
|
|
+ if (soldQuantity >= totalQuantity) return "SOLD_OUT";
|
|
|
|
|
+ return "ACTIVE";
|
|
|
|
|
+ }
|
|
|
|
|
+ default: return "DISABLED";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> list(int page, int size, String keyword, String runStatus) {
|
|
|
|
|
+ int offset = (page - 1) * size;
|
|
|
|
|
+ java.util.List<java.util.Map<String, Object>> records =
|
|
|
|
|
+ specialPriceDealMapper.listWithProductName(keyword, runStatus, size, offset);
|
|
|
|
|
+ long total = specialPriceDealMapper.countByKeyword(keyword, runStatus);
|
|
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
|
|
+ data.put("records", records);
|
|
|
|
|
+ data.put("total", total);
|
|
|
|
|
+ return data;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public SpecialPriceDeal getById(Long id) {
|
|
|
|
|
+ return specialPriceDealMapper.selectById(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public SpecialPriceDeal create(SpecialPriceDeal deal) {
|
|
|
|
|
+ // 校验:特价必须低于商品原价
|
|
|
|
|
+ Product product = productMapper.selectById(deal.getProductId());
|
|
|
|
|
+ if (product == null) throw new IllegalArgumentException("商品不存在");
|
|
|
|
|
+ int productPrice = product.getPrice() != null ? product.getPrice() : 0;
|
|
|
|
|
+ if (deal.getSpecialPrice() == null || deal.getSpecialPrice() >= productPrice) {
|
|
|
|
|
+ throw new IllegalArgumentException("特价必须低于商品原价");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 校验时间
|
|
|
|
|
+ if (deal.getStartTime() != null && deal.getEndTime() != null
|
|
|
|
|
+ && deal.getStartTime().after(deal.getEndTime())) {
|
|
|
|
|
+ throw new IllegalArgumentException("开始时间必须早于结束时间");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 校验同商品互斥 active
|
|
|
|
|
+ int activeCount = specialPriceDealMapper.countActiveByProduct(deal.getProductId());
|
|
|
|
|
+ if (activeCount > 0) {
|
|
|
|
|
+ throw new IllegalStateException("该商品已有进行中的特价活动,无法新建/启用");
|
|
|
|
|
+ }
|
|
|
|
|
+ deal.setStatus("draft");
|
|
|
|
|
+ deal.setCreatedAt(new java.util.Date());
|
|
|
|
|
+ deal.setUpdatedAt(new java.util.Date());
|
|
|
|
|
+ specialPriceDealMapper.insert(deal);
|
|
|
|
|
+ return deal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void update(SpecialPriceDeal deal) {
|
|
|
|
|
+ // 仅 draft/disabled 可修改
|
|
|
|
|
+ SpecialPriceDeal old = getById(deal.getId());
|
|
|
|
|
+ if (old == null) throw new IllegalArgumentException("特价活动不存在");
|
|
|
|
|
+ if (!"draft".equals(old.getStatus()) && !"disabled".equals(old.getStatus())) {
|
|
|
|
|
+ throw new IllegalStateException("仅草稿/已停用可修改");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 修改前校验特价<原价
|
|
|
|
|
+ Product product = productMapper.selectById(deal.getProductId());
|
|
|
|
|
+ if (product != null) {
|
|
|
|
|
+ int productPrice = product.getPrice() != null ? product.getPrice() : 0;
|
|
|
|
|
+ if (deal.getSpecialPrice() == null || deal.getSpecialPrice() >= productPrice) {
|
|
|
|
|
+ throw new IllegalArgumentException("特价必须低于商品原价");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ deal.setUpdatedAt(new java.util.Date());
|
|
|
|
|
+ specialPriceDealMapper.updateById(deal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void updateStatus(Long id, String newStatus) {
|
|
|
|
|
+ SpecialPriceDeal d = getById(id);
|
|
|
|
|
+ if (d == null) throw new IllegalArgumentException("特价活动不存在");
|
|
|
|
|
+ if (!"draft".equals(d.getStatus()) && !"disabled".equals(d.getStatus())) {
|
|
|
|
|
+ throw new IllegalStateException("仅草稿/已停用状态可启用或停用");
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("active".equals(newStatus)) {
|
|
|
|
|
+ // 再次检查互斥(并发安全:DB 唯一索引更好,此处双重检查)
|
|
|
|
|
+ int activeCount = specialPriceDealMapper.countActiveByProduct(d.getProductId());
|
|
|
|
|
+ if (activeCount > 0) {
|
|
|
|
|
+ throw new IllegalStateException("该商品已有进行中的特价活动,无法启用");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ d.setStatus(newStatus);
|
|
|
|
|
+ d.setUpdatedAt(new java.util.Date());
|
|
|
|
|
+ specialPriceDealMapper.updateById(d);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void delete(Long id) {
|
|
|
|
|
+ SpecialPriceDeal d = getById(id);
|
|
|
|
|
+ if (d == null) throw new IllegalArgumentException("特价活动不存在");
|
|
|
|
|
+ // 检查是否已有特价订单
|
|
|
|
|
+ int orderCount = specialPriceDealMapper.countOrdersByDeal(id);
|
|
|
|
|
+ if (orderCount > 0) {
|
|
|
|
|
+ throw new IllegalStateException("该活动已有 " + orderCount + " 笔特价订单,不可删除(请改为停用)");
|
|
|
|
|
+ }
|
|
|
|
|
+ specialPriceDealMapper.deleteById(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 原子扣量:影响行数=1 成功;=0 失败(已售罄 / 非活动期 / 非 active)。
|
|
|
|
|
+ */
|
|
|
|
|
+ public boolean consumeQuota(Long dealId, int qty) {
|
|
|
|
|
+ int affected = specialPriceDealMapper.consumeQuota(dealId, qty);
|
|
|
|
|
+ if (affected == 0) throw new IllegalStateException("已售罄或活动不可用");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 回滚释放:用于取消 / 退款 / 超时取消。sold_quantity 不跌到 < 0。
|
|
|
|
|
+ */
|
|
|
|
|
+ public void releaseQuota(Long dealId, int qty) {
|
|
|
|
|
+ if (dealId == null) return;
|
|
|
|
|
+ specialPriceDealMapper.releaseQuota(dealId, qty);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public int getUserPurchased(Long dealId, Long buyerId) {
|
|
|
|
|
+ return specialPriceDealMapper.userPurchased(dealId, buyerId);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|