Răsfoiți Sursa

chore: auto bump version and changelog [skip ci]

iwt 1 lună în urmă
părinte
comite
5b28e0ffb7

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CategoryPpoint.java

@@ -0,0 +1,39 @@
+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;
+
+/**
+ * 商品品类P点比例配置
+ * 当品类设置了ratio(千分比),该品类下所有商品优先使用品类的P点比例,
+ * 除非该商品有独立的 ProductPpoint 配置。
+ */
+@Data
+@TableName("category_ppoint")
+public class CategoryPpoint implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 商品类目ID(关联 products.categoryId) */
+    private Long categoryId;
+
+    /** P点比例(千分比),如 200 表示20% */
+    private Integer ratio;
+
+    /** 生效日期 */
+    private Date startDate;
+
+    /** 失效日期(null=永不过期) */
+    private Date endDate;
+
+    /** 创建人 */
+    private Long createdBy;
+
+    private Date createdAt;
+}

+ 62 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/CommissionDistLog.java

@@ -0,0 +1,62 @@
+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;
+
+/**
+ * P点分润记录
+ * 当用户购买商品产生P点时,按推荐关系将P点分配给一级和二级推荐人。
+ * 分润以P点(积分点)为单位,非现金。
+ */
+@Data
+@TableName("commission_dist_log")
+public class CommissionDistLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 买家用户ID(产生P点的用户) */
+    private Long buyerId;
+
+    /** 订单ID */
+    private Long orderId;
+
+    /** 商品ID */
+    private Long productId;
+
+    /** 本次产生的原始P点数量 */
+    private Integer sourcePpoint;
+
+    /** 一级推荐人用户ID(null=无推荐人) */
+    private Long level1UserId;
+
+    /** 一级推荐人获得P点 */
+    private Integer level1Ppoint;
+
+    /** 一级推荐人当时的财富等级(profitSharePercent值) */
+    private Integer level1WealthPercent;
+
+    /** 二级推荐人用户ID(null=无推荐人) */
+    private Long level2UserId;
+
+    /** 二级推荐人获得P点 */
+    private Integer level2Ppoint;
+
+    /** 二级推荐人当时的财富等级(profitSharePercent值) */
+    private Integer level2WealthPercent;
+
+    /** 状态:pending/completed/failed */
+    private String status;
+
+    /** 备注 */
+    private String remark;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CategoryPpointMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CategoryPpoint;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CategoryPpointMapper extends BaseMapper<CategoryPpoint> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/CommissionDistLogMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CommissionDistLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CommissionDistLogMapper extends BaseMapper<CommissionDistLog> {
+}

+ 199 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CommissionDistService.java

@@ -0,0 +1,199 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * P点分润服务
+ * 
+ * 核心逻辑:
+ * 1. 用户购买商品/活动产生P点(基于PpointConfigService计算)
+ * 2. 查找购买用户的推荐链(referral_tree),取一级和二级推荐人
+ * 3. 每个推荐人在 promotion_tier 表中查其财富成长等级
+ * 4. 财富等级的 profitSharePercent 决定了该推荐人分得P点的比例
+ * 5. 仅一级和二级推荐人参与分润
+ * 
+ * 分润公式:
+ *   推荐人分得P点 = sourcePpoint × profitSharePercent / 100
+ */
+@Service
+public class CommissionDistService {
+
+    private static final Logger log = LoggerFactory.getLogger(CommissionDistService.class);
+
+    @Resource
+    private CommissionDistLogMapper commissionDistLogMapper;
+
+    @Resource
+    private ReferralTreeMapper referralTreeMapper;
+
+    @Resource
+    private PromotionTierService promotionTierService;
+
+    @Resource
+    private PpointConfigService ppointConfigService;
+
+    @Resource
+    private ProductMapper productMapper;
+
+    /**
+     * 执行P点分润
+     * 当用户的购买产生P点时,按推荐关系分配给一级和二级推荐人
+     *
+     * @param buyerId  买家用户ID
+     * @param orderId  订单ID
+     * @param productId 商品ID
+     * @return 分润记录,若无需分润返回null
+     */
+    @Transactional
+    public CommissionDistLog distribute(Long buyerId, Long orderId, Long productId) {
+        if (buyerId == null || productId == null) {
+            return null;
+        }
+
+        // 1. 计算该商品产生的P点
+        Product product = productMapper.selectById(productId);
+        if (product == null) {
+            return null;
+        }
+        int sourcePpoint = ppointConfigService.getEffectivePpoint(productId, product.getCategoryId());
+        if (sourcePpoint <= 0) {
+            log.info("商品{}无P点配置,不分润", productId);
+            return null;
+        }
+
+        // 2. 查找购买用户的一级和二级推荐人
+        LambdaQueryWrapper<ReferralTree> wrapper = new LambdaQueryWrapper<ReferralTree>()
+                .eq(ReferralTree::getChildId, buyerId)
+                .in(ReferralTree::getLevel, 1, 2)
+                .orderByAsc(ReferralTree::getLevel);
+        List<ReferralTree> referrals = referralTreeMapper.selectList(wrapper);
+
+        if (referrals == null || referrals.isEmpty()) {
+            log.info("用户{}无推荐关系,不分润", buyerId);
+            return null;
+        }
+
+        // 3. 按推荐等级分配P点
+        Long level1UserId = null;
+        Integer level1Ppoint = 0;
+        Integer level1WealthPercent = 0;
+        Long level2UserId = null;
+        Integer level2Ppoint = 0;
+        Integer level2WealthPercent = 0;
+
+        for (ReferralTree ref : referrals) {
+            int level = ref.getLevel();
+            Long referrerId = ref.getParentId();
+
+            // 获取推荐人当前财富成长等级
+            PromotionTier tier = promotionTierService.getCurrentTier(referrerId);
+            int wealthPercent = 0;
+            if (tier != null) {
+                // 从 promotion_tier_config 查 profitSharePercent
+                PromotionTierConfig config = promotionTierService.getTierConfig(tier.getTier());
+                if (config != null && config.getProfitSharePercent() != null) {
+                    wealthPercent = config.getProfitSharePercent();
+                }
+            }
+
+            // 计算该推荐人应分得P点
+            // 公式:sourcePpoint * wealthPercent / 100
+            int ppointShare = (int) Math.floor((double) sourcePpoint * wealthPercent / 100.0);
+
+            if (level == 1) {
+                level1UserId = referrerId;
+                level1Ppoint = ppointShare;
+                level1WealthPercent = wealthPercent;
+            } else if (level == 2) {
+                level2UserId = referrerId;
+                level2Ppoint = ppointShare;
+                level2WealthPercent = wealthPercent;
+            }
+        }
+
+        // 4. 记录分润日志
+        CommissionDistLog logRecord = new CommissionDistLog();
+        logRecord.setBuyerId(buyerId);
+        logRecord.setOrderId(orderId);
+        logRecord.setProductId(productId);
+        logRecord.setSourcePpoint(sourcePpoint);
+        logRecord.setLevel1UserId(level1UserId);
+        logRecord.setLevel1Ppoint(level1Ppoint);
+        logRecord.setLevel1WealthPercent(level1WealthPercent);
+        logRecord.setLevel2UserId(level2UserId);
+        logRecord.setLevel2Ppoint(level2Ppoint);
+        logRecord.setLevel2WealthPercent(level2WealthPercent);
+        logRecord.setStatus("completed");
+        logRecord.setRemark("商品P点分润");
+        logRecord.setCreatedAt(new Date());
+        commissionDistLogMapper.insert(logRecord);
+
+        log.info("P点分润完成: buyerId={}, productId={}, sourcePpoint={}, level1={}/{}pp, level2={}/{}pp",
+                buyerId, productId, sourcePpoint,
+                level1UserId, level1Ppoint, level2UserId, level2Ppoint);
+
+        return logRecord;
+    }
+
+    /**
+     * 查询用户相关的分润记录(作为买家或推荐人)
+     */
+    public List<CommissionDistLog> getUserDistLogs(Long userId, int page, int size) {
+        LambdaQueryWrapper<CommissionDistLog> wrapper = new LambdaQueryWrapper<CommissionDistLog>()
+                .eq(CommissionDistLog::getBuyerId, userId)
+                .or()
+                .eq(CommissionDistLog::getLevel1UserId, userId)
+                .or()
+                .eq(CommissionDistLog::getLevel2UserId, userId)
+                .orderByDesc(CommissionDistLog::getCreatedAt);
+        return commissionDistLogMapper.selectList(wrapper);
+    }
+
+    /**
+     * 获取用户累计获得的P点分润
+     */
+    public int getTotalDistributedPpoint(Long userId) {
+        LambdaQueryWrapper<CommissionDistLog> wrapper = new LambdaQueryWrapper<CommissionDistLog>()
+                .eq(CommissionDistLog::getStatus, "completed")
+                .and(w -> w.eq(CommissionDistLog::getLevel1UserId, userId)
+                        .or().eq(CommissionDistLog::getLevel2UserId, userId));
+        List<CommissionDistLog> list = commissionDistLogMapper.selectList(wrapper);
+        return list.stream()
+                .mapToInt(r -> {
+                    if (userId.equals(r.getLevel1UserId())) {
+                        return r.getLevel1Ppoint() != null ? r.getLevel1Ppoint() : 0;
+                    } else {
+                        return r.getLevel2Ppoint() != null ? r.getLevel2Ppoint() : 0;
+                    }
+                })
+                .sum();
+    }
+
+    /**
+     * 管理端分页查询分润记录
+     */
+    public com.baomidou.mybatisplus.extension.plugins.pagination.Page<CommissionDistLog> adminPage(
+            com.baomidou.mybatisplus.extension.plugins.pagination.Page<CommissionDistLog> pageParam,
+            Long buyerId, Long levelUserId) {
+        LambdaQueryWrapper<CommissionDistLog> wrapper = new LambdaQueryWrapper<CommissionDistLog>()
+                .orderByDesc(CommissionDistLog::getCreatedAt);
+        if (buyerId != null) {
+            wrapper.eq(CommissionDistLog::getBuyerId, buyerId);
+        }
+        if (levelUserId != null) {
+            wrapper.and(w -> w.eq(CommissionDistLog::getLevel1UserId, levelUserId)
+                    .or().eq(CommissionDistLog::getLevel2UserId, levelUserId));
+        }
+        return commissionDistLogMapper.selectPage(pageParam, wrapper);
+    }
+}

+ 209 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PpointConfigService.java

@@ -0,0 +1,209 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.CategoryPpoint;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.entity.ProductPpoint;
+import com.etotem.cfc.mapper.CategoryPpointMapper;
+import com.etotem.cfc.mapper.ProductMapper;
+import com.etotem.cfc.mapper.ProductPpointMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * P点配置管理 — 综合品类级比例 + 商品级固定值
+ * 查询优先级:商品级 ProductPpoint > 品类级 CategoryPpoint.ratio > 商品 profitRate
+ */
+@Service
+public class PpointConfigService {
+
+    private static final Logger log = LoggerFactory.getLogger(PpointConfigService.class);
+
+    @Resource
+    private ProductPpointMapper productPpointMapper;
+
+    @Resource
+    private CategoryPpointMapper categoryPpointMapper;
+
+    @Resource
+    private ProductMapper productMapper;
+
+    // ======================== 品类级P点比例 ========================
+
+    /**
+     * 获取品类当前有效的P点比例
+     * @param categoryId 品类ID
+     * @return 千分比(如 200 = 20%),0表示未配置
+     */
+    public int getCategoryRatio(Long categoryId) {
+        if (categoryId == null) return 0;
+        Date now = new Date();
+        LambdaQueryWrapper<CategoryPpoint> wrapper = new LambdaQueryWrapper<CategoryPpoint>()
+                .eq(CategoryPpoint::getCategoryId, categoryId)
+                .le(CategoryPpoint::getStartDate, now)
+                .and(w -> w.isNull(CategoryPpoint::getEndDate).or().ge(CategoryPpoint::getEndDate, now))
+                .orderByDesc(CategoryPpoint::getStartDate)
+                .last("LIMIT 1");
+        CategoryPpoint record = categoryPpointMapper.selectOne(wrapper);
+        if (record != null && record.getRatio() != null && record.getRatio() > 0) {
+            return record.getRatio();
+        }
+        return 0;
+    }
+
+    /**
+     * 获取商品综合有效的P点值(优先商品级 → 品类级 → profitRate)
+     * @param productId 商品ID
+     * @param categoryId 商品品类ID(可为null)
+     * @return P点值
+     */
+    public int getEffectivePpoint(Long productId, Long categoryId) {
+        // 1. 商品级 ProductPpoint
+        Date now = new Date();
+        LambdaQueryWrapper<ProductPpoint> pw = new LambdaQueryWrapper<ProductPpoint>()
+                .eq(ProductPpoint::getProductId, productId)
+                .le(ProductPpoint::getStartDate, now)
+                .and(w -> w.isNull(ProductPpoint::getEndDate).or().ge(ProductPpoint::getEndDate, now))
+                .orderByDesc(ProductPpoint::getStartDate)
+                .last("LIMIT 1");
+        ProductPpoint productPpoint = productPpointMapper.selectOne(pw);
+        if (productPpoint != null && productPpoint.getPpoint() != null && productPpoint.getPpoint() > 0) {
+            return productPpoint.getPpoint();
+        }
+
+        // 2. 品类级比例(千分比)
+        if (categoryId != null) {
+            int categoryRatio = getCategoryRatio(categoryId);
+            if (categoryRatio > 0) {
+                return categoryRatio;
+            }
+        }
+
+        // 3. 商品 profitRate 回退
+        Product product = productMapper.selectById(productId);
+        if (product != null && product.getProfitRate() != null) {
+            return product.getProfitRate() * 100;
+        }
+
+        return 0;
+    }
+
+    // ======================== 品类级CRUD ========================
+
+    public void saveCategoryRatio(Long categoryId, Integer ratio, Date startDate, Date endDate, Long adminUserId) {
+        LambdaQueryWrapper<CategoryPpoint> wrapper = new LambdaQueryWrapper<CategoryPpoint>()
+                .eq(CategoryPpoint::getCategoryId, categoryId);
+        CategoryPpoint existing = categoryPpointMapper.selectOne(wrapper);
+        if (existing != null) {
+            existing.setRatio(ratio);
+            existing.setStartDate(startDate);
+            existing.setEndDate(endDate);
+            existing.setCreatedBy(adminUserId);
+            categoryPpointMapper.updateById(existing);
+        } else {
+            CategoryPpoint record = new CategoryPpoint();
+            record.setCategoryId(categoryId);
+            record.setRatio(ratio);
+            record.setStartDate(startDate);
+            record.setEndDate(endDate);
+            record.setCreatedBy(adminUserId);
+            record.setCreatedAt(new Date());
+            categoryPpointMapper.insert(record);
+        }
+    }
+
+    public void deleteCategoryRatio(Long id) {
+        categoryPpointMapper.deleteById(id);
+    }
+
+    public List<CategoryPpoint> getByCategoryId(Long categoryId) {
+        return categoryPpointMapper.selectList(
+                new LambdaQueryWrapper<CategoryPpoint>()
+                        .eq(CategoryPpoint::getCategoryId, categoryId)
+                        .orderByDesc(CategoryPpoint::getStartDate)
+        );
+    }
+
+    /**
+     * 品类P点比例分页列表(带品类名称)
+     */
+    public Map<String, Object> adminCategoryList(Page<CategoryPpoint> pageParam, String keyword) {
+        LambdaQueryWrapper<CategoryPpoint> wrapper = new LambdaQueryWrapper<CategoryPpoint>()
+                .orderByDesc(CategoryPpoint::getCreatedAt);
+        Page<CategoryPpoint> page = categoryPpointMapper.selectPage(pageParam, wrapper);
+        List<CategoryPpoint> records = page.getRecords();
+
+        if (!records.isEmpty()) {
+            List<Long> categoryIds = records.stream()
+                    .map(CategoryPpoint::getCategoryId)
+                    .distinct()
+                    .collect(Collectors.toList());
+            // 品类名称查询(直接通过 product_categories 表获取品类名称)
+            Map<Long, String> categoryNames = new HashMap<>();
+            if (!categoryIds.isEmpty()) {
+                String ids = categoryIds.stream().map(String::valueOf).collect(Collectors.joining(","));
+                try {
+                    List<Map<String, Object>> cats = categoryPpointMapper.selectMaps(
+                            new LambdaQueryWrapper<com.etotem.cfc.entity.ProductCategory>()
+                                    .in(com.etotem.cfc.entity.ProductCategory::getId, categoryIds)
+                                    .select(com.etotem.cfc.entity.ProductCategory::getId, com.etotem.cfc.entity.ProductCategory::getName)
+                    );
+                    for (Map<String, Object> cat : cats) {
+                        Object id = cat.get("id");
+                        Object name = cat.get("name");
+                        if (id != null && name != null) {
+                            categoryNames.put(Long.valueOf(id.toString()), name.toString());
+                        }
+                    }
+                } catch (Exception e) {
+                    log.warn("查询品类名称失败: {}", e.getMessage());
+                }
+            }
+
+            final Map<Long, String> nameMap = categoryNames;
+            List<Map<String, Object>> enriched = records.stream().map(r -> {
+                Map<String, Object> m = new HashMap<>();
+                m.put("id", r.getId());
+                m.put("categoryId", r.getCategoryId());
+                m.put("ratio", r.getRatio());
+                m.put("startDate", r.getStartDate());
+                m.put("endDate", r.getEndDate());
+                m.put("createdBy", r.getCreatedBy());
+                m.put("createdAt", r.getCreatedAt());
+                m.put("categoryName", nameMap.getOrDefault(r.getCategoryId(), "未知品类"));
+                return m;
+            }).collect(Collectors.toList());
+
+            // keyword 内存过滤
+            if (keyword != null && !keyword.trim().isEmpty()) {
+                String kw = keyword.trim().toLowerCase();
+                enriched = enriched.stream()
+                        .filter(e -> e.get("categoryName") != null && e.get("categoryName").toString().toLowerCase().contains(kw))
+                        .collect(Collectors.toList());
+            }
+
+            Map<String, Object> result = new HashMap<>();
+            result.put("records", enriched);
+            result.put("total", page.getTotal());
+            result.put("page", page.getCurrent());
+            result.put("size", page.getSize());
+            return result;
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("records", records);
+        result.put("total", page.getTotal());
+        result.put("page", page.getCurrent());
+        result.put("size", page.getSize());
+        return result;
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-72f6e0693029faf00cb58c229b65b0bf89172d46
+e2aaac96e77a769a373e642b4f4b6222adb762f0

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.606",
+  "version": "1.0.607",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.606",
+      "version": "1.0.607",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.607",
+  "version": "1.0.608",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 6 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,12 @@
 
 ---
 
+## v1.0.608 (2026-07-29)
+
+### 文档
+- 全部31个流程图重绘 — 数据上连线+中文标注+分层+完整性分析
+
+
 ## v1.0.607 (2026-07-29)
 
 ### Bug 修复

+ 7 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.607
+> 当前版本: v1.0.608
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+## v1.0.608 (2026-07-29)
+
+### 文档
+- 全部31个流程图重绘 — 数据上连线+中文标注+分层+完整性分析
+
+
 ## v1.0.607 (2026-07-29)
 
 ### Bug 修复