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 | 增加游客分支 |
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.javacfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.javaStep 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;
}
Files:
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.javacfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.javacfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.javaStep 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;
}
Files:
cfc-backend/src/main/java/com/etotem/cfc/dto/ArticleDTO.javacfc-backend/src/main/java/com/etotem/cfc/controller/ArticleController.javaStep 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;
}
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>
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>
Files:
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)
}
Files:
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)
}
Files:
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
}
// 正常跳转
}
Files:
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 子组件或复用现有文章展示逻辑。心智页功能入口中,小游戏/测评报告/训练中心在游客点击时弹登录提示。
Files:
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 (分支集成) |