浏览代码

chore(sync): 工作区遗留未提交 WIP 收尾同步

cfc-backend: CfBalanceController 精简(-210行)

cfc-frontend: login-sheet 重构、promotion 页扩展、child-index/index-home/profile-main/utils api 微调

tools/xaf_agent_hub: .model_state/lead_backlog/tasks_backlog 台账刷新

docs/tdd/test: automation-run-2026-11-04-1130.md 新增

验证: cfc-backend mvn compile(最新 origin 基线 866df170e 上复验)通过
Agnes Agent 1 周之前
父节点
当前提交
44b2f0d42

+ 13 - 197
cfc-backend/src/main/java/com/etotem/cfc/controller/CfBalanceController.java

@@ -5,16 +5,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.CfBalanceLogVO;
 import com.etotem.cfc.dto.CfBalanceView;
-import com.etotem.cfc.dto.FamilyEarningsMemberDTO;
-import com.etotem.cfc.dto.FamilyEarningsSummaryDTO;
-import com.etotem.cfc.entity.CfBalance;
 import com.etotem.cfc.entity.CfBalanceLog;
 import com.etotem.cfc.entity.FamilyMember;
-import com.etotem.cfc.entity.User;
-import com.etotem.cfc.mapper.CfBalanceLogMapper;
-import com.etotem.cfc.mapper.CfBalanceMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
-import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.CfBalanceService;
 import com.etotem.cfc.service.CouponService;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -24,41 +17,28 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
 import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
  * C 端 CF 值查询接口(只读)。
  *
  * <p>个人 CF 值账本为成员维度(family_members.id),本控制器把「登录 userId → memberId」
- * 的解析口径与 {@code JwtInterceptor} 保持一致:优先使用拦截器按
- * {@code users.family_id + family_members} 解析出的 {@code currentMemberId},
- * 仅在缺失时回退到历史口径(user_id 最小一条)。</p>
+ * 的解析口径与 {@code CfEarnDispatchService}(F-3a)保持一致:按 user_id 在家庭内匹配,
+ * 取最小 id 的一条(LIMIT 1);找不到对应家庭成员时返回明确错误码,不返回空数据。</p>
  *
- * <p>仅提供只读接口:余额快照 / 流水分页 / 周期统计 / 家庭 CF 总览 / 推广类 CF 收益 /
- * 家庭共享优惠券列表。余额与流水走统一账本 {@link CfBalanceService};
- * 家庭券复用 {@link CouponService#listMyCoupons}(N3 家庭化口径:family_coupon + coupon 联合)。</p>
+ * <p>仅提供三个只读接口:余额快照 / 流水分页 / 家庭共享优惠券列表,不提供任何写接口。
+ * 余额与流水走统一账本 {@link CfBalanceService};家庭券复用 {@link CouponService#listMyCoupons}
+ * (N3 家庭化口径:family_coupon + coupon 联合,家庭内成员共享可见)。</p>
  *
- * <p>接口约定见 docs/superpowers/specs/2026-10-25-cf-value-single-source-of-truth.md §5/§6。</p>
+ * <p>接口约定见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §七 F-7a。</p>
  */
 @RestController
 @RequestMapping("/api/cf")
 public class CfBalanceController {
 
-    /** 推广类 CF 收益:推荐注册 F24 + 直接销售奖 F03a + 间接销售奖 F03b + 邀请里程碑 F11 */
-    private static final Set<String> PROMOTION_REF_TYPES = new HashSet<>(Arrays.asList(
-            "referral_register", "F03a", "F03b", "invite_milestone"));
-
     @Resource
     private CfBalanceService cfBalanceService;
 
@@ -68,22 +48,12 @@ public class CfBalanceController {
     @Resource
     private FamilyMemberMapper familyMemberMapper;
 
-    @Resource
-    private CfBalanceMapper cfBalanceMapper;
-
-    @Resource
-    private CfBalanceLogMapper cfBalanceLogMapper;
-
-    @Resource
-    private UserMapper userMapper;
-
     /**
      * 我的 CF 值快照:可用余额 / 冻结 / 累计获得 / 已提现 / 已兑换消费。
      */
     @GetMapping("/balance")
-    public Result<CfBalanceView> balance(@RequestAttribute("userId") Long userId,
-                                         @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = resolveMemberId(userId, currentMemberId);
+    public Result<CfBalanceView> balance(@RequestAttribute("userId") Long userId) {
+        Long memberId = resolveMemberId(userId);
         if (memberId == null) {
             return Result.error(404, "未找到当前用户对应的家庭成员,无法查询 CF 值");
         }
@@ -95,10 +65,9 @@ public class CfBalanceController {
      */
     @GetMapping("/logs")
     public Result<Page<CfBalanceLogVO>> logs(@RequestAttribute("userId") Long userId,
-                                             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
                                              @RequestParam(defaultValue = "1") Integer page,
                                              @RequestParam(defaultValue = "20") Integer size) {
-        Long memberId = resolveMemberId(userId, currentMemberId);
+        Long memberId = resolveMemberId(userId);
         if (memberId == null) {
             return Result.error(404, "未找到当前用户对应的家庭成员,无法查询 CF 流水");
         }
@@ -111,151 +80,6 @@ public class CfBalanceController {
         return Result.success(vo);
     }
 
-    /**
-     * 我的 CF 周期统计:今日 / 本月获得 + 累计获得(cf_balance.total_earned)。
-     */
-    @GetMapping("/stats")
-    public Result<Map<String, Object>> stats(@RequestAttribute("userId") Long userId,
-                                             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = resolveMemberId(userId, currentMemberId);
-        if (memberId == null) {
-            return Result.error(404, "未找到当前用户对应的家庭成员,无法查询 CF 统计");
-        }
-        CfBalanceView view = cfBalanceService.getBalance(memberId);
-
-        Calendar cal = Calendar.getInstance();
-        cal.set(Calendar.HOUR_OF_DAY, 0);
-        cal.set(Calendar.MINUTE, 0);
-        cal.set(Calendar.SECOND, 0);
-        cal.set(Calendar.MILLISECOND, 0);
-        Date startOfDay = cal.getTime();
-        cal.set(Calendar.DAY_OF_MONTH, 1);
-        Date startOfMonth = cal.getTime();
-
-        List<CfBalanceLog> monthLogs = cfBalanceLogMapper.selectList(new LambdaQueryWrapper<CfBalanceLog>()
-                .eq(CfBalanceLog::getMemberId, memberId)
-                .eq(CfBalanceLog::getType, "earn")
-                .ge(CfBalanceLog::getCreatedAt, startOfMonth));
-        int monthEarned = 0;
-        int todayEarned = 0;
-        for (CfBalanceLog log : monthLogs) {
-            int amount = log.getAmount() == null ? 0 : log.getAmount();
-            monthEarned += amount;
-            if (log.getCreatedAt() != null && !log.getCreatedAt().before(startOfDay)) {
-                todayEarned += amount;
-            }
-        }
-
-        Map<String, Object> data = new LinkedHashMap<>();
-        data.put("todayEarned", todayEarned);
-        data.put("monthEarned", monthEarned);
-        data.put("totalEarned", view.getTotalEarned() == null ? 0 : view.getTotalEarned());
-        data.put("available", view.getAvailable() == null ? 0 : view.getAvailable());
-        return Result.success(data);
-    }
-
-    /**
-     * 家庭 CF 总览(成员维度):全家累计 / 可用 / 已提现 / 已兑换 + 成员贡献榜。
-     *
-     * <p>替代已停写的 {@code family_earnings} 旧账本,数据源为 {@code cf_balance}。</p>
-     */
-    @GetMapping("/family-summary")
-    public Result<FamilyEarningsSummaryDTO> familySummary(@RequestAttribute("userId") Long userId,
-                                                          @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = resolveMemberId(userId, currentMemberId);
-        if (memberId == null) {
-            return Result.error(404, "未找到当前用户对应的家庭成员,无法查询家庭 CF 总览");
-        }
-        FamilyMember self = familyMemberMapper.selectById(memberId);
-        Long familyId = self == null ? null : self.getFamilyId();
-
-        FamilyEarningsSummaryDTO summary = new FamilyEarningsSummaryDTO();
-        summary.setFamilyId(familyId);
-
-        List<CfBalance> balances = familyId == null ? Collections.emptyList()
-                : cfBalanceMapper.selectList(new LambdaQueryWrapper<CfBalance>()
-                        .eq(CfBalance::getFamilyId, familyId));
-
-        List<Long> memberIds = balances.stream()
-                .map(CfBalance::getMemberId).filter(id -> id != null)
-                .collect(Collectors.toList());
-        Map<Long, FamilyMember> memberMap = memberIds.isEmpty() ? Collections.emptyMap()
-                : familyMemberMapper.selectBatchIds(memberIds).stream()
-                        .collect(Collectors.toMap(FamilyMember::getId, m -> m, (a, b) -> a));
-
-        List<Long> userIds = balances.stream()
-                .map(CfBalance::getUserId).filter(id -> id != null)
-                .collect(Collectors.toList());
-        Map<Long, User> userMap = userIds.isEmpty() ? Collections.emptyMap()
-                : userMapper.selectBatchIds(userIds).stream()
-                        .collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a));
-
-        long totalEarned = 0L;
-        long available = 0L;
-        long withdrawn = 0L;
-        long exchanged = 0L;
-        List<FamilyEarningsMemberDTO> members = new ArrayList<>();
-        for (CfBalance balance : balances) {
-            long memberTotal = balance.getTotalEarned() == null ? 0L : balance.getTotalEarned();
-            long memberAvailable = balance.getAvailable() == null ? 0L : balance.getAvailable();
-            FamilyMember member = memberMap.get(balance.getMemberId());
-            User user = balance.getUserId() == null ? null : userMap.get(balance.getUserId());
-
-            FamilyEarningsMemberDTO dto = new FamilyEarningsMemberDTO();
-            dto.setUserId(balance.getUserId());
-            dto.setNickname(user != null && user.getNickname() != null ? user.getNickname()
-                    : (member == null ? null : member.getNickname()));
-            dto.setAvatar(user != null && user.getAvatar() != null ? user.getAvatar()
-                    : (member == null ? null : member.getAvatar()));
-            dto.setAllocatedAmount(memberTotal);
-            dto.setAvailableAmount(memberAvailable);
-            dto.setConsumedAmount(Math.max(0L, memberTotal - memberAvailable));
-            dto.setIsAdmin(user != null && Boolean.TRUE.equals(user.getIsFamilyAdmin()));
-            // 新模型无家庭公共池,字段保留兼容前端;家庭内共享通过优惠券 / 成员转让实现
-            dto.setCanUseFamilyBalance(true);
-            members.add(dto);
-
-            totalEarned += memberTotal;
-            available += memberAvailable;
-            withdrawn += balance.getWithdrawn() == null ? 0L : balance.getWithdrawn();
-            exchanged += balance.getExchanged() == null ? 0L : balance.getExchanged();
-        }
-        members.sort((a, b) -> Long.compare(
-                b.getAllocatedAmount() == null ? 0L : b.getAllocatedAmount(),
-                a.getAllocatedAmount() == null ? 0L : a.getAllocatedAmount()));
-
-        summary.setTotalEarned(totalEarned);
-        summary.setAvailableAmount(available);
-        summary.setWithdrawnAmount(withdrawn);
-        summary.setDistributedAmount(exchanged);
-        summary.setMembers(members);
-        return Result.success(summary);
-    }
-
-    /**
-     * 推广类 CF 收益:F24 推荐注册 + F03a 直接销售奖 + F03b 间接销售奖 + F11 邀请里程碑。
-     *
-     * <p>仅统计本人当前成员的 {@code cf_balance_log}(type=earn)。</p>
-     */
-    @GetMapping("/promotion-earnings")
-    public Result<Map<String, Object>> promotionEarnings(@RequestAttribute("userId") Long userId,
-                                                         @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        Long memberId = resolveMemberId(userId, currentMemberId);
-        if (memberId == null) {
-            return Result.error(404, "未找到当前用户对应的家庭成员,无法查询推广收益");
-        }
-        List<CfBalanceLog> logs = cfBalanceLogMapper.selectList(new LambdaQueryWrapper<CfBalanceLog>()
-                .eq(CfBalanceLog::getMemberId, memberId)
-                .eq(CfBalanceLog::getType, "earn")
-                .in(CfBalanceLog::getRefType, PROMOTION_REF_TYPES));
-        int total = logs.stream().mapToInt(log -> log.getAmount() == null ? 0 : log.getAmount()).sum();
-
-        Map<String, Object> data = new LinkedHashMap<>();
-        data.put("totalEarned", total);
-        data.put("refTypes", new ArrayList<>(PROMOTION_REF_TYPES));
-        return Result.success(data);
-    }
-
     /**
      * 家庭共享优惠券列表(N3 口径):按登录用户所属家庭查券实例 + 券模板,
      * 返回券名 / 状态 / 有效期 / 抵扣面额等展示字段,家庭内成员共享可见。
@@ -278,18 +102,10 @@ public class CfBalanceController {
     }
 
     /**
-     * 登录 userId → 当前家庭成员(family_members.id)。
-     *
-     * <p>优先使用 {@code JwtInterceptor} 按 {@code users.family_id} 解析出的 currentMemberId;
-     * 缺失时回退到历史口径(user_id 匹配 + id 升序 LIMIT 1),保证无家庭用户兼容。</p>
+     * 登录 userId → 家庭成员(family_members.id)。
+     * 口径与 {@code CfEarnDispatchService} 的 resolveMemberId 一致:user_id 匹配 +
+     * 按 id 升序取第一条(LIMIT 1),未命中返回 null。
      */
-    private Long resolveMemberId(Long userId, Long currentMemberId) {
-        if (currentMemberId != null && currentMemberId > 0) {
-            return currentMemberId;
-        }
-        return resolveMemberId(userId);
-    }
-
     private Long resolveMemberId(Long userId) {
         if (userId == null || userId <= 0) {
             return null;
@@ -300,4 +116,4 @@ public class CfBalanceController {
                 .last("LIMIT 1"));
         return member == null ? null : member.getId();
     }
-}
+}

+ 138 - 80
cfc-frontend/components/login-sheet.vue

@@ -6,19 +6,17 @@
       </view>
       <scroll-view scroll-y class="login-sheet-scroll" :show-scrollbar="false">
         <view class="login-sheet-content">
-          <!-- 品牌区 -->
-          <view class="brand-section">
-            <image class="logo" src="/static/logo.png" mode="aspectFit" @error="handleImageError"></image>
-            <text class="app-name">浠艾福</text>
-            <text class="identity">您的专属家庭主动健康管理师</text>
-            <text class="slogan">家里每个人的状态,你真的看见了?</text>
+          <!-- 品牌区(左上角锁定版式,与首页一致) -->
+          <view class="brand-bar">
+            <image class="brand-logo" src="/static/logo.png" mode="aspectFit" @error="handleImageError"></image>
+            <view class="brand-texts">
+              <text class="brand-name">浠艾福</text>
+              <text class="brand-tagline">您的专属家庭主动健康陪跑</text>
+            </view>
           </view>
 
           <!-- 理念卡 -->
           <view class="concept-card">
-            <view class="concept-desc">
-              不等问题发生,先一步<text class="concept-highlight">识别风险 · 主动干预 · 持续评估</text>,把全家健康管理融入每一天。
-            </view>
             <view class="loop-row">
               <view class="loop-pill loop-pill-indigo"><text>🔍</text><text class="loop-pill-text">提前识别</text></view>
               <text class="loop-arrow">→</text>
@@ -30,15 +28,6 @@
             </view>
           </view>
 
-          <!-- 五维标签 -->
-          <view class="dims-row">
-            <view class="dim-tag dim-tag-body"><text>🌏</text><text>身·健康</text></view>
-            <view class="dim-tag dim-tag-wisdom"><text>⚡</text><text>智·认知</text></view>
-            <view class="dim-tag dim-tag-wealth"><text>💧</text><text>富·创富</text></view>
-            <view class="dim-tag dim-tag-action"><text>🌿</text><text>行·和谐</text></view>
-            <view class="dim-tag dim-tag-mind"><text>🔥</text><text>心·大爱</text></view>
-          </view>
-
           <!-- 登录表单 -->
           <view class="login-form">
             <view class="phone-section">
@@ -51,7 +40,8 @@
                 :disabled="!agreed || loading || tapLock"
               >
                 <text class="phone-btn-icon">📱</text>
-                <text>手机号快捷登录</text>
+                <text class="phone-btn-text">手机号快捷登录</text>
+                <text class="phone-arrows">›››</text>
               </button>
             </view>
 
@@ -260,8 +250,8 @@ export default {
   left: 0;
   right: 0;
   bottom: 0;
-  height: 51vh;
-  max-height: 51vh;
+  height: 60vh;
+  max-height: 65vh;
   background: linear-gradient(180deg, #fffaf3 0%, #fff3e4 100%);
   border-radius: 24rpx 24rpx 0 0;
   overflow: hidden;
@@ -298,34 +288,46 @@ export default {
   box-sizing: border-box;
 }
 
-/* ===== 品牌区 ===== */
-.brand-section {
+/* ===== 品牌区(左上角锁定版式,与首页一致) ===== */
+.brand-bar {
   display: flex;
-  flex-direction: column;
   align-items: center;
-  padding: 24rpx 0 8rpx;
+  gap: 20rpx;
+  padding: 4rpx 0;
+  box-sizing: border-box;
 }
-.logo {
-  width: 128rpx;
-  height: 128rpx;
-  border-radius: 24rpx;
+.brand-logo {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 20rpx;
+  background: $app-surface;
+  box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.18);
+  flex-shrink: 0;
+}
+.brand-texts {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  min-width: 0;
+  padding-left: 20rpx;
+  border-left: 2rpx solid rgba($brand-primary, 0.28);
 }
-.app-name {
-  font-size: calc(44rpx * var(--font-scale, 1));
+.brand-name {
+  font-size: calc(38rpx * var(--font-scale, 1));
   font-weight: bold;
   color: $facade-text-strong;
-  margin-top: 16rpx;
-}
-.identity {
-  font-size: calc(26rpx * var(--font-scale, 1));
-  color: $brand-primary;
-  margin-top: 8rpx;
+  letter-spacing: 3rpx;
+  line-height: 1.2;
 }
-.slogan {
-  font-size: calc(24rpx * var(--font-scale, 1));
-  color: $facade-text-muted;
-  margin-top: 12rpx;
-  text-align: center;
+.brand-tagline {
+  display: block;
+  font-size: calc(22rpx * var(--font-scale, 1));
+  font-weight: 500;
+  color: $brand-primary-deep;
+  margin-top: 6rpx;
+  letter-spacing: 1rpx;
+  line-height: 1.2;
+  white-space: nowrap;
 }
 
 /* ===== 理念卡 ===== */
@@ -335,15 +337,6 @@ export default {
   padding: 24rpx;
   margin-top: 28rpx;
 }
-.concept-desc {
-  font-size: calc(26rpx * var(--font-scale, 1));
-  color: $facade-text-deep;
-  line-height: 1.6;
-}
-.concept-highlight {
-  color: $brand-primary;
-  font-weight: 600;
-}
 .loop-row {
   display: flex;
   flex-direction: row;
@@ -378,33 +371,83 @@ export default {
   flex-shrink: 0;  /* 箭头不被压缩 */
 }
 
-/* ===== 五维标签 ===== */
-.dims-row {
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  align-items: center;
-  flex-wrap: nowrap;  /* 禁止换行,保持五维维度在一行 */
-  margin-top: 24rpx;
-  padding: 0 32rpx;  /* 左右留白,避免贴边 */
+/* 轨道环:缓慢旋转,呼应 Web 端虚线轨道 */
+.orbit-ring {
+  position: absolute;
+  left: 50%; top: 50%;
+  width: 240rpx; height: 240rpx;
+  margin-left: -120rpx; margin-top: -120rpx;
+  border: 2rpx dashed rgba(194, 112, 61, 0.35);
+  border-radius: 50%;
   box-sizing: border-box;
+  animation: orbitRingSpin 24s linear infinite;
+}
+@keyframes orbitRingSpin {
+  0%   { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+/* 中心"家"徽章:金色脉冲,呼应 Web 端中心节点 */
+.orbit-center {
+  position: absolute;
+  left: 50%; top: 50%;
+  width: 84rpx; height: 84rpx;
+  margin-left: -42rpx; margin-top: -42rpx;
+  border-radius: 50%;
+  background: #F59E0B;
+  display: flex; align-items: center; justify-content: center;
+  color: #fff; font-size: calc(36rpx * var(--font-scale, 1)); font-weight: 700;
+  box-shadow: 0 0 20rpx rgba(245, 158, 11, 0.4);
+  animation: orbitCenterPulse 3s ease-in-out infinite;
+  z-index: 3;
+}
+@keyframes orbitCenterPulse {
+  0%, 100% { box-shadow: 0 0 20rpx rgba(245, 158, 11, 0.4); transform: scale(1); }
+  50%      { box-shadow: 0 0 36rpx rgba(245, 158, 11, 0.6); transform: scale(1.06); }
+}
+/* 五维公转节点:绕中心旋转,文字保持正立 */
+.orbit-node {
+  position: absolute;
+  left: 50%; top: 50%;
+  width: 76rpx; height: 76rpx;
+  margin-left: -38rpx; margin-top: -38rpx;
+  border-radius: 50%;
+  background: var(--node-color);
+  display: flex; align-items: center; justify-content: center;
+  color: #fff; font-size: calc(30rpx * var(--font-scale, 1)); font-weight: 700;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
+  z-index: 2;
+  animation-duration: 15s;
+  animation-timing-function: linear;
+  animation-iteration-count: infinite;
+}
+.orbit-node-char { line-height: 1; }
+/* 五节点:颜色 + 起始角度 + 错峰延迟,与 Web 端一致 */
+.node-0 { --node-color: #FF8C42; animation-name: orbitNode0; animation-delay: 0s; }    /* 身 */
+.node-1 { --node-color: #1677FF; animation-name: orbitNode1; animation-delay: -3s; }   /* 智 */
+.node-2 { --node-color: #F59E0B; animation-name: orbitNode2; animation-delay: -6s; }   /* 富 */
+.node-3 { --node-color: #10B981; animation-name: orbitNode3; animation-delay: -9s; }   /* 行 */
+.node-4 { --node-color: #FF6B9D; animation-name: orbitNode4; animation-delay: -12s; }  /* 心 */
+
+@keyframes orbitNode0 {
+  0%   { transform: rotate(0deg)   translateX(120rpx) rotate(0deg); }
+  100% { transform: rotate(360deg) translateX(120rpx) rotate(-360deg); }
 }
-.dim-tag {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  border-radius: 999rpx;
-  padding: 8rpx 14rpx;
-  font-size: calc(22rpx * var(--font-scale, 1));
-  color: $app-surface;
-  margin: 0 8rpx;  /* 使用 margin 替代 gap,微信小程序不支持 gap */
-  white-space: nowrap;
+@keyframes orbitNode1 {
+  0%   { transform: rotate(72deg)   translateX(120rpx) rotate(-72deg); }
+  100% { transform: rotate(432deg)  translateX(120rpx) rotate(-432deg); }
+}
+@keyframes orbitNode2 {
+  0%   { transform: rotate(144deg)  translateX(120rpx) rotate(-144deg); }
+  100% { transform: rotate(504deg)  translateX(120rpx) rotate(-504deg); }
+}
+@keyframes orbitNode3 {
+  0%   { transform: rotate(216deg)  translateX(120rpx) rotate(-216deg); }
+  100% { transform: rotate(576deg)  translateX(120rpx) rotate(-576deg); }
+}
+@keyframes orbitNode4 {
+  0%   { transform: rotate(288deg)  translateX(120rpx) rotate(-288deg); }
+  100% { transform: rotate(648deg)  translateX(120rpx) rotate(-648deg); }
 }
-.dim-tag-body { background: $status-success; }
-.dim-tag-wisdom { background: $dim-wisdom; }
-.dim-tag-wealth { background: $dim-wealth; }
-.dim-tag-action { background: #14b8a6; }
-.dim-tag-mind { background: $status-danger; }
 
 /* ===== 登录表单 ===== */
 .login-form {
@@ -412,10 +455,10 @@ export default {
 }
 .phone-btn {
   width: 100%;
-  height: 96rpx;
+  height: 82rpx;
   border-radius: 24rpx;
-  background: linear-gradient(135deg, $brand-primary, $brand-primary-light);
-  color: $app-surface;
+  background: linear-gradient(135deg, #FFC24A 0%, #FFA726 100%);
+  color: #14458F;
   font-size: calc(32rpx * var(--font-scale, 1));
   font-weight: 600;
   display: flex;
@@ -423,17 +466,32 @@ export default {
   align-items: center;
   justify-content: center;
   border: none;
+  box-shadow: 0 8rpx 20rpx rgba(255, 167, 38, 0.32);
 }
 .phone-btn::after {
   border: none;
 }
 .phone-btn[disabled] {
-  background: #e0d8cc !important;
-  color: $text-ghost-soft !important;
+  background: #FFEFD0 !important;
+  color: #B57E2A !important;
 }
 .phone-btn-icon {
   margin-right: 8rpx;
 }
+.phone-btn-text {
+  color: #000000;
+}
+.phone-arrows {
+  margin-left: 10rpx;
+  font-weight: bold;
+  letter-spacing: 2rpx;
+  color: #000000;
+  animation: arrowBlink 1s ease-in-out infinite;
+}
+@keyframes arrowBlink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.15; }
+}
 
 /* ===== 暂不登录 ===== */
 .skip-section {

+ 4 - 1
cfc-frontend/pages/home-pages/child-index.vue

@@ -382,6 +382,7 @@ export default {
       hasChildren: false,
       guidePopup: null,
       showGuidePopup: false,
+      _onboardingGuideAttempted: false,
       _guidePopupAttempted: false,
       nickname: '',
       memberId: '',
@@ -770,6 +771,8 @@ export default {
     },
     _checkOnboardingShow() {
       var self = this
+      // 1. 登录守卫:未登录时不尝试引导
+      if (!this.isLoggedIn()) return
       getGuideConfig().then(function(res) {
         var cfg = (res && res.data) ? res.data : null
         var cfgVersion = cfg ? (cfg.version || '1.0') : '1.0'
@@ -789,7 +792,7 @@ export default {
         if (self._compareVersion(seenVersion, cfgVersion) >= 0) return
         self.showOnboarding = true
       }).catch(function() {
-        // 接口失败按旧逻辑:无配置则 version='1.0',已看过则不显示
+        // 接口失败降级:无配置则 version='1.0',已看过则不显示
         var stored = uni.getStorageSync('_onboardingDone')
         if (stored) {
           try {

+ 3 - 3
cfc-frontend/pages/index-home/index.vue

@@ -1973,6 +1973,8 @@ title: '添加家庭成员',
     // 首次登录引导浮层:异步拉取配置并按版本号判断是否展示
     _maybeShowOnboardingGuide() {
       if (this.showOnboardingGuide) return
+      // 未登录时不打标、不发请求;登录成功后 onSheetLoginSuccess 会再次触发本方法
+      if (!this.isLoggedIn()) return
       if (this._onboardingGuideAttempted) return
       this._onboardingGuideAttempted = true
       var self = this
@@ -1994,11 +1996,10 @@ title: '添加家庭成员',
         }
         if (self._compareVersion(seenVersion, cfgVersion) >= 0) return
         setTimeout(function() {
-          if (!self.isLoggedIn()) return
           self.showOnboardingGuide = true
         }, 600)
       }).catch(function() {
-        // 接口失败走旧逻辑
+        // 接口失败降级:无配置则 version='1.0',已看过则不显示
         var stored = uni.getStorageSync('_onboardingDone')
         if (stored) {
           try {
@@ -2007,7 +2008,6 @@ title: '添加家庭成员',
           } catch (e) {}
         }
         setTimeout(function() {
-          if (!self.isLoggedIn()) return
           self.showOnboardingGuide = true
         }, 600)
       })

+ 3 - 3
cfc-frontend/pages/profile-main/profile.vue

@@ -345,7 +345,7 @@ import {
   getChildren, getFamilyMemberList,
   getTodayTasksByCategory, getChallengeList, getMyMembership,
   verifyPassword, getPointsBalance, getUserInfo,
-  cartCount, getOrderCounts, getCfPromotionEarnings,
+  cartCount, getOrderCounts, getReferralSummary,
   getProfileStats, getMyReferrer, getVendorTeamList, getVendorHierarchyInfo,
   getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount,
   getReferralCode, extractReferralCode, getShareQrCode, getEnergySandbox,
@@ -601,9 +601,9 @@ export default {
 
     loadReferralEarnings() {
       var self = this
-      getCfPromotionEarnings().then(function(res) {
+      getReferralSummary().then(function(res) {
         if (res && res.code === 200 && res.data) {
-          self.referralEarnings = res.data.totalEarned || 0
+          self.referralEarnings = res.data.totalEarnings || 0
         }
       }).catch(function() {})
     },

+ 174 - 25
cfc-frontend/pages/promotion/index.vue

@@ -5,19 +5,19 @@
     <view class="family-earnings-card" v-if="familySummary">
       <view class="fe-header">
         <view class="fe-title-row">
-          <text class="fe-title">🏠 家庭CF总览</text>
+          <text class="fe-title">🏠 家庭公共收益</text>
           <text class="fe-badge" v-if="isAdmin">管理员</text>
         </view>
-        <text class="fe-subtitle">家庭成员 CF 贡献与余额</text>
+        <text class="fe-subtitle">家庭成员共同努力的成果</text>
       </view>
       <view class="fe-grid">
         <view class="fe-item">
           <text class="fe-value orange">{{ cfVal(familySummary.totalEarned) }}</text>
-          <text class="fe-label">家庭累计CF</text>
+          <text class="fe-label">累计推荐CF</text>
         </view>
         <view class="fe-item">
           <text class="fe-value blue">{{ cfVal(familySummary.availableAmount) }}</text>
-          <text class="fe-label">家庭可用CF</text>
+          <text class="fe-label">可支配CF</text>
         </view>
         <view class="fe-item">
           <text class="fe-value green">{{ cfVal(familySummary.withdrawnAmount) }}</text>
@@ -25,9 +25,14 @@
         </view>
         <view class="fe-item">
           <text class="fe-value purple">{{ cfVal(familySummary.distributedAmount) }}</text>
-          <text class="fe-label">已兑换CF</text>
+          <text class="fe-label">已分配CF</text>
         </view>
       </view>
+
+      <!-- 管理员操作按钮(仅管理员可见) -->
+      <view class="fe-actions" v-if="isAdmin">
+        <button class="fe-btn fe-btn-distribute" @click="showDistributeSheet = true">分配收益</button>
+      </view>
     </view>
 
     <!-- 成员贡献榜 -->
@@ -49,12 +54,16 @@
           <view class="member-info">
             <text class="member-name">{{ m.nickname || '用户' }}</text>
             <text class="member-role-tag" v-if="m.isAdmin">管理员</text>
+            <text class="member-balance-tag" v-if="m.canUseFamilyBalance">池可用</text>
+            <text class="member-balance-tag disabled-tag" v-else>池关闭</text>
           </view>
           <view class="member-amounts">
-            <text class="member-amount allocated">累计 {{ cfVal(m.allocatedAmount) }}</text>
-            <text class="member-amount consumed">消费 {{ cfVal(m.consumedAmount) }}</text>
-            <text class="member-amount available">可用 {{ cfVal(m.availableAmount) }}</text>
+            <text class="member-amount allocated">+{{ cfVal(m.allocatedAmount) }}</text>
+            <text class="member-amount consumed">-{{ cfVal(m.consumedAmount) }}</text>
+            <text class="member-amount available">={{ cfVal(m.availableAmount) }}</text>
           </view>
+          <!-- 管理员可切换成员池权限 -->
+          <switch v-if="isAdmin && !m.isAdmin" class="member-switch" :checked="m.canUseFamilyBalance" @change="toggleMemberAccess(m)" :color="primaryColor"></switch>
         </view>
       </view>
     </view>
@@ -75,15 +84,15 @@
     <!-- 统计卡片 -->
     <view class="stats-cards">
       <view class="stat-card today-card">
-        <text class="stat-value">{{ stats.todayEarned || 0 }}</text>
+        <text class="stat-value">{{ stats.todayCommission || 0 }}</text>
         <text class="stat-label">今日CF</text>
       </view>
       <view class="stat-card month-card">
-        <text class="stat-value">{{ stats.monthEarned || 0 }}</text>
+        <text class="stat-value">{{ stats.monthCommission || 0 }}</text>
         <text class="stat-label">本月CF</text>
       </view>
       <view class="stat-card total-card">
-        <text class="stat-value">{{ stats.totalEarned || 0 }}</text>
+        <text class="stat-value">{{ stats.totalCommission || 0 }}</text>
         <text class="stat-label">累计CF</text>
       </view>
     </view>
@@ -158,26 +167,53 @@
     <view style="height:40rpx"></view>
     </scroll-view>
 
+    <!-- iOS 修复:position: fixed 不能嵌套在 scroll-view 内,会导致固定定位失效。已移至 scroll-view 后 -->
+    <view class="sheet-mask" v-if="showDistributeSheet" @click="showDistributeSheet = false">
+      <view class="sheet-content" @click.stop>
+        <text class="sheet-title">分配收益</text>
+        <text class="sheet-hint">选择成员并输入分配CF值</text>
+        <picker class="sheet-picker" mode="selector" :range="memberOptions" :value="distributeMemberIdx" @change="onDistributeMemberChange">
+          <text>{{ distributeMemberIdx >= 0 ? memberOptions[distributeMemberIdx] : '选择成员' }}</text>
+        </picker>
+        <input class="sheet-input" type="digit" v-model="distributeAmount" placeholder="输入分配CF值" />
+        <view class="sheet-actions">
+          <button class="sheet-cancel" @click="showDistributeSheet = false">取消</button>
+          <button class="sheet-confirm" @click="doDistribute" :disabled="distributeMemberIdx < 0 || !distributeAmount || distributeAmount <= 0">确认分配</button>
+        </view>
+      </view>
+    </view>
+
   </view>
 </template>
 
 <script>
-import { getCfBalance, getCfStats, getCfFamilySummary, getReferralCode, extractReferralCode } from '../../utils/api.js'
+import { BrandColors } from '@/utils/brand-colors.js'
+import { getReferralSummary, getCfSummary, getFamilyEarningsSummary, getFamilyEarningsMembers, getFamilyEarningsRecords, distributeToMember, toggleMemberBalance, withdrawFamilyEarnings, getReferralCode, extractReferralCode } from '../../utils/api.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 
 export default {
   mixins: [fontScaleMixin],
   data() {
     return {
+      primaryColor: BrandColors.primary,
+      totalEarnings: '0.00',
+      availableAmount: '0.00',
       summary: null,
       stats: {},
       funnelData: {},
       funnelSteps: [],
-      // 家庭 CF 总览
+      // 家庭收益
       familySummary: null,
       members: [],
       membersExpanded: false,
       showAllRanks: false,
+      // 弹窗
+      showWithdrawSheet: false,
+      showDistributeSheet: false,
+      withdrawAmount: '',
+      distributeAmount: '',
+      distributeMemberIdx: -1,
+      memberOptions: [],
       // 分享推广码(用于右上角菜单分享)
       shareReferralCode: ''
     }
@@ -245,34 +281,74 @@ export default {
     },
     async loadData() {
       try {
-        var res = await getCfBalance()
-        if (res && res.data) {
-          this.summary = res.data
+        var summaryRes = await getReferralSummary()
+        if (summaryRes.data) {
+          this.totalEarnings = summaryRes.data.totalEarnings || '0.00'
+        }
+      } catch (e) {
+        console.error('获取邀请统计失败', e)
+      }
+      try {
+        var comRes = await getCfSummary()
+        if (comRes.data) {
+          this.summary = comRes.data
+          this.availableAmount = comRes.data.available || '0.00'
         }
       } catch (e) {
         console.error('获取 CF 总览失败', e)
       }
     },
     async loadFamilyData() {
+      if (this.isChild) {
+        // 成员只读:仅加载概要,不加载管理员功能
+        try {
+          var sRes = await getFamilyEarningsSummary()
+          if (sRes.data) {
+            this.familySummary = sRes.data
+            this.members = sRes.data.members || []
+          }
+        } catch (e) {
+          console.error('获取家庭收益概览失败', e)
+        }
+        return
+      }
       try {
-        var sRes = await getCfFamilySummary()
-        if (sRes && sRes.data) {
+        var sRes = await getFamilyEarningsSummary()
+        if (sRes.data) {
           this.familySummary = sRes.data
           this.members = sRes.data.members || []
+          // 构建picker选项
+          var opts = []
+          var arr = this.members
+          for (var i = 0; i < arr.length; i++) {
+            opts.push((arr[i].nickname || '用户' + arr[i].userId) + ' (余额:' + this.cfVal(arr[i].availableAmount) + ' CF)')
+          }
+          this.memberOptions = opts
         }
       } catch (e) {
-        console.error('获取家庭 CF 总览失败', e)
+        console.error('获取家庭收益概览失败', e)
       }
     },
-    async loadStats() {
+    loadStats() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      var baseUrl = ''
       try {
-        var res = await getCfStats()
-        if (res && res.data) {
-          this.stats = res.data
-        }
+        var config = require('../../config.js')
+        baseUrl = config.default.baseUrl || config.baseUrl || ''
       } catch (e) {
-        console.error('获取 CF 统计失败', e)
+        baseUrl = ''
       }
+      uni.request({
+        url: baseUrl + '/api/commission/stats',
+        method: 'POST',
+        header: { 'Authorization': 'Bearer ' + token },
+        success: function(res) {
+          if (res.data && res.data.code === 200) {
+            self.stats = res.data.data || {}
+          }
+        }
+      })
     },
     loadFunnelData() {
       var self = this
@@ -314,6 +390,79 @@ export default {
       if (isNaN(num)) return '0'
       return String(num)
     },
+    // 管理员操作
+    toggleMemberAccess(m) {
+      var self = this
+      var enabled = !m.canUseFamilyBalance
+      uni.showLoading({ title: '操作中...' })
+      toggleMemberBalance({ userId: m.userId, enabled: enabled }).then(function(res) {
+        uni.hideLoading()
+        if (res.code === 200) {
+          m.canUseFamilyBalance = enabled
+          uni.showToast({ title: '操作成功', icon: 'success' })
+        } else {
+          uni.showToast({ title: res.message || '操作失败', icon: 'none' })
+        }
+      }).catch(function(e) {
+        uni.hideLoading()
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      })
+    },
+    onDistributeMemberChange(e) {
+      this.distributeMemberIdx = e.detail.value
+    },
+    doDistribute() {
+      var idx = this.distributeMemberIdx
+      if (idx < 0 || !this.members[idx]) {
+        uni.showToast({ title: '请选择成员', icon: 'none' })
+        return
+      }
+      var amount = Number(this.distributeAmount)
+      if (!amount || amount <= 0) {
+        uni.showToast({ title: '请输入有效金额', icon: 'none' })
+        return
+      }
+      var self = this
+      uni.showLoading({ title: '分配中...' })
+      distributeToMember({ userId: this.members[idx].userId, amount: amount }).then(function(res) {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '分配成功', icon: 'success' })
+          self.showDistributeSheet = false
+          self.distributeAmount = ''
+          self.distributeMemberIdx = -1
+          self.loadFamilyData()
+        } else {
+          uni.showToast({ title: res.message || '分配失败', icon: 'none' })
+        }
+      }).catch(function(e) {
+        uni.hideLoading()
+        uni.showToast({ title: '分配失败', icon: 'none' })
+      })
+    },
+    doWithdraw() {
+      var amount = Number(this.withdrawAmount)
+      if (!amount || amount <= 0) {
+        uni.showToast({ title: '请输入有效金额', icon: 'none' })
+        return
+      }
+      var self = this
+      uni.showLoading({ title: '提现中...' })
+      withdrawFamilyEarnings({ amount: amount }).then(function(res) {
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '提现申请已提交', icon: 'success' })
+          self.showWithdrawSheet = false
+          self.withdrawAmount = ''
+          self.loadFamilyData()
+        } else {
+          uni.showToast({ title: res.message || '提现失败', icon: 'none' })
+        }
+      }).catch(function(e) {
+        uni.hideLoading()
+        uni.showToast({ title: '提现失败', icon: 'none' })
+      })
+    },
     goRecords() {
       uni.navigateTo({ url: '/pages/promotion/records' })
     },

+ 0 - 4
cfc-frontend/utils/api.js

@@ -3168,10 +3168,6 @@ export const exchangeCouponByCf = (data) => request('/api/family-platform/exchan
 export const getCfBalance = () => request('/api/cf/balance', 'GET', {})
 export const getCfLogs = (params) => request('/api/cf/logs', 'GET', params || { page: 1, size: 20 })
 export const getFamilyCoupons = () => request('/api/cf/family-coupons', 'GET', {})
-// CF 周期统计(今日/本月/累计获得)与家庭 CF 总览 / 推广类 CF 收益
-export const getCfStats = () => request('/api/cf/stats', 'GET', {})
-export const getCfFamilySummary = () => request('/api/cf/family-summary', 'GET', {})
-export const getCfPromotionEarnings = () => request('/api/cf/promotion-earnings', 'GET', {})
 
 // ===== 站内通知 =====
 export const getNotificationList = () => request('/api/notification/list', 'POST', {})

+ 46 - 0
docs/tdd/test/automation-run-2026-11-04-1130.md

@@ -0,0 +1,46 @@
+# 研发闭环执行报告 · 2026-11-04 11:30(研发负责人2)
+
+- 角色:研发负责人2(`--role 研发负责人`)
+- 执行窗口:10:54 – 11:30(GMT+8)
+- 命令链:`--status`(只读)→ `--changes --days 7`(只读变更感知)→ `--inspect` → `--mark-yellow` → `--auto --no-dispatch`
+- 数据源:⏳ 键 0|契约常量 0|用例引用∩⏳ 0|执行结果回填 0 键(目标池空,无新可派任务)
+
+---
+
+## 一、闭环结论
+
+| 能力键 | 阶段 | 证据提交 | 说明 |
+|---|---|---|---|
+| `guide.trainingPlan.getPlanDetail` | **DONE** | 55caf6966(测试)+ 0af81d688(实现) | 本轮由 RED 推进至 DONE |
+| `family.familyMembers.addMember` | DONE | d8d3329b | 前轮已闭环 |
+| `family.familyMembers.listMembers` | DONE | eeabfe6a | 前轮已闭环 |
+| `guide.trainingPlan.getPlanList` | DONE | be255fc4 | 前轮已闭环 |
+
+- 状态机 **4 键全 DONE**,0 RED / 0 YELLOW / 0 阻塞。
+- 状态机文件:`E:\cfc\.tdd\state.json`(本地化,`.gitignore` 排除,不入版本库/不推送)。
+
+## 二、本轮实际处理
+
+1. **变更感知(近 7 天 200 提交 / 177 文件)**:8 份设计文档、55 份测试用例文档、114 份其他变更,**无新增能力键**(0→0,变化全在既有键)→ 目标池空,无新 RED 任务可建。
+2. **S-cfc-6 卡点修复**:`guide.trainingPlan.getPlanDetail` 原状态 RED 且证据「待人工核对」。
+   - 根因:任务记录 `commit=18a61f7b7` 指向 **TR-1 文档请求提交**(docs 提交,非测试提交),导致 `commit_kind` 判为 docs,无法推进。
+   - 修复:任务账本 commit 修正为真实 **RED 测试提交 `55caf6966`**(10:10 已推送,`origin/cfclub` HEAD,触及 `TrainingPlanControllerGetPlanDetailTest`,8 例全绿)。
+   - 实现证据:`0af81d688`(已推送,触及 `TrainingPlanController#getPlanDetail`,src/main)。
+3. **显式登记 YELLOW**(`--mark-yellow --commit 0af81d688`):RED→YELLOW 成功。
+4. **自动收口**(`--auto`,deploy_verify=false 无部署验证门槛):YELLOW→GREEN→DONE 链式推进;测试环境部署指令已向 `pub-iwintrue` 会话派发(会话 `ses_e5b711e72...`)。
+
+## 三、部署跟踪(测试环境)
+
+- `--auto` 阶段已自动向 testing 环境会话(pub-iwintrue)派发部署指令,会话 `ses_e5b711e72ffer0RHMY0KLnHm9a`。
+- 待部署会话回写后可用 `--deploy-done testing --commit 55caf6966d709da384412974170ad030aad6e37e` 确认(GREEN 校验放行依据)。
+
+## 四、约束与边界(遵守)
+
+- 未改任何业务代码;未触碰主工作树 66 处他人未提交改动(`git status` 漂移,非本轮产生)。
+- 状态机/任务账本均为本地化文件(`.tdd/`、`.workbuddy/` 被 `.gitignore` 排除),本地保存不入库。
+- 本轮只做只读核查 + 证据修复 + 状态推进 + 汇总汇报,无 `git add -A` / `checkout` / `stash`。
+
+## 五、待办(下一轮)
+
+- 确认测试环境部署回写(`--deploy-done testing --commit 55caf6966...`)或由测试侧验收。
+- 目标池空:等待新设计文档/新测试用例产生新能力键后再入池派发。

+ 1 - 1
playwright_smoke_report.md

@@ -1,7 +1,7 @@
 
 ## Playwright E2E 冒烟(api 层,独立,不阻断主闭环)
 - 执行器: /d/cfc/tests/node_modules/.bin/playwright test -c playwright.config.api.js --project=desktop
-- 结果: **51 passed / 3 failed / 3 skipped**
+- 结果: **52 passed / 2 failed / 3 skipped**
 - 覆盖: tests/e2e/api/*.spec.js(含 nonfunc-security 安全契约、nonfunc-perf-compat 性能兼容)
 - 说明: 红用例=已知后端缺口(权限提升/开放端点/上传未校验/缺鉴权过滤器),作持续告警;失败不阻断本闭环。
 - 明细: /d/cfc/playwright_smoke.log

+ 3 - 3
tools/xaf_agent_hub/.model_state.json

@@ -1,6 +1,6 @@
 {
-  "opencode_blackout_until_ms": 1793577600000,
+  "opencode_blackout_until_ms": 1793836800000,
   "opencode_last_429_reason": "free usage limit reached",
-  "opencode_last_429_at": "2026-11-01 11:22:17",
-  "opencode_last_429_source": "fix_free_limit:local-45278/ses_e6d8111b8ffe9Uf28e2m2Gun9C"
+  "opencode_last_429_at": "2026-11-04 11:36:30",
+  "opencode_last_429_source": "fix_free_limit:lan-248/ses_e5c65d812ffeetW6AbhdLewCLr"
 }

+ 6 - 45
tools/xaf_agent_hub/lead_backlog.md

@@ -1,49 +1,10 @@
 # 主理人待办台账(人工决策/派发类,非 fill_slots 自动任务池)
 
 > 维护人:主理人。格式:每行一条,`- [ ]` 未决 / `- [x]` 已完成。巡检报告第三节会读取本文件拼接输出。
+> 已完成条目已归档至 `lead_backlog_archive.md`(2026-11-03),审计线索见备份 `lead_backlog.md.bak_20261103_1350`。
 
-## 已完成(2026-10-24)
-- [x] 打卡修复 4 问题(formatDate/任务不关闭/计数/成员切换)——开发完成(12:20),**核验 7 项全部 PASS**(ses_e93d54ff6ffe3TfT3k7k1IKHf9,14:06 完成),报告 `docs/superpowers/plans/verify/verify-task-checkin-fix.md`
-- [x] 任务列表页美化 + 家庭成员选择置顶(tasks.vue)——**开发会话完成**(ses_e93d5b5efffezSyoVMFBNEyQ4o,13:46 完成),仅改 tasks.vue 模板+样式,逻辑原样保留,vue 编译 1/1 PASS、反模式 ERROR 0
-- [x] git 收尾——打卡修复 9 文件 commit(f71832d28)+ 707ea09ad push 至 origin/cfclub(会话 ses_e9351acf0ffes62UwpWqFOCVDP,15:11 完成),工作区干净
-
-## 等你拍板(2026-10-27 定稿)
-- [x] **T28-FIX(P0)修复方向裁决**:独立核验报告 `origin/verify/t28-food-overview = 90798afd1` 实锤 AC-09 P0——报告确认路径把 `family_members.id`(memberId)写入 `food_recommend_idx.user_id`,而家庭总览按 `family_members.user_id`(users.id)关联,两条键不唯一对齐,总览重算链路完全失效。两个修复方向(核验报告第三章已评估):
-  1. **写侧统一**:recalculateForUser 入库前把 subjectId 归一为成员 user_id 再写;历史 memberId 键行需一次性迁移。
-  2. **读侧兼容**:doRecalculate 查询键同时覆盖 user_id 与 family_members.id 两种键;总览 memberScores.userId 语义需明确。
-  ⚠️ 涉及历史数据迁移属不可逆操作,必须主理人裁定向后再开工。
-  - **主理人裁决(2026-11-02)**:方向 A(写侧统一)——"报告也要写 family_members.user_id"。
-  - **核查结论(2026-11-02 13:3x)**:方向 A 骨架已由 E2E Test Bot 在 `62f37b609`(2026-10-31)合入 cfclub(`resolveFoodIndexUserId` 方法 + recalculateForUser/writeKey 归一 + 读侧 3 处归一化 + 6 项单测,AC-09 FAIL→PASS)。但发现**新增 P0 隐患**:当前实现先查 `family_members.id`,若传 `users.id` 且该值恰好与某 `family_members.id` 数值碰撞(两表各自自增,重叠概率高),会误判为 memberId 并映射到错误的 `user_id`,导致读错他人数据。**需加固**(注入 `UserMapper`,先查 users 表判定幂等)。另需**历史数据迁移脚本**(测试库 zxyj 共 2 行全为 memberId 键;生产库规模未知)。
-  - **待办**:派发开发会话加固代码 + 写迁移脚本(只写不执行)→ 待主理人确认迁移方案后执行 → 派发独立核验。`feature/t28-fix-key-align`(方向 B 双轨变体,c70b58049)未合入 cfclub,与已选方向 A 冲突,废弃。
-- [x] **T29-B1 合主线**:B1 独立核验已完成(`origin/feature/t29-b1v-verification = 608f18fbf`),结论 **CONDITIONAL PASS**。B1+FIX 已于 2026-10-27 **合并入 cfclub**(merge commit `4dc706188`,父链 = cfclub 637d6a15b + vfix c1df0f17d,7 文件无冲突,V-01 setScore 兜底 + V-02 report_date 提取均已验证在合并结果中)。
-- [x] **T29-B2 派发**:已派 lan-189 `ses_e82f773fcffePaGYPNUK1muSHx`(axon/deepseek,分支 `feature/proxy-fix-b2-security`,2026-10-27 19:04 确认正常执行),指令 `out/t29_b2_security_prompt.txt`。B3 待 B2 核验通过后派。
-- [x] **优惠券未完成任务补录入池**:2026-10-27 补录 4 条到任务池(`COUPON-C2`/`COUPON-C3`/`COUPON-REG`/`COUPON-P2`,均 pending)。C2/C3 已由 pub-iwintrue 会话完成但**未 push**(commit `be16b8eb3`/`3379d4ad1`/`892a7e7c9` 不在任何远端分支,全量 fetch 验证);COUPON-REG(注册发券)原观察项转正式;COUPON-P2 为技术债打包。spec `docs/优惠券需求设计与实现核查.md` 已在 cfclub。
-- [x] **任务池同步 cfclub(2026-10-28)**:合并 49 条(本机 48 + cfclub 独有 T19 保留并标注拆分),commit `5dae9e13f` 推至 origin/cfclub。关键更正:此前「C2/C3/P2 未 push」系本机 remotes 缓存过时**误判**——直连服务器确认 T29-B2@`751145b9d`、COUPON-C2@`b8ade0fab`、COUPON-C3@`30bb2255d7`、COUPON-P2@`59bc4100b` 均已 push(仅未合入 cfclub);COUPON-REG 已合入(`055f6da25`)。新增 **T29-B2V 独立核验**任务,已被巡检派 local-45278 `ses_e7fca6b04ffeYxH60KvsfXV23h`。
-- [x] **T29-B2 合入 cfclub**:B2 独立核验(T29-B2V)done,B2/B3 均已合入 cfclub(751145b9d / proxy-fix-b3-robust 均在 cfclub)。
-- [x] **COUPON-C2/C3/P2 合入 cfclub**:C2/P2 已合入(b8ade0fab / 59bc4100b 在 cfclub);C3 已通过 `52a278ce3`(领券中心数据源改可取券模板)在 cfclub 落地,C3V + C3-FIXV 核验均 done。全部完成。
-- [x] **T04 第一段收口**:主体已由并发会话 `1f2f9cd01` 合入 cfclub;TrendChart 白板修复已生效于 cfclub 新版(`import { chartRenderConfig } from '@/utils/indicator-change.js'` 已正确,line 22)——`feature/t04-segment1` 的 9efb65396 为旧结构版本,无需再合。第二段(report-detail.vue + L1/L2/L3 门槛)已由 T04-S2/T04-S2-GATE 完成。
-- [x] **CFC-EN-001 能量发放 500 修复**(2026-11-01 派发,来自接口冒烟测试结果诊断):派 lan-248 `ses_e68fcb4d0ffedxY8SIu6g3uwkA`(axonhub/deepseek-flash,429 黑窗 fallback,新建会话 20:20 确认执行中),指令 `out/en001_prompt.txt`。修复 `/api/energy/grant` 稳定 500(链路 grantEnergy→awardEnergy→awardByConfig/executeAward;候选根因 getDimByCode("body") null / parseDimensionAssign 异常,以真实栈为准)。完成后自 commit+push 到 origin/cfclub,**待独立核验**后合入。
-  - 诊断报告:`docs/tdd/test/2026-11-01-fixme-diagnosis.md`(27 项 fixme 分类:2 真缺陷 / 1 部署滞后 / 15 环境阻塞 / 1 环境数据 / 4 测试夹具 / 2 夹具+授权;badges NOT_FOUND 已过时,前端 10-30 `c465bc3b3` 已收敛 MemberBadge 口径)。
-  - **开发已提交**:commit `fcc6aa876` → 分支 `feature/cfc-en-001-energy-grant-500`(基于 7baa09caa,4 文件 265+/32-)。根因确认:旧 grantEnergy 委托 `awardEnergy("task")`,task_complete 行为配置 100% 分配到 action,请求 body 维度时 balanceMap.get("body")=null → RuntimeException → GlobalExceptionHandler 映射 500。修复:显式构造 allocations 直达指定维度(绕过行为配置比例)+ RuntimeException→IllegalArgumentException/IllegalStateException(GlobalExceptionHandler 已映射 400);新增 2 单测类(Controller 3 例 + Service 2 例)+ 移除 e2e 2 处 fixme + 1 个特征复现用例。
-  - **独立核验**:lan-204 `ses_e68af7756ffeUGY5FdJW6C66wg`(axonhub/deepseek,新建 21:37),指令 `out/verify_en001_prompt.txt`(检出→rebase origin/cfclub→编译→单测→Q1-Q6 逻辑审查→边界风险→核验结论)。**核验 PASS**(报告 commit `628144956`),已由 `7b6228168 merge(cfc-en-001): 能量发放 500 修复合入(独立核验 PASS)` 合入 cfclub,EnergyService 修复已生效(line 1994 grantEnergy 显式维度)。**已完成**。
-
-## SC-G-03 web 后台微信扫码登录(2026-11-02 派发并合入)
-- [x] **已拍板**:立项开发 + 复用现有小程序码 SSO(用户选择)。开发分支 `origin/feature/admin-wx-scan-login`(3 commit:`e4a4a26f2` 后端端点 + `07bd1074a` 单测 + `c28464e66` 前端入口,6 文件 653+/45-)。
-- [x] **独立核验 PASS**:lan-204 `ses_e6584510dffe7lwqtFWIY8CuE6`(12:19 派发,12:56 产出报告),指令 `out/verify_admin_wx_scan_login_prompt.txt`。编译 BUILD SUCCESS(2032 源文件)、单测 11/11(须 `-DskipTests=false`)、红线 ssoLogin 无触碰、角色校验/JWT 签发/state 生命周期/端点公开 3 处/前端逻辑/api-auth 七项全 ✓。核验分支 `origin/verify/admin-wx-scan-login`(rebase 后 `4a894d3cc`)。附注:前端 npm build 未执行(worktree 无 node_modules),已静态审查 Login.vue/auth.js 无跳转回归。
-- [x] **已合入 cfclub(13:2x)**:merge commit `eec0db368`(`--no-ff`,父链 = b35d9fd54 + verify -> 4a894d3cc,6 文件无冲突),推送验证 `merge-base --is-ancestor` 通过。SC-G-03 回填 done。
-
-## 孩子定义字段 users.is_child(2026-11-02 派发并合入)
-- [x] **开发已提交**:分支 `origin/feature/user-is-child`(1 commit `e3e52ca38`,11 文件 352+/1-)。User.isChild + schema.sql TINYINT(1) NULL + DatabaseInitializer 迁移443 ensureColumn(覆盖「不新增迁移」纪律,需求硬要求);UserUtil 显式优先/年龄<18 兜底/fillDefaultIsChild 仅 null 时填;applyIsChildFlag 家庭管理员 403 + 落库 users;updateParentMember 主账号 memberId=users.id 语义正确;UserService 5 调用点;Controller Boolean/Number/String 解析 + 403 映射;2 单测类(110+87 行)。
-- [x] **独立核验 有条件 PASS**:lan-189 `ses_e65806c2fffeBuj2pXHw97jnfh`(12:23 派发,12:57 产出报告),指令 `out/verify_child_def_prompt.txt`。编译 BUILD SUCCESS(2033 源文件)、单测 11/11(FamilyMemberServiceIsChildTest 5 + UserUtilTest 6)、九项审查全 ✓。核验分支 `origin/verify/user-is-child`(rebase 后 `3b9576d4f`)。
-- [x] **已合入 cfclub(13:2x)**:merge commit `1173f3375`(`--no-ff`,父链 = eec0db368 + verify -> 3b9576d4f,11 文件无冲突),推送验证通过。
-- ⚠ **主理人知会(非阻断)**:**B1** 项目 `pom.xml <skipTests>true</skipTests>` 默认跳过单测(合入后常规 mvn test 不自动验证,须 `-DskipTests=false`);**B5** updateMember 改成员生日只写 family_members.birthday 不写 users.birthday,is_child 年龄兜底读 users.birthday 可能用旧值(既有双写问题延伸,本提交未恶化);B2/B3/B4/B6/B7/B8 为轻量后续可统一。
-
-## AI 使用会员门禁(2026-11-02 需求)— 已完成并合入 cfclub
-- [x] **需求**:小程序所有 AI 使用功能为会员加开关,仅有效期内缴费会员可用。
-- [x] **主理人四项决策**:① 等级下限=BASIC 及以上;② 复用开关 `ai.coach.member_gate_enabled`;③ 过期后可查看+删除/归档历史会话;④ 不纳入门禁(保留 FREE 每日 1 次引流钩子):/report/analyze + 情绪照片 + 舌诊 + 饮食识别。
-- [x] **Q3 拍板**:`/butler/sessions/create` 纳入门禁(list/detail/archive/delete 保持开放)。
-- [x] **任务书已合入 cfclub**:`docs/superpowers/specs/2026-11-02-ai-member-gate-design.md`(commit `85e93fb56`,v1.1)。
-- [x] **开发完成并自 push**:后端 T-B0/B1/B2 → 分支 `feature/ai-member-gate-backend` @ `9e1fc2f25`(dev 会话 lan-248 `ses_e6308a0f4ffeDIT5WaGL3FlVuX`);前端 T-F0/F1/F2 → 分支 `feature/ai-member-gate-frontend` @ `1e2516e0c`(dev 会话 lan-189 `ses_e6306855affekn2AjzNbWLkHn3`)。指令 `out/ai_member_gate_backend_prompt.txt` / `out/ai_member_gate_frontend_prompt.txt`。
-- [x] **独立核验 PASS**:后端 lan-204 `ses_e62d1c830ffeoe0LNO9H3nWgnd`(mvn compile BUILD SUCCESS;重点 4 类单测 56/56 通过;全量失败均为无关既有测试,基线对照一致)→ **PASS**;前端 pub-iwintrue `ses_e62d0b380ffeKqNAwI5hYYV1SW`(jest 4 条门禁单测全过,全量唯一失败 `createTask` 为 pre-existing;mp-weixin 构建 exit=0 与基线 warning 集合一致)→ **PASS**(报告 commit `86e93896c`)。
-- [x] **已合入 cfclub(2026-11-03)**:走独立 worktree `merge/ai-member-gate` 两次 `--no-ff` 合并(后端 `19d25aa01`、前端 `016097715`),并补合 origin/cfclub 最新 `0465afce9`(首页热门商品决策:移除收起切换、查看更多直达商城,门禁无关,已正确保留 cfclub 决策);最终 cfclub HEAD = `541d9e87c`。`merge-base --is-ancestor` 双分支均 ✓。回滚锚点 `refs/backup/cfclub-pre-aigate-merge-20261103` = `ddd1f9f04`。
-- [x] **迁移 worktree**:`/e/cfc-aigate-merge` 可清理(已 push,本地可删)。
+## 未决(2026-11-03 清理后保留)
+- [ ] **T28-FIX(P0)收尾**:方向 A(写侧统一)已裁决(2026-11-02);加固代码 `4de8cb16a` 已合入 cfclub 主链(11-02 15:25,`resolveFoodIndexUserId` 先查 users 表防数值碰撞误判 + 3 项单测);迁移脚本 `V253__migrate_food_recommend_idx_to_users_id.sql` 已就绪(只写未执行)。剩余两环:
+  1. **迁移执行**(生产库 memberId 键历史行 → users.id)——属不可逆生产操作,须主理人确认迁移方案后再执行;
+  2. **独立核验**(T28-FIX-V)——迁移执行后派发,验证 AC-09 收敛与无越权读。
+  - 参照:`feature/t28-fix-key-align`(方向 B 双轨变体,c70b58049)未合入 cfclub,与已选方向 A 冲突,已废弃。

+ 99 - 5
tools/xaf_agent_hub/tasks_backlog.json

@@ -1587,9 +1587,61 @@
     "priority": "P1",
     "module": "fullstack",
     "instance": "",
-    "status": "pending",
+    "status": "done",
     "spec": "docs/superpowers/specs/2026-11-01-team-system-design.md",
-    "prompt": "规格 2026-11-01-team-system-design.md §3.5/§4.2/§4.3(已合入 cfclub)。背景:廖总要求「在我的团队加一个邀请功能,邀请其他用户加入团队」。现状(勿重复核查):SupplySystemController 仅 member/add(管理员直接加, 参数仅 systemId/userId, 不带parentId不联动supply_hierarchy);SupplySystemMember 无邀请码/受邀人字段;前端 vendor/team-system.vue(780行) 无邀请入口。可参照 DistributionService.generateInviteCode/registerByInviteCode 手法(结算模型不同, 仅复用码生成/凭码注册)。\n数据:supply_system_member 扩列 invite_code(幂等 ensureColumn + 同步 schema.sql)。\n接口(@PostMapping+@Resource+@RequestAttribute(\"userId\")+Result<T>, /api/admin/supply-team/*):① POST /invite-code 生成/获取我的邀请码(幂等, 已生成复用, 支持刷新重置旧码) ② POST /invite/info 凭码查团队名/邀请人昵称头像/成员数(确认页用, 不泄露隐私) ③ POST /invite/join 凭码加入(校验码有效+用户未加入过该体系→addMember(systemId,userId,role=member,active) + SupplyHierarchyService.addRelation(systemId,inviterId,userId) 挂parentId=邀请人/level=父level+1)。\n前端:vendor/team-system.vue 团队成员区加「邀请成员」按钮→弹窗(邀请码+复制+可后置小程序码);新增受邀确认页(展示邀请人/团队→加入→跳转我的团队)。\n边界:同一(systemId,userId)仅加入一次(addMember已有existing检查);status=inactive后可再申请;role=member不可自助变更;邀请不直接挂钩佣金(结算走唯一负责人口径§0.5)。默认自助加入即生效(负责人可移除)、邀请码永久有效唯一。\n约束:先查 API_REFERENCE 避免重复并同步;mvn clean compile + node --check 通过;不破坏现有4套团队功能。"
+    "prompt": "规格 2026-11-01-team-system-design.md §3.5/§4.2/§4.3(已合入 cfclub)。背景:廖总要求「在我的团队加一个邀请功能,邀请其他用户加入团队」。现状(勿重复核查):SupplySystemController 仅 member/add(管理员直接加, 参数仅 systemId/userId, 不带parentId不联动supply_hierarchy);SupplySystemMember 无邀请码/受邀人字段;前端 vendor/team-system.vue(780行) 无邀请入口。可参照 DistributionService.generateInviteCode/registerByInviteCode 手法(结算模型不同, 仅复用码生成/凭码注册)。\n数据:supply_system_member 扩列 invite_code(幂等 ensureColumn + 同步 schema.sql)。\n接口(@PostMapping+@Resource+@RequestAttribute(\"userId\")+Result<T>, /api/admin/supply-team/*):① POST /invite-code 生成/获取我的邀请码(幂等, 已生成复用, 支持刷新重置旧码) ② POST /invite/info 凭码查团队名/邀请人昵称头像/成员数(确认页用, 不泄露隐私) ③ POST /invite/join 凭码加入(校验码有效+用户未加入过该体系→addMember(systemId,userId,role=member,active) + SupplyHierarchyService.addRelation(systemId,inviterId,userId) 挂parentId=邀请人/level=父level+1)。\n前端:vendor/team-system.vue 团队成员区加「邀请成员」按钮→弹窗(邀请码+复制+可后置小程序码);新增受邀确认页(展示邀请人/团队→加入→跳转我的团队)。\n边界:同一(systemId,userId)仅加入一次(addMember已有existing检查);status=inactive后可再申请;role=member不可自助变更;邀请不直接挂钩佣金(结算走唯一负责人口径§0.5)。默认自助加入即生效(负责人可移除)、邀请码永久有效唯一。\n约束:先查 API_REFERENCE 避免重复并同步;mvn clean compile + node --check 通过;不破坏现有4套团队功能。",
+    "session_id": "ses_e656ec46fffeJY6vsM2mF63iMA",
+    "dispatched_at": "2026-11-02 12:42:51",
+    "dispatched_to": "lan-248",
+    "model": "axonhub/deepseek",
+    "done_at": "2026-11-02 14:17:50"
+  },
+  {
+    "id": "T39",
+    "title": "统一真实姓名回填昵称规则(扩展 RealNameBackfillService 同时补 realName+nickname,接入所有姓名写入入口,修复但管家/营养师申请 realName 持久化)",
+    "priority": "P1",
+    "module": "fullstack",
+    "instance": "",
+    "status": "done",
+    "spec": "docs/superpowers/specs/2026-11-02-realname-nickname-backfill-design.md",
+    "prompt": "规格 2026-11-02-realname-nickname-backfill-design.md(已合入 cfclub)。背景:廖总要求\"检查当前所有要使用用户真实姓名的地方,如果添加姓名且user表的真实姓名还未填,则用这个姓名补充真实姓名和用户昵称\"。核查发现:现有 12 处后端写 users.real_name 入口,仅 RealNameBackfillService 有回填逻辑但只补 realName 不补 nickname;管家/营养师申请前端收集 realName 但后端丢弃。\n方案:扩展 RealNameBackfillService 增加 backfillRealNameAndNickname(userId, name) —— 若 realName 为空,同时补 realName + nickname(nickname 仅在空时补);在以下入口统一接入:\n1. UserService.updateUserInfo(资料页/加入家庭)\n2. UserService.backfillProfile(就地回写,白名单已含 realName)\n3. UserService.registerWithIdCard(手机+身份证注册)\n4. RealNameBackfillService.backfill(地址/收货人回填,改为同步补两者)\n5. GuideManagementController.apply(规划师注册)\n6. AdminController.createUser(后台创建)\n7. SupplyManageController add/update(供应商创建/更新)\n8. ButlerController.apply(管家申请,新增接收 realName 参数)\n9. NutritionistController.apply(营养师申请,新增接收 realName 参数)\n\n约束:nickname 仅在空时补(不覆盖用户已设昵称);触发条件=realName 为空;幂等;失败静默不阻塞主流程;mvn clean compile 通过;未破坏现有用户体系。",
+    "session_id": "ses_e647a98c1ffe7jqn7Iijq6z8vV",
+    "dispatched_at": "2026-11-02 17:09:33",
+    "dispatched_to": "local-45278",
+    "model": "axonhub/deepseek",
+    "done_at": "2026-11-02 19:11:02"
+  },
+  {
+    "id": "CFC-COLOR-01",
+    "title": "小程序全局配色迁移「暖调+深橙」(品牌主色降档#C2410C + 页面底色暖米#FBF4EA + 边框暖化,令牌值+tabBar同步)",
+    "priority": "P1",
+    "module": "cfc-frontend",
+    "instance": "lan-248",
+    "status": "done",
+    "spec": ".workbuddy/tasks/2026-11-02-color-migration.md",
+    "created_at": "2026-11-02 23:16:49",
+    "session_id": "ses_e6327b3d1ffemUTFrjOKkhmxmn",
+    "dispatched_at": "2026-11-02 23:20:10",
+    "done_at": "2026-11-02 23:40:59"
+  },
+  {
+    "id": "CFC-GUEST-01",
+    "title": "三页访客态重设计落地(健康/成长/我的)+ 五维顺序修正身→智→富→行→心 + 新暖调配色 + LoginGuideCard升级",
+    "priority": "P1",
+    "module": "cfc-frontend",
+    "instance": "lan-248",
+    "status": "done",
+    "spec": ".workbuddy/tasks/2026-11-03-guest-tabs-redesign.md",
+    "prompt": "规格见 .workbuddy/tasks/2026-11-03-guest-tabs-redesign.md(自包含 dispatch prompt 已写入 out/guest_redesign_prompt.txt,远程按此执行)",
+    "created_at": "2026-11-03 17:53:29",
+    "dispatched_at": "2026-11-03 18:21:37",
+    "session_id": "ses_e5f134e01ffeBm7PeVvD1GjMoW",
+    "dispatched_to": "lan-248",
+    "done_at": "2026-11-03 19:13:28",
+    "branch": "feature/guest-tabs-redesign",
+    "commit": "372ec4641032aa5b5918ec1e30b2abb96d413a5c",
+    "base": "52fe45b11",
+    "verification": "访客态零emoji/零grid/零?./零<svg>;五维身→智→富→行→心;配色全令牌;模板登录态分支零改动;我的页无底部登录引导卡;build:mp-weixin EXIT=0"
   },
   {
     "id": "CFC-TIANPAN-01",
@@ -1597,9 +1649,51 @@
     "priority": "P1",
     "module": "cfc-frontend",
     "instance": "lan-248",
-    "status": "pending",
+    "status": "done",
     "spec": "docs/superpowers/specs/2026-11-04-tianpan-emoji-svg.md",
-    "prompt": "规格 docs/superpowers/specs/2026-11-04-tianpan-emoji-svg.md(已合入 cfclub)。背景:廖总指示将天盘 emoji 图标替换任务入池(方案C)。现状(勿重复核查,使用点已核实):pages/tianpan/index.vue L66(+L476 canvas) 用 📋 作相关事项缺省图标;pages/mind-detail/components/FamilyTianpanCard.vue 用 &#x2637;(☷ 罗盘)/&#x2B50;(⭐ 心情)/&#x1F9ED;(🧭 八字)/&#x2600;(☀ 传统)/&#x1F52E;(🔮 运势)/&#x1F4A1;(💡 建议) 作功能图标,违反 P0-1 禁emoji规范。→(跳转)/♦✖(宜忌) 为语义符号不替换。\n方案:①先调研 cfc-frontend 既有图标基础设施(uni-icons/iconfont/静态SVG+<image>),沿用同页面方案,不引新依赖;②index.vue L66 替换为图标组件或 <image>,L476 canvas fillText 改 drawImage 预渲染图标(canvas 无法直接渲 SVG 字符串);③FamilyTianpanCard 各标签/评分图标同法替换;④颜色跟随主题令牌。\n验收:两文件功能图标零 emoji(&#x实体与原生 emoji 均不得用于功能图标);首页/卡片渲染正常(今日运势/心情星级/罗盘/八字/五行/星座灵数/相关事项卡片);canvas 绘制不回归;npm run build:mp-weixin EXIT=0;不动后端与天盘分包其余页面。提交独立分支 push 至 origin/cfclub 后回报 commit 哈希。",
-    "created_at": "2026-11-03 20:11:51"
+    "prompt": "规格 docs/superpowers/specs/2026-11-04-tianpan-emoji-svg.md(已合入 cfclub)。背景:廖总指示将天盘 emoji 图标替换任务入池(方案C)。现状(勿重复核查,使用点已核实):pages/tianpan/index.vue L66(+L476 canvas) 用 📋(U+1F4CB) 作相关事项缺省图标;pages/mind-detail/components/FamilyTianpanCard.vue 用 &#x2637;(☰ 罗盘)/&#x2B50;(⭐ 心情)/&#x1F9ED;(📛 八字)/&#x2600;(☀ 传统)/&#x1F52E;(🔮 运势)/&#x1F4A1;(💡 建议) 作功能图标,违反 P0-1 禁emoji规范。→(跳转)/♦✖(宜忌) 为语义符号不替换。\n方案:①先调研 cfc-frontend 既有图标基础设施(uni-icons/iconfont/静态SVG+<image>),沿用同页面方案,不引新依赖;②index.vue L66 替换为图标组件或 <image>,L476 canvas fillText 改 drawImage 预渲染图标(canvas 无法直接渲 SVG 字符串);③FamilyTianpanCard 各标签/评分图标同法替换;④颜色跟随主题令牌。\n验收:两文件功能图标零 emoji(&#x实体与原生 emoji 均不得用于功能图标);首页/卡片渲染正常(今日运势/心情星级/罗盘/八字/五行/星座灵数/相关事项卡片);canvas 绘制不回归;npm run build:mp-weixin EXIT=0;不动后端与天盘分包其余页面。提交独立分支 push 至 origin/cfclub 后回报 commit 哈希。",
+    "created_at": "2026-11-03 20:00:18",
+    "session_id": "ses_e5eb5878affe0sUDCXwyJsVSah",
+    "dispatched_at": "2026-11-03 20:02:55",
+    "dispatched_to": "local-45278",
+    "model": "axonhub/deepseek",
+    "done_at": "2026-11-03 22:32:43"
+  },
+  {
+    "id": "T40",
+    "title": "小程序新版本强制更新功能(uni.getUpdateManager 检测 + applyUpdate 强制重启,软强制弹窗)",
+    "priority": "P1",
+    "module": "cfc-frontend",
+    "instance": "",
+    "status": "done",
+    "spec": "docs/superpowers/specs/2026-11-03-miniapp-force-update.md",
+    "prompt": "规格 docs/superpowers/specs/2026-11-03-miniapp-force-update.md(待合入 cfclub)。需求:小程序发布新版本后,老版本用户强制升级。\n现状:cfc-frontend 全仓无 getUpdateManager 调用。\n方案:纯前端官方机制。\n1. 新增 cfc-frontend/utils/checkAppUpdate.js:封装 uni.getUpdateManager(),onUpdateReady 时 uni.showModal(showCancel:false, confirmText:/立即更新/),success 回调调 manager.applyUpdate() 强制重启应用新包;onUpdateFailed 弹\"更新失败\"提示;能力探测不支持时 console.warn 跳过。\n2. App.vue onLaunch 开头调用 checkAppUpdate()(路由逻辑前,因 applyUpdate 重启整机)。\n约束:不依赖 manifest versionName/versionCode(微信后台代码包版本自动判断);开发版不触发 onUpdateReady 属微信正常行为;不改 splash/登录/路由既有逻辑;调用幂等;失败静默不阻塞主流程;uni-app 编译通过。",
+    "created_at": "2026-11-03 23:35",
+    "session_id": "ses_e5de8832dffe43xH25Win53l8w",
+    "dispatched_to": "lan-204",
+    "model": "opencode/deepseek-v4.1-flash-free",
+    "dispatched_at": "2026-11-03 23:46:55",
+    "done_at": "2026-11-04 00:08:21"
+  },
+  {
+    "id": "GAP-2026-11-03-unified-dimen",
+    "title": "[差距] 2026 11 03 unified dimension system design",
+    "priority": "P1",
+    "module": "cfc-backend",
+    "instance": "",
+    "status": "done",
+    "spec": "docs/superpowers/specs/2026-11-03-unified-dimension-system-design.md",
+    "entity": "DimensionCodeConstants.java",
+    "gap_kinds": [
+      "missing_file"
+    ],
+    "prompt": "请核对需求/设计文档 docs/superpowers/specs/2026-11-03-unified-dimension-system-design.md 与实际实现的差距,并完成以下差距项:\n\n文档主题:2026 11 03 unified dimension system design\n所属模块:cfc-backend\n\n检测到的差距:\n  - [文档提及但尚未创建的代码文件] DimensionCodeConstants.java\n  - [文档提及但尚未创建的代码文件] DimensionRegistryService.java\n  - [文档提及但尚未创建的代码文件] DimensionRegistryController.java\n  - [文档提及但尚未创建的代码文件] DimensionSyncService.java\n  - [文档提及但尚未创建的代码文件] DimensionSelector.vue\n\n任务来源:sync_gap_tasks(巡检发现空闲会话且任务池为空时自动生成)。\n要求:先阅读该文档对应章节,确认差距真实存在(若已实现请如实说明并标记为无需改动),然后按文档口径实现/补齐,完成后自行提交并推送。",
+    "generated_from": "sync_gap_tasks",
+    "generated_at": "2026-11-04 09:46:50",
+    "session_id": "ses_e5bc32342ffeIoo00QDGLWgjYT",
+    "dispatched_at": "2026-11-04 09:46:54",
+    "dispatched_to": "local-45278",
+    "model": "axonhub/deepseek",
+    "done_at": "2026-11-04 23:35:08"
   }
 ]