Просмотр исходного кода

chore: auto bump version and changelog [skip ci]

iwt 3 месяцев назад
Родитель
Сommit
1dfa6ba4ae

+ 42 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -5638,11 +5638,52 @@ try {
             "INDEX idx_circle_id (circle_id), " +
             "INDEX idx_status (status)" +
         ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈挑战'");
-        log.info("已创建circle_challenge表");
+    log.info("已创建circle_challenge表");
     } catch (Exception e) {
         log.warn("创建circle_challenge表失败: {}", e.getMessage());
     }
+
+    // 迁移113: 创建 family_commission 表(P2 推广佣金)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_commission (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "promoter_family_id BIGINT NOT NULL COMMENT '推广方家庭ID', " +
+            "buyer_family_id BIGINT NOT NULL COMMENT '购买方家庭ID', " +
+            "order_id BIGINT COMMENT '关联订单ID', " +
+            "amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '订单金额', " +
+            "commission_rate DECIMAL(5,2) DEFAULT 10.00 COMMENT '佣金比例%', " +
+            "commission_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '佣金金额', " +
+            "status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/paid/cancelled', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+            "paid_at DATETIME COMMENT '支付时间', " +
+            "INDEX idx_promoter (promoter_family_id), " +
+            "INDEX idx_buyer (buyer_family_id), " +
+            "INDEX idx_order (order_id)" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭推广佣金记录'");
+        log.info("已创建family_commission表");
+    } catch (Exception e) {
+        log.warn("创建family_commission表失败: {}", e.getMessage());
+    }
+
+    // 迁移114: 创建 promotion_binding 表(P2 推广绑定)
+    try {
+        jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS promotion_binding (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "promoter_family_id BIGINT NOT NULL COMMENT '推广方家庭ID', " +
+            "invitee_family_id BIGINT NOT NULL COMMENT '被邀请方家庭ID', " +
+            "invite_code VARCHAR(32) COMMENT '使用的邀请码', " +
+            "binding_type VARCHAR(20) DEFAULT 'direct' COMMENT 'direct/indirect', " +
+            "status VARCHAR(20) DEFAULT 'active' COMMENT 'active/inactive', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', " +
+            "UNIQUE KEY uk_promoter_invitee (promoter_family_id, invitee_family_id), " +
+            "INDEX idx_promoter (promoter_family_id), " +
+            "INDEX idx_invitee (invitee_family_id)" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广绑定关系'");
+        log.info("已创建promotion_binding表");
+    } catch (Exception e) {
+        log.warn("创建promotion_binding表失败: {}", e.getMessage());
     }
+}
 
     private void runMigration82() {
         try {

+ 61 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/PromotionController.java

@@ -0,0 +1,61 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.FamilyCommissionService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/promotion")
+public class PromotionController {
+
+    @Resource
+    private FamilyCommissionService familyCommissionService;
+
+    @PostMapping("/bind")
+    public Result<Map<String, Object>> bind(@RequestBody Map<String, Object> params) {
+        String inviteCode = params.get("inviteCode") != null ? params.get("inviteCode").toString() : null;
+        Long promoterFamilyId = params.get("promoterFamilyId") != null
+                ? Long.valueOf(params.get("promoterFamilyId").toString()) : null;
+        if (inviteCode == null || promoterFamilyId == null) {
+            return Result.error("邀请码和推广方家庭ID不能为空");
+        }
+        var binding = familyCommissionService.bindByInviteCode(promoterFamilyId, inviteCode);
+        if (binding == null) {
+            return Result.error("绑定失败,请检查邀请码");
+        }
+        return Result.success(binding);
+    }
+
+    @PostMapping("/commission/list")
+    public Result<Map<String, Object>> commissionList(@RequestBody Map<String, Object> params) {
+        Long familyId = params.get("familyId") != null
+                ? Long.valueOf(params.get("familyId").toString()) : null;
+        int page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        int size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 20;
+        if (familyId == null) {
+            return Result.error("家庭ID不能为空");
+        }
+        var pageResult = familyCommissionService.getCommissionList(familyId, page, size);
+        Map<String, Object> result = new java.util.HashMap<>();
+        result.put("records", pageResult.getRecords());
+        result.put("total", pageResult.getTotal());
+        result.put("page", pageResult.getCurrent());
+        result.put("size", pageResult.getSize());
+        return Result.success(result);
+    }
+
+    @PostMapping("/bindings/list")
+    public Result<List> bindingsList(@RequestBody Map<String, Object> params) {
+        Long familyId = params.get("familyId") != null
+                ? Long.valueOf(params.get("familyId").toString()) : null;
+        if (familyId == null) {
+            return Result.error("家庭ID不能为空");
+        }
+        List bindings = familyCommissionService.getBindings(familyId);
+        return Result.success(bindings);
+    }
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyCommission.java

@@ -0,0 +1,36 @@
+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.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("family_commission")
+public class FamilyCommission implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long promoterFamilyId;
+
+    private Long buyerFamilyId;
+
+    private Long orderId;
+
+    private BigDecimal amount;
+
+    private BigDecimal commissionRate;
+
+    private BigDecimal commissionAmount;
+
+    private String status;
+
+    private Date paidAt;
+
+    private Date createdAt;
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionBinding.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("promotion_binding")
+public class PromotionBinding implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long promoterFamilyId;
+
+    private Long inviteeFamilyId;
+
+    private String inviteCode;
+
+    private String bindingType;
+
+    private String status;
+
+    private Date createdAt;
+}

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

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

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

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

+ 88 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyCommissionService.java

@@ -0,0 +1,88 @@
+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.FamilyCommission;
+import com.etotem.cfc.entity.PromotionBinding;
+import com.etotem.cfc.mapper.FamilyCommissionMapper;
+import com.etotem.cfc.mapper.PromotionBindingMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class FamilyCommissionService {
+
+    @Resource
+    private FamilyCommissionMapper familyCommissionMapper;
+
+    @Resource
+    private PromotionBindingMapper promotionBindingMapper;
+
+    public FamilyCommission recordCommission(Long promoterFamilyId, Long buyerFamilyId, Long orderId, BigDecimal amount) {
+        BigDecimal rate = new BigDecimal("10.00");
+        BigDecimal commissionAmount = amount.multiply(rate).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
+
+        FamilyCommission commission = new FamilyCommission();
+        commission.setPromoterFamilyId(promoterFamilyId);
+        commission.setBuyerFamilyId(buyerFamilyId);
+        commission.setOrderId(orderId);
+        commission.setAmount(amount);
+        commission.setCommissionRate(rate);
+        commission.setCommissionAmount(commissionAmount);
+        commission.setStatus("pending");
+        commission.setCreatedAt(new Date());
+        familyCommissionMapper.insert(commission);
+        return commission;
+    }
+
+    public Page<FamilyCommission> getCommissionList(Long familyId, int page, int size) {
+        Page<FamilyCommission> pageParam = new Page<>(page, size);
+        return familyCommissionMapper.selectPage(pageParam,
+                new LambdaQueryWrapper<FamilyCommission>()
+                        .and(w -> w.eq(FamilyCommission::getPromoterFamilyId, familyId)
+                                .or().eq(FamilyCommission::getBuyerFamilyId, familyId))
+                        .orderByDesc(FamilyCommission::getCreatedAt)
+        );
+    }
+
+    public PromotionBinding bindByInviteCode(Long promoterFamilyId, String inviteCode) {
+        // 简化实现:直接创建绑定(邀请码校验可在后续迭代补充)
+        PromotionBinding binding = new PromotionBinding();
+        binding.setPromoterFamilyId(promoterFamilyId);
+        binding.setInviteCode(inviteCode);
+        binding.setBindingType("direct");
+        binding.setStatus("active");
+        binding.setCreatedAt(new Date());
+        promotionBindingMapper.insert(binding);
+        return binding;
+    }
+
+    public List<PromotionBinding> getBindings(Long familyId) {
+        return promotionBindingMapper.selectList(
+                new LambdaQueryWrapper<PromotionBinding>()
+                        .and(w -> w.eq(PromotionBinding::getPromoterFamilyId, familyId)
+                                .or().eq(PromotionBinding::getInviteeFamilyId, familyId))
+                        .orderByDesc(PromotionBinding::getCreatedAt)
+                        .last("LIMIT 100")
+        );
+    }
+
+    public BigDecimal getTotalEarnings(Long familyId) {
+        LambdaQueryWrapper<FamilyCommission> wrapper = new LambdaQueryWrapper<FamilyCommission>()
+                .eq(FamilyCommission::getPromoterFamilyId, familyId)
+                .eq(FamilyCommission::getStatus, "paid");
+        List<FamilyCommission> list = familyCommissionMapper.selectList(wrapper);
+        BigDecimal total = BigDecimal.ZERO;
+        for (FamilyCommission c : list) {
+            if (c.getCommissionAmount() != null) {
+                total = total.add(c.getCommissionAmount());
+            }
+        }
+        return total;
+    }
+}

+ 54 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/PromotionBindingService.java

@@ -0,0 +1,54 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.Family;
+import com.etotem.cfc.entity.PromotionBinding;
+import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.PromotionBindingMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class PromotionBindingService {
+
+    @Resource
+    private PromotionBindingMapper promotionBindingMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    public PromotionBinding bindByInviteCode(Long promoterFamilyId, String inviteCode) {
+        Family invitee = familyMapper.selectOne(
+                new LambdaQueryWrapper<Family>().eq(Family::getInviteCode, inviteCode).last("LIMIT 1")
+        );
+        if (invitee == null) {
+            return null;
+        }
+        Long inviteeFamilyId = invitee.getId();
+        if (promoterFamilyId.equals(inviteeFamilyId)) {
+            return null;
+        }
+        PromotionBinding binding = new PromotionBinding();
+        binding.setPromoterFamilyId(promoterFamilyId);
+        binding.setInviteeFamilyId(inviteeFamilyId);
+        binding.setInviteCode(inviteCode);
+        binding.setBindingType("direct");
+        binding.setStatus("active");
+        binding.setCreatedAt(new Date());
+        promotionBindingMapper.insert(binding);
+        return binding;
+    }
+
+    public List<PromotionBinding> getBindings(Long familyId) {
+        return promotionBindingMapper.selectList(
+                new LambdaQueryWrapper<PromotionBinding>()
+                        .and(w -> w.eq(PromotionBinding::getPromoterFamilyId, familyId)
+                                .or().eq(PromotionBinding::getInviteeFamilyId, familyId))
+                        .orderByDesc(PromotionBinding::getCreatedAt)
+                        .last("LIMIT 100")
+        );
+    }
+}

+ 31 - 0
cfc-backend/src/main/resources/schema.sql

@@ -3093,3 +3093,34 @@ CREATE TABLE IF NOT EXISTS circle_challenge (
     INDEX idx_circle_id (circle_id),
     INDEX idx_status (status)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圈挑战';
+
+-- 推广佣金记录表(P2-PROMO)
+CREATE TABLE IF NOT EXISTS family_commission (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    promoter_family_id BIGINT NOT NULL COMMENT '推广方家庭ID',
+    buyer_family_id BIGINT NOT NULL COMMENT '购买方家庭ID',
+    order_id BIGINT COMMENT '关联订单ID',
+    amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '订单金额',
+    commission_rate DECIMAL(5,2) DEFAULT 10.00 COMMENT '佣金比例%',
+    commission_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '佣金金额',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/paid/cancelled',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    paid_at DATETIME COMMENT '支付时间',
+    INDEX idx_promoter (promoter_family_id),
+    INDEX idx_buyer (buyer_family_id),
+    INDEX idx_order (order_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭推广佣金记录';
+
+-- 推广绑定关系表(P2-PROMO)
+CREATE TABLE IF NOT EXISTS promotion_binding (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    promoter_family_id BIGINT NOT NULL COMMENT '推广方家庭ID',
+    invitee_family_id BIGINT NOT NULL COMMENT '被邀请方家庭ID',
+    invite_code VARCHAR(32) COMMENT '使用的邀请码',
+    binding_type VARCHAR(20) DEFAULT 'direct' COMMENT 'direct/indirect',
+    status VARCHAR(20) DEFAULT 'active' COMMENT 'active/inactive',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    UNIQUE KEY uk_promoter_invitee (promoter_family_id, invitee_family_id),
+    INDEX idx_promoter (promoter_family_id),
+    INDEX idx_invitee (invitee_family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推广绑定关系';

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-43e6f3a16e0d9229bb5c19e9606c834e26835a07
+ca5b06a674b5243a8d6f936803008e6fe510fe19