Procházet zdrojové kódy

Merge remote-tracking branch 'origin/cfclub' into cfclub

liaoxg před 2 týdny
rodič
revize
44ded5ee2b

+ 13 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -10496,5 +10496,18 @@ public class DatabaseInitializer implements CommandLineRunner {
         } catch (Exception e) {
             log.warn("迁移299: 插入新人引导任务模板失败或已存在: {}", e.getMessage());
         }
+
+        // 迁移301: platform_balance_log 加 (user_id, ref_type, ref_id) 唯一索引(earn 幂等 DB 兜底,防止并发重复发放)
+        try {
+            jdbcTemplate.execute("DELETE l1 FROM platform_balance_log l1 " +
+                    "INNER JOIN platform_balance_log l2 ON l1.user_id = l2.user_id " +
+                    "AND l1.ref_type = l2.ref_type AND l1.ref_id = l2.ref_id AND l1.id > l2.id " +
+                    "WHERE l1.ref_id IS NOT NULL AND l1.ref_type IS NOT NULL");
+            log.info("迁移301: platform_balance_log 历史重复流水已清理");
+            jdbcTemplate.execute("ALTER TABLE platform_balance_log ADD UNIQUE KEY uk_user_ref (user_id, ref_type, ref_id)");
+            log.info("迁移301: platform_balance_log 已添加 uk_user_ref 唯一索引");
+        } catch (Exception e) {
+            log.warn("迁移301: platform_balance_log 唯一索引处理失败: {}", e.getMessage());
+        }
     }
 }

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java

@@ -54,9 +54,9 @@ public class CfTransferController {
         if (from.getFamilyId() == null || !from.getFamilyId().equals(to.getFamilyId())) {
             return Result.error("仅限同一家庭成员间转让");
         }
-        // 每次转让使用唯一 refId(避免 earn 幂等键 (ref_type, ref_id) 碰撞)
-        Long transferRefId = UUID.randomUUID().getMostSignificantBits();
-        if (transferRefId == null || transferRefId == 0) transferRefId = System.currentTimeMillis();
+        // 每次转让使用唯一 refId(避免 earn 幂等键 (ref_type, ref_id) 碰撞),并保证非负
+        Long transferRefId = UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
+        if (transferRefId == 0) transferRefId = System.currentTimeMillis();
         // 转出扣减 + 转入增加(同一事务)
         platformPointsService.spend(fromUserId, amount, "cf_transfer_out", transferRefId,
                 "转给成员: " + (to.getNickname() == null ? toUserId : to.getNickname()));

+ 6 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/OperationLogController.java

@@ -49,8 +49,13 @@ public class OperationLogController {
             // 日期解析失败
         }
 
+        @SuppressWarnings("unchecked")
+        List<Map<String, String>> sortSpecs = params.get("sort") != null
+                ? (List<Map<String, String>>) params.get("sort")
+                : null;
+
         Page<UserOperationLog> result = logService.queryLogs(
-                userId, operationType, startDate, endDate, page, size);
+                userId, operationType, startDate, endDate, page, size, sortSpecs);
 
         return Result.success(result);
     }

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

@@ -25,7 +25,10 @@ import com.etotem.cfc.util.SortUtil;
  * 
  * 分润公式:
  *   推荐人分得P点 = sourcePpoint × profitSharePercent / 100
+ *
+ * @deprecated 已由 CfCommissionService 统一 CF 值分佣替代,本服务不再被业务订单调用,保留历史数据读取。
  */
+@Deprecated
 @Service
 public class CommissionDistService {
 

+ 0 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java

@@ -6,11 +6,9 @@ import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.MemberSubscription;
 import com.etotem.cfc.entity.MemberSubscriptionOrder;
 import com.etotem.cfc.entity.SubscriptionBenefitLog;
-import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.MemberSubscriptionMapper;
 import com.etotem.cfc.mapper.MemberSubscriptionOrderMapper;
-import com.etotem.cfc.mapper.UserMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
@@ -34,12 +32,6 @@ public class MemberSubscriptionService {
     @Resource
     private CfCommissionService cfCommissionService;
 
-    @Resource
-    private PromotionTierService promotionTierService;
-
-    @Resource
-    private UserMapper userMapper;
-
     @Resource
     private ButlerService butlerService;
 

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -800,7 +800,7 @@ public class ProductOrderService {
             for (ProductOrderGift gift : pendingGifts) {
                 if ("cf".equals(gift.getGiftType()) && gift.getCfValue() != null && gift.getCfValue() > 0) {
                     platformPointsService.earn(order.getBuyerId(), gift.getCfValue(),
-                            "product_gift", order.getId(),
+                            "product_gift", gift.getId(),
                             "购买商品赠品: " + (gift.getGiftProductName() != null ? gift.getGiftProductName() : "CF值"));
                 } else if ("product".equals(gift.getGiftType()) && gift.getGiftProductId() != null) {
                     int qty = gift.getQuantity() != null ? gift.getQuantity() : 1;

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/UserOperationLogService.java

@@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
 
 @Slf4j
@@ -95,7 +96,8 @@ public class UserOperationLogService {
      */
     public Page<UserOperationLog> queryLogs(Long userId, String operationType, 
                             Date startDate, Date endDate,
-                            Integer page, Integer size) {
+                            Integer page, Integer size,
+                            List<Map<String, String>> sortSpecs) {
         Page<UserOperationLog> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<UserOperationLog> wrapper = new LambdaQueryWrapper<>();
         
@@ -114,7 +116,7 @@ public class UserOperationLogService {
         
         wrapper.orderByDesc(UserOperationLog::getCreatedAt);
         
-        SortUtil.applySort(wrapper);
+        SortUtil.applySort(wrapper, sortSpecs, null);
         return logMapper.selectPage(pageParam, wrapper);
     }
 

+ 2 - 1
cfc-backend/src/main/resources/schema.sql

@@ -4769,7 +4769,8 @@ CREATE TABLE IF NOT EXISTS platform_balance_log (
     ref_id        BIGINT       COMMENT '关联业务ID',
     remark        VARCHAR(255) COMMENT '备注',
     created_at    DATETIME,
-    INDEX idx_user_time (user_id, created_at)
+    INDEX idx_user_time (user_id, created_at),
+    UNIQUE KEY uk_user_ref (user_id, ref_type, ref_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流水';
 
 

+ 7 - 0
cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceTest.java

@@ -1,8 +1,11 @@
 package com.etotem.cfc.service;
 
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
 import com.etotem.cfc.entity.CfRateTier;
 import com.etotem.cfc.mapper.CfRateTierMapper;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
@@ -51,6 +54,8 @@ class CfCommissionServiceTest {
     @BeforeEach
     void setUp() {
         MockitoAnnotations.openMocks(this);
+        // 初始化 CfRateTier 的 lambda 缓存,使 LambdaQueryWrapper.getParamNameValuePairs() 能填充参数值
+        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), CfRateTier.class);
         seedTiers = new ArrayList<>();
         seedTiers.add(tier("铜牌", 0, 5));
         seedTiers.add(tier("银牌", 3, 10));
@@ -76,6 +81,8 @@ class CfCommissionServiceTest {
      * enabled 恒为 1,取其余整型即 teamSize。
      */
     private int extractTeamSize(LambdaQueryWrapper<CfRateTier> wrapper) {
+        // 先触发 SQL 生成,参数值才会绑定到 paramNameValuePairs
+        wrapper.getSqlSegment();
         Map<String, Object> params = wrapper.getParamNameValuePairs();
         return params.values().stream()
                 .filter(v -> v instanceof Integer && !v.equals(1))

+ 3 - 1
cfc-frontend/pages/invite/join.vue

@@ -185,10 +185,12 @@ export default {
     },
 
     // 校验家庭邀请码有效性(仅获取家庭信息用于展示)
+    // 兼容两种格式:32位hex为邀请令牌token(走 validateInvitation),否则为家庭短邀请码(走 validateFamilyCode)
     async validateFamilyInvite(code) {
       this.loading = true
       try {
-        var res = await validateFamilyCode(code)
+        var isToken = /^[0-9a-fA-F]{32}$/.test(code)
+        var res = isToken ? await validateInvitation(code) : await validateFamilyCode(code)
         if (res && res.code === 200) {
           this.inviteInfo = res.data
           // 已登录用户:检查是否已在目标家庭

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-6b17e8719d191a41a671fee907668b39e7d3a39a
+61cc2134a4e22c299cff38af93313cde7c450290

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

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1314",
+  "version": "1.0.1317",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1314",
+      "version": "1.0.1317",
       "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.1315",
+  "version": "1.0.1318",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

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

@@ -4,6 +4,34 @@
 
 ---
 
+## v1.0.1318 (2026-09-06)
+
+### Bug 修复
+- 操作日志列表排序生效 + 搜索框高度调整
+
+### 其他
+- - UserOperationLogService.queryLogs 加 sortSpecs 参数,显式 SortUtil.applySort
+-   (不再依赖 SortContext ThreadLocal 时序,确保排序可靠生效)
+- - OperationLogs.vue 搜索表单加 size=small 统一高度
+- 
+
+
+## v1.0.1317 (2026-09-06)
+
+### Bug 修复
+- 落地页按格式分流校验邀请令牌与短邀请码
+
+### 其他
+- 按格式分流:32位hex走 validateInvitation,否则走 validateFamilyCode。
+- 
+
+
+## v1.0.1316 (2026-09-06)
+
+### 新功能
+- 首页 Next Best Action 引导条 - 解决首屏'不知道该干啥'
+
+
 ## v1.0.1315 (2026-09-06)
 
 ### 文档

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1315
+> 当前版本: v1.0.1318
 
 ## 历史版本
 
@@ -8,6 +8,34 @@
 
 ---
 
+## v1.0.1318 (2026-09-06)
+
+### Bug 修复
+- 操作日志列表排序生效 + 搜索框高度调整
+
+### 其他
+- - UserOperationLogService.queryLogs 加 sortSpecs 参数,显式 SortUtil.applySort
+-   (不再依赖 SortContext ThreadLocal 时序,确保排序可靠生效)
+- - OperationLogs.vue 搜索表单加 size=small 统一高度
+- 
+
+
+## v1.0.1317 (2026-09-06)
+
+### Bug 修复
+- 落地页按格式分流校验邀请令牌与短邀请码
+
+### 其他
+- 按格式分流:32位hex走 validateInvitation,否则走 validateFamilyCode。
+- 
+
+
+## v1.0.1316 (2026-09-06)
+
+### 新功能
+- 首页 Next Best Action 引导条 - 解决首屏'不知道该干啥'
+
+
 ## v1.0.1315 (2026-09-06)
 
 ### 文档

+ 1 - 1
cfc-web/src/views/OperationLogs.vue

@@ -2,7 +2,7 @@
   <div class="operation-logs admin-page">
     <!-- 筛选条件 -->
     <el-card class="filter-card">
-      <el-form :inline="true" :model="filterForm" class="filter-form">
+      <el-form :inline="true" :model="filterForm" class="filter-form" size="small">
         <el-form-item label="用户名/ID">
           <el-input v-model="filterForm.userId" placeholder="请输入用户名或ID" clearable></el-input>
         </el-form-item>