# 游客模式实现计划 > **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** ```java 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 方法** ```java @Resource private ProductService productService; @PostMapping("/list") public Result> list( @RequestBody Map 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 query = new LambdaQueryWrapper() .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 pageResult = productService.page(new Page<>(page, size), query); boolean isGuest = (userId == null); // 转为 DTO List dtoList = pageResult.getRecords().stream() .map(p -> toDTO(p, isGuest)) .collect(Collectors.toList()); Page 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` 中追加: ```java /** 可见性: public(所有人) / member(登录用户) / private(指定用户) */ private String visibility; /** 可见用户ID列表,JSON数组格式 ["1","2","3"] */ private String visibleTo; ``` **Step 2: ActivityDTO 补充 priceLabel** ```java // 在 ActivityDTO 中追加 private String priceLabel; // 游客 = "登录查看" ``` **Step 3: 改造 ActivityController.list** ```java @PostMapping("/list") public Result> list( @RequestBody Map 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 query = new LambdaQueryWrapper() .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 pageResult = activityService.page(new Page<>(page, size), query); boolean isGuest = (userId == null); List dtoList = pageResult.getRecords().stream() .map(a -> toDTO(a, isGuest)) .collect(Collectors.toList()); Page 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** ```java 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** ```java @Resource private ArticleService articleService; @PostMapping("/list") public Result> list( @RequestBody Map 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
query = new LambdaQueryWrapper
() .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
pageResult = articleService.page(new Page<>(page, size), query); List dtoList = pageResult.getRecords().stream() .map(this::toDTO) .collect(Collectors.toList()); Page 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` ```vue ``` --- ### Task 5: 前端 HealthTips 组件(身体页游客专属) **Files:** - Create: `cfc-frontend/components/HealthTips.vue` ```vue ``` --- ### Task 6: 前端 DimensionProducts 支持 priceLabel **Files:** - Modify: `cfc-frontend/components/DimensionProducts.vue` 改造商品卡片渲染,优先使用 `priceLabel`: ```vue ``` 在 `