2026-06-13-guest-mode-implementation.md 28 KB

游客模式实现计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 实现身体/心智/行动三个 TabBar 页的游客(未登录)模式,支持商品/活动/文章公开浏览但不显示价格

Architecture: 后端所有列表接口增加 @RequestAttribute("userId") 参数,根据是否登录返回不同数据集(游客仅 public 内容,价格置 null)。前端各页面通过 isLoggedIn 变量分支渲染,个性化区块隐藏并显示登录引导卡,通用区块接收后端返回的 priceLabel 替代价格数字。

Tech Stack: Spring Boot 2.7.18 (Java), uni-app Vue 2 (微信小程序)


文件结构

新增文件

文件 类型 说明
cfc-frontend/components/LoginGuideCard.vue component 登录引导卡(三页公用,文案可配)
cfc-frontend/components/HealthTips.vue component 身体页游客健康小贴士(从文章库取 body 类文章)
cfc-backend/.../dto/ProductDTO.java dto 商品 DTO(含 priceLabel 字段)
cfc-backend/.../dto/ArticleDTO.java dto 文章 DTO(可选,若尚无)

修改文件

文件 类型 说明
cfc-backend/.../entity/Activity.java entity 补充 visibility + visibleTo 字段
cfc-backend/.../dto/ActivityDTO.java dto 补充 priceLabel
cfc-backend/.../controller/ProductController.java controller list 增加 auth 感知 + 游客过滤 + 价格隐藏
cfc-backend/.../controller/ActivityController.java controller list 增加可见性过滤 + 价格隐藏
cfc-backend/.../controller/ArticleController.java controller list 增加可见性过滤
cfc-frontend/components/DimensionProducts.vue component 支持 priceLabel 渲染
cfc-frontend/components/DimensionActivities.vue component 支持 priceLabel 渲染
cfc-frontend/pages/body/index.vue page 增加游客分支
cfc-frontend/pages/mind/index.vue page 增加游客分支
cfc-frontend/pages/action/index.vue page 增加游客分支

实现任务

Task 1: 后端 ProductController 游客模式

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java

Step 1: 创建 ProductDTO

package com.etotem.cfc.dto;

import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;

@Data
public class ProductDTO {
    private Long id;
    private String name;
    private String description;
    private String intro;
    private String coverImage;
    private String images;
    private BigDecimal price;          // 游客 = null
    private String priceLabel;         // 游客 = "登录查看价格",登录后 = null
    private String vendorName;
    private String productType;
    private String status;
    private BigDecimal memberPrice;
    private String memberEligible;
    private Long energyDimensionId;
    private Date createdAt;
}

Step 2: 修改 ProductController.list 方法

@Resource
private ProductService productService;

@PostMapping("/list")
public Result<Page<ProductDTO>> list(
        @RequestBody Map<String, Object> params,
        @RequestAttribute(value = "userId", required = false) Long userId) {

    String dimensionCode = (String) params.get("dimensionCode");
    Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
    Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;

    LambdaQueryWrapper<Product> query = new LambdaQueryWrapper<Product>()
            .eq(Product::getStatus, "approved");

    // 游客过滤:排除会员专属商品
    if (userId == null) {
        query.ne(Product::getMemberEligible, "yes");
    }

    // 维度筛选(若有 energyDimensionId 字段)
    if (dimensionCode != null && !dimensionCode.isEmpty()) {
        // 根据 dimensionCode 查 energy_dimension 表获取 ID
        // 简化做法:先用 domain 字段模糊匹配
        query.like(Product::getDomain, dimensionCode);
    }

    Page<Product> pageResult = productService.page(new Page<>(page, size), query);
    boolean isGuest = (userId == null);

    // 转为 DTO
    List<ProductDTO> dtoList = pageResult.getRecords().stream()
            .map(p -> toDTO(p, isGuest))
            .collect(Collectors.toList());

    Page<ProductDTO> dtoPage = new Page<>(pageResult.getCurrent(), pageResult.getSize(), pageResult.getTotal());
    dtoPage.setRecords(dtoList);
    return Result.success(dtoPage);
}

private ProductDTO toDTO(Product p, boolean isGuest) {
    ProductDTO dto = new ProductDTO();
    dto.setId(p.getId());
    dto.setName(p.getName());
    dto.setDescription(p.getDescription());
    dto.setIntro(p.getIntro());
    dto.setCoverImage(p.getCoverImage());
    dto.setImages(p.getImages());
    dto.setVendorName(p.getVendorName());
    dto.setProductType(p.getProductType());
    dto.setStatus(p.getStatus());
    dto.setMemberPrice(p.getMemberPrice());
    dto.setMemberEligible(p.getMemberEligible());
    dto.setEnergyDimensionId(p.getEnergyDimensionId());
    dto.setCreatedAt(p.getCreatedAt());
    if (isGuest) {
        dto.setPrice(null);
        dto.setPriceLabel("登录查看价格");
    } else {
        dto.setPrice(p.getPrice());
        dto.setPriceLabel(null);
    }
    return dto;
}

Task 2: 后端 Activity 补充可见性字段 + 游客模式

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java
  • Modify: cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java
  • Modify: cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java

Step 1: Activity 实体补充字段

Activity.java 中追加:

/** 可见性: public(所有人) / member(登录用户) / private(指定用户) */
private String visibility;

/** 可见用户ID列表,JSON数组格式 ["1","2","3"] */
private String visibleTo;

Step 2: ActivityDTO 补充 priceLabel

// 在 ActivityDTO 中追加
private String priceLabel;    // 游客 = "登录查看"

Step 3: 改造 ActivityController.list

@PostMapping("/list")
public Result<Page<ActivityDTO>> list(
        @RequestBody Map<String, Object> params,
        @RequestAttribute(value = "userId", required = false) Long userId) {

    String dimensionCode = (String) params.get("dimensionCode");
    Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
    Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;

    LambdaQueryWrapper<Activity> query = new LambdaQueryWrapper<Activity>()
            .eq(Activity::getStatus, "published");

    if (userId == null) {
        // 游客:只看公开活动
        query.eq(Activity::getVisibility, "public");
    } else {
        // 登录用户:public + member + 自己可见的 private
        query.and(w -> w
            .eq(Activity::getVisibility, "public")
            .or().eq(Activity::getVisibility, "member")
            .or().and(w2 -> w2
                .eq(Activity::getVisibility, "private")
                .apply("JSON_CONTAINS(visible_to, {0})", "\"" + userId + "\"")));
    }

    if (dimensionCode != null && !dimensionCode.isEmpty()) {
        query.eq(Activity::getDimensionCode, dimensionCode);
    }

    Page<Activity> pageResult = activityService.page(new Page<>(page, size), query);
    boolean isGuest = (userId == null);

    List<ActivityDTO> dtoList = pageResult.getRecords().stream()
            .map(a -> toDTO(a, isGuest))
            .collect(Collectors.toList());

    Page<ActivityDTO> dtoPage = new Page<>(pageResult.getCurrent(), pageResult.getSize(), pageResult.getTotal());
    dtoPage.setRecords(dtoList);
    return Result.success(dtoPage);
}

private ActivityDTO toDTO(Activity a, boolean isGuest) {
    ActivityDTO dto = new ActivityDTO();
    dto.setId(a.getId());
    dto.setTitle(a.getTitle());
    dto.setDescription(a.getDescription());
    dto.setCoverImage(a.getCoverImage());
    dto.setDimensionCode(a.getDimensionCode());
    dto.setActivityType(a.getActivityType());
    dto.setStatus(a.getStatus());
    dto.setStartTime(a.getStartTime() != null ? a.getStartTime().toString() : null);
    dto.setEndTime(a.getEndTime() != null ? a.getEndTime().toString() : null);
    dto.setLocation(a.getLocation());
    dto.setMaxParticipants(a.getMaxParticipants());
    dto.setCurrentParticipants(a.getCurrentParticipants());
    dto.setVendorName(a.getVendorName());
    if (isGuest) {
        dto.setPrice(null);
        dto.setPriceLabel("登录查看");
    } else {
        dto.setPrice(a.getPrice());
        dto.setPriceLabel(null);
    }
    return dto;
}

Task 3: 后端 ArticleController 游客可见性过滤

Files:

  • Create: cfc-backend/src/main/java/com/etotem/cfc/dto/ArticleDTO.java
  • Modify: cfc-backend/src/main/java/com/etotem/cfc/controller/ArticleController.java

Step 1: 创建 ArticleDTO

package com.etotem.cfc.dto;

import lombok.Data;
import java.util.Date;

@Data
public class ArticleDTO {
    private Long id;
    private Long categoryId;
    private String categoryName;
    private String title;
    private String summary;
    private String coverImage;
    private String content;
    private String tags;
    private String author;
    private Integer readTime;
    private String relatedDimensions;
    private String visibility;
    private Date publishedAt;
    private Integer viewCount;
}

Step 2: 改造 ArticleController.list

@Resource
private ArticleService articleService;

@PostMapping("/list")
public Result<Page<ArticleDTO>> list(
        @RequestBody Map<String, Object> params,
        @RequestAttribute(value = "userId", required = false) Long userId) {

    String dimension = (String) params.get("dimension");
    Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
    Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;

    LambdaQueryWrapper<Article> query = new LambdaQueryWrapper<Article>()
            .eq(Article::getStatus, "published");

    if (userId == null) {
        // 游客:只看公开文章
        query.eq(Article::getVisibility, "public");
    } else {
        // 登录用户:public + member + 指定给自己的 private
        query.and(w -> w
            .eq(Article::getVisibility, "public")
            .or().eq(Article::getVisibility, "member")
            .or().and(w2 -> w2
                .eq(Article::getVisibility, "private")
                .apply("JSON_CONTAINS(visible_to, {0})", "\"" + userId + "\"")));
    }

    // 维度筛选(relatedDimensions 字段存 "body,mind,wisdom" 等逗号分隔值)
    if (dimension != null && !dimension.isEmpty()) {
        query.like(Article::getRelatedDimensions, dimension);
    }

    Page<Article> pageResult = articleService.page(new Page<>(page, size), query);

    List<ArticleDTO> dtoList = pageResult.getRecords().stream()
            .map(this::toDTO)
            .collect(Collectors.toList());

    Page<ArticleDTO> dtoPage = new Page<>(pageResult.getCurrent(), pageResult.getSize(), pageResult.getTotal());
    dtoPage.setRecords(dtoList);
    return Result.success(dtoPage);
}

private ArticleDTO toDTO(Article a) {
    ArticleDTO dto = new ArticleDTO();
    dto.setId(a.getId());
    dto.setCategoryId(a.getCategoryId());
    dto.setTitle(a.getTitle());
    dto.setSummary(a.getSummary());
    dto.setCoverImage(a.getCoverImage());
    dto.setContent(a.getContent());
    dto.setTags(a.getTags());
    dto.setAuthor(a.getAuthor());
    dto.setReadTime(a.getReadTime());
    dto.setRelatedDimensions(a.getRelatedDimensions());
    dto.setVisibility(a.getVisibility());
    dto.setPublishedAt(a.getPublishedAt());
    dto.setViewCount(a.getViewCount());
    return dto;
}

Task 4: 前端 LoginGuideCard 组件

Files:

  • Create: cfc-frontend/components/LoginGuideCard.vue

    <template>
    <view class="login-guide-card">
    <view class="guide-header">
      <text class="guide-icon">🔒</text>
    </view>
    <text class="guide-title">{{ title }}</text>
    <view class="guide-desc">
      <text class="guide-desc-item" v-for="(item, idx) in items" :key="idx">
        · {{ item }}
      </text>
    </view>
    <view class="guide-actions">
      <view class="guide-btn primary" @click="goLogin">立即登录</view>
      <view class="guide-btn secondary" @click="goRegister">注册账号</view>
    </view>
    </view>
    </template>
    
    <script>
    export default {
    props: {
    title: { type: String, default: '登录后查看完整内容' },
    items: {
      type: Array,
      default: function() {
        return ['查看个性化数据', '获取更多推荐']
      }
    }
    },
    methods: {
    goLogin: function() {
      uni.navigateTo({ url: '/pages/login/login' })
    },
    goRegister: function() {
      uni.navigateTo({ url: '/pages/login/register' })
    }
    }
    }
    </script>
    
    <style scoped>
    .login-guide-card {
    margin: 20rpx 30rpx;
    background: linear-gradient(135deg, #FFF7ED, #FFE4D6);
    border-radius: 24rpx;
    padding: 40rpx 32rpx;
    text-align: center;
    }
    .guide-icon { font-size: 48rpx; }
    .guide-title {
    font-size: 32rpx;
    font-weight: bold;
    color: #333;
    margin: 16rpx 0;
    display: block;
    }
    .guide-desc { margin-bottom: 24rpx; }
    .guide-desc-item {
    font-size: 26rpx;
    color: #666;
    line-height: 1.8;
    display: block;
    }
    .guide-actions {
    display: flex;
    flex-direction: row;
    justify-content: center;
    gap: 20rpx;
    }
    .guide-btn {
    padding: 16rpx 40rpx;
    border-radius: 40rpx;
    font-size: 28rpx;
    font-weight: 500;
    }
    .guide-btn.primary {
    background: linear-gradient(135deg, #F97316, #FB923C);
    color: #fff;
    }
    .guide-btn.secondary {
    background: #fff;
    color: #F97316;
    border: 2rpx solid #F97316;
    }
    .guide-btn:active { opacity: 0.8; }
    </style>
    

Task 5: 前端 HealthTips 组件(身体页游客专属)

Files:

  • Create: cfc-frontend/components/HealthTips.vue

    <template>
    <view class="section" v-if="tips.length > 0">
    <view class="section-header">
      <text class="section-title">💡 健康小贴士</text>
      <text class="section-more" @click="goMore">更多 ›</text>
    </view>
    <view class="tip-list">
      <view class="tip-item" v-for="tip in tips" :key="tip.id" @click="goDetail(tip)">
        <text class="tip-icon">📋</text>
        <view class="tip-content">
          <text class="tip-title">{{ tip.title }}</text>
          <text class="tip-summary">{{ tip.summary }}</text>
        </view>
      </view>
    </view>
    </view>
    </template>
    
    <script>
    import { getArticleList } from '../../utils/api.js'
    
    export default {
    props: {
    dimension: { type: String, default: 'body' },
    limit: { type: Number, default: 4 }
    },
    data: function() {
    return { tips: [] }
    },
    created: function() {
    this.loadTips()
    },
    methods: {
    loadTips: function() {
      var self = this
      getArticleList({ dimension: this.dimension, size: this.limit }).then(function(res) {
        if (res.code === 200 && res.data) {
          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
          self.tips = list.slice(0, self.limit)
        }
      }).catch(function() {})
    },
    goDetail: function(tip) {
      uni.navigateTo({ url: '/pages/mind/article-detail?id=' + tip.id })
    },
    goMore: function() {
      uni.navigateTo({ url: '/pages/mind/articles?dimension=' + this.dimension })
    }
    }
    }
    </script>
    
    <style scoped>
    .section { margin: 20rpx 30rpx; }
    .section-header {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16rpx;
    }
    .section-title { font-size: 30rpx; font-weight: bold; color: #333; }
    .section-more { font-size: 24rpx; color: #999; }
    .tip-list {
    background: #fff;
    border-radius: 20rpx;
    overflow: hidden;
    box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
    }
    .tip-item {
    display: flex;
    flex-direction: row;
    align-items: center;
    padding: 24rpx 20rpx;
    border-bottom: 1rpx solid #f5f5f5;
    }
    .tip-item:last-child { border-bottom: none; }
    .tip-item:active { background: #fafafa; }
    .tip-icon { font-size: 36rpx; margin-right: 16rpx; flex-shrink: 0; }
    .tip-content { flex: 1; }
    .tip-title { font-size: 28rpx; color: #333; font-weight: 500; display: block; }
    .tip-summary { font-size: 24rpx; color: #999; margin-top: 4rpx; display: block; }
    </style>
    

Task 6: 前端 DimensionProducts 支持 priceLabel

Files:

  • Modify: cfc-frontend/components/DimensionProducts.vue

改造商品卡片渲染,优先使用 priceLabel

<!-- 修改模板中的价格部分 -->
<template>
  <view class="product-card" v-for="prod in products" :key="prod.id" @click="$emit('productClick', prod)">
    <image class="product-img" :src="prod.coverImage || '/static/default-product.png'" mode="aspectFill" />
    <text class="product-name line-clamp-2">{{ prod.name }}</text>
    <!-- 改造点:有 priceLabel 时显示标签,否则显示价格 -->
    <text v-if="prod.priceLabel" class="product-price-label">{{ prod.priceLabel }}</text>
    <text v-else class="product-price">¥{{ prod.price }}</text>
  </view>
</template>

<style> 中新增 .product-price-label 样式:

.product-price-label {
  font-size: 24rpx;
  color: #999;
  padding: 8rpx 16rpx 16rpx;
  display: block;
}

同时改造点击事件:如果商品是游客模式(priceLabel 存在),点击时弹登录提示:

// 在 DimensionProducts 的 methods 中增加
handleProductClick: function(prod) {
  if (prod.priceLabel) {
    // 游客模式:弹登录提示
    uni.showModal({
      title: '提示',
      content: '请先登录查看商品详情和价格',
      success: function(res) {
        if (res.confirm) uni.navigateTo({ url: '/pages/login/login' })
      }
    })
    return
  }
  this.$emit('productClick', prod)
}

Task 7: 前端 DimensionActivities 支持 priceLabel

Files:

  • Modify: cfc-frontend/components/DimensionActivities.vue

改造活动卡片渲染,优先使用 priceLabel

<!-- 修改模板中的活动信息部分 -->
<view class="activity-info">
  <text class="activity-name">{{ act.title }}</text>
  <text class="activity-meta">
    {{ act.startTime ? act.startTime.slice(0,10) : '' }} · {{ act.location || '' }}
  </text>
  <text v-if="act.priceLabel" class="activity-price-label">{{ act.priceLabel }}</text>
  <text v-else class="activity-price">¥{{ act.price }}</text>
</view>

新增样式:

.activity-price-label {
  font-size: 24rpx;
  color: #999;
  margin-top: 8rpx;
  display: block;
}

同样在点击事件中增加游客模式处理:

handleActivityClick: function(act) {
  if (act.priceLabel) {
    uni.showModal({
      title: '提示',
      content: '请先登录查看活动详情和价格',
      success: function(res) {
        if (res.confirm) uni.navigateTo({ url: '/pages/login/login' })
      }
    })
    return
  }
  this.$emit('activityClick', act)
}

Task 8: 前端身体页游客分支

Files:

  • Modify: cfc-frontend/pages/body/index.vue

onShow 中增加游客分支逻辑:

// 在 onShow 中
onShow() {
  var token = uni.getStorageSync('token')
  this.isLoggedIn = !!token

  if (this.isLoggedIn) {
    this.loadEnergyData()
    this.loadHealthData()
    this.loadCheckinData()
  }
  // 公共数据无论登录与否都加载
  this.loadPublicData()
},

methods: {
  loadPublicData: function() {
    // 健康小贴士仅在游客模式显示,但组件内部会自动判断
    // 已由 HealthTips 组件内部调用 API
  }
}

模板中游客分支渲染:

<template>
  <view class="container">
    <PageBanner theme="body"
      tagline="科学管理全家健康"
      quote="身体是革命的本钱" />

    <!-- 登录后专属区块 -->
    <template v-if="isLoggedIn">
      <FamilyEnergyBar dimensionCode="body" :sandboxData="sandboxData" />
      <UserQuickEntry ... />
      <!-- 健康报告区块 -->
      <!-- 今日身体数据 -->
      <!-- 健康打卡 -->
    </template>

    <!-- 游客专属区块 -->
    <template v-else>
      <!-- 登录引导卡 -->
      <LoginGuideCard
        title="登录后查看全家健康数据"
        :items="[
          '查看全家身体能量数据和健康报告',
          '记录每日健康打卡,获取身体能量',
          '查看专属健康商品和活动价格'
        ]" />
      <!-- 健康小贴士(从文章库取 body 类) -->
      <HealthTips dimension="body" />
    </template>

    <!-- 通用区块(登录/游客共用) -->
    <DimensionProducts dimensionCode="body" />
    <DimensionActivities dimensionCode="body" />

    <!-- 功能入口 -->
    <view class="func-section">
      <view class="func-grid">
        <view class="func-item" @click="onFuncClick('sports')">
          <view class="func-icon-wrap"><text class="func-icon">🏃</text></view>
          <text class="func-label">运动</text>
        </view>
        <!-- ... 其他入口 ... -->
      </view>
    </view>
  </view>
</template>

登录/游客共享的功能入口点击逻辑:

// 功能入口点击(游客需要登录时弹提示)
onFuncClick: function(type) {
  if (!this.isLoggedIn && needLoginMap[type]) {
    uni.showModal({
      title: '提示',
      content: '请先登录后使用此功能',
      success: function(res) { if (res.confirm) uni.navigateTo({ url: '/pages/login/login' }) }
    })
    return
  }
  // 正常跳转
}

Task 9: 前端心智页游客分支

Files:

  • Modify: cfc-frontend/pages/mind/index.vue

心智页模板增加游客分支:

<template>
  <view class="container">
    <PageBanner theme="mind-wisdom" ... />

    <!-- 登录后 -->
    <template v-if="isLoggedIn">
      <!-- 双能量柱状图 -->
      <view class="dual-energy-section" v-if="sandboxData">
        <FamilyEnergyBar dimensionCode="mind" :sandboxData="sandboxData" />
        <FamilyEnergyBar dimensionCode="wisdom" :sandboxData="sandboxData" />
      </view>
      <UserQuickEntry ... />
    </template>

    <!-- 游客登录引导卡(双 Tab 上方) -->
    <LoginGuideCard
      v-if="!isLoggedIn"
      title="登录后查看心智成长报告"
      :items="[
        '查看 EMI 心理测评四维分析',
        '查看 DAN 认知六维雷达图',
        '阅读专属文章、参与认知训练'
      ]" />

    <!-- 双 Tab 切换(登录/游客都有,但内容不同) -->
    <view class="tab-bar">
      <view :class="['tab-item', activeTab === 'emotion' ? 'tab-active' : '']"
        @click="switchTab('emotion')">
        <text class="tab-text">心理</text>
      </view>
      <view :class="['tab-item', activeTab === 'cognitive' ? 'tab-active' : '']"
        @click="switchTab('cognitive')">
        <text class="tab-text">认知</text>
      </view>
    </view>

    <!-- 心理 Tab -->
    <view v-if="activeTab === 'emotion'">
      <!-- 登录后:EMI 心理四维 -->
      <template v-if="isLoggedIn">
        <!-- 保留现有 EMI 区块 -->
      </template>
      <!-- 游客:推荐阅读(mind 维度) -->
      <template v-if="!isLoggedIn">
        <view class="section">
          <view class="section-header">
            <text class="section-title">推荐阅读(心理)</text>
          </view>
          <ArticleList dimension="mind" :limit="5" />
        </view>
      </template>
    </view>

    <!-- 认知 Tab -->
    <view v-if="activeTab === 'cognitive'">
      <!-- 登录后:DAN + 雷达图 -->
      <template v-if="isLoggedIn">
        <!-- 保留现有 DAN/雷达图区块 -->
      </template>
      <!-- 游客:推荐阅读(wisdom 维度) -->
      <template v-if="!isLoggedIn">
        <view class="section">
          <view class="section-header">
            <text class="section-title">推荐阅读(认知)</text>
          </view>
          <ArticleList dimension="wisdom" :limit="5" />
        </view>
      </template>
    </view>

    <!-- 通用区块 -->
    <DimensionProducts dimensionCode="mind" />
    <DimensionActivities dimensionCode="mind" />

    <!-- 功能入口 -->
    ...
  </view>
</template>

需要新增 ArticleList 子组件或复用现有文章展示逻辑。心智页功能入口中,小游戏/测评报告/训练中心在游客点击时弹登录提示。


Task 10: 前端行动页游客分支

Files:

  • Modify: cfc-frontend/pages/action/index.vue

行动页模板增加游客分支:

<template>
  <view class="container">
    <PageBanner theme="action" ... />

    <!-- 登录后 -->
    <template v-if="isLoggedIn">
      <FamilyEnergyBar dimensionCode="action" :sandboxData="sandboxData" />
      <UserQuickEntry ... />
      <DimensionTasks dimensionCode="action" />
    </template>

    <!-- 游客 -->
    <template v-else>
      <LoginGuideCard
        title="登录后查看任务和活动记录"
        :items="[
          '查看今日任务,完成打卡获取行动能量',
          '报名参加热门活动,记录成长足迹',
          '查看专属商品和活动价格'
        ]" />
    </template>

    <!-- 通用区块 -->
    <DimensionProducts dimensionCode="action" />
    <DimensionActivities dimensionCode="action" />

    <!-- 功能入口 -->
    <view class="func-section">
      <view class="func-grid">
        <view class="func-item" @click="onFuncClick({page:'/pages/tasks/tasks', needLogin: true})">
          ...
        </view>
        <view class="func-item" @click="onFuncClick({page:'/pages/discover/index?type=activity'})">
          ...
        </view>
        <view class="func-item" @click="onFuncClick({page:'/pages/shop/index'})">
          ...
        </view>
        <view class="func-item" @click="onFuncClick({page:'/pages/discover/index?type=course'})">
          ...
        </view>
      </view>
    </view>
  </view>
</template>

功能入口中,「任务」需要登录 → 游客点击弹登录;「活动」「商城」「课程」可以公开浏览 → 直接跳转。

// onFuncClick(改造现有方法)
onFuncClick: function(item) {
  if (item.needLogin && !this.isLoggedIn) {
    uni.showModal({
      title: '提示',
      content: '请先登录后使用此功能',
      success: function(res) { if (res.confirm) uni.navigateTo({ url: '/pages/login/login' }) }
    })
    return
  }
  if (item.page) uni.navigateTo({ url: item.page })
}

执行顺序

Task 1 (ProductDTO + Controller)  ┐
Task 2 (Activity 可见性 + 价格)    ├── 可并行(无依赖)
Task 3 (Article 可见性过滤)        ┘
         ↓
Task 4 (LoginGuideCard 组件)   ┐
Task 5 (HealthTips 组件)        ├── 可并行(无依赖)
Task 6 (DimensionProducts 改造) ┘
Task 7 (DimensionActivities 改造)
         ↓
Task 8 (身体页游客分支)  ┐
Task 9 (心智页游客分支)   ├── 可并行(仅依赖前面组件)
Task 10 (行动页游客分支)  ┘

设计覆盖率检查

设计原则 对应任务
A: 商品/活动/文章登录前可浏览 Task 1/2/3 (后端过滤) + Task 6/7 (前端渲染) + Task 8/9/10 (页面集成)
B: 不显示价格 Task 1 (ProductDTO priceLabel) + Task 2 (ActivityDTO priceLabel) + Task 6/7 (前端渲染)
C: 私密活动仅授权用户可见 Task 2 (Activity visibility + JSON_CONTAINS 匹配)
D: 文章按可见性过滤 Task 3 (Article visibility 过滤)
E: 后端统一过滤 Task 1/2/3 (@RequestAttribute("userId") 判断)
F: 个性化板块→引导 Task 4 (LoginGuideCard) + Task 5 (HealthTips) + Task 8/9/10 (分支集成)