Compare commits

...

59 Commits

Author SHA1 Message Date
peanut 6bc00863fa docs: 更新本地服务管理规则
- 强制使用 dev-services.py 控制本地服务
- 明确前端/H5 固定端口 5178-5181
- 新增禁止无意义重启规则

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:40:28 +08:00
peanut 1b39d873ab chore: 统一前端/H5 服务固定端口
- web: 5178
- web-admin: 5179
- mini-program: 5180
- life-script: 5181
2026-06-28 10:37:36 +08:00
peanut 75de32828a feat: dev-services.py restart 命令新增热加载保护
- 无必须重启的变更时禁止 restart
- 支持 --force 参数强制重启
- 避免无意义重启前端热加载服务
2026-06-28 10:34:54 +08:00
peanut 4dc3d4cfdc feat: dev-services.py 强制前端服务使用固定端口
- 新增 FIXED_FRONTEND_PORTS 映射
- 前端/H5 端口从 5178 开始固定分配
- 端口冲突时不再自动递增前端服务端口,改为报错
2026-06-28 10:30:50 +08:00
peanut a43bb1a555 docs:新增本地服务管理规则优化实现计划 2026-06-28 10:20:54 +08:00
peanut 5b31845b33 docs:新增本地服务管理规则优化设计文档
- 强制使用 dev-services.py 控制本地服务
- 前端/H5 固定端口从 5178 开始累加
- 优化 restart 命令,避免无意义重启
2026-06-28 10:16:29 +08:00
peanut 8cd52f76ae refactor: 迁移 story-card 相关样式到 MessageCard
删除 ScriptView.vue 中已迁移到 MessageCard 的样式类

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:19:53 +08:00
peanut c2ed051de0 feat: ScriptView 复用 MessageCard 组件
- story-card 使用 MessageCard
- result-chat-list 中的 assistant 消息按内容长度自动切换短消息/完整卡片模式

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:16:37 +08:00
peanut cbf5157dcf feat: 创建 MessageCard 组件
封装长消息/故事卡片结构,支持短消息简洁气泡模式
2026-06-28 09:13:34 +08:00
peanut 8dc2454beb docs:新增 MessageCard 组件实现计划 2026-06-28 09:06:32 +08:00
peanut f7dba6ba60 docs:新增 MessageCard 组件统一设计文档
- 提取 MessageCard.vue 独立组件
- 按 100 字阈值自动切换短消息/完整卡片模式
- story-card 和 result-chat-list 复用同一组件
2026-06-27 23:25:26 +08:00
peanut 847e80aa0e docs:新增 AI 消息卡片功能按钮实现计划 2026-06-27 22:29:27 +08:00
peanut d724cab581 style: 添加消息按钮组样式
- .message-actions: 2 列网格布局,间距 14rpx
2026-06-27 22:22:28 +08:00
peanut 0db434c21f feat: 在 result-chat-list 中添加功能按钮组
为每条 assistant 消息添加完整功能按钮:
- 复制:复制消息内容到剪贴板
- 换个方向:进入换方向修订确认
- 不像我:进入不像我修订确认
- 继续生成:收起故事并聚焦输入框
- 播放:播放故事 TTS 音频
2026-06-27 22:20:28 +08:00
peanut 9b3006d128 feat: 添加消息卡片功能方法
- copyMessageContent: 复制消息内容到剪贴板
- playMessageTts: 播放故事 TTS 音频
- toggleMessageCollapse: 添加展开/收起埋点
2026-06-27 22:18:11 +08:00
peanut 944f6a0a13 fix: 修复 story-card 展开失效 bug
移除 resultHasScriptReplies 条件包裹,让故事正文始终由 storyCollapsed 控制展开/收起
2026-06-27 22:16:37 +08:00
peanut e014450aa3 docs:新增 AI 消息卡片功能按钮统一设计文档
- 修复 story-card 展开失效 bug
- 为每条 AI 消息添加完整功能按钮组
- 采用方案 A:在现有单文件组件内修复 + 扩展
2026-06-27 20:40:37 +08:00
peanut 2198781c68 chore: 从版本控制中移除 .idea 目录(已在 .gitignore 中) 2026-06-27 17:42:12 +08:00
peanut 8e6b69f6c7 fix: 修复遗漏的 backend-single 引用(Java源码/前端注释/配置文件/IDE配置) 2026-06-27 17:32:18 +08:00
peanut 591f70a2f6 docs: 批量更新历史文档中的 backend-single 引用为 server 2026-06-27 17:27:27 +08:00
peanut 5586dc5eac docs: 更新核心文档和前端代码中的后端目录引用为 server 2026-06-27 17:26:50 +08:00
peanut 8f51701f54 refactor: 更新根部署脚本和 .gitignore 中的后端目录引用为 server 2026-06-27 17:25:54 +08:00
peanut 6cb9496abb refactor: 更新 Maven artifactId 和部署脚本 JAR 名称为 server 2026-06-27 17:24:46 +08:00
peanut 0d77d76858 refactor: 重命名 backend-single 目录为 server 2026-06-27 17:23:53 +08:00
peanut 8daf51301a fix: 修复小程序登录页变量初始化顺序错误导致微信登录失败 2026-06-27 13:07:17 +08:00
peanut e3b21cac3e feat:管理后台系统设置页面 + 小程序登录页条件渲染 2026-06-27 10:27:14 +08:00
peanut 9131203d1c feat:新增系统配置模块(Entity/Mapper/DTO/Service/Controller)+ 登录配置公开接口 2026-06-27 10:25:17 +08:00
peanut ed741ac8bf docs:短信登录开关与通用系统配置实现计划 2026-06-27 10:18:00 +08:00
peanut 4b235fa5d3 docs:短信登录开关与通用系统配置设计文档 2026-06-27 10:09:24 +08:00
peanut 7679e973d0 feat:管理页删除确认接入自定义弹窗,标签文字不换行 2026-06-24 22:51:12 +08:00
peanut f11fb142e1 feat:编辑页添加按钮简化为添加,字数限制4字,标签不换行,接入自定义弹窗 2026-06-24 22:47:15 +08:00
peanut 57a42474d7 refactor:TagDialog 移除冗余 glass-card 类引用 2026-06-24 22:43:56 +08:00
peanut bd0f4d6bea feat:新增 TagDialog 自定义弹窗组件,深色太空主题样式 2026-06-24 22:40:43 +08:00
peanut 637334adb8 docs:添加按钮简化与自定义弹窗实现计划 2026-06-24 22:37:48 +08:00
peanut 1900ba6efc docs:添加标签按钮简化与自定义弹窗设计 2026-06-24 22:31:54 +08:00
peanut 11f1638eef docs:添加强制规则,禁止本地后端验收,必须使用 deploy.py 部署到服务器验收 2026-06-24 22:17:06 +08:00
peanut 843ddd4c69 feat:新增管理标签独立页面,支持左滑与 × 按钮删除 2026-06-24 21:56:46 +08:00
peanut 53e895797e fix:onShow 重复触发时先 off 再 on,避免事件监听累积 2026-06-24 21:55:51 +08:00
peanut 2600cb2164 feat:编辑资料页保存携带库字段,onShow 监听管理页更新事件 2026-06-24 21:53:26 +08:00
peanut c7e84f5a13 style:删除 .custom 死代码 CSS 块 2026-06-24 21:51:41 +08:00
peanut 7dc75cbfb4 feat:编辑资料页标签区 UI 重构,新增管理入口与添加标签弹窗 2026-06-24 21:46:02 +08:00
peanut 071af02482 feat:编辑资料页 form 增加库字段并对老用户兼容初始化
- form reactive 新增 personalityTagLibrary 和 hobbyLibrary 字段
- syncFromStore 增加库字段加载逻辑,后端有则用后端数据
- 老用户(后端无库字段)使用预设 + 已选合并去重初始化
- 新增 buildInitialLibrary 辅助函数处理数组合并去重
2026-06-24 21:40:34 +08:00
peanut a446848d8e feat:前端数据层与路由注册支持标签库字段 2026-06-24 21:36:31 +08:00
peanut 341b08c02e feat:UserProfile 新增性格标签库与兴趣爱好库字段
- t_user_profile 表新增 personality_tag_library 和 hobby_library 两个 TEXT 字段
- UserProfile 实体类新增对应字段,使用 @TableField 映射
- UserProfileUpdateRequest、UserProfileCreateRequest、UserProfileResponse DTO 同步新增字段
- 迁移脚本命名符合 V{YYYYMMDD}__描述.sql 规范

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:30:11 +08:00
peanut 433ba4c498 docs:小程序标签库扩展功能实现计划 2026-06-24 21:26:53 +08:00
peanut 1cdee47f3a docs:小程序编辑资料页性格标签与兴趣爱好库扩展设计 2026-06-24 21:20:41 +08:00
peanut 3cd7de1f55 fix:ScriptView 语音按钮改为麦克风图标 2026-06-23 22:56:15 +08:00
peanut 1c45689f03 fix:ScriptView 底部输入框区域增加左右边距 2026-06-23 22:54:52 +08:00
peanut 99a06fcc28 fix:ScriptView 结果页顶部操作栏固定不随滚动 2026-06-23 22:54:11 +08:00
peanut 4cb6d6de9b docs:ScriptView 对话结果页 UI 调整实现计划 2026-06-23 22:51:55 +08:00
peanut 6b6af757d0 docs:ScriptView 对话结果页 UI 调整设计文档 2026-06-23 22:49:15 +08:00
peanut ae1463e400 fix:剧本列表工具函数去除默认值兜底 2026-06-23 22:19:30 +08:00
peanut 70434c1325 fix:剧本列表删除 fallbackScripts 假数据 2026-06-23 22:18:32 +08:00
peanut 04b184a3c1 fix:编辑资料页无昵称时显示未设置昵称 2026-06-23 22:17:45 +08:00
peanut e7531f86cf docs:去除编辑资料页与剧本列表 mock 数据实现计划 2026-06-23 22:16:27 +08:00
peanut a00ce0bf95 docs:去除编辑资料页与剧本列表 mock 数据设计文档 2026-06-23 22:14:31 +08:00
peanut 0eaf44ed93 fix:编辑资料页昵称未填时增加 Toast 提示 2026-06-23 21:47:32 +08:00
peanut eb50358e3d docs:小程序编辑资料页昵称必填提示优化实现计划 2026-06-23 21:34:53 +08:00
peanut 41ed8257f6 docs:小程序编辑资料页昵称必填提示优化设计文档 2026-06-23 21:29:54 +08:00
579 changed files with 10453 additions and 3196 deletions
-81
View File
@@ -1,81 +0,0 @@
---
type: "always_apply"
---
# 项目开发规则
## 基础设置
1. 保持对话语言为中文
2. 不允许在未经允许的情况下删除代码和文件,不允许破坏已经正常的业务代码
3. 执行终端命令时要关注执行情况,避免无效等待
## 代码规范
4. 生成代码时必须添加类级和函数级注释
5. 使用import导包,禁止使用全限定名称引用类
6. 禁止使用枚举类型作为entity、request、response、dto对象的字段类型,禁止以枚举类作为方法的入参,确保接口的通用性和可扩展性
7. 新增数据的id使用已存在的雪花算法生成器生成
## 架构规范
8. 所有开发必须遵循当前项目规范
9. Controller层禁止添加业务逻辑
10. 使用全局异常处理,禁止使用try-catch
11. 前端接口访问尽可能走网关调用
## 接口设计规范
12. Controller层接口定义要完整:
- 入参使用request封装传递到service层
- service层的方法命名与controller层定义的接口的方法名称保持一致
- 出参使用response封装由service层传递到controller层
- 禁止在controller层做entity/domain对象与request/response的转换
- 使用项目已有的Result做接口返回
13. 接口和方法参数不允许超过两个,超过时使用request或DTO对象封装
14. Controller层路由禁止添加/api前缀
15. Controller层接口的Mapping注解value属性值不允许重复且不允许为空,必须明确指定value属性值且使用驼峰结构命名,避免使用下划线分隔符。所有接口注解必须显式指定value属性,如@GetMapping(value = "/page")、@PostMapping(value = "/create")等,禁止使用@GetMapping()或@PostMapping等空注解形式
16. 用户相关接口禁止直接传递用户id,需要后端根据token获取当前登录用户信息
17. 禁止使用/{param}格式的路径参数,避免网关路由冲突和分发错误
18. 路径参数统一使用@RequestParam而非@PathVariable,确保网关分发准确性
19. 接口路径命名应具有明确的语义,避免使用通用词汇如/get、/list等
20. 批量操作接口应使用专门的Request对象封装参数,而非直接传递List
21. 接口路径应避免层级过深,建议不超过3级路径结构
22. 更新操作应直接使用封装了ID和其他数据的Request对象,而不是单独传递ID参数。Service层方法应保持参数简洁,业务逻辑所需数据应全部包含在Request对象中
23. Controller层接口应保持简洁,避免为特定字段创建独立的更新方法(如updateStatus等),应只保留一个通用的update方法,具体的业务逻辑在Service层实现
24. Service层在执行更新操作时,应对每个字段进行空值检查,只更新非空字段,避免空字段覆盖原有值,确保数据完整性
25. Controller层应避免创建多个特定条件的查询接口,只保留一个分页查询方法,通过在请求对象中包含所有可查询字段来支持不同的查询需求
26. 分页查询接口应支持模糊查询和精确查询,通过在Service层根据字段类型和业务需求判断查询方式
## 环境配置
27. 为不同环境(local、dev、prod)创建单独配置文件,部署时通过参数选择
28. 启动服务时禁止擅自修改端口号,使用配置文件中的端口设置
## 数据库规范
29. 所有数据表必须包含创建时间(create_time)和更新时间(update_time)字段
30. 删除操作优先使用逻辑删除,添加deleted字段标识
31. 数据库字段命名使用下划线分隔,Java实体类使用驼峰命名
32. 优先使用LambdaQueryWrapper构造条件查询,避免硬编码字段名
33. 使用Lambda表达式引用实体类属性,提高代码可维护性和类型安全
34. 复杂查询条件应使用LambdaQueryWrapper的链式调用,保持代码清晰
35. 避免在查询条件中使用字符串字段名,防止字段名变更导致的运行时错误
## 安全规范
36. 所有外部输入必须进行参数校验
37. 敏感信息不得在日志中输出
38. 数据库操作必须使用参数化查询,防止SQL注入
## 性能规范
39. 避免N+1查询问题,合理使用批量查询
40. 大数据量查询必须分页处理
41. 缓存策略要考虑数据一致性问题
## 日志规范
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
-184
View File
@@ -1,184 +0,0 @@
---
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Goal
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
## Operating Constraints
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
## Execution Steps
### 1. Initialize Analysis Context
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths:
- SPEC = FEATURE_DIR/spec.md
- PLAN = FEATURE_DIR/plan.md
- TASKS = FEATURE_DIR/tasks.md
Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command).
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
### 2. Load Artifacts (Progressive Disclosure)
Load only the minimal necessary context from each artifact:
**From spec.md:**
- Overview/Context
- Functional Requirements
- Non-Functional Requirements
- User Stories
- Edge Cases (if present)
**From plan.md:**
- Architecture/stack choices
- Data Model references
- Phases
- Technical constraints
**From tasks.md:**
- Task IDs
- Descriptions
- Phase grouping
- Parallel markers [P]
- Referenced file paths
**From constitution:**
- Load `.specify/memory/constitution.md` for principle validation
### 3. Build Semantic Models
Create internal representations (do not include raw artifacts in output):
- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`)
- **User story/action inventory**: Discrete user actions with acceptance criteria
- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases)
- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements
### 4. Detection Passes (Token-Efficient Analysis)
Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary.
#### A. Duplication Detection
- Identify near-duplicate requirements
- Mark lower-quality phrasing for consolidation
#### B. Ambiguity Detection
- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria
- Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.)
#### C. Underspecification
- Requirements with verbs but missing object or measurable outcome
- User stories missing acceptance criteria alignment
- Tasks referencing files or components not defined in spec/plan
#### D. Constitution Alignment
- Any requirement or plan element conflicting with a MUST principle
- Missing mandated sections or quality gates from constitution
#### E. Coverage Gaps
- Requirements with zero associated tasks
- Tasks with no mapped requirement/story
- Non-functional requirements not reflected in tasks (e.g., performance, security)
#### F. Inconsistency
- Terminology drift (same concept named differently across files)
- Data entities referenced in plan but absent in spec (or vice versa)
- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note)
- Conflicting requirements (e.g., one requires Next.js while other specifies Vue)
### 5. Severity Assignment
Use this heuristic to prioritize findings:
- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality
- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion
- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case
- **LOW**: Style/wording improvements, minor redundancy not affecting execution order
### 6. Produce Compact Analysis Report
Output a Markdown report (no file writes) with the following structure:
## Specification Analysis Report
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------|
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
(Add one row per finding; generate stable IDs prefixed by category initial.)
**Coverage Summary Table:**
| Requirement Key | Has Task? | Task IDs | Notes |
|-----------------|-----------|----------|-------|
**Constitution Alignment Issues:** (if any)
**Unmapped Tasks:** (if any)
**Metrics:**
- Total Requirements
- Total Tasks
- Coverage % (requirements with >=1 task)
- Ambiguity Count
- Duplication Count
- Critical Issues Count
### 7. Provide Next Actions
At end of report, output a concise Next Actions block:
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
### 8. Offer Remediation
Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.)
## Operating Principles
### Context Efficiency
- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation
- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis
- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow
- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts
### Analysis Guidelines
- **NEVER modify files** (this is read-only analysis)
- **NEVER hallucinate missing sections** (if absent, report them accurately)
- **Prioritize constitution violations** (these are always CRITICAL)
- **Use examples over exhaustive rules** (cite specific instances, not generic patterns)
- **Report zero issues gracefully** (emit success report with coverage statistics)
## Context
$ARGUMENTS
-294
View File
@@ -1,294 +0,0 @@
---
description: Generate a custom checklist for the current feature based on user requirements.
---
## Checklist Purpose: "Unit Tests for English"
**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain.
**NOT for verification/testing**:
- ❌ NOT "Verify the button clicks correctly"
- ❌ NOT "Test error handling works"
- ❌ NOT "Confirm the API returns 200"
- ❌ NOT checking if code/implementation matches the spec
**FOR requirements quality validation**:
- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness)
- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity)
- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency)
- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage)
- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases)
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Execution Steps
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list.
- All file paths must be absolute.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
- Only ask about information that materially changes checklist content
- Be skipped individually if already unambiguous in `$ARGUMENTS`
- Prefer precision over breadth
Generation algorithm:
1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts").
2. Cluster signals into candidate focus areas (max 4) ranked by relevance.
3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit.
4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria.
5. Formulate questions chosen from these archetypes:
- Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?")
- Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?")
- Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?")
- Audience framing (e.g., "Will this be used by the author only or peers during PR review?")
- Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?")
- Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?")
Question formatting rules:
- If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters
- Limit to AE options maximum; omit table if a free-form answer is clearer
- Never ask the user to restate what they already said
- Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope."
Defaults when interaction impossible:
- Depth: Standard
- Audience: Reviewer (PR) if code-related; Author otherwise
- Focus: Top 2 relevance clusters
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted followups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
- Derive checklist theme (e.g., security, review, deploy, ux)
- Consolidate explicit must-have items mentioned by user
- Map focus selections to category scaffolding
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
4. **Load feature context**: Read from FEATURE_DIR:
- spec.md: Feature requirements and scope
- plan.md (if exists): Technical details, dependencies
- tasks.md (if exists): Implementation tasks
**Context Loading Strategy**:
- Load only necessary portions relevant to active focus areas (avoid full-file dumping)
- Prefer summarizing long sections into concise scenario/requirement bullets
- Use progressive disclosure: add follow-on retrieval only if gaps detected
- If source docs are large, generate interim summary items instead of embedding raw text
5. **Generate checklist** - Create "Unit Tests for Requirements":
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
- Generate unique checklist filename:
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
- Format: `[domain].md`
- If file exists, append to existing file
- Number items sequentially starting from CHK001
- Each `/speckit.checklist` run creates a NEW file (never overwrites existing checklists)
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
- **Completeness**: Are all necessary requirements present?
- **Clarity**: Are requirements unambiguous and specific?
- **Consistency**: Do requirements align with each other?
- **Measurability**: Can requirements be objectively verified?
- **Coverage**: Are all scenarios/edge cases addressed?
**Category Structure** - Group items by requirement quality dimensions:
- **Requirement Completeness** (Are all necessary requirements documented?)
- **Requirement Clarity** (Are requirements specific and unambiguous?)
- **Requirement Consistency** (Do requirements align without conflicts?)
- **Acceptance Criteria Quality** (Are success criteria measurable?)
- **Scenario Coverage** (Are all flows/cases addressed?)
- **Edge Case Coverage** (Are boundary conditions defined?)
- **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?)
- **Dependencies & Assumptions** (Are they documented and validated?)
- **Ambiguities & Conflicts** (What needs clarification?)
**HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**:
**WRONG** (Testing implementation):
- "Verify landing page displays 3 episode cards"
- "Test hover states work on desktop"
- "Confirm logo click navigates home"
**CORRECT** (Testing requirements quality):
- "Are the exact number and layout of featured episodes specified?" [Completeness]
- "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity]
- "Are hover state requirements consistent across all interactive elements?" [Consistency]
- "Are keyboard navigation requirements defined for all interactive UI?" [Coverage]
- "Is the fallback behavior specified when logo image fails to load?" [Edge Cases]
- "Are loading states defined for asynchronous episode data?" [Completeness]
- "Does the spec define visual hierarchy for competing UI elements?" [Clarity]
**ITEM STRUCTURE**:
Each item should follow this pattern:
- Question format asking about requirement quality
- Focus on what's WRITTEN (or not written) in the spec/plan
- Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.]
- Reference spec section `[Spec §X.Y]` when checking existing requirements
- Use `[Gap]` marker when checking for missing requirements
**EXAMPLES BY QUALITY DIMENSION**:
Completeness:
- "Are error handling requirements defined for all API failure modes? [Gap]"
- "Are accessibility requirements specified for all interactive elements? [Completeness]"
- "Are mobile breakpoint requirements defined for responsive layouts? [Gap]"
Clarity:
- "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]"
- "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]"
- "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]"
Consistency:
- "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]"
- "Are card component requirements consistent between landing and detail pages? [Consistency]"
Coverage:
- "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]"
- "Are concurrent user interaction scenarios addressed? [Coverage, Gap]"
- "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]"
Measurability:
- "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]"
- "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]"
**Scenario Classification & Coverage** (Requirements Quality Focus):
- Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios
- For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?"
- If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]"
- Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]"
**Traceability Requirements**:
- MINIMUM: ≥80% of items MUST include at least one traceability reference
- Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]`
- If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]"
**Surface & Resolve Issues** (Requirements Quality Problems):
Ask questions about the requirements themselves:
- Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]"
- Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]"
- Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]"
- Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]"
- Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]"
**Content Consolidation**:
- Soft cap: If raw candidate items > 40, prioritize by risk/impact
- Merge near-duplicates checking the same requirement aspect
- If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]"
**🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test:
- ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior
- ❌ References to code execution, user actions, system behavior
- ❌ "Displays correctly", "works properly", "functions as expected"
- ❌ "Click", "navigate", "render", "load", "execute"
- ❌ Test cases, test plans, QA procedures
- ❌ Implementation details (frameworks, APIs, algorithms)
**✅ REQUIRED PATTERNS** - These test requirements quality:
- ✅ "Are [requirement type] defined/specified/documented for [scenario]?"
- ✅ "Is [vague term] quantified/clarified with specific criteria?"
- ✅ "Are requirements consistent between [section A] and [section B]?"
- ✅ "Can [requirement] be objectively measured/verified?"
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
- ✅ "Does the spec define [missing aspect]?"
6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
7. **Report**: Output full path to created checklist, item count, and remind user that each run creates a new file. Summarize:
- Focus areas selected
- Depth level
- Actor/timing
- Any explicit user-specified must-have items incorporated
**Important**: Each `/speckit.checklist` command invocation creates a checklist file using short, descriptive names unless file already exists. This allows:
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
- Simple, memorable filenames that indicate checklist purpose
- Easy identification and navigation in the `checklists/` folder
To avoid clutter, use descriptive types and clean up obsolete checklists when done.
## Example Checklist Types & Sample Items
**UX Requirements Quality:** `ux.md`
Sample items (testing the requirements, NOT the implementation):
- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]"
- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]"
- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]"
- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]"
- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]"
- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]"
**API Requirements Quality:** `api.md`
Sample items:
- "Are error response formats specified for all failure scenarios? [Completeness]"
- "Are rate limiting requirements quantified with specific thresholds? [Clarity]"
- "Are authentication requirements consistent across all endpoints? [Consistency]"
- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]"
- "Is versioning strategy documented in requirements? [Gap]"
**Performance Requirements Quality:** `performance.md`
Sample items:
- "Are performance requirements quantified with specific metrics? [Clarity]"
- "Are performance targets defined for all critical user journeys? [Coverage]"
- "Are performance requirements under different load conditions specified? [Completeness]"
- "Can performance requirements be objectively measured? [Measurability]"
- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]"
**Security Requirements Quality:** `security.md`
Sample items:
- "Are authentication requirements specified for all protected resources? [Coverage]"
- "Are data protection requirements defined for sensitive information? [Completeness]"
- "Is the threat model documented and requirements aligned to it? [Traceability]"
- "Are security requirements consistent with compliance obligations? [Consistency]"
- "Are security failure/breach response requirements defined? [Gap, Exception Flow]"
## Anti-Examples: What NOT To Do
**❌ WRONG - These test implementation, not requirements:**
```markdown
- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001]
- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003]
- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010]
- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005]
```
**✅ CORRECT - These test requirements quality:**
```markdown
- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001]
- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003]
- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010]
- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005]
- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap]
- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001]
```
**Key Differences:**
- Wrong: Tests if the system works correctly
- Correct: Tests if the requirements are written correctly
- Wrong: Verification of behavior
- Correct: Validation of requirement quality
- Wrong: "Does it do X?"
- Correct: "Is X clearly specified?"
-181
View File
@@ -1,181 +0,0 @@
---
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
Execution steps:
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
- `FEATURE_DIR`
- `FEATURE_SPEC`
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
Functional Scope & Behavior:
- Core user goals & success criteria
- Explicit out-of-scope declarations
- User roles / personas differentiation
Domain & Data Model:
- Entities, attributes, relationships
- Identity & uniqueness rules
- Lifecycle/state transitions
- Data volume / scale assumptions
Interaction & UX Flow:
- Critical user journeys / sequences
- Error/empty/loading states
- Accessibility or localization notes
Non-Functional Quality Attributes:
- Performance (latency, throughput targets)
- Scalability (horizontal/vertical, limits)
- Reliability & availability (uptime, recovery expectations)
- Observability (logging, metrics, tracing signals)
- Security & privacy (authN/Z, data protection, threat assumptions)
- Compliance / regulatory constraints (if any)
Integration & External Dependencies:
- External services/APIs and failure modes
- Data import/export formats
- Protocol/versioning assumptions
Edge Cases & Failure Handling:
- Negative scenarios
- Rate limiting / throttling
- Conflict resolution (e.g., concurrent edits)
Constraints & Tradeoffs:
- Technical constraints (language, storage, hosting)
- Explicit tradeoffs or rejected alternatives
Terminology & Consistency:
- Canonical glossary terms
- Avoided synonyms / deprecated terms
Completion Signals:
- Acceptance criteria testability
- Measurable Definition of Done style indicators
Misc / Placeholders:
- TODO markers / unresolved decisions
- Ambiguous adjectives ("robust", "intuitive") lacking quantification
For each category with Partial or Missing status, add a candidate question opportunity unless:
- Clarification would not materially change implementation or validation strategy
- Information is better deferred to planning phase (note internally)
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
- Maximum of 10 total questions across the whole session.
- Each question must be answerable with EITHER:
- A short multiplechoice selection (25 distinct, mutually exclusive options), OR
- A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words").
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
4. Sequential questioning loop (interactive):
- Present EXACTLY ONE question at a time.
- For multiplechoice questions:
- **Analyze all options** and determine the **most suitable option** based on:
- Best practices for the project type
- Common patterns in similar implementations
- Risk reduction (security, performance, maintainability)
- Alignment with any explicit project goals or constraints visible in the spec
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
- Format as: `**Recommended:** Option [X] - <reasoning>`
- Then render all options as a Markdown table:
| Option | Description |
|--------|-------------|
| A | <Option A description> |
| B | <Option B description> |
| C | <Option C description> (add D/E as needed up to 5) |
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
- For shortanswer style (no meaningful discrete options):
- Provide your **suggested answer** based on best practices and context.
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
- After the user answers:
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
- Stop asking further questions when:
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
- User signals completion ("done", "good", "no more"), OR
- You reach 5 asked questions.
- Never reveal future queued questions in advance.
- If no valid questions exist at start, immediately report no critical ambiguities.
5. Integration after EACH accepted answer (incremental update approach):
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
- For the first integrated answer in this session:
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
- Then immediately apply the clarification to the most appropriate section(s):
- Functional ambiguity → Update or add a bullet in Functional Requirements.
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
- Keep each inserted clarification minimal and testable (avoid narrative drift).
6. Validation (performed after EACH write plus final pass):
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
- Total asked (accepted) questions ≤ 5.
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
- No contradictory earlier statement remains (scan for now-invalid alternative choices removed).
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
- Terminology consistency: same canonical term used across all updated sections.
7. Write the updated spec back to `FEATURE_SPEC`.
8. Report completion (after questioning loop ends or early termination):
- Number of questions asked & answered.
- Path to updated spec.
- Sections touched (list names).
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
- Suggested next command.
Behavior rules:
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
- Respect user early termination signals ("stop", "done", "proceed").
- If no questions asked due to full coverage, output a compact coverage summary (all categories Clear) then suggest advancing.
- If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale.
Context for prioritization: $ARGUMENTS
-84
View File
@@ -1,84 +0,0 @@
---
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
handoffs:
- label: Build Specification
agent: speckit.specify
prompt: Implement the feature specification based on the updated constitution. I want to build...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
Follow this execution flow:
1. Load the existing constitution at `.specify/memory/constitution.md`.
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
2. Collect/derive values for placeholders:
- If user input (conversation) supplies a value, use it.
- Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded).
- For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous.
- `CONSTITUTION_VERSION` must increment according to semantic versioning rules:
- MAJOR: Backward incompatible governance/principle removals or redefinitions.
- MINOR: New principle/section added or materially expanded guidance.
- PATCH: Clarifications, wording, typo fixes, non-semantic refinements.
- If version bump type ambiguous, propose reasoning before finalizing.
3. Draft the updated constitution content:
- Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left).
- Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance.
- Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing nonnegotiable rules, explicit rationale if not obvious.
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
4. Consistency propagation checklist (convert prior checklist into active validations):
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
- Version change: old → new
- List of modified principles (old title → new title if renamed)
- Added sections
- Removed sections
- Templates requiring updates (✅ updated / ⚠ pending) with file paths
- Follow-up TODOs if any placeholders intentionally deferred.
6. Validation before final output:
- No remaining unexplained bracket tokens.
- Version line matches report.
- Dates ISO format YYYY-MM-DD.
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite).
8. Output a final summary to the user with:
- New version and bump rationale.
- Any files flagged for manual follow-up.
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
Formatting & Style Requirements:
- Use Markdown headings exactly as in the template (do not demote/promote levels).
- Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks.
- Keep a single blank line between sections.
- Avoid trailing whitespace.
If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps.
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file.
-135
View File
@@ -1,135 +0,0 @@
---
description: Execute the implementation plan by processing and executing all tasks defined in tasks.md
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
- Scan all checklist files in the checklists/ directory
- For each checklist, count:
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
- Completed items: Lines matching `- [X]` or `- [x]`
- Incomplete items: Lines matching `- [ ]`
- Create a status table:
```text
| Checklist | Total | Completed | Incomplete | Status |
|-----------|-------|-----------|------------|--------|
| ux.md | 12 | 12 | 0 | ✓ PASS |
| test.md | 8 | 5 | 3 | ✗ FAIL |
| security.md | 6 | 6 | 0 | ✓ PASS |
```
- Calculate overall status:
- **PASS**: All checklists have 0 incomplete items
- **FAIL**: One or more checklists have incomplete items
- **If any checklist is incomplete**:
- Display the table with incomplete item counts
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
- Wait for user response before continuing
- If user says "no" or "wait" or "stop", halt execution
- If user says "yes" or "proceed" or "continue", proceed to step 3
- **If all checklists are complete**:
- Display the table showing all checklists passed
- Automatically proceed to step 3
3. Load and analyze the implementation context:
- **REQUIRED**: Read tasks.md for the complete task list and execution plan
- **REQUIRED**: Read plan.md for tech stack, architecture, and file structure
- **IF EXISTS**: Read data-model.md for entities and relationships
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
- **IF EXISTS**: Read research.md for technical decisions and constraints
- **IF EXISTS**: Read quickstart.md for integration scenarios
4. **Project Setup Verification**:
- **REQUIRED**: Create/verify ignore files based on actual project setup:
**Detection & Creation Logic**:
- Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so):
```sh
git rev-parse --git-dir 2>/dev/null
```
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc* exists → create/verify .eslintignore
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc* exists → create/verify .prettierignore
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
- Check if terraform files (*.tf) exist → create/verify .terraformignore
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
**If ignore file missing**: Create with full pattern set for detected technology
**Common Patterns by Technology** (from plan.md tech stack):
- **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*`
- **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/`
- **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/`
- **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/`
- **Go**: `*.exe`, `*.test`, `vendor/`, `*.out`
- **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/`
- **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env`
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `Makefile`, `config.log`, `.idea/`, `*.log`, `.env*`
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
**Tool-Specific Patterns**:
- **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/`
- **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js`
- **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
- **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl`
- **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt`
5. Parse tasks.md structure and extract:
- **Task phases**: Setup, Tests, Core, Integration, Polish
- **Task dependencies**: Sequential vs parallel execution rules
- **Task details**: ID, description, file paths, parallel markers [P]
- **Execution flow**: Order and dependency requirements
6. Execute implementation following the task plan:
- **Phase-by-phase execution**: Complete each phase before moving to the next
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
- **File-based coordination**: Tasks affecting the same files must run sequentially
- **Validation checkpoints**: Verify each phase completion before proceeding
7. Implementation execution rules:
- **Setup first**: Initialize project structure, dependencies, configuration
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
- **Core development**: Implement models, services, CLI commands, endpoints
- **Integration work**: Database connections, middleware, logging, external services
- **Polish and validation**: Unit tests, performance optimization, documentation
8. Progress tracking and error handling:
- Report progress after each completed task
- Halt execution if any non-parallel task fails
- For parallel tasks [P], continue with successful tasks, report failed ones
- Provide clear error messages with context for debugging
- Suggest next steps if implementation cannot proceed
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
9. Completion validation:
- Verify all required tasks are completed
- Check that implemented features match the original specification
- Validate that tests pass and coverage meets requirements
- Confirm the implementation follows the technical plan
- Report final status with summary of completed work
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
-89
View File
@@ -1,89 +0,0 @@
---
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
handoffs:
- label: Create Tasks
agent: speckit.tasks
prompt: Break the plan into tasks
send: true
- label: Create Checklist
agent: speckit.checklist
prompt: Create a checklist for the following domain...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load context**: Read FEATURE_SPEC and `.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied).
3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
- Fill Constitution Check section from constitution
- Evaluate gates (ERROR if violations unjustified)
- Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
- Phase 1: Generate data-model.md, contracts/, quickstart.md
- Phase 1: Update agent context by running the agent script
- Re-evaluate Constitution Check post-design
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
## Phases
### Phase 0: Outline & Research
1. **Extract unknowns from Technical Context** above:
- For each NEEDS CLARIFICATION → research task
- For each dependency → best practices task
- For each integration → patterns task
2. **Generate and dispatch research agents**:
```text
For each unknown in Technical Context:
Task: "Research {unknown} for {feature context}"
For each technology choice:
Task: "Find best practices for {tech} in {domain}"
```
3. **Consolidate findings** in `research.md` using format:
- Decision: [what was chosen]
- Rationale: [why chosen]
- Alternatives considered: [what else evaluated]
**Output**: research.md with all NEEDS CLARIFICATION resolved
### Phase 1: Design & Contracts
**Prerequisites:** `research.md` complete
1. **Extract entities from feature spec** → `data-model.md`:
- Entity name, fields, relationships
- Validation rules from requirements
- State transitions if applicable
2. **Generate API contracts** from functional requirements:
- For each user action → endpoint
- Use standard REST/GraphQL patterns
- Output OpenAPI/GraphQL schema to `/contracts/`
3. **Agent context update**:
- Run `.specify/scripts/bash/update-agent-context.sh cursor-agent`
- These scripts detect which AI agent is in use
- Update the appropriate agent-specific context file
- Add only new technology from current plan
- Preserve manual additions between markers
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
## Key rules
- Use absolute paths
- ERROR on gate failures or unresolved clarifications
-258
View File
@@ -1,258 +0,0 @@
---
description: Create or update the feature specification from a natural language feature description.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
- label: Clarify Spec Requirements
agent: speckit.clarify
prompt: Clarify specification requirements
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
Given that feature description, do this:
1. **Generate a concise short name** (2-4 words) for the branch:
- Analyze the feature description and extract the most meaningful keywords
- Create a 2-4 word short name that captures the essence of the feature
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
- Keep it concise but descriptive enough to understand the feature at a glance
- Examples:
- "I want to add user authentication" → "user-auth"
- "Implement OAuth2 integration for the API" → "oauth2-api-integration"
- "Create a dashboard for analytics" → "analytics-dashboard"
- "Fix payment processing timeout bug" → "fix-payment-timeout"
2. **Check for existing branches before creating new one**:
a. First, fetch all remote branches to ensure we have the latest information:
```bash
git fetch --all --prune
```
b. Find the highest feature number across all sources for the short-name:
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
c. Determine the next available number:
- Extract all numbers from all three sources
- Find the highest number N
- Use N+1 for the new branch number
d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name:
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
- Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"`
- PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
**IMPORTANT**:
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
- Only match branches/directories with the exact short-name pattern
- If no existing branches/directories found with this short-name, start with number 1
- You must only ever run this script once per feature
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
3. Load `.specify/templates/spec-template.md` to understand required sections.
4. Follow this execution flow:
1. Parse user description from Input
If empty: ERROR "No feature description provided"
2. Extract key concepts from description
Identify: actors, actions, data, constraints
3. For unclear aspects:
- Make informed guesses based on context and industry standards
- Only mark with [NEEDS CLARIFICATION: specific question] if:
- The choice significantly impacts feature scope or user experience
- Multiple reasonable interpretations exist with different implications
- No reasonable default exists
- **LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total**
- Prioritize clarifications by impact: scope > security/privacy > user experience > technical details
4. Fill User Scenarios & Testing section
If no clear user flow: ERROR "Cannot determine user scenarios"
5. Generate Functional Requirements
Each requirement must be testable
Use reasonable defaults for unspecified details (document assumptions in Assumptions section)
6. Define Success Criteria
Create measurable, technology-agnostic outcomes
Include both quantitative metrics (time, performance, volume) and qualitative measures (user satisfaction, task completion)
Each criterion must be verifiable without implementation details
7. Identify Key Entities (if data involved)
8. Return: SUCCESS (spec ready for planning)
5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items:
```markdown
# Specification Quality Checklist: [FEATURE NAME]
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: [DATE]
**Feature**: [Link to spec.md]
## Content Quality
- [ ] No implementation details (languages, frameworks, APIs)
- [ ] Focused on user value and business needs
- [ ] Written for non-technical stakeholders
- [ ] All mandatory sections completed
## Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable
- [ ] Success criteria are technology-agnostic (no implementation details)
- [ ] All acceptance scenarios are defined
- [ ] Edge cases are identified
- [ ] Scope is clearly bounded
- [ ] Dependencies and assumptions identified
## Feature Readiness
- [ ] All functional requirements have clear acceptance criteria
- [ ] User scenarios cover primary flows
- [ ] Feature meets measurable outcomes defined in Success Criteria
- [ ] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
```
b. **Run Validation Check**: Review the spec against each checklist item:
- For each item, determine if it passes or fails
- Document specific issues found (quote relevant spec sections)
c. **Handle Validation Results**:
- **If all items pass**: Mark checklist complete and proceed to step 6
- **If items fail (excluding [NEEDS CLARIFICATION])**:
1. List the failing items and specific issues
2. Update the spec to address each issue
3. Re-run validation until all items pass (max 3 iterations)
4. If still failing after 3 iterations, document remaining issues in checklist notes and warn user
- **If [NEEDS CLARIFICATION] markers remain**:
1. Extract all [NEEDS CLARIFICATION: ...] markers from the spec
2. **LIMIT CHECK**: If more than 3 markers exist, keep only the 3 most critical (by scope/security/UX impact) and make informed guesses for the rest
3. For each clarification needed (max 3), present options to user in this format:
```markdown
## Question [N]: [Topic]
**Context**: [Quote relevant spec section]
**What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
**Suggested Answers**:
| Option | Answer | Implications |
|--------|--------|--------------|
| A | [First suggested answer] | [What this means for the feature] |
| B | [Second suggested answer] | [What this means for the feature] |
| C | [Third suggested answer] | [What this means for the feature] |
| Custom | Provide your own answer | [Explain how to provide custom input] |
**Your choice**: _[Wait for user response]_
```
4. **CRITICAL - Table Formatting**: Ensure markdown tables are properly formatted:
- Use consistent spacing with pipes aligned
- Each cell should have spaces around content: `| Content |` not `|Content|`
- Header separator must have at least 3 dashes: `|--------|`
- Test that the table renders correctly in markdown preview
5. Number questions sequentially (Q1, Q2, Q3 - max 3 total)
6. Present all questions together before waiting for responses
7. Wait for user to respond with their choices for all questions (e.g., "Q1: A, Q2: Custom - [details], Q3: B")
8. Update the spec by replacing each [NEEDS CLARIFICATION] marker with the user's selected or provided answer
9. Re-run validation after all clarifications are resolved
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
## General Guidelines
## Quick Guidelines
- Focus on **WHAT** users need and **WHY**.
- Avoid HOW to implement (no tech stack, APIs, code structure).
- Written for business stakeholders, not developers.
- DO NOT create any checklists that are embedded in the spec. That will be a separate command.
### Section Requirements
- **Mandatory sections**: Must be completed for every feature
- **Optional sections**: Include only when relevant to the feature
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
### For AI Generation
When creating this spec from a user prompt:
1. **Make informed guesses**: Use context, industry standards, and common patterns to fill gaps
2. **Document assumptions**: Record reasonable defaults in the Assumptions section
3. **Limit clarifications**: Maximum 3 [NEEDS CLARIFICATION] markers - use only for critical decisions that:
- Significantly impact feature scope or user experience
- Have multiple reasonable interpretations with different implications
- Lack any reasonable default
4. **Prioritize clarifications**: scope > security/privacy > user experience > technical details
5. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
6. **Common areas needing clarification** (only if no reasonable default exists):
- Feature scope and boundaries (include/exclude specific use cases)
- User types and permissions (if multiple conflicting interpretations possible)
- Security/compliance requirements (when legally/financially significant)
**Examples of reasonable defaults** (don't ask about these):
- Data retention: Industry-standard practices for the domain
- Performance targets: Standard web/mobile app expectations unless specified
- Error handling: User-friendly messages with appropriate fallbacks
- Authentication method: Standard session-based or OAuth2 for web apps
- Integration patterns: RESTful APIs unless specified otherwise
### Success Criteria Guidelines
Success criteria must be:
1. **Measurable**: Include specific metrics (time, percentage, count, rate)
2. **Technology-agnostic**: No mention of frameworks, languages, databases, or tools
3. **User-focused**: Describe outcomes from user/business perspective, not system internals
4. **Verifiable**: Can be tested/validated without knowing implementation details
**Good examples**:
- "Users can complete checkout in under 3 minutes"
- "System supports 10,000 concurrent users"
- "95% of searches return results in under 1 second"
- "Task completion rate improves by 40%"
**Bad examples** (implementation-focused):
- "API response time is under 200ms" (too technical, use "Users see results instantly")
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
- "React components render efficiently" (framework-specific)
- "Redis cache hit rate above 80%" (technology-specific)
-137
View File
@@ -1,137 +0,0 @@
---
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
handoffs:
- label: Analyze For Consistency
agent: speckit.analyze
prompt: Run a project analysis for consistency
send: true
- label: Implement Project
agent: speckit.implement
prompt: Start the implementation in phases
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load design documents**: Read from FEATURE_DIR:
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
- **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios)
- Note: Not all projects have all documents. Generate tasks based on what's available.
3. **Execute task generation workflow**:
- Load plan.md and extract tech stack, libraries, project structure
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
- If data-model.md exists: Extract entities and map to user stories
- If contracts/ exists: Map endpoints to user stories
- If research.md exists: Extract decisions for setup tasks
- Generate tasks organized by user story (see Task Generation Rules below)
- Generate dependency graph showing user story completion order
- Create parallel execution examples per user story
- Validate task completeness (each user story has all needed tasks, independently testable)
4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
- Correct feature name from plan.md
- Phase 1: Setup tasks (project initialization)
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)
- Phase 3+: One phase per user story (in priority order from spec.md)
- Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks
- Final Phase: Polish & cross-cutting concerns
- All tasks must follow the strict checklist format (see Task Generation Rules below)
- Clear file paths for each task
- Dependencies section showing story completion order
- Parallel execution examples per story
- Implementation strategy section (MVP first, incremental delivery)
5. **Report**: Output path to generated tasks.md and summary:
- Total task count
- Task count per user story
- Parallel opportunities identified
- Independent test criteria for each story
- Suggested MVP scope (typically just User Story 1)
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
Context for task generation: $ARGUMENTS
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
## Task Generation Rules
**CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
**Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature specification or if user requests TDD approach.
### Checklist Format (REQUIRED)
Every task MUST strictly follow this format:
```text
- [ ] [TaskID] [P?] [Story?] Description with file path
```
**Format Components**:
1. **Checkbox**: ALWAYS start with `- [ ]` (markdown checkbox)
2. **Task ID**: Sequential number (T001, T002, T003...) in execution order
3. **[P] marker**: Include ONLY if task is parallelizable (different files, no dependencies on incomplete tasks)
4. **[Story] label**: REQUIRED for user story phase tasks only
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
- Setup phase: NO story label
- Foundational phase: NO story label
- User Story phases: MUST have story label
- Polish phase: NO story label
5. **Description**: Clear action with exact file path
**Examples**:
- ✅ CORRECT: `- [ ] T001 Create project structure per implementation plan`
- ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py`
- ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py`
- ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py`
- ❌ WRONG: `- [ ] Create User model` (missing ID and Story label)
- ❌ WRONG: `T001 [US1] Create model` (missing checkbox)
- ❌ WRONG: `- [ ] [US1] Create User model` (missing Task ID)
- ❌ WRONG: `- [ ] T001 [US1] Create model` (missing file path)
### Task Organization
1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION:
- Each user story (P1, P2, P3...) gets its own phase
- Map all related components to their story:
- Models needed for that story
- Services needed for that story
- Endpoints/UI needed for that story
- If tests requested: Tests specific to that story
- Mark story dependencies (most stories should be independent)
2. **From Contracts**:
- Map each contract/endpoint → to the user story it serves
- If tests requested: Each contract → contract test task [P] before implementation in that story's phase
3. **From Data Model**:
- Map each entity to the user story(ies) that need it
- If entity serves multiple stories: Put in earliest story or Setup phase
- Relationships → service layer tasks in appropriate story phase
4. **From Setup/Infrastructure**:
- Shared infrastructure → Setup phase (Phase 1)
- Foundational/blocking tasks → Foundational phase (Phase 2)
- Story-specific setup → within that story's phase
### Phase Structure
- **Phase 1**: Setup (project initialization)
- **Phase 2**: Foundational (blocking prerequisites - MUST complete before user stories)
- **Phase 3+**: User Stories in priority order (P1, P2, P3...)
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
- Each phase should be a complete, independently testable increment
- **Final Phase**: Polish & Cross-Cutting Concerns
-30
View File
@@ -1,30 +0,0 @@
---
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
tools: ['github/github-mcp-server/issue_write']
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
1. From the executed script, extract the path to **tasks**.
1. Get the Git remote by running:
```bash
git config --get remote.origin.url
```
> [!CAUTION]
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
> [!CAUTION]
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
-80
View File
@@ -1,80 +0,0 @@
---
alwaysApply: true
---
# 项目开发规则
## 基础设置
1. 保持对话语言为中文
2. 不允许在未经允许的情况下删除代码和文件,不允许破坏已经正常的业务代码
3. 执行终端命令时要关注执行情况,避免无效等待
## 代码规范
4. 生成代码时必须添加类级和函数级注释
5. 使用import导包,禁止使用全限定名称引用类
6. 禁止使用枚举类型作为entity、request、response、dto对象的字段类型,禁止以枚举类作为方法的入参,确保接口的通用性和可扩展性
7. 新增数据的id使用已存在的雪花算法生成器生成
## 架构规范
8. 所有开发必须遵循当前项目规范
9. Controller层禁止添加业务逻辑
10. 使用全局异常处理,禁止使用try-catch
11. 前端接口访问尽可能走网关调用
## 接口设计规范
12. Controller层接口定义要完整:
- 入参使用request封装传递到service层
- service层的方法命名与controller层定义的接口的方法名称保持一致
- 出参使用response封装由service层传递到controller层
- 禁止在controller层做entity/domain对象与request/response的转换
- 使用项目已有的Result做接口返回
13. 接口和方法参数不允许超过两个,超过时使用request或DTO对象封装
14. Controller层路由禁止添加/api前缀
15. Controller层接口的Mapping注解value属性值不允许重复且不允许为空,必须明确指定value属性值且使用驼峰结构命名,避免使用下划线分隔符。所有接口注解必须显式指定value属性,如@GetMapping(value = "/page")、@PostMapping(value = "/create")等,禁止使用@GetMapping()或@PostMapping等空注解形式
16. 用户相关接口禁止直接传递用户id,需要后端根据token获取当前登录用户信息
17. 禁止使用/{param}格式的路径参数,避免网关路由冲突和分发错误
18. 路径参数统一使用@RequestParam而非@PathVariable,确保网关分发准确性
19. 接口路径命名应具有明确的语义,避免使用通用词汇如/get、/list等
20. 批量操作接口应使用专门的Request对象封装参数,而非直接传递List
21. 接口路径应避免层级过深,建议不超过3级路径结构
22. 更新操作应直接使用封装了ID和其他数据的Request对象,而不是单独传递ID参数。Service层方法应保持参数简洁,业务逻辑所需数据应全部包含在Request对象中
23. Controller层接口应保持简洁,避免为特定字段创建独立的更新方法(如updateStatus等),应只保留一个通用的update方法,具体的业务逻辑在Service层实现
24. Service层在执行更新操作时,应对每个字段进行空值检查,只更新非空字段,避免空字段覆盖原有值,确保数据完整性
25. Controller层应避免创建多个特定条件的查询接口,只保留一个分页查询方法,通过在请求对象中包含所有可查询字段来支持不同的查询需求
26. 分页查询接口应支持模糊查询和精确查询,通过在Service层根据字段类型和业务需求判断查询方式
## 环境配置
27. 为不同环境(local、dev、prod)创建单独配置文件,部署时通过参数选择
28. 启动服务时禁止擅自修改端口号,使用配置文件中的端口设置
## 数据库规范
29. 所有数据表必须包含创建时间(create_time)和更新时间(update_time)字段
30. 删除操作优先使用逻辑删除,添加deleted字段标识
31. 数据库字段命名使用下划线分隔,Java实体类使用驼峰命名
32. 优先使用LambdaQueryWrapper构造条件查询,避免硬编码字段名
33. 使用Lambda表达式引用实体类属性,提高代码可维护性和类型安全
34. 复杂查询条件应使用LambdaQueryWrapper的链式调用,保持代码清晰
35. 避免在查询条件中使用字符串字段名,防止字段名变更导致的运行时错误
## 安全规范
36. 所有外部输入必须进行参数校验
37. 敏感信息不得在日志中输出
38. 数据库操作必须使用参数化查询,防止SQL注入
## 性能规范
39. 避免N+1查询问题,合理使用批量查询
40. 大数据量查询必须分页处理
41. 缓存策略要考虑数据一致性问题
## 日志规范
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
+1 -1
View File
@@ -383,7 +383,7 @@ OpenClaw*.md
aiconfig-*.png
# 运行时错误日志
backend-single/backend.err
server/backend.err
# Windows 临时空文件
nul
-10
View File
@@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
File diff suppressed because one or more lines are too long
-29
View File
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/common/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.projectlombok:lombok:1.18.30" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-annotation:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-core:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-extension:3.5.4" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:6.1.1" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:6.1.1" level="project" />
</component>
</module>
-82
View File
@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="emotion-museum-backend" />
<module name="emotion-single" />
<module name="backend-single" />
</profile>
<profile name="Annotation profile for emotion-museum-server" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" />
</processorPath>
<module name="emotion-museum-server" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="admin" target="17" />
<module name="admin-api" target="17" />
<module name="admin-server" target="17" />
<module name="ai" target="17" />
<module name="ai-api" target="17" />
<module name="ai-server" target="17" />
<module name="auth" target="17" />
<module name="auth-api" target="17" />
<module name="auth-server" target="17" />
<module name="backend" target="17" />
<module name="common" target="17" />
<module name="customer" target="17" />
<module name="emotion-ai" target="17" />
<module name="emotion-auth" target="1.5" />
<module name="emotion-common" target="17" />
<module name="emotion-explore" target="17" />
<module name="emotion-gateway" target="17" />
<module name="emotion-growth" target="17" />
<module name="emotion-record" target="17" />
<module name="emotion-reward" target="17" />
<module name="emotion-stats" target="17" />
<module name="emotion-user" target="17" />
<module name="explore" target="17" />
<module name="explore-api" target="17" />
<module name="explore-server" target="17" />
<module name="gateway" target="17" />
<module name="growth" target="17" />
<module name="growth-api" target="17" />
<module name="growth-server" target="17" />
<module name="record" target="17" />
<module name="record-api" target="17" />
<module name="record-server" target="17" />
<module name="reward" target="17" />
<module name="reward-api" target="17" />
<module name="reward-server" target="17" />
<module name="server" target="1.5" />
<module name="service" target="17" />
<module name="stats" target="17" />
<module name="stats-api" target="17" />
<module name="stats-server" target="17" />
<module name="user" target="17" />
<module name="user-api" target="17" />
<module name="user-server" target="17" />
<module name="websocket" target="17" />
<module name="websocket-api" target="17" />
<module name="websocket-server" target="17" />
</bytecodeTargetLevel>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="backend" options="" />
<module name="common" options="" />
<module name="customer" options="" />
<module name="emotion-museum-backend" options="-parameters" />
<module name="emotion-museum-server" options="-parameters" />
</option>
</component>
</project>
-61
View File
@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="localhost" uuid="5d3e10c1-409f-42d2-98e4-fb21f0c9102b">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="emotion_museum" uuid="01470282-aaf1-4f16-a5c2-d558bf1bc142">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend/emotion-websocket/src/main/resources/application-prod.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://101.200.208.45:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="@localhost" uuid="c0166ff4-a42d-4e97-b828-e64e5885093a">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend-single/src/main/resources/application-local.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="emotion_museum@101.200.208.45" uuid="8f6bf0e5-3002-455c-9ab9-a1176155030c">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend-single/src/main/resources/application-prod.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://101.200.208.45:3306/emotion_museum?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
-105
View File
@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend-single/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend-single/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-websocket/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/resources" charset="UTF-8" />
</component>
</project>
-65
View File
@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.huaweicloud.com/repository/maven/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-ai" />
<option name="name" value="Spring AI Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-milestones" />
<option name="name" value="Spring Milestones" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-snapshots" />
<option name="name" value="Spring Snapshots" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyunmaven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-37f43507:191123ac8a3:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/server/pom.xml" />
<option value="$PROJECT_DIR$/backend/pom.xml" />
<option value="$PROJECT_DIR$/backend-single/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/EmotionMuseum.iml" filepath="$PROJECT_DIR$/.idea/EmotionMuseum.iml" />
</modules>
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/sql/emotion_museum.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/sql/emotion_museum_ddl.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/sql/emotion_museum_init.sql" dialect="MySQL" />
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+46 -17
View File
@@ -20,7 +20,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
```
.
├── backend-single/ # Spring Boot 单体后端服务
├── server/ # Spring Boot 单体后端服务
├── web/ # 用户前端 (Vue3 + TS + Vite)
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
@@ -34,11 +34,11 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
## 常用命令
### 后端 (backend-single)
### 后端 (server)
```bash
# 进入后端目录
cd backend-single
cd server
# 编译打包
mvn clean package -DskipTests
@@ -47,7 +47,7 @@ mvn clean package -DskipTests
mvn spring-boot:run
# 或直接运行 JAR
java -jar target/backend-single-1.0.0.jar
java -jar target/server-1.0.0.jar
# 运行测试
mvn test
@@ -216,24 +216,53 @@ bash deploy-all.sh admin
---
## 验证规则(强制
## 服务器端验收强制规则(最高优先级,不可违反
**修改任何前后端代码后,必须使用浏览器进行验证**
**所有前端功能验收(小程序、用户端 web、管理后台 admin)必须使用服务器上部署的后端服务,禁止使用本地后端服务验收。**
### 验证步骤
### 强制流程
1. **使用浏览器工具打开对应页面**
- 使用 `agent-browser` skill 或 MCP 工具
- 前台页面:通常是 `http://localhost:5173` 或项目指定端口
- 管理端页面:通常是 `http://localhost:5174` 或项目指定路径
修改任意后端代码后,必须按以下顺序操作:
2. **验证标准**(全部满足才算通过)
- ✅ 前端修改已生效
- ✅ 页面没有报错
- ✅ 可以正常访问
- ✅ 浏览器 Console 中没有任何错误
1. **编译验证**:本地 `mvn clean install -DskipTests -am` 必须通过,零报错。
2. **部署到服务器**:执行 `python deploy.py backend`(或其他对应模块名),确保部署成功、无报错。
3. **服务器验收**:在服务器上已部署的应用进行功能验收。
3. **只有验证通过才算任务完成**
### 各前端验收方式
| 前端 | 验收方式 |
|---|---|
| 小程序 | 本地 `npm run dev:h5` 启动 H5`.env.development` 已默认指向 `lifescript.happylifeos.com`),浏览器访问本地 H5 URL,后端请求走服务器 |
| 用户端 web | 直接访问服务器 URL(`https://lifescript.happylifeos.com/`)验收 |
| 管理后台 admin | 直接访问服务器 URL`https://lifescript.happylifeos.com/emotion-museum-admin/`)验收 |
### 禁止行为
- ❌ 禁止使用 `localhost` / `127.0.0.1` 后端服务做功能验收
- ❌ 禁止绕过 `deploy.py` 手动部署
- ❌ 禁止在后端编译有报错的情况下部署
- ❌ 禁止部署后不验收就认为任务完成
### 允许的唯一例外
- ✅ 单元测试 / 集成测试(测试代码可连本地或测试数据库)
- ✅ 纯前端样式/UI 调整(不涉及后端接口,可本地启动 H5 自测)
- ✅ 后端开发阶段的结构验证(仅 mvn 编译,不部署不验收)
### deploy.py 模块名速查
- `backend` — 部署后端到服务器
- `frontend` — 部署用户端 web
- `admin` — 部署管理后台
- `all` — 部署所有服务
- `verify` — 验证部署状态
### 验收通过标准
- 浏览器 Console 无任何错误
- 服务器接口响应正常(Network 面板查看)
- 业务功能按预期工作
- 只有验收通过才算任务完成
---
+85 -39
View File
@@ -20,7 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
```
.
├── backend-single/ # Spring Boot 单体后端服务
├── server/ # Spring Boot 单体后端服务
├── web/ # 用户前端 (Vue3 + TS + Vite)
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
@@ -34,20 +34,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 常用命令
### 后端 (backend-single)
### 后端 (server)
```bash
# 进入后端目录
cd backend-single
cd server
# 编译打包
mvn clean package -DskipTests
# 本地运行
mvn spring-boot:run
# 本地运行(必须使用 dev-services.py
python dev-services.py start server
# 或直接运行 JAR
java -jar target/backend-single-1.0.0.jar
java -jar target/server-1.0.0.jar
# 运行测试
mvn test
@@ -65,8 +65,8 @@ cd web
# 安装依赖
npm install
# 启动开发服务器 (端口 5173)
npm run dev
# 启动开发服务器(必须使用 dev-services.py,固定端口 5178
python dev-services.py start web
# 类型检查
npm run type-check
@@ -93,8 +93,8 @@ cd web-admin
# 安装依赖
npm install
# 启动开发服务器 (端口 5174)
npm run dev
# 启动开发服务器(必须使用 dev-services.py,固定端口 5179
python dev-services.py start web-admin
# 类型检查
npm run type-check
@@ -130,10 +130,10 @@ npm run build:h5
**H5 开发模式**(日常开发,推荐):
```bash
cd mini-program
# 或使用启动脚本
start-h5-dev.bat
# 或使用 dev-services.py 启动(固定端口 5180
python dev-services.py start mini-program
```
访问 `http://localhost:5173`,登录态保留,支持热更新。
访问 `http://localhost:5180`,登录态保留,支持热更新。
**微信小程序开发模式**(仅用于小程序特性验证):
```bash
@@ -216,48 +216,94 @@ bash deploy-all.sh admin
---
## 验证规则(强制
## 服务器端验收强制规则(最高优先级,不可违反
**修改任何前后端代码后,必须使用浏览器进行验证**
**所有前端功能验收(小程序、用户端 web、管理后台 admin)必须使用服务器上部署的后端服务,禁止使用本地后端服务验收。**
### 验证步骤
### 强制流程
1. **使用浏览器工具打开对应页面**
- 使用 `agent-browser` skill 或 MCP 工具
- 前台页面:通常是 `http://localhost:5173` 或项目指定端口
- 管理端页面:通常是 `http://localhost:5174` 或项目指定路径
修改任意后端代码后,必须按以下顺序操作:
2. **验证标准**(全部满足才算通过)
- ✅ 前端修改已生效
- ✅ 页面没有报错
- ✅ 可以正常访问
- ✅ 浏览器 Console 中没有任何错误
1. **编译验证**:本地 `mvn clean install -DskipTests -am` 必须通过,零报错。
2. **部署到服务器**:执行 `python deploy.py backend`(或其他对应模块名),确保部署成功、无报错。
3. **服务器验收**:在服务器上已部署的应用进行功能验收。
3. **只有验证通过才算任务完成**
### 各前端验收方式
| 前端 | 验收方式 |
|---|---|
| 小程序 | 本地 `npm run dev:h5` 启动 H5`.env.development` 已默认指向 `lifescript.happylifeos.com`),浏览器访问本地 H5 URL,后端请求走服务器 |
| 用户端 web | 直接访问服务器 URL(`https://lifescript.happylifeos.com/`)验收 |
| 管理后台 admin | 直接访问服务器 URL`https://lifescript.happylifeos.com/emotion-museum-admin/`)验收 |
### 禁止行为
- ❌ 禁止使用 `localhost` / `127.0.0.1` 后端服务做功能验收
- ❌ 禁止绕过 `deploy.py` 手动部署
- ❌ 禁止在后端编译有报错的情况下部署
- ❌ 禁止部署后不验收就认为任务完成
### 允许的唯一例外
- ✅ 单元测试 / 集成测试(测试代码可连本地或测试数据库)
- ✅ 纯前端样式/UI 调整(不涉及后端接口,可本地启动 H5 自测)
- ✅ 后端开发阶段的结构验证(仅 mvn 编译,不部署不验收)
### deploy.py 模块名速查
- `backend` — 部署后端到服务器
- `frontend` — 部署用户端 web
- `admin` — 部署管理后台
- `all` — 部署所有服务
- `verify` — 验证部署状态
### 验收通过标准
- 浏览器 Console 无任何错误
- 服务器接口响应正常(Network 面板查看)
- 业务功能按预期工作
- 只有验收通过才算任务完成
---
## 热加载规则(强制)
## 本地服务管理规则(强制)
**修改前后端代码后,优先利用热加载,避免不必要的重启**
**启动、重启、停止本地前后端服务时,必须使用项目根目录下的 `dev-services.py` 脚本。**
### 判断逻辑
### 强制要求
- ✅ 必须使用:`python dev-services.py [start|stop|restart|status|discover|logs] [服务名]`
- ❌ 禁止直接使用 `npm run dev` / `pnpm run dev` 启动前端
- ❌ 禁止直接使用 `npm run dev:h5` 启动小程序 H5
- ❌ 禁止直接使用 `mvn spring-boot:run` 启动后端
- ❌ 禁止无意义重启支持热加载的服务
### 前端/H5 固定端口
| 服务 | 目录 | 固定端口 |
|---|---|---|
| web | `web/` | 5178 |
| web-admin | `web-admin/` | 5179 |
| mini-program | `mini-program/` | 5180 |
| life-script | `life-script/` | 5181 |
后端服务端口保持原有配置不变。
### 禁止无意义重启
- 只有修改必须重启才能生效的文件时,才允许执行 `restart`
- 修改源码文件(`.vue``.tsx``.ts``.js``.css``.scss` 等)时,`dev-services.py restart` 会拒绝重启
- 确需强制重启时,使用 `python dev-services.py restart --force`
### 热加载规则
1. **不需要重启的场景**(热加载自动生效)
- 前端:修改 `.vue``.tsx``.ts``.js``.css``.scss` 等源码文件
- 后端:修改业务逻辑、API 接口、组件代码等支持热更新的文件
- 样式、模板、静态资源等修改
2. **需要重启的场景**(热加载无法生效)
2. **需要重启的场景**
- 前端:修改 `vite.config.ts``tsconfig.json``.env``package.json`、别名配置等
- 后端:修改启动配置、数据库连接、中间件注册、环境变量等
- 新增或删除依赖包后
### 操作原则
- 修改后先观察终端输出,确认热加载是否成功
- 只有修改了必须重启才能生效的配置时,才执行重启操作
- 重启前后端服务前,需告知用户
- 后端:按现有后端规则处理(本地不启动后端,使用服务器验收)
---
+1 -1
View File
@@ -70,7 +70,7 @@
│ ├── emotion-reward/ # 成就奖励服务
│ ├── emotion-stats/ # 统计分析服务
│ └── emotion-common/ # 公共模块
├── backend-single/ # SpringBoot单体后端服务
├── server/ # SpringBoot单体后端服务
├── web/ # 旧版Web前端
├── web-new/ # 新版Web前端(Vue3+TS)
└── 开心APP网页代码v1.1/ # 早期网页版本
+3 -3
View File
@@ -395,14 +395,14 @@ def deploy_nginx():
# ============================================================================
# 后端 - 调用 backend-single/deploy.sh remote
# 后端 - 调用 server/deploy.sh remote
# ============================================================================
def deploy_backend():
"""部署后端服务"""
log_section("部署后端服务")
deploy_script = PROJECT_DIR / "backend-single" / "deploy.py"
deploy_script = PROJECT_DIR / "server" / "deploy.py"
if not deploy_script.exists():
log_error(f"后端部署脚本不存在: {deploy_script}")
return False
@@ -410,7 +410,7 @@ def deploy_backend():
log_info("执行后端部署...")
ok, _, err = run_command(
f"{PYTHON} deploy.py remote",
cwd=str(PROJECT_DIR / "backend-single"),
cwd=str(PROJECT_DIR / "server"),
timeout=600,
capture=False
)
+4 -4
View File
@@ -74,16 +74,16 @@ deploy_nginx() {
}
# ============================================================================
# 后端 - 调用 backend-single/deploy.sh
# 后端 - 调用 server/deploy.sh
# ============================================================================
deploy_backend() {
log_section "部署后端服务"
if [ ! -f "backend-single/deploy.sh" ]; then
log_error "后端部署脚本不存在: backend-single/deploy.sh"
if [ ! -f "server/deploy.sh" ]; then
log_error "后端部署脚本不存在: server/deploy.sh"
return 1
fi
log_info "执行后端部署..."
cd backend-single
cd server
if bash deploy.sh remote; then
cd ..
log_info "✅ 后端部署完成"
+112 -8
View File
@@ -67,6 +67,25 @@ DEFAULT_PORTS = {
"spring-boot-gradle": 8080,
}
# 前端/H5 服务固定端口映射(按项目目录名匹配)
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
# 必须重启才能生效的文件模式
RESTART_REQUIRED_PATTERNS = [
re.compile(r'vite\.config\.(ts|js|mjs|cjs)$'),
re.compile(r'tsconfig\.json$'),
re.compile(r'\.env(\.[^/]*)?$'),
re.compile(r'package\.json$'),
re.compile(r'application.*\.ya?ml$'),
re.compile(r'pom\.xml$'),
re.compile(r'build\.gradle(\.kts)?$'),
]
# ANSI 颜色
COLORS = {
"INFO": "\033[37m",
@@ -328,36 +347,103 @@ def _update_service_port(svc: Service, port: int):
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
has_port_arg = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
if part == "--port":
has_port_arg = True
if index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
has_port_arg = True
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
# 前端框架如果没有 --port 参数,追加 --port
if not has_port_arg and svc.project_type in {"vite", "uniapp", "taro", "next"}:
updated.extend(["--port", str(port)])
svc.start_cmd = updated
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
# 按目录名匹配固定前端端口
dir_name_to_service = {}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
dir_name_to_service[dir_name] = svc
# 先为固定前端服务分配端口
for dir_name, svc in dir_name_to_service.items():
fixed_port = FIXED_FRONTEND_PORTS[dir_name]
if svc.port != fixed_port:
log(f"{svc.name} 使用固定前端端口 {fixed_port}", "INFO")
_update_service_port(svc, fixed_port)
# 检查端口冲突
used_ports = {}
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
if port in used_ports:
other = used_ports[port]
log(f"端口冲突: {svc.name} ({port}) 与 {other.name} ({port}) 冲突", "ERROR")
sys.exit(1)
used_ports[port] = svc
# 非固定前端服务按原有逻辑处理冲突
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
continue
port = svc.port
if port in used_ports and used_ports[port] is not svc:
while port in used_ports:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
def _should_restart_for_changes(service_dir: Path) -> tuple[bool, list[str]]:
"""
检查指定服务目录下是否有必须重启才能生效的变更。
返回: (是否需要重启, 已修改文件列表)
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--", str(service_dir)],
capture_output=True,
text=True,
cwd=SCRIPT_DIR,
check=True,
)
changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
except subprocess.CalledProcessError:
# 非 git 仓库或 git 命令失败,默认允许重启
return True, []
except FileNotFoundError:
return True, []
if not changed_files:
return False, []
for file_path in changed_files:
for pattern in RESTART_REQUIRED_PATTERNS:
if pattern.search(file_path):
return True, changed_files
return False, changed_files
def _extract_port_from_pom(pom_path: Path) -> Optional[int]:
"""从 pom.xml 提取 server.port"""
try:
@@ -1553,6 +1639,7 @@ def main():
parser.add_argument("--type", dest="svc_type", default=None, help="服务类型过滤: vite|python|spring-boot|node-backend")
parser.add_argument("--port", dest="port", type=int, default=None, help="端口号过滤")
parser.add_argument("--foreground", "-f", action="store_true", help="前台模式运行")
parser.add_argument("--force", action="store_true", help="强制重启,忽略热加载保护")
args = parser.parse_args()
@@ -1631,6 +1718,23 @@ def main():
return
if args.action == "restart":
if not args.force:
blocked = []
for svc in services:
should_restart, changed_files = _should_restart_for_changes(svc.project_dir)
if not should_restart:
blocked.append((svc.name, changed_files))
if blocked:
log("检测到以下服务无需重启(当前修改支持热加载):", "WARN")
for name, files in blocked:
if files:
log(f" - {name}: 修改文件 {', '.join(files)}", "WARN")
else:
log(f" - {name}: 未检测到代码变更", "WARN")
log("如需强制重启,请使用: python dev-services.py restart --force", "WARN")
sys.exit(0)
for svc in services:
restart_service(svc)
else:
@@ -570,13 +570,13 @@ deploy_ssl() {
deploy_backend() {
log_section "部署后端服务"
if [ ! -f "backend-single/deploy.sh" ]; then
if [ ! -f "server/deploy.sh" ]; then
log_error "后端部署脚本不存在"
return 1
fi
log_info "执行后端部署..."
cd backend-single
cd server
bash deploy.sh remote
local result=$?
cd ..
@@ -828,7 +828,7 @@ git commit -m "config: 更新管理后台部署脚本访问地址提示"
### Task 9: 检查并修改后端 CORS 配置
**Files:**
- Modify: `backend-single/src/main/resources/application.yml` (或相应配置文件)
- Modify: `server/src/main/resources/application.yml` (或相应配置文件)
- [ ] **Step 1: 读取当前后端配置文件**
@@ -841,7 +841,7 @@ git commit -m "config: 更新管理后台部署脚本访问地址提示"
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/resources/application.yml
git add server/src/main/resources/application.yml
git commit -m "config: 更新 CORS 配置允许新域名访问"
```
@@ -4,7 +4,7 @@
**Goal:** Add mini program behavior analytics collection, backend event storage and aggregation, and a web-admin behavior analytics dashboard.
**Architecture:** The mini program queues analytics events locally and flushes them to `POST /analytics/events/batch`. `backend-single` stores raw events in `t_analytics_event` and exposes admin-only aggregation endpoints under `/admin/analytics`. `web-admin` renders overview, trend, funnel, preference, and top-event stats using Element Plus and ECharts.
**Architecture:** The mini program queues analytics events locally and flushes them to `POST /analytics/events/batch`. `server` stores raw events in `t_analytics_event` and exposes admin-only aggregation endpoints under `/admin/analytics`. `web-admin` renders overview, trend, funnel, preference, and top-event stats using Element Plus and ECharts.
**Tech Stack:** Spring Boot 2.7, MyBatis Plus, MySQL JSON, Java 17, uni-app/Vue, Vue 3, Element Plus, ECharts.
@@ -13,25 +13,25 @@
## File Structure
- Create `sql/2026-05-17-analytics-event.sql`: migration for `t_analytics_event`.
- Create `backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java`: MyBatis Plus entity.
- Create `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`: mapper plus aggregation SQL.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`: mini program batch request.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`: one event payload.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`: admin query filters.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsBatchResponse.java`: accepted/rejected counts.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsOverviewResponse.java`: overview cards.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`: trend item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsFunnelItem.java`: funnel item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`: preference item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`: top event item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsUserItem.java`: user ranking item.
- Create `backend-single/src/main/java/com/emotion/service/AnalyticsService.java`: write and aggregate contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`: validation, persistence, aggregation.
- Create `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`: mini program write endpoint.
- Create `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`: admin read endpoints.
- Modify `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`: allow anonymous analytics batch endpoint while keeping admin analytics protected.
- Create `backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java`: service validation and aggregation tests.
- Create `backend-single/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`: endpoint tests.
- Create `server/src/main/java/com/emotion/entity/AnalyticsEvent.java`: MyBatis Plus entity.
- Create `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`: mapper plus aggregation SQL.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`: mini program batch request.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`: one event payload.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`: admin query filters.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsBatchResponse.java`: accepted/rejected counts.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsOverviewResponse.java`: overview cards.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`: trend item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsFunnelItem.java`: funnel item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`: preference item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`: top event item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsUserItem.java`: user ranking item.
- Create `server/src/main/java/com/emotion/service/AnalyticsService.java`: write and aggregate contract.
- Create `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`: validation, persistence, aggregation.
- Create `server/src/main/java/com/emotion/controller/AnalyticsController.java`: mini program write endpoint.
- Create `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`: admin read endpoints.
- Modify `server/src/main/java/com/emotion/config/WebMvcConfig.java`: allow anonymous analytics batch endpoint while keeping admin analytics protected.
- Create `server/src/test/java/com/emotion/service/AnalyticsServiceTest.java`: service validation and aggregation tests.
- Create `server/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`: endpoint tests.
- Create `mini-program/src/services/analytics.js`: client SDK.
- Modify `mini-program/src/App.vue`: initialize and flush analytics.
- Modify `mini-program/src/pages/main/index.vue`: track main tab/page activity if this page owns tab switching.
@@ -121,11 +121,11 @@ git commit -m "feat: add analytics event table"
### Task 2: Add Backend DTOs and Entity
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`
- Create: response DTO files under `backend-single/src/main/java/com/emotion/dto/response/analytics/`
- Create: `server/src/main/java/com/emotion/entity/AnalyticsEvent.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`
- Create: response DTO files under `server/src/main/java/com/emotion/dto/response/analytics/`
- [ ] **Step 1: Add the entity**
@@ -387,7 +387,7 @@ public class AnalyticsUserItem {
Run:
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -396,7 +396,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java backend-single/src/main/java/com/emotion/dto/request/analytics backend-single/src/main/java/com/emotion/dto/response/analytics
git add server/src/main/java/com/emotion/entity/AnalyticsEvent.java server/src/main/java/com/emotion/dto/request/analytics server/src/main/java/com/emotion/dto/response/analytics
git commit -m "feat: add analytics DTOs and entity"
```
@@ -405,10 +405,10 @@ git commit -m "feat: add analytics DTOs and entity"
### Task 3: Add Analytics Mapper and Service
**Files:**
- Create: `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Create: `backend-single/src/main/java/com/emotion/service/AnalyticsService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java`
- Create: `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Create: `server/src/main/java/com/emotion/service/AnalyticsService.java`
- Create: `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/AnalyticsServiceTest.java`
- [ ] **Step 1: Write service tests**
@@ -462,7 +462,7 @@ public class AnalyticsServiceTest {
Run:
```bash
cd backend-single
cd server
mvn -Dtest=AnalyticsServiceTest test
```
@@ -676,7 +676,7 @@ public class AnalyticsServiceImpl extends ServiceImpl<AnalyticsEventMapper, Anal
Run:
```bash
cd backend-single
cd server
mvn -Dtest=AnalyticsServiceTest test
```
@@ -685,7 +685,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 7: Commit**
```bash
git add backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java backend-single/src/main/java/com/emotion/service/AnalyticsService.java backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java
git add server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java server/src/main/java/com/emotion/service/AnalyticsService.java server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java server/src/test/java/com/emotion/service/AnalyticsServiceTest.java
git commit -m "feat: add analytics service"
```
@@ -694,10 +694,10 @@ git commit -m "feat: add analytics service"
### Task 4: Add Analytics Controllers and Auth Exclusion
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`
- Create: `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`
- Test: `backend-single/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`
- Create: `server/src/main/java/com/emotion/controller/AnalyticsController.java`
- Create: `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/config/WebMvcConfig.java`
- Test: `server/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`
- [ ] **Step 1: Add write controller**
@@ -794,7 +794,7 @@ Do not exclude `/admin/analytics/**`.
Run:
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -803,7 +803,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AnalyticsController.java backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java backend-single/src/main/java/com/emotion/config/WebMvcConfig.java
git add server/src/main/java/com/emotion/controller/AnalyticsController.java server/src/main/java/com/emotion/controller/AdminAnalyticsController.java server/src/main/java/com/emotion/config/WebMvcConfig.java
git commit -m "feat: expose analytics APIs"
```
@@ -1323,7 +1323,7 @@ git commit -m "feat: add analytics admin dashboard"
- [ ] **Step 1: Run backend tests**
```bash
cd backend-single
cd server
mvn test
```
@@ -4,7 +4,7 @@
**Goal:** Add CPU-friendly open-source text-to-speech generation for user scripts, with cached backend tasks and mini program playback.
**Architecture:** `backend-single` creates TTS tasks, validates source ownership, cleans script text, checks audio cache, and calls a private Python FastAPI TTS service. The Python service runs on `101.200.208.45` and writes audio files under `/data/uploads/emotion-museum/tts`. The mini program requests generation from `ScriptDetailView.vue`, polls status, and plays the returned audio URL with `uni.createInnerAudioContext()`.
**Architecture:** `server` creates TTS tasks, validates source ownership, cleans script text, checks audio cache, and calls a private Python FastAPI TTS service. The Python service runs on `101.200.208.45` and writes audio files under `/data/uploads/emotion-museum/tts`. The mini program requests generation from `ScriptDetailView.vue`, polls status, and plays the returned audio URL with `uni.createInnerAudioContext()`.
**Tech Stack:** Spring Boot 2.7, MyBatis Plus, Java 17, MySQL, Python FastAPI, MeloTTS or equivalent CPU-friendly Chinese TTS engine, systemd, uni-app/Vue.
@@ -13,22 +13,22 @@
## File Structure
- Create `sql/2026-05-17-tts-task.sql`: `t_tts_task` migration.
- Create `backend-single/src/main/java/com/emotion/entity/TtsTask.java`: task entity.
- Create `backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java`: task mapper.
- Create `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`: create request.
- Create `backend-single/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`: task response.
- Create `backend-single/src/main/java/com/emotion/service/TtsTaskService.java`: contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`: ownership, cache, async processing.
- Create `backend-single/src/main/java/com/emotion/service/TtsEngineClient.java`: internal engine client contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`: calls Python service.
- Create `backend-single/src/main/java/com/emotion/controller/TtsController.java`: user-facing TTS endpoints.
- Modify `backend-single/src/main/resources/application.yml`: TTS config defaults.
- Modify `backend-single/src/main/resources/application-prod.yml`: production paths and internal URL.
- Create `backend-single/src/test/java/com/emotion/service/TtsTaskServiceTest.java`: service tests.
- Create `backend-single/tts-service/requirements.txt`: FastAPI deps; MeloTTS is installed from the official repository because the official docs use editable install plus `python -m unidic download`.
- Create `backend-single/tts-service/app.py`: FastAPI service.
- Create `backend-single/tts-service/README.md`: server setup.
- Create `backend-single/tts-service/emotion-museum-tts.service`: systemd unit.
- Create `server/src/main/java/com/emotion/entity/TtsTask.java`: task entity.
- Create `server/src/main/java/com/emotion/mapper/TtsTaskMapper.java`: task mapper.
- Create `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`: create request.
- Create `server/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`: task response.
- Create `server/src/main/java/com/emotion/service/TtsTaskService.java`: contract.
- Create `server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`: ownership, cache, async processing.
- Create `server/src/main/java/com/emotion/service/TtsEngineClient.java`: internal engine client contract.
- Create `server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`: calls Python service.
- Create `server/src/main/java/com/emotion/controller/TtsController.java`: user-facing TTS endpoints.
- Modify `server/src/main/resources/application.yml`: TTS config defaults.
- Modify `server/src/main/resources/application-prod.yml`: production paths and internal URL.
- Create `server/src/test/java/com/emotion/service/TtsTaskServiceTest.java`: service tests.
- Create `server/tts-service/requirements.txt`: FastAPI deps; MeloTTS is installed from the official repository because the official docs use editable install plus `python -m unidic download`.
- Create `server/tts-service/app.py`: FastAPI service.
- Create `server/tts-service/README.md`: server setup.
- Create `server/tts-service/emotion-museum-tts.service`: systemd unit.
- Create `mini-program/src/services/tts.js`: TTS API client.
- Create `mini-program/src/components/ScriptAudioPlayer.vue`: player component.
- Modify `mini-program/src/pages/main/ScriptDetailView.vue`: add player.
@@ -93,10 +93,10 @@ git commit -m "feat: add tts task table"
### Task 2: Add Backend TTS Entity, DTOs, and Mapper
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/TtsTask.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`
- Create: `server/src/main/java/com/emotion/entity/TtsTask.java`
- Create: `server/src/main/java/com/emotion/mapper/TtsTaskMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`
- [ ] **Step 1: Add entity**
@@ -205,7 +205,7 @@ public class TtsTaskResponse {
- [ ] **Step 4: Compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -214,7 +214,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/TtsTask.java backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java backend-single/src/main/java/com/emotion/dto/request/tts backend-single/src/main/java/com/emotion/dto/response/tts
git add server/src/main/java/com/emotion/entity/TtsTask.java server/src/main/java/com/emotion/mapper/TtsTaskMapper.java server/src/main/java/com/emotion/dto/request/tts server/src/main/java/com/emotion/dto/response/tts
git commit -m "feat: add tts task model"
```
@@ -223,13 +223,13 @@ git commit -m "feat: add tts task model"
### Task 3: Add TTS Service and Engine Client
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/TtsTaskService.java`
- Create: `backend-single/src/main/java/com/emotion/service/TtsEngineClient.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `backend-single/src/main/resources/application-prod.yml`
- Test: `backend-single/src/test/java/com/emotion/service/TtsTaskServiceTest.java`
- Create: `server/src/main/java/com/emotion/service/TtsTaskService.java`
- Create: `server/src/main/java/com/emotion/service/TtsEngineClient.java`
- Create: `server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`
- Create: `server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`
- Modify: `server/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application-prod.yml`
- Test: `server/src/test/java/com/emotion/service/TtsTaskServiceTest.java`
- [ ] **Step 1: Add service contracts**
@@ -503,7 +503,7 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
- [ ] **Step 5: Run compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -512,7 +512,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 6: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/TtsTaskService.java backend-single/src/main/java/com/emotion/service/TtsEngineClient.java backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java backend-single/src/main/resources/application.yml backend-single/src/main/resources/application-prod.yml
git add server/src/main/java/com/emotion/service/TtsTaskService.java server/src/main/java/com/emotion/service/TtsEngineClient.java server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java server/src/main/resources/application.yml server/src/main/resources/application-prod.yml
git commit -m "feat: add tts backend service"
```
@@ -521,7 +521,7 @@ git commit -m "feat: add tts backend service"
### Task 4: Add TTS Controller
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/TtsController.java`
- Create: `server/src/main/java/com/emotion/controller/TtsController.java`
- [ ] **Step 1: Add controller**
@@ -569,7 +569,7 @@ public class TtsController {
- [ ] **Step 2: Run compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -578,7 +578,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/TtsController.java
git add server/src/main/java/com/emotion/controller/TtsController.java
git commit -m "feat: expose tts task APIs"
```
@@ -587,10 +587,10 @@ git commit -m "feat: expose tts task APIs"
### Task 5: Add Python TTS Service Skeleton
**Files:**
- Create: `backend-single/tts-service/requirements.txt`
- Create: `backend-single/tts-service/app.py`
- Create: `backend-single/tts-service/README.md`
- Create: `backend-single/tts-service/emotion-museum-tts.service`
- Create: `server/tts-service/requirements.txt`
- Create: `server/tts-service/app.py`
- Create: `server/tts-service/README.md`
- Create: `server/tts-service/emotion-museum-tts.service`
- [ ] **Step 1: Add requirements**
@@ -717,7 +717,7 @@ curl http://127.0.0.1:19110/health
- [ ] **Step 5: Run local syntax check**
```bash
python -m py_compile backend-single/tts-service/app.py
python -m py_compile server/tts-service/app.py
```
Expected: no output and exit code 0.
@@ -725,7 +725,7 @@ Expected: no output and exit code 0.
- [ ] **Step 6: Commit**
```bash
git add backend-single/tts-service
git add server/tts-service
git commit -m "feat: add private tts service scaffold"
```
@@ -960,7 +960,7 @@ git commit -m "feat: add tts control to script detail"
- [ ] **Step 1: Upload service directory**
```bash
scp -r backend-single/tts-service root@101.200.208.45:/data/programs/emotion-museum/tts-service
scp -r server/tts-service root@101.200.208.45:/data/programs/emotion-museum/tts-service
```
Expected: files are copied.
@@ -988,7 +988,7 @@ Expected:
- [ ] **Step 4: Install systemd unit after health passes**
```bash
scp backend-single/tts-service/emotion-museum-tts.service root@101.200.208.45:/etc/systemd/system/emotion-museum-tts.service
scp server/tts-service/emotion-museum-tts.service root@101.200.208.45:/etc/systemd/system/emotion-museum-tts.service
ssh root@101.200.208.45 "systemctl daemon-reload && systemctl enable emotion-museum-tts && systemctl restart emotion-museum-tts && systemctl status emotion-museum-tts --no-pager"
```
@@ -997,7 +997,7 @@ Expected: service status is active.
- [ ] **Step 5: Commit deployment doc tweaks if needed**
```bash
git add backend-single/tts-service/README.md
git add server/tts-service/README.md
git commit -m "docs: update tts deployment notes"
```
@@ -1013,7 +1013,7 @@ Only run this commit if README changed.
- [ ] **Step 1: Run backend tests**
```bash
cd backend-single
cd server
mvn test
```
@@ -44,21 +44,21 @@ This plan does not implement:
Backend:
- Create: `backend-single/src/main/java/com/emotion/entity/SocialContentItem.java`
- Create: `backend-single/src/main/java/com/emotion/entity/SocialProfileInsight.java`
- Create: `backend-single/src/main/java/com/emotion/entity/UserConsentLog.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/SocialContentItemMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/SocialProfileInsightMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/UserConsentLogMapper.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/social/*.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/social/*.java`
- Create: `backend-single/src/main/java/com/emotion/controller/SocialContentController.java`
- Create: `backend-single/src/main/java/com/emotion/controller/SocialInsightController.java`
- Create: `backend-single/src/main/java/com/emotion/service/SocialContentService.java`
- Create: `backend-single/src/main/java/com/emotion/service/SocialInsightService.java`
- Create: `backend-single/src/main/java/com/emotion/service/ScriptContextService.java`
- Create implementations under `backend-single/src/main/java/com/emotion/service/impl/`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- Create: `server/src/main/java/com/emotion/entity/SocialContentItem.java`
- Create: `server/src/main/java/com/emotion/entity/SocialProfileInsight.java`
- Create: `server/src/main/java/com/emotion/entity/UserConsentLog.java`
- Create: `server/src/main/java/com/emotion/mapper/SocialContentItemMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/SocialProfileInsightMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/UserConsentLogMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/social/*.java`
- Create: `server/src/main/java/com/emotion/dto/response/social/*.java`
- Create: `server/src/main/java/com/emotion/controller/SocialContentController.java`
- Create: `server/src/main/java/com/emotion/controller/SocialInsightController.java`
- Create: `server/src/main/java/com/emotion/service/SocialContentService.java`
- Create: `server/src/main/java/com/emotion/service/SocialInsightService.java`
- Create: `server/src/main/java/com/emotion/service/ScriptContextService.java`
- Create implementations under `server/src/main/java/com/emotion/service/impl/`
- Modify: `server/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- Modify SQL schema/migration file used by this repo.
Mini program:
@@ -212,7 +212,7 @@ public interface SocialContentItemMapper extends BaseMapper<SocialContentItem> {
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -221,8 +221,8 @@ Expected: compile succeeds.
## Task 3: Social Content API
**Files:**
- Create DTOs under `backend-single/src/main/java/com/emotion/dto/request/social/`
- Create DTOs under `backend-single/src/main/java/com/emotion/dto/response/social/`
- Create DTOs under `server/src/main/java/com/emotion/dto/request/social/`
- Create DTOs under `server/src/main/java/com/emotion/dto/response/social/`
- Create: `SocialContentController.java`
- Create: `SocialContentService.java`
- Create: `SocialContentServiceImpl.java`
@@ -317,7 +317,7 @@ Do not accept screenshots larger than the configured limit.
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -405,7 +405,7 @@ Rules:
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -414,8 +414,8 @@ Expected: compile succeeds.
## Task 5: Script Context Integration
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ScriptContextService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/ScriptContextServiceImpl.java`
- Create: `server/src/main/java/com/emotion/service/ScriptContextService.java`
- Create: `server/src/main/java/com/emotion/service/impl/ScriptContextServiceImpl.java`
- Modify: `EpicScriptServiceImpl.java`
- [ ] **Step 1: Implement context service**
@@ -468,7 +468,7 @@ When social insight context is non-empty, add analytics/event hook if backend an
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -658,7 +658,7 @@ Expected: build succeeds.
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -707,7 +707,7 @@ Check:
- [ ] **Step 5: Commit**
```powershell
git add backend-single mini-program sql docs/superpowers/specs/2026-05-19-social-data-import-script-profile-design.md docs/superpowers/plans/2026-05-19-social-data-import-script-profile-plan.md
git add server mini-program sql docs/superpowers/specs/2026-05-19-social-data-import-script-profile-design.md docs/superpowers/plans/2026-05-19-social-data-import-script-profile-plan.md
git commit -m "feat: add social data import design and profile plan"
```
@@ -13,10 +13,10 @@
### Task 1: ASR Service
**Files:**
- Create: `backend-single/asr-service/app.py`
- Create: `backend-single/asr-service/requirements.txt`
- Create: `backend-single/asr-service/emotion-museum-asr.service`
- Create: `backend-single/asr-service/README.md`
- Create: `server/asr-service/app.py`
- Create: `server/asr-service/requirements.txt`
- Create: `server/asr-service/emotion-museum-asr.service`
- Create: `server/asr-service/README.md`
- [x] Add a FastAPI service with `/health` and `/transcribe`.
- [x] Accept an uploaded audio file, save it under `/tmp/emotion-museum-asr`, run the ASR model, and return JSON with `success`, `text`, `language`, `durationMs`, `engine`, and `errorMessage`.
@@ -25,12 +25,12 @@
### Task 2: Java Backend Proxy
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/response/asr/AsrTranscribeResponse.java`
- Create: `backend-single/src/main/java/com/emotion/service/AsrService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/AsrServiceImpl.java`
- Create: `backend-single/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `backend-single/src/main/resources/application-prod.yml`
- Create: `server/src/main/java/com/emotion/dto/response/asr/AsrTranscribeResponse.java`
- Create: `server/src/main/java/com/emotion/service/AsrService.java`
- Create: `server/src/main/java/com/emotion/service/impl/AsrServiceImpl.java`
- Create: `server/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `server/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application-prod.yml`
- [x] Add `emotion.asr` config for `enabled`, `engine-url`, `max-file-size`, and `allowed-types`.
- [x] Validate upload presence, size, and suffix.
@@ -14,10 +14,10 @@
| 文件 | 职责 | 操作 |
|------|------|------|
| `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` | DTO:新增 endpointId 字段,更新 RESERVED_KEYS | 修改 |
| `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java` | Service 接口:新增 testEndpoint + invokeEndpointStream | 修改 |
| `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` | Service 实现:新增两个方法的完整实现 | 修改 |
| `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` | Controller:新增 /endpoint/test 和 /endpoint/stream | 修改 |
| `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` | DTO:新增 endpointId 字段,更新 RESERVED_KEYS | 修改 |
| `server/src/main/java/com/emotion/service/AiRuntimeService.java` | Service 接口:新增 testEndpoint + invokeEndpointStream | 修改 |
| `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` | Service 实现:新增两个方法的完整实现 | 修改 |
| `server/src/main/java/com/emotion/controller/AiRoutingController.java` | Controller:新增 /endpoint/test 和 /endpoint/stream | 修改 |
| `web-admin/src/types/aiconfig.ts` | TypeScript 类型:新增 AiEndpointRuntimeRequest | 修改 |
| `web-admin/src/api/aiconfig.ts` | API 层:新增 testEndpointRuntime、streamEndpointRuntime、提取 fetchSseStream | 修改 |
| `web-admin/src/views/aiconfig/AiRoutingList.vue` | Vue 组件:增加行内测试按钮 + endpoint 测试对话框 | 修改 |
@@ -27,7 +27,7 @@
### Task 1: DTO 增强 — AiRuntimeRequest 新增 endpointId
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- [ ] **Step 1: 新增 endpointId 字段和 RESERVED_KEYS**
@@ -82,13 +82,13 @@ public static AiRuntimeRequest fromPayload(JSONObject payload) {
- [ ] **Step 3: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESSendpointId 字段新增不破坏任何编译,因为这是新增字段)
- [ ] **Step 4: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java
git add server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java
git commit -m "feat: AiRuntimeRequest DTO 新增 endpointId 字段
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -99,7 +99,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 2: Service 接口层 — 新增 testEndpoint + invokeEndpointStream
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `server/src/main/java/com/emotion/service/AiRuntimeService.java`
- [ ] **Step 1: 新增两个接口方法**
@@ -128,13 +128,13 @@ public interface AiRuntimeService {
- [ ] **Step 2: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS(接口新增方法,但实现类尚未修改,此时不会编译失败 — 因为 ServiceImpl 编译是单独的任务)
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/AiRuntimeService.java
git add server/src/main/java/com/emotion/service/AiRuntimeService.java
git commit -m "feat: AiRuntimeService 接口新增 endpoint 测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -145,7 +145,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 3: Service 实现层 — testEndpoint + invokeEndpointStream 实现
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- [ ] **Step 1: 新增 testEndpoint 方法**
@@ -271,13 +271,13 @@ public void invokeEndpointStream(String endpointId, Map<String, Object> inputs,
- [ ] **Step 3: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS
- [ ] **Step 4: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git commit -m "feat: AiRuntimeServiceImpl 实现 endpoint 直接测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -288,7 +288,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 4: Controller 层 — 新增 /endpoint/test 和 /endpoint/stream
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [ ] **Step 1: 在 runtimeStream 方法之后新增两个 endpoint 方法**
@@ -323,13 +323,13 @@ public SseEmitter endpointStream(@RequestBody JSONObject payload) {
- [ ] **Step 2: 编译并验证完整后端**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
git commit -m "feat: AiRoutingController 新增 /endpoint/test 和 /endpoint/stream 接口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -13,10 +13,10 @@
### Task 1: Backend Test Sample Contract
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/response/ai/AiTestTemplateResponse.java`
- Modify: `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Create: `server/src/main/java/com/emotion/dto/response/ai/AiTestTemplateResponse.java`
- Modify: `server/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [x] Add a response DTO with `sceneCode`, `endpointId`, `endpointCode`, `providerType`, `inputs`, and `paramFields`.
- [x] Add service methods `buildEndpointTestTemplate(String endpointId)` and `buildSceneTestTemplate(String sceneCode)`.
@@ -26,9 +26,9 @@
### Task 2: Provider Runtime Compatibility
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/ai/ProviderHttpSupport.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/ProviderHttpSupport.java`
- Modify: `server/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- [x] Add `Accept: text/event-stream` for stream requests.
- [x] Make Coze workflow body match historical calls: `workflow_id`, `user_id`, `stream`, `parameters`, and optional `bot_id`.
@@ -22,9 +22,9 @@
### Task 2: User Runtime Request Shape
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- [x] Accept user calls as `sceneCode` plus `inputs` JSON.
- [x] Keep user identity server-side by reading `UserContextHolder`.
@@ -13,8 +13,8 @@
### Task 1: Backend Chat Bridge To Runtime
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java`
- Modify: `server/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java`
- [x] Route chat generation through `AiRuntimeService` with `sceneCode=chat`.
- [x] Push `start`, `delta`, `done`, and `error` events over the existing user WebSocket topic.
@@ -13,8 +13,8 @@
### Task 1: Backend Stream Text Normalization
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- [x] Add a private helper that parses JSON string wrappers and returns `output`, `answer`, `content`, `text`, `result`, `data.*`, or `outputs.*`.
- [x] Call the helper before returning every extracted delta string.
@@ -54,7 +54,7 @@
**Commands:**
- `cd web-admin && npm run build`
- `cd backend-single && mvn test`
- `cd server && mvn test`
- `git diff --check`
- [x] Build passes.
@@ -2,7 +2,7 @@
> **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:** 为 backend-single 下全部 39 个 Controller 及其关联 Request DTO 补全 OpenAPI 中文注解(@Tag/@Operation/@Parameter/@Schema),使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
**Goal:** 为 server 下全部 39 个 Controller 及其关联 Request DTO 补全 OpenAPI 中文注解(@Tag/@Operation/@Parameter/@Schema),使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
**Architecture:** 在不改变现有代码结构和业务逻辑的前提下,为每个 Controller 类添加 `@Tag` 注解、为每个 public 方法添加 `@Operation` 注解、为 `@RequestParam`/`@PathVariable` 参数添加 `@Parameter` 注解、为被 Controller 直接引用的 Request DTO 字段添加 `@Schema` 注解。分 3 批实施,每批完成后编译验证。
@@ -25,12 +25,12 @@
### Task 1.1: AiChatController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiChatController.java`
- Modify: `backend-single/src/main/java/com/com/emotion/dto/request/AiChatRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/AiSummaryRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/GuestChatRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ConversationCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ChatStatsRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiChatController.java`
- Modify: `server/src/main/java/com/com/emotion/dto/request/AiChatRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/AiSummaryRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/GuestChatRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ConversationCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ChatStatsRequest.java`
- [ ] **Step 1: 为 AiChatController 添加 @Tag@Operation 注解**
@@ -162,8 +162,8 @@ public class ChatStatsRequest extends BaseRequest {
### Task 1.2: AiRoutingController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- [ ] **Step 1: 为 AiRoutingController 添加注解**
@@ -215,10 +215,10 @@ public class AiRuntimeRequest {
### Task 1.3: CozeApiCallController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CozeApiCallController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CozeApiCallController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallUpdateRequest.java`
- [ ] **Step 1: 为 CozeApiCallController 添加 @Tag 注解**
@@ -339,10 +339,10 @@ public class CozeApiCallUpdateRequest {
### Task 1.4: CommunityPostController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CommunityPostController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CommunityPostController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -387,10 +387,10 @@ CommunityPostPageRequest 各字段加 @Schema(description = "页码")、@Schema(
### Task 1.5: CommentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CommentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CommentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -429,10 +429,10 @@ public class CommentController {
### Task 1.6: SocialContentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/SocialContentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentManualImportRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentLinkImportRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentApprovalRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/SocialContentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentManualImportRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentLinkImportRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentApprovalRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -476,9 +476,9 @@ public class SocialContentController {
### Task 1.7: SocialInsightController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/SocialInsightController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialInsightGenerateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialInsightUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/SocialInsightController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialInsightGenerateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialInsightUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -514,8 +514,8 @@ public class SocialInsightController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS,无编译错误。
@@ -523,8 +523,8 @@ Expected: BUILD SUCCESS,无编译错误。
- [ ] **Step 2: 提交 Batch 1 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiChatController.java
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiChatController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
# ... 添加 Batch 1 所有修改的文件
git commit -m "feat: Batch 1 — AI + 社区 Controller 中文注解补全"
```
@@ -536,10 +536,10 @@ git commit -m "feat: Batch 1 — AI + 社区 Controller 中文注解补全"
### Task 2.1: EmotionAnalysisController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionAnalysisController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -578,10 +578,10 @@ public class EmotionAnalysisController {
### Task 2.2: EmotionRecordController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionRecordController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionRecordController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -625,9 +625,9 @@ public class EmotionRecordController {
### Task 2.3: EmotionSummaryController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionSummaryController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionSummaryGenerateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionSummaryStatusRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionSummaryController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionSummaryGenerateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionSummaryStatusRequest.java`
- [ ] **Step 1: 补充缺失的 @Operation 注解**
@@ -646,10 +646,10 @@ public class EmotionRecordController {
### Task 2.4: DiaryPostController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DiaryPostController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DiaryPostController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -706,9 +706,9 @@ public class DiaryPostController {
### Task 2.5: DiaryCommentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DiaryCommentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryCommentCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryCommentPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DiaryCommentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryCommentCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryCommentPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -759,10 +759,10 @@ public class DiaryCommentController {
### Task 2.6: GrowthTopicController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/GrowthTopicController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/GrowthTopicController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -793,10 +793,10 @@ public class GrowthTopicController {
### Task 2.7: TopicInteractionController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/TopicInteractionController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/TopicInteractionController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -829,8 +829,8 @@ public class TopicInteractionController {
### Task 2.8: ConversationController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/ConversationController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ConversationPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/ConversationController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ConversationPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -872,11 +872,11 @@ public class ConversationController {
### Task 2.9: MessageController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/MessageController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessagePageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageSearchRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageRecentRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/MessageController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessagePageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageSearchRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageRecentRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -915,10 +915,10 @@ public class MessageController {
### Task 2.10: UserProfileController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java`
- [ ] **Step 1: 补充 @Operation 注解(该 Controller 已有 @Tag 但方法级注解缺失)**
@@ -951,12 +951,12 @@ public class MessageController {
### Task 2.11: AchievementController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AchievementController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementProgressUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementUnlockRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AchievementController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementProgressUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementUnlockRequest.java`
- [ ] **Step 1: 补充 @Parameter 注解(已有 @Tag@Operation,但缺少 @RequestParam@Parameter**
@@ -1042,8 +1042,8 @@ public class MessageController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS
@@ -1051,7 +1051,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 2: 提交 Batch 2 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java
git add server/src/main/java/com/emotion/controller/EmotionAnalysisController.java
# ... 添加 Batch 2 所有修改的文件
git commit -m "feat: Batch 2 — 情绪 + 日记 + 互动 Controller 中文注解补全"
```
@@ -1063,10 +1063,10 @@ git commit -m "feat: Batch 2 — 情绪 + 日记 + 互动 Controller 中文注
### Task 3.1: LifePathController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/LifePathController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/LifePathController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1113,10 +1113,10 @@ public class LifePathController {
### Task 3.2: RewardController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/RewardController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/RewardController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1149,11 +1149,11 @@ public class RewardController {
### Task 3.3: UserStatsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/UserStatsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsIncrementRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsUpdateValueRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/UserStatsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsIncrementRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsUpdateValueRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1194,11 +1194,11 @@ public class UserStatsController {
### Task 3.4: EpicScriptController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EpicScriptController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptInspirationRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EpicScriptController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptInspirationRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1247,10 +1247,10 @@ public class EpicScriptController {
### Task 3.5: DictionaryController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DictionaryController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DictionaryController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1266,10 +1266,10 @@ public class DictionaryController {
### Task 3.6: GuestUserController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/GuestUserController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/GuestUserController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1285,8 +1285,8 @@ public class GuestUserController {
### Task 3.7: TtsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/TtsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/TtsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1302,7 +1302,7 @@ public class TtsController {
### Task 3.8: AsrController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `server/src/main/java/com/emotion/controller/AsrController.java`
- [ ] **Step 1: Controller 注解**
@@ -1318,7 +1318,7 @@ public class AsrController {
### Task 3.9: HealthController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/HealthController.java`
- Modify: `server/src/main/java/com/emotion/controller/HealthController.java`
- [ ] **Step 1: Controller 注解**
@@ -1334,10 +1334,10 @@ public class HealthController {
### Task 3.10: LifeEventController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/LifeEventController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/LifeEventController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1353,8 +1353,8 @@ public class LifeEventController {
### Task 3.11: ChatWebSocketController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/ChatWebSocketController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/WebSocketRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/ChatWebSocketController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/WebSocketRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1370,8 +1370,8 @@ public class ChatWebSocketController {
### Task 3.12: AnalyticsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1387,13 +1387,13 @@ public class AnalyticsController {
### Task 3.13: AiConfigController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiConfigController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigEnableRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigDefaultRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigTestUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiConfigController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigEnableRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigDefaultRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigTestUpdateRequest.java`
- [ ] **Step 1: 补充缺失的 @Operation 注解(已有 @Tag 但方法级注解不全)**
@@ -1404,8 +1404,8 @@ public class AnalyticsController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS
@@ -1413,7 +1413,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 2: 提交 Batch 3 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/LifePathController.java
git add server/src/main/java/com/emotion/controller/LifePathController.java
# ... 添加 Batch 3 所有修改的文件
git commit -m "feat: Batch 3 — 其他 Controller 中文注解补全"
```
@@ -1559,8 +1559,8 @@ private String fieldName;
- [ ] **Step 1: 全量编译**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
- [ ] **Step 2: 启动服务验证 OpenAPI 同步**
@@ -13,10 +13,10 @@
### Task 1: Backend Analytics Dictionary
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/analytics/AnalyticsDictionary.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`
- Create: `server/src/main/java/com/emotion/service/analytics/AnalyticsDictionary.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`
- [x] Add event, event type, page, dimension, value, and API label maps.
- [x] Add DTO fields for Chinese labels while keeping original keys.
@@ -25,8 +25,8 @@
### Task 2: Backend Aggregation Enrichment
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- [x] Query `page_path` and `api_path` for top events.
- [x] Fill `eventLabel`, `eventTypeLabel`, `pageLabel`, `apiLabel` in top events.
@@ -58,7 +58,7 @@
### Task 5: Verification
**Commands:**
- `cd backend-single && mvn test`
- `cd server && mvn test`
- `cd web-admin && npm run build`
- `cd mini-program && npm run build:mp-weixin` or available project build command
- `git diff --check`
@@ -13,10 +13,10 @@
### Task 1: 数据库实体 + Mapper 层
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java`
- Create: `backend-single/src/main/java/com/emotion/entity/ApiParam.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java`
- Create: `server/src/main/java/com/emotion/entity/ApiEndpoint.java`
- Create: `server/src/main/java/com/emotion/entity/ApiParam.java`
- Create: `server/src/main/java/com/emotion/mapper/ApiEndpointMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/ApiParamMapper.java`
- [ ] **Step 1: 创建 ApiEndpoint 实体**
@@ -153,10 +153,10 @@ public interface ApiParamMapper extends BaseMapper<ApiParam> {
- [ ] **Step 4: 提交**
```bash
git add backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java
git add backend-single/src/main/java/com/emotion/entity/ApiParam.java
git add backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java
git add backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java
git add server/src/main/java/com/emotion/entity/ApiEndpoint.java
git add server/src/main/java/com/emotion/entity/ApiParam.java
git add server/src/main/java/com/emotion/mapper/ApiEndpointMapper.java
git add server/src/main/java/com/emotion/mapper/ApiParamMapper.java
git commit -m "feat: 添加接口端点实体和Mapper"
```
@@ -165,11 +165,11 @@ git commit -m "feat: 添加接口端点实体和Mapper"
### Task 2: DTO 层 — Request/Response
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java`
- Create: `server/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java`
- [ ] **Step 1: 创建 ApiEndpointListRequest(分页查询入参)**
@@ -309,11 +309,11 @@ public class ApiTestProxyRequest {
- [ ] **Step 6: 提交**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java
git add backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java
git add server/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java
git add server/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java
git add server/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java
git add server/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java
git add server/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java
git commit -m "feat: 添加接口管理DTO"
```
@@ -322,8 +322,8 @@ git commit -m "feat: 添加接口管理DTO"
### Task 3: Service 层 — 核心解析与同步逻辑
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java`
- Create: `server/src/main/java/com/emotion/service/ApiEndpointService.java`
- Create: `server/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java`
- [ ] **Step 1: 创建 Service 接口**
@@ -676,8 +676,8 @@ public class ApiEndpointServiceImpl implements ApiEndpointService {
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/service/ApiEndpointService.java
git add backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java
git add server/src/main/java/com/emotion/service/ApiEndpointService.java
git add server/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java
git commit -m "feat: 添加接口端点Service层"
```
@@ -686,7 +686,7 @@ git commit -m "feat: 添加接口端点Service层"
### Task 4: ApplicationRunner — 启动自动同步
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java`
- Create: `server/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java`
- [ ] **Step 1: 创建 SyncRunner**
@@ -729,7 +729,7 @@ public class ApiEndpointSyncRunner implements ApplicationRunner {
- [ ] **Step 2: 提交**
```bash
git add backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java
git add server/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java
git commit -m "feat: 添加启动自动同步接口数据"
```
@@ -738,7 +738,7 @@ git commit -m "feat: 添加启动自动同步接口数据"
### Task 5: Controller 层 — 分页查询 + 详情 + 手动同步
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java`
- Create: `server/src/main/java/com/emotion/controller/ApiEndpointController.java`
- [ ] **Step 1: 创建 Controller**
@@ -815,7 +815,7 @@ public class ApiEndpointController {
- [ ] **Step 2: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java
git add server/src/main/java/com/emotion/controller/ApiEndpointController.java
git commit -m "feat: 添加接口管理Controller"
```
@@ -824,8 +824,8 @@ git commit -m "feat: 添加接口管理Controller"
### Task 6: Controller 层 — 代理测试接口
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java`
- Create: `server/src/main/java/com/emotion/controller/ApiTestProxyController.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java`
- [ ] **Step 1: 创建 ApiTestProxyResponse**
@@ -990,8 +990,8 @@ public class ApiTestProxyController {
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java
git add server/src/main/java/com/emotion/controller/ApiTestProxyController.java
git add server/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java
git commit -m "feat: 添加接口代理测试Controller"
```
@@ -1002,8 +1002,8 @@ git commit -m "feat: 添加接口代理测试Controller"
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
预期:BUILD SUCCESS
@@ -15,28 +15,28 @@
### 后端改动文件(按批次)
**Batch 1 (P0):**
- `backend-single/src/main/java/com/emotion/controller/AdminAuthController.java` — 已有 @Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/AuthController.java` — 补全缺少的 @Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/AdminController.java` — 已有 @Tag/@Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/TokenController.java` — 已有 @Operation,检查完整度
- `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/controller/UserController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/controller/UserProfileController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/dto/request/AdminLoginRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminCreateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminPageRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserPageRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/controller/AdminAuthController.java` — 已有 @Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/AuthController.java` — 补全缺少的 @Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/AdminController.java` — 已有 @Tag/@Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/TokenController.java` — 已有 @Operation,检查完整度
- `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/controller/UserController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/controller/UserProfileController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/dto/request/AdminLoginRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminCreateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminPageRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/RegisterRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/LoginRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserCreateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserPageRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java` — 补全 @Schema
### 前端改动文件
@@ -48,8 +48,8 @@
## Task 1: 后端 Batch 1 - AdminAuth + Auth Controller 注解补全
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminAuthController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminAuthController.java`
- Modify: `server/src/main/java/com/emotion/controller/AuthController.java`
- [ ] **Step 1: 查看 AdminAuthController 已有注解**
@@ -85,7 +85,7 @@ AuthController 已有 @Tag。需要补全以下方法的 @Operation:
- [ ] **Step 3: 为 AuthController 缺少的方法补全注解**
修改 `backend-single/src/main/java/com/emotion/controller/AuthController.java`
修改 `server/src/main/java/com/emotion/controller/AuthController.java`
为以下方法添加 @Operation(在方法上方的空白行插入):
@@ -133,7 +133,7 @@ AdminAuthController 的 6 个方法全部有 @Operation,无需修改。
- [ ] **Step 5: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/AuthController.java
git add server/src/main/java/com/emotion/controller/AuthController.java
git commit -m "feat: 补全 AuthController 缺失的 @Operation@Parameter 注解"
```
@@ -142,16 +142,16 @@ git commit -m "feat: 补全 AuthController 缺失的 @Operation 和 @Parameter
## Task 2: 后端 Batch 1 - Admin + Token Controller 注解补全
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/TokenController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/UserController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminController.java`
- Modify: `server/src/main/java/com/emotion/controller/TokenController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/controller/UserController.java`
- Modify: `server/src/main/java/com/emotion/controller/UserProfileController.java`
- [ ] **Step 1: 读取 AdminController**
```bash
cat backend-single/src/main/java/com/emotion/controller/AdminController.java
cat server/src/main/java/com/emotion/controller/AdminController.java
```
检查每个方法的 @Operation 是否有 @Parameter,为路径参数和查询参数补全 @Parameter 注解。
@@ -159,7 +159,7 @@ cat backend-single/src/main/java/com/emotion/controller/AdminController.java
- [ ] **Step 2: 读取 TokenController**
```bash
cat backend-single/src/main/java/com/emotion/controller/TokenController.java
cat server/src/main/java/com/emotion/controller/TokenController.java
```
已有 3 个 @Operation,检查是否缺少 @Parameter,补全。
@@ -194,11 +194,11 @@ public Result<AnalyticsOverviewResponse> overview(@Validated AnalyticsQueryReque
- [ ] **Step 6: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/AdminController.java
git add backend-single/src/main/java/com/emotion/controller/TokenController.java
git add backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java
git add backend-single/src/main/java/com/emotion/controller/UserController.java
git add backend-single/src/main/java/com/emotion/controller/UserProfileController.java
git add server/src/main/java/com/emotion/controller/AdminController.java
git add server/src/main/java/com/emotion/controller/TokenController.java
git add server/src/main/java/com/emotion/controller/AdminAnalyticsController.java
git add server/src/main/java/com/emotion/controller/UserController.java
git add server/src/main/java/com/emotion/controller/UserProfileController.java
git commit -m "feat: 为 Admin/Token/AdminAnalytics/User/UserProfile Controller 补全 OpenAPI 注解"
```
@@ -257,21 +257,21 @@ import io.swagger.v3.oas.annotations.media.Schema;
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminLoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminCreateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminPageRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserPageRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java
git add server/src/main/java/com/emotion/dto/request/LoginRequest.java
git add server/src/main/java/com/emotion/dto/request/RegisterRequest.java
git add server/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminLoginRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminPageRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/UserCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/UserUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/UserPageRequest.java
git add server/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java
git commit -m "feat: 为 Batch 1 请求 DTO 补全 @Schema 中文描述注解"
```
@@ -665,7 +665,7 @@ git commit -m "feat: 测试面板增加请求头展示区域"
- [ ] **Step 1: 本地构建后端**
```bash
cd backend-single
cd server
mvn clean install -DskipTests
```
@@ -19,10 +19,10 @@
| `web-admin/src/components/JsonViewer.vue` | 创建 | 通用 JSON 格式化高亮 + 复制组件 |
| `web-admin/src/views/aiconfig/components/AiCallLogDetailDialog.vue` | 创建 | 调用日志详情弹窗 |
| `web-admin/src/views/aiconfig/AiRoutingList.vue` | 修改 | 调用日志 Tab 增加筛选栏、展开行、分页 |
| `backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java` | 创建 | 日志查询请求 DTO |
| `backend-single/src/main/java/com/emotion/service/AiCallLogService.java` | 修改 | 新增 `query` 方法签名 |
| `backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java` | 修改 | 实现 `query` 分页查询 |
| `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` | 修改 | 新增 POST /call-logs 接口 |
| `server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java` | 创建 | 日志查询请求 DTO |
| `server/src/main/java/com/emotion/service/AiCallLogService.java` | 修改 | 新增 `query` 方法签名 |
| `server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java` | 修改 | 实现 `query` 分页查询 |
| `server/src/main/java/com/emotion/controller/AiRoutingController.java` | 修改 | 新增 POST /call-logs 接口 |
---
@@ -428,18 +428,18 @@ git commit -m "feat: 新增 AI 调用日志详情弹窗组件"
## Task 5: 后端 AiCallLogQueryRequest DTO
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
- [ ] **Step 1: 确认 dto/request/ai 目录存在**
```bash
ls backend-single/src/main/java/com/emotion/dto/request/ai/
ls server/src/main/java/com/emotion/dto/request/ai/
```
如果不存在则创建:
```bash
mkdir -p backend-single/src/main/java/com/emotion/dto/request/ai
mkdir -p server/src/main/java/com/emotion/dto/request/ai
```
- [ ] **Step 2: 创建 AiCallLogQueryRequest.java**
@@ -498,7 +498,7 @@ public class AiCallLogQueryRequest {
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java
git add server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java
git commit -m "feat: 新增 AI 调用日志查询请求 DTO"
```
@@ -507,8 +507,8 @@ git commit -m "feat: 新增 AI 调用日志查询请求 DTO"
## Task 6: 后端 Service 接口和实现
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/AiCallLogService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/AiCallLogService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java`
- [ ] **Step 1: 在 AiCallLogService 接口中新增 query 方法**
@@ -562,8 +562,8 @@ public PageResult<AiCallLog> query(AiCallLogQueryRequest request) {
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/AiCallLogService.java
git add backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java
git add server/src/main/java/com/emotion/service/AiCallLogService.java
git add server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java
git commit -m "feat: AI 调用日志 Service 增加分页查询方法"
```
@@ -572,7 +572,7 @@ git commit -m "feat: AI 调用日志 Service 增加分页查询方法"
## Task 7: 后端 Controller 新增 POST 接口
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [ ] **Step 1: 在 AiRoutingController 中导入新类型**
@@ -599,7 +599,7 @@ public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLog
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
git commit -m "feat: Controller 新增 AI 调用日志分页查询接口"
```
@@ -949,7 +949,7 @@ git commit -m "feat: 调用日志 Tab 增加筛选栏、展开行、分页和详
- [ ] **Step 1: 编译后端**
```bash
cd backend-single
cd server
mvn clean install -DskipTests
```
@@ -970,7 +970,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 1: 启动后端**
```bash
cd backend-single
cd server
mvn spring-boot:run
```
@@ -16,10 +16,10 @@
| 操作 | 文件 | 说明 |
|------|------|------|
| Modify | `backend-single/src/main/java/com/emotion/entity/AiCallLog.java` | 实体新增 `userName` 字段 |
| Create | `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql` | 数据库迁移脚本 |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:68-73` | invokeStream 写入 userName |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:219-225` | invokeEndpointStream 写入 userName |
| Modify | `server/src/main/java/com/emotion/entity/AiCallLog.java` | 实体新增 `userName` 字段 |
| Create | `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql` | 数据库迁移脚本 |
| Modify | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:68-73` | invokeStream 写入 userName |
| Modify | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:219-225` | invokeEndpointStream 写入 userName |
| Modify | `web-admin/src/types/aiconfig.ts` | AiCallLog 接口新增 userName |
| Modify | `web-admin/src/views/aiconfig/AiRoutingList.vue` | 列表新增"调用用户"列 + userDisplay 函数 |
| Modify | `web-admin/src/views/aiconfig/components/AiCallLogDetailDialog.vue` | 详情弹窗"用户 ID"改为"调用用户" |
@@ -29,9 +29,9 @@
### Task 1: 后端实体 + 数据库迁移
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/entity/AiCallLog.java:29-31`
- Create: `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`
- Test: `backend-single/` 目录下执行编译验证
- Modify: `server/src/main/java/com/emotion/entity/AiCallLog.java:29-31`
- Create: `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`
- Test: `server/` 目录下执行编译验证
- [ ] **Step 1: 在 AiCallLog 实体中新增 userName 字段**
@@ -46,7 +46,7 @@ private String userName;
- [ ] **Step 2: 创建数据库迁移脚本**
创建 `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`:
创建 `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`:
```sql
-- 调用日志表新增 user_name 字段
@@ -56,7 +56,7 @@ ALTER TABLE t_ai_call_log ADD COLUMN user_name VARCHAR(100) DEFAULT NULL COMMENT
- [ ] **Step 3: 编译验证后端代码**
```bash
cd backend-single
cd server
mvn clean compile -DskipTests
```
@@ -65,8 +65,8 @@ mvn clean compile -DskipTests
- [ ] **Step 4: 提交**
```bash
git add backend-single/src/main/java/com/emotion/entity/AiCallLog.java
git add backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql
git add server/src/main/java/com/emotion/entity/AiCallLog.java
git add server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql
git commit -m "feat: 调用日志实体新增 userName 字段及数据库迁移脚本"
```
@@ -75,8 +75,8 @@ git commit -m "feat: 调用日志实体新增 userName 字段及数据库迁移
### Task 2: 后端日志保存逻辑写入 userName
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`
- [ ] **Step 1: 在 invokeStream 方法中写入 userName**
@@ -123,7 +123,7 @@ callLog.setUserName(request.getUserName());
- [ ] **Step 3: 更新单元测试,验证 userName 被正确写入**
修改 `backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`,在 `invokeStreamRecoversWhenOutputExists` 测试中:
修改 `server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`,在 `invokeStreamRecoversWhenOutputExists` 测试中:
在已有的 `request.setRequestId("client-request-1");` 之后添加 `request.setUserName("测试用户");`,然后在断言部分新增 userName 的验证:
@@ -138,7 +138,7 @@ assertEquals("测试用户", savedLog.getUserName());
- [ ] **Step 4: 运行测试验证**
```bash
cd backend-single
cd server
mvn test -Dtest=AiRuntimeServiceImplTest
```
@@ -147,13 +147,13 @@ mvn test -Dtest=AiRuntimeServiceImplTest
- [ ] **Step 5: 编译验证 + 提交**
```bash
cd backend-single
cd server
mvn clean compile -DskipTests
```
```bash
git add backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java
git add server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java
git commit -m "feat: 日志保存时写入 userName 字段"
```
@@ -16,28 +16,28 @@
| Action | File | Responsibility |
| --- | --- | --- |
| Create | `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java` | Binds WeChat mini program appid, secret, and API base URL from application config. |
| Create | `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql` | Prevents duplicate non-null WeChat third-party identities. |
| Modify | `backend-single/src/main/resources/application.yml` | Adds default WeChat config keys and public login endpoint allowlist. |
| Modify | `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java` | Excludes `/auth/wechat/login` from JWT interception. |
| Modify | `backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java` | Excludes `/api/auth/wechat/login` from the interceptor's internal public endpoint list. |
| Create | `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java` | Request body for WeChat openid login. |
| Create | `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java` | Request body for WeChat phone binding. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | Internal DTO for WeChat `code2Session` response. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java` | Internal DTO for WeChat access token response. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java` | Internal DTO for WeChat phone number response. |
| Create | `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java` | Interface for WeChat API calls. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | RestTemplate-backed WeChat client with Redis token caching. |
| Create | `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java` | Defines temporary WeChat user to phone user data transfer. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java` | Transfers user-owned rows and avoids singleton row conflicts. |
| Modify | `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java` | Adds non-sensitive `wechatBound` flag for frontend display logic. |
| Modify | `backend-single/src/main/java/com/emotion/service/UserService.java` | Adds third-party identity lookup helper. |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java` | Implements third-party identity lookup. |
| Modify | `backend-single/src/main/java/com/emotion/service/AuthService.java` | Adds WeChat login and phone binding operations. |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` | Implements openid login, phone identity merge/bind, and token issuing reuse. |
| Modify | `backend-single/src/main/java/com/emotion/controller/AuthController.java` | Adds `/auth/wechat/login` and `/auth/wechat/phone`. |
| Test | `backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java` | Verifies controller contract. |
| Test | `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java` | Verifies identity matching and binding behavior. |
| Create | `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java` | Binds WeChat mini program appid, secret, and API base URL from application config. |
| Create | `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql` | Prevents duplicate non-null WeChat third-party identities. |
| Modify | `server/src/main/resources/application.yml` | Adds default WeChat config keys and public login endpoint allowlist. |
| Modify | `server/src/main/java/com/emotion/config/WebMvcConfig.java` | Excludes `/auth/wechat/login` from JWT interception. |
| Modify | `server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java` | Excludes `/api/auth/wechat/login` from the interceptor's internal public endpoint list. |
| Create | `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java` | Request body for WeChat openid login. |
| Create | `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java` | Request body for WeChat phone binding. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | Internal DTO for WeChat `code2Session` response. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java` | Internal DTO for WeChat access token response. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java` | Internal DTO for WeChat phone number response. |
| Create | `server/src/main/java/com/emotion/service/WechatMiniProgramService.java` | Interface for WeChat API calls. |
| Create | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | RestTemplate-backed WeChat client with Redis token caching. |
| Create | `server/src/main/java/com/emotion/service/UserIdentityMergeService.java` | Defines temporary WeChat user to phone user data transfer. |
| Create | `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java` | Transfers user-owned rows and avoids singleton row conflicts. |
| Modify | `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java` | Adds non-sensitive `wechatBound` flag for frontend display logic. |
| Modify | `server/src/main/java/com/emotion/service/UserService.java` | Adds third-party identity lookup helper. |
| Modify | `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java` | Implements third-party identity lookup. |
| Modify | `server/src/main/java/com/emotion/service/AuthService.java` | Adds WeChat login and phone binding operations. |
| Modify | `server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` | Implements openid login, phone identity merge/bind, and token issuing reuse. |
| Modify | `server/src/main/java/com/emotion/controller/AuthController.java` | Adds `/auth/wechat/login` and `/auth/wechat/phone`. |
| Test | `server/src/test/java/com/emotion/controller/AuthControllerTest.java` | Verifies controller contract. |
| Test | `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java` | Verifies identity matching and binding behavior. |
| Modify | `mini-program/src/services/auth.js` | Adds `wechatLogin` and `bindWechatPhone`. |
| Modify | `mini-program/src/stores/app.js` | Adds store methods and tracks returned auth user info. |
| Modify | `mini-program/src/pages/login/index.vue` | Adds WeChat primary login while keeping phone/SMS form. |
@@ -48,20 +48,20 @@
### Task 1: Backend WeChat Configuration And DTOs
**Files:**
- Create: `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`
- Create: `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Create: `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`
- Create: `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`
- Create: `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`
- Modify: `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Modify: `server/src/main/resources/application.yml`
- Test: backend compile
- [ ] **Step 1: Create WeChat configuration properties**
Create `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`:
Create `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`:
```java
package com.emotion.config;
@@ -82,7 +82,7 @@ public class WechatMiniProgramProperties {
- [ ] **Step 2: Create public request DTOs**
Create `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`:
Create `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`:
```java
package com.emotion.dto.request;
@@ -102,7 +102,7 @@ public class WechatLoginRequest {
}
```
Create `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`:
Create `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`:
```java
package com.emotion.dto.request;
@@ -120,7 +120,7 @@ public class WechatPhoneBindRequest {
- [ ] **Step 3: Create internal WeChat API response DTOs**
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -147,7 +147,7 @@ public class WechatCodeSessionResponse {
}
```
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -173,7 +173,7 @@ public class WechatAccessTokenResponse {
}
```
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -213,7 +213,7 @@ public class WechatPhoneNumberResponse {
- [ ] **Step 4: Add WeChat config keys**
In `backend-single/src/main/resources/application.yml`, under `emotion:`, add:
In `server/src/main/resources/application.yml`, under `emotion:`, add:
```yaml
wechat:
@@ -239,7 +239,7 @@ HAVING COUNT(*) > 1;
Expected: no rows. If rows exist, resolve duplicates before adding the index.
Create `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`:
Create `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`:
```sql
ALTER TABLE t_user
@@ -250,7 +250,7 @@ MySQL permits multiple `NULL` values in a unique index, so users without WeChat
- [ ] **Step 6: Add non-sensitive WeChat binding flag**
In `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`, add:
In `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`, add:
```java
/**
@@ -264,7 +264,7 @@ private Boolean wechatBound;
Run:
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -273,15 +273,15 @@ Expected: command exits with status `0`.
- [ ] **Step 8: Commit**
```bash
git add backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
git add backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql
git add backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/main/resources/application.yml
git add server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
git add server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql
git add server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java
git add server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java
git add server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java
git add server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java
git add server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java
git add server/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add server/src/main/resources/application.yml
git commit -m "feat: add wechat mini program auth DTOs and config"
```
@@ -290,13 +290,13 @@ git commit -m "feat: add wechat mini program auth DTOs and config"
### Task 2: Backend WeChat API Client
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`
- Create: `server/src/main/java/com/emotion/service/WechatMiniProgramService.java`
- Create: `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`
- [ ] **Step 1: Create service interface**
Create `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java`:
Create `server/src/main/java/com/emotion/service/WechatMiniProgramService.java`:
```java
package com.emotion.service;
@@ -313,7 +313,7 @@ public interface WechatMiniProgramService {
- [ ] **Step 2: Create RestTemplate implementation**
Create `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`:
Create `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`:
```java
package com.emotion.service.impl;
@@ -434,7 +434,7 @@ public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
- [ ] **Step 3: Create focused unit tests for error behavior**
Create `backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`:
Create `server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`:
```java
package com.emotion.service;
@@ -470,7 +470,7 @@ class WechatMiniProgramServiceImplTest {
- [ ] **Step 4: Run test**
```bash
cd backend-single
cd server
mvn -q -Dtest=WechatMiniProgramServiceImplTest test
```
@@ -479,9 +479,9 @@ Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java
git add backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java
git add server/src/main/java/com/emotion/service/WechatMiniProgramService.java
git add server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java
git commit -m "feat: add wechat mini program API client"
```
@@ -490,13 +490,13 @@ git commit -m "feat: add wechat mini program API client"
### Task 3: Backend User Identity Merge Service
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`
- Create: `server/src/main/java/com/emotion/service/UserIdentityMergeService.java`
- Create: `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`
- [ ] **Step 1: Create merge service interface**
Create `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`:
Create `server/src/main/java/com/emotion/service/UserIdentityMergeService.java`:
```java
package com.emotion.service;
@@ -508,7 +508,7 @@ public interface UserIdentityMergeService {
- [ ] **Step 2: Create JdbcTemplate implementation**
Create `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`:
Create `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`:
```java
package com.emotion.service.impl;
@@ -592,7 +592,7 @@ public class UserIdentityMergeServiceImpl implements UserIdentityMergeService {
- [ ] **Step 3: Add merge service test**
Create `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`:
Create `server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`:
```java
package com.emotion.service;
@@ -626,7 +626,7 @@ class UserIdentityMergeServiceImplTest {
- [ ] **Step 4: Run merge service test**
```bash
cd backend-single
cd server
mvn -q -Dtest=UserIdentityMergeServiceImplTest test
```
@@ -635,9 +635,9 @@ Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java
git add server/src/main/java/com/emotion/service/UserIdentityMergeService.java
git add server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java
git add server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java
git commit -m "feat: add wechat user identity merge service"
```
@@ -646,22 +646,22 @@ git commit -m "feat: add wechat user identity merge service"
### Task 4: Backend User Lookup And Auth Service Methods
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/UserService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/service/AuthService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Test: `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`
- Modify: `server/src/main/java/com/emotion/service/UserService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/AuthService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Test: `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java`
- [ ] **Step 1: Add third-party lookup to UserService**
In `backend-single/src/main/java/com/emotion/service/UserService.java`, add:
In `server/src/main/java/com/emotion/service/UserService.java`, add:
```java
User getByThirdParty(String thirdPartyType, String thirdPartyId);
```
In `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java`, add:
In `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java`, add:
```java
@Override
@@ -679,7 +679,7 @@ public User getByThirdParty(String thirdPartyType, String thirdPartyId) {
- [ ] **Step 2: Add AuthService methods**
In `backend-single/src/main/java/com/emotion/service/AuthService.java`, import the two request DTOs and add:
In `server/src/main/java/com/emotion/service/AuthService.java`, import the two request DTOs and add:
```java
AuthResponse wechatLogin(WechatLoginRequest request);
@@ -858,7 +858,7 @@ public AuthResponse bindWechatPhone(String currentUserId, WechatPhoneBindRequest
- [ ] **Step 8: Compile backend**
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -866,7 +866,7 @@ Expected: command exits with status `0`.
- [ ] **Step 9: Add service tests for identity rules**
Create `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`:
Create `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java`:
```java
package com.emotion.service;
@@ -1002,7 +1002,7 @@ class AuthServiceWechatTest {
- [ ] **Step 10: Run auth tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,UserIdentityMergeServiceImplTest test
```
@@ -1011,12 +1011,12 @@ Expected: command exits with status `0`.
- [ ] **Step 11: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java
git add backend-single/src/main/java/com/emotion/service/AuthService.java
git add backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java
git add server/src/main/java/com/emotion/service/UserService.java
git add server/src/main/java/com/emotion/service/impl/UserServiceImpl.java
git add server/src/main/java/com/emotion/service/AuthService.java
git add server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java
git add server/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add server/src/test/java/com/emotion/service/AuthServiceWechatTest.java
git commit -m "feat: add wechat auth identity binding logic"
```
@@ -1025,10 +1025,10 @@ git commit -m "feat: add wechat auth identity binding logic"
### Task 5: Backend Auth Controller And Security Allowlist
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`
- Modify: `backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java`
- Test: `backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java`
- Modify: `server/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `server/src/main/java/com/emotion/config/WebMvcConfig.java`
- Modify: `server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java`
- Test: `server/src/test/java/com/emotion/controller/AuthControllerTest.java`
- [ ] **Step 1: Add controller endpoints**
@@ -1126,7 +1126,7 @@ public void testWechatPhoneBind() throws Exception {
- [ ] **Step 5: Run controller tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest test
```
@@ -1135,10 +1135,10 @@ Expected: command exits with status `0`.
- [ ] **Step 6: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AuthController.java
git add backend-single/src/main/java/com/emotion/config/WebMvcConfig.java
git add backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java
git add backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java
git add server/src/main/java/com/emotion/controller/AuthController.java
git add server/src/main/java/com/emotion/config/WebMvcConfig.java
git add server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java
git add server/src/test/java/com/emotion/controller/AuthControllerTest.java
git commit -m "feat: expose wechat auth endpoints"
```
@@ -1596,7 +1596,7 @@ git commit -m "feat: add wechat phone binding entry"
- [ ] **Step 1: Run backend auth tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,WechatMiniProgramServiceImplTest,UserIdentityMergeServiceImplTest test
```
@@ -1605,7 +1605,7 @@ Expected: command exits with status `0`.
- [ ] **Step 2: Compile backend**
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -1633,7 +1633,7 @@ Open `mini-program/unpackage/dist/build/mp-weixin` in WeChat DevTools and verify
- [ ] **Step 5: Add README note if config is not documented elsewhere**
If no deployment doc already lists WeChat env vars, add this section to `mini-program/README.md` or `backend-single/部署说明.md`:
If no deployment doc already lists WeChat env vars, add this section to `mini-program/README.md` or `server/部署说明.md`:
```markdown
### WeChat Mini Program Login
@@ -15,10 +15,10 @@
| 文件 | 改动 | 职责 |
|------|------|------|
| `sql/emotion_museum_ddl.sql` | 追加 | 数据库迁移:新增5列 |
| `backend-single/.../entity/UserProfile.java` | 修改 | ORM 实体新增字段 |
| `backend-single/.../request/UserProfileCreateRequest.java` | 修改 | 创建请求 DTO 新增字段 |
| `backend-single/.../request/UserProfileUpdateRequest.java` | 修改 | 更新请求 DTO 新增字段 |
| `backend-single/.../response/UserProfileResponse.java` | 修改 | 响应 DTO 新增字段 |
| `server/.../entity/UserProfile.java` | 修改 | ORM 实体新增字段 |
| `server/.../request/UserProfileCreateRequest.java` | 修改 | 创建请求 DTO 新增字段 |
| `server/.../request/UserProfileUpdateRequest.java` | 修改 | 更新请求 DTO 新增字段 |
| `server/.../response/UserProfileResponse.java` | 修改 | 响应 DTO 新增字段 |
| `mini-program/src/services/userProfile.js` | 修改 | 前后端数据格式双向转换 |
| `mini-program/src/stores/app.js` | 修改 | 全局状态 registrationData 扩展 |
| `mini-program/src/pages/main/RecordView.vue` | 修改 | 去除写死默认值和 sampleEvents |
@@ -57,7 +57,7 @@ git commit -m "db: add city, industry, company, personality_tags, birthday to t_
### Task 2: 后端实体扩展 — UserProfile.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/entity/UserProfile.java`
- Modify: `server/src/main/java/com/emotion/entity/UserProfile.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -100,7 +100,7 @@ git commit -m "db: add city, industry, company, personality_tags, birthday to t_
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/UserProfile.java
git add server/src/main/java/com/emotion/entity/UserProfile.java
git commit -m "feat: extend UserProfile entity with city/industry/company/personalityTags/birthday"
```
@@ -109,7 +109,7 @@ git commit -m "feat: extend UserProfile entity with city/industry/company/person
### Task 3: 后端创建请求 DTO 扩展 — UserProfileCreateRequest.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -145,7 +145,7 @@ git commit -m "feat: extend UserProfile entity with city/industry/company/person
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java
git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/personalityTags/birthday"
```
@@ -154,7 +154,7 @@ git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/
### Task 4: 后端更新请求 DTO 扩展 — UserProfileUpdateRequest.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -190,7 +190,7 @@ git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java
git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/personalityTags/birthday"
```
@@ -199,7 +199,7 @@ git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/
### Task 5: 后端响应 DTO 扩展 — UserProfileResponse.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- Modify: `server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -236,7 +236,7 @@ git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java
git add server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java
git commit -m "feat: extend UserProfileResponse with city/industry/company/personalityTags/birthday"
```
@@ -631,7 +631,7 @@ git commit -m "fix: remove hardcoded defaults from profile edit page"
- [ ] **Step 1: 后端编译验证**
```bash
cd backend-single && mvn compile -q
cd server && mvn compile -q
```
预期:编译成功,无错误。
@@ -20,13 +20,13 @@ purpose: 微信小程序登录修复 spec 的实施计划(TDD + 频繁 commit
| 状态 | 文件 | 责任 | 行数 |
|---|---|---|---|
| 重写 | `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java` | 统一 RestTemplate BeanUser-Agent + 超时 + 拦截器 + 错误处理) | 13 → 40 |
| 新增 | `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | 请求/响应日志(INFO+ 脱敏 bodyDEBUG | 60 |
| 新增 | `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | 4xx/5xx 不抛异常 | 30 |
| 新增 | `backend-single/src/main/java/com/emotion/exception/WechatApiException.java` | 业务异常类(HTTP 风格 code + rawBody | 30 |
| 重写 | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | `code2Session` 五步防御 | 49 → 90 |
| 修改 | `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | 新增 `handleWechatApiException` + 脱敏日志 | +25 |
| 修改 | `backend-single/src/main/resources/application.yml` | 移除 `app-id` / `app-secret` 硬编码默认值 | 2 行 |
| 重写 | `server/src/main/java/com/emotion/config/RestTemplateConfig.java` | 统一 RestTemplate BeanUser-Agent + 超时 + 拦截器 + 错误处理) | 13 → 40 |
| 新增 | `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | 请求/响应日志(INFO+ 脱敏 bodyDEBUG | 60 |
| 新增 | `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | 4xx/5xx 不抛异常 | 30 |
| 新增 | `server/src/main/java/com/emotion/exception/WechatApiException.java` | 业务异常类(HTTP 风格 code + rawBody | 30 |
| 重写 | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | `code2Session` 五步防御 | 49 → 90 |
| 修改 | `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | 新增 `handleWechatApiException` + 脱敏日志 | +25 |
| 修改 | `server/src/main/resources/application.yml` | 移除 `app-id` / `app-secret` 硬编码默认值 | 2 行 |
**总计**:2 重写 + 3 新增 + 2 修改,约 280 行 Java + 2 行 YAML
@@ -38,9 +38,9 @@ purpose: 微信小程序登录修复 spec 的实施计划(TDD + 频繁 commit
cd "G:\IdeaProjects\emotion-museun"
git status # 确认在 master 分支
git log --oneline -1 # 确认最新 commit 是 82c308b
ls backend-single/src/main/java/com/emotion/exception/ # 确认 GlobalExceptionHandler.java 存在
ls backend-single/src/main/java/com/emotion/config/ # 确认 RestTemplateConfig.java 存在
ls backend-single/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniProgramServiceImpl.java 存在
ls server/src/main/java/com/emotion/exception/ # 确认 GlobalExceptionHandler.java 存在
ls server/src/main/java/com/emotion/config/ # 确认 RestTemplateConfig.java 存在
ls server/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniProgramServiceImpl.java 存在
```
期望:全部存在,工作树只有 spec 文件修改(无业务代码污染)
@@ -50,7 +50,7 @@ ls backend-single/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniPr
## Task 1: 创建 WechatApiException 异常类
**Files:**
- Create: `backend-single/src/main/java/com/emotion/exception/WechatApiException.java`
- Create: `server/src/main/java/com/emotion/exception/WechatApiException.java`
- [ ] **Step 1.1: 写入 WechatApiException.java**
@@ -95,7 +95,7 @@ public class WechatApiException extends RuntimeException {
- [ ] **Step 1.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -105,7 +105,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/exception/WechatApiException.java
git add server/src/main/java/com/emotion/exception/WechatApiException.java
git commit -m "feat(wechat): 新增 WechatApiException 业务异常类
- 继承 RuntimeException
@@ -119,12 +119,12 @@ git commit -m "feat(wechat): 新增 WechatApiException 业务异常类
## Task 2: 创建 WechatApiLoggingInterceptor(含脱敏单元测试)
**Files:**
- Create: `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
- Create: `backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
- Create: `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
- Create: `server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
- [ ] **Step 2.1: 写失败测试(验证脱敏逻辑)**
创建 `backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
创建 `server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
```java
package com.emotion.interceptor;
@@ -197,7 +197,7 @@ class WechatApiLoggingInterceptorTest {
- [ ] **Step 2.2: 运行测试验证失败**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatApiLoggingInterceptorTest
```
@@ -205,7 +205,7 @@ mvn test -Dtest=WechatApiLoggingInterceptorTest
- [ ] **Step 2.3: 写最小实现让测试通过**
创建 `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
创建 `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
```java
package com.emotion.interceptor;
@@ -282,7 +282,7 @@ public class WechatApiLoggingInterceptor implements ClientHttpRequestInterceptor
- [ ] **Step 2.4: 运行测试验证通过**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatApiLoggingInterceptorTest
```
@@ -292,8 +292,8 @@ mvn test -Dtest=WechatApiLoggingInterceptorTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java
git add backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java
git add server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java
git add server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java
git commit -m "feat(wechat): 新增 WechatApiLoggingInterceptor 拦截器(含脱敏单元测试)
- ClientHttpRequestInterceptor 实现
@@ -309,7 +309,7 @@ git commit -m "feat(wechat): 新增 WechatApiLoggingInterceptor 拦截器(含
## Task 3: 创建 WechatResponseErrorHandler
**Files:**
- Create: `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java`
- Create: `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java`
- [ ] **Step 3.1: 写入 WechatResponseErrorHandler.java**
@@ -354,7 +354,7 @@ public class WechatResponseErrorHandler extends DefaultResponseErrorHandler {
- [ ] **Step 3.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -364,7 +364,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java
git add server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java
git commit -m "feat(wechat): 新增 WechatResponseErrorHandler
- 继承 DefaultResponseErrorHandler
@@ -378,12 +378,12 @@ git commit -m "feat(wechat): 新增 WechatResponseErrorHandler
## Task 4: 重写 RestTemplateConfig 集成新组件
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java`
- Modify: `server/src/main/java/com/emotion/config/RestTemplateConfig.java`
- [ ] **Step 4.1: 备份并读取原文件**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single\src\main\java\com\emotion\config"
cd "G:\IdeaProjects\emotion-museun\server\src\main\java\com\emotion\config"
copy RestTemplateConfig.java RestTemplateConfig.java.bak
```
@@ -467,8 +467,8 @@ public class RestTemplateConfig {
- [ ] **Step 4.3: mvn 编译验证(确保 7 个其他使用点不受影响)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
mvn clean install -pl :backend-single -am -DskipTests
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean install -pl :server -am -DskipTests
```
期望:`BUILD SUCCESS`,无编译错误(如果 7 个其他使用点有编译错误,说明 RestTemplate API 不兼容,需要回滚)
@@ -476,7 +476,7 @@ mvn clean install -pl :backend-single -am -DskipTests
- [ ] **Step 4.4: 删除备份文件**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single\src\main\java\com\emotion\config"
cd "G:\IdeaProjects\emotion-museun\server\src\main\java\com\emotion\config"
del RestTemplateConfig.java.bak
```
@@ -484,7 +484,7 @@ del RestTemplateConfig.java.bak
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java
git add server/src/main/java/com/emotion/config/RestTemplateConfig.java
git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
解决问题:
@@ -514,8 +514,8 @@ git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
## Task 5: 重写 WechatMiniProgramServiceImpl.code2SessionTDD 10 个场景)
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Create: `backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
- Modify: `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Create: `server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
- [ ] **Step 5.1: 读取当前 WechatMiniProgramServiceImpl.java**
@@ -523,7 +523,7 @@ git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
- [ ] **Step 5.2: 写失败测试(覆盖 spec 10 个场景)**
创建 `backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
创建 `server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
```java
package com.emotion.service.impl;
@@ -659,7 +659,7 @@ class WechatMiniProgramServiceImplTest {
- [ ] **Step 5.3: 运行测试验证失败(确认 service 现有方法未做防御)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatMiniProgramServiceImplTest
```
@@ -667,7 +667,7 @@ mvn test -Dtest=WechatMiniProgramServiceImplTest
- [ ] **Step 5.4: 重写 code2Session 方法(五步防御)**
修改 `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
修改 `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
在 class 顶部添加 imports
```java
@@ -748,7 +748,7 @@ public WechatCodeSessionResponse code2Session(String code) {
- [ ] **Step 5.5: 运行测试验证通过**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatMiniProgramServiceImplTest
```
@@ -758,8 +758,8 @@ mvn test -Dtest=WechatMiniProgramServiceImplTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java
git add server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java
git commit -m "feat(wechat): 重写 code2Session 五步防御
解决问题:
@@ -783,7 +783,7 @@ git commit -m "feat(wechat): 重写 code2Session 五步防御
## Task 6: 修改 application.yml 移除硬编码
**Files:**
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application.yml`
- [ ] **Step 6.1: 定位并修改微信配置**
@@ -820,7 +820,7 @@ $env:WECHAT_MINI_PROGRAM_APP_SECRET = "4f087bd363a4147ef543d634f9f6950d"
- [ ] **Step 6.3: mvn 编译验证(确保 YAML 可加载)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -830,7 +830,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/resources/application.yml
git add server/src/main/resources/application.yml
git commit -m "fix(wechat): 移除 app-id/app-secret 硬编码默认值
安全问题:
@@ -854,7 +854,7 @@ git commit -m "fix(wechat): 移除 app-id/app-secret 硬编码默认值
## Task 7: 修改 GlobalExceptionHandler 添加 WechatApiException handler
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java`
- Modify: `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java`
- [ ] **Step 7.1: 定位文件并添加新方法**
@@ -911,7 +911,7 @@ public Result<Void> handleWechatApiException(WechatApiException e, HttpServletRe
- [ ] **Step 7.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -919,7 +919,7 @@ mvn clean compile -DskipTests
- [ ] **Step 7.3: 写简单测试验证 handler 行为**
创建 `backend-single/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java`
创建 `server/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java`
```java
package com.emotion.exception;
@@ -976,7 +976,7 @@ class GlobalExceptionHandlerWechatTest {
- [ ] **Step 7.4: 运行测试**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=GlobalExceptionHandlerWechatTest
```
@@ -986,8 +986,8 @@ mvn test -Dtest=GlobalExceptionHandlerWechatTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java
git add backend-single/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java
git add server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java
git add server/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java
git commit -m "feat(wechat): GlobalExceptionHandler 新增 WechatApiException handler
解决问题:
@@ -1014,7 +1014,7 @@ git commit -m "feat(wechat): GlobalExceptionHandler 新增 WechatApiException ha
```bash
cd "G:\IdeaProjects\emotion-museun"
mvn clean install -pl :backend-single -am -DskipTests
mvn clean install -pl :server -am -DskipTests
```
期望:`BUILD SUCCESS`
@@ -1023,7 +1023,7 @@ mvn clean install -pl :backend-single -am -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
mvn test -pl :backend-single
mvn test -pl :server
```
期望:所有测试通过,无失败
@@ -0,0 +1,125 @@
# 小程序编辑资料页昵称必填提示优化实现计划
> **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:** 在小程序编辑资料页面,当用户点击保存按钮但昵称未填写时,弹出 Toast 提示「请填写昵称」,避免静默无反馈。
**Architecture:** 在现有 `saveProfile` 方法内部调整校验顺序:先拦截重复提交,再校验昵称非空;校验失败时调用 `uni.showToast` 后返回。不引入新文件,不改动其他字段逻辑。
**Tech Stack:** Vue 3 + UniApp + TypeScript + Pinia
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 编辑资料页面,包含 `saveProfile` 保存方法 | 修改 |
---
### Task 1: 修改 `saveProfile` 校验逻辑
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue:310-328`
- [ ] **Step 1: 查看当前 `saveProfile` 方法**
确认当前代码如下:
```javascript
const saveProfile = async () => {
if (!form.nickname.trim() || saving.value) return
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
saving.value = false
if (!res.success) {
uni.showToast({ title: res.error || '保存失败', icon: 'none' })
return
}
syncFromStore()
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}, 350)
}
```
- [ ] **Step 2: 修改校验顺序并添加 Toast 提示**
将第一行拆分为两步:先判断 `saving.value`,再校验昵称;昵称为空时弹出 Toast 后返回。
修改后代码:
```javascript
const saveProfile = async () => {
if (saving.value) return
if (!form.nickname.trim()) {
uni.showToast({ title: '请填写昵称', icon: 'none' })
return
}
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
saving.value = false
if (!res.success) {
uni.showToast({ title: res.error || '保存失败', icon: 'none' })
return
}
syncFromStore()
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}, 350)
}
```
- [ ] **Step 3: 在 H5 模式下验证**
1. 确保 mini-program H5 开发服务器已启动:
```bash
cd mini-program
npm run dev:h5
```
访问 `http://localhost:5173`
2. 进入编辑资料页面(从「我的」→「个人档案」进入)。
3. 清空昵称输入框,点击「保存」。
4. 期望结果:
- 页面顶部弹出 Toast「请填写昵称」。
- 页面不跳转。
- 按钮文字保持「保存」,不显示「保存中」。
- 浏览器 Console 无报错。
5. 填写昵称后再次点击「保存」。
6. 期望结果:
- 弹出 Toast「已保存」。
- 编辑模式下返回上一页。
- 浏览器 Console 无报错。
- [ ] **Step 4: 提交代码**
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "fix:编辑资料页昵称未填时增加 Toast 提示"
```
---
## Self-Review
1. **Spec coverage:** 设计文档要求「昵称未填时点击保存给出 Toast 提示」,Task 1 Step 2 直接实现该逻辑。
2. **Placeholder scan:** 无 TBD、TODO 或模糊描述,代码和命令均已给出。
3. **Type consistency:** 仅使用现有 `form.nickname``saving.value``uni.showToast`,与源码保持一致。
@@ -0,0 +1,218 @@
# 去除编辑资料页与剧本列表 mock 数据实现计划
> **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:** 将编辑资料页顶部默认昵称从「Zoey」改为「未设置昵称」,并彻底删除剧本列表中的 `fallbackScripts` 数组及各工具函数的兜底默认值。
**Architecture:** 直接修改两个 Vue 文件:在 `onboarding/index.vue` 替换默认文案;在 `ScriptLibraryView.vue` 删除 `fallbackScripts`、让 `scripts` 直接返回 `store.scripts`,并清理 `getTags``getWordCount``getDateText``getProgress``getChapterCount` 中的默认假值。
**Tech Stack:** Vue 3 + UniApp + TypeScript + Pinia
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 编辑资料页面顶部预览卡片 | 修改 |
| `mini-program/src/pages/main/ScriptLibraryView.vue` | 剧本列表页面 | 修改 |
---
### Task 1: 修改编辑资料页顶部默认昵称
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue:22`
- [ ] **Step 1: 查看当前代码**
确认第 22 行代码:
```vue
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
```
- [ ] **Step 2: 替换默认文案**
将第 22 行改为:
```vue
<text class="hero-name">{{ form.nickname || '未设置昵称' }}</text>
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "fix:编辑资料页无昵称时显示未设置昵称"
```
---
### Task 2: 删除剧本列表 fallbackScripts 数组
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue:178-254`
- [ ] **Step 1: 查看当前代码**
确认第 178249 行为 `fallbackScripts` 数组:
```javascript
const fallbackScripts = [
{
id: 'demo-1',
title: '逆袭人生:从低谷到巅峰',
// ...
},
// ... 共 6 条
]
```
确认第 251254 行为 `scripts` computed
```javascript
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
```
- [ ] **Step 2: 删除 fallbackScripts 并修改 scripts computed**
删除 `fallbackScripts` 数组及其后的空行,将 `scripts` computed 改为:
```javascript
const scripts = computed(() => store.scripts || [])
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "fix:剧本列表删除 fallbackScripts 假数据"
```
---
### Task 3: 清理剧本列表工具函数默认值
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue:297-317`
- [ ] **Step 1: 查看当前工具函数代码**
确认以下函数当前实现:
```javascript
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return [script.style || '逆袭成长', '都市', '事业', '热血']
}
const getChapterCount = (script) => script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (!count) return '3.1万字'
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}字`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || '2025.05.10'}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || '2025.05.08'}`
return `最近更新:${script.updatedAt || script.date || '今天 21:30'}`
}
const getProgress = (script) => Math.max(1, Math.min(99, Number(script.progress || 28)))
```
- [ ] **Step 2: 修改各函数去掉兜底假值**
将上述函数改为:
```javascript
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return []
}
const getChapterCount = (script) => script.chapterCount || script.chapters || 0
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}字`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || ''}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || ''}`
return `最近更新:${script.updatedAt || script.date || ''}`
}
const getProgress = (script) => Math.max(0, Math.min(99, Number(script.progress || 0)))
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "fix:剧本列表工具函数去除默认值兜底"
```
---
### Task 4: 编译与 H5 验证
**Files:**
- 无新增/修改文件
- [ ] **Step 1: 运行 H5 生产构建**
```bash
cd mini-program
npm run build:h5
```
期望输出包含 `DONE Build complete.`,无编译错误。
- [ ] **Step 2: 启动 H5 开发服务器**
确保 5173/5175 端口未被占用,然后:
```bash
cd mini-program
npm run dev:h5
```
- [ ] **Step 3: 验证编辑资料页**
1. 在浏览器中打开 `http://localhost:5173`(或实际分配的端口)。
2. 登录后进入编辑资料页面。
3. 清空昵称输入框,点击保存,确认顶部显示「未设置昵称」。
- [ ] **Step 4: 验证剧本列表空状态**
1. 从爽文生成页面点击左上角「历史」按钮进入剧本列表。
2. 如果当前账号没有剧本数据,确认页面显示「还没有人生剧本」空状态,不展示任何 demo 剧本。
3. 如果账号有真实剧本数据,确认真实数据正常显示。
- [ ] **Step 5: 检查浏览器 Console**
确认没有新增的 JavaScript 错误。
---
## Self-Review
1. **Spec coverage:**
- 编辑资料页默认昵称 → Task 1
- 删除 fallbackScripts → Task 2
- 清理工具函数默认值 → Task 3
- 编译与验证 → Task 4
2. **Placeholder scan:** 无 TBD、TODO、"implement later" 或模糊描述。
3. **Type consistency:** 所有函数签名、字段名与源码保持一致。
@@ -0,0 +1,285 @@
# ScriptView 对话结果页 UI 调整实现计划
> **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:** 让 ScriptView.vue 结果页顶部的历史按钮和关闭按钮不随内容滚动,底部输入框增加左右边距,语音按钮改为麦克风图标。
**Architecture:** 将 `result-top-actions``scroll-view` 内部移到外部作为独立层,利用 flex 布局使其固定;调整 `.result-chat-bar` 内边距和语音按钮内容。
**Tech Stack:** Vue 3 + UniApp + CSS (rpx)
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/main/ScriptView.vue` | 剧本生成/对话结果页 | 修改 |
---
### Task 1: 将顶部操作栏移出滚动区域
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:135-158`
- [ ] **Step 1: 查看当前结构**
当前 `result-page` 结构(关键部分):
```vue
<view v-else class="result-page">
<scroll-view
class="result-view"
scroll-y
...
>
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
...
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
...
</view>
</scroll-view>
...
</view>
```
- [ ] **Step 2: 移动 result-top-actions 到 scroll-view 外部**
`result-top-actions` 块从 `result-scroll-content` 中移出,放到 `scroll-view` 之前:
```vue
<view v-else class="result-page">
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
<view class="history-lines">
<view></view>
<view></view>
<view></view>
</view>
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
<scroll-view
class="result-view"
scroll-y
:scroll-top="resultCommandScrollTop"
:scroll-into-view="resultScrollTarget"
:scroll-with-animation="true"
:enhanced="true"
:show-scrollbar="false"
@scroll="handleResultScroll"
>
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
...
</view>
</scroll-view>
...
</view>
```
- [ ] **Step 3: 调整 result-scroll-content 的 padding**
`.result-scroll-content``padding: 0 0 220rpx;`。由于顶部栏已移出,内容顶部不再需要额外留白,保持 `padding-bottom: 220rpx` 即可:
```css
.result-scroll-content {
min-height: 100%;
box-sizing: border-box;
padding: 0 0 220rpx;
}
```
- [ ] **Step 4: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 结果页顶部操作栏固定不随滚动"
```
---
### Task 2: 底部输入框增加左右边距
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:3021-3034`
- [ ] **Step 1: 查看当前样式**
```css
.result-chat-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
min-height: 104rpx;
display: flex;
align-items: flex-end;
gap: 12rpx;
padding: 14rpx 0 22rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(5, 2, 13, 0), rgba(5, 2, 13, 0.9) 30%, rgba(5, 2, 13, 0.96));
}
```
- [ ] **Step 2: 修改 padding 增加左右边距**
`padding: 14rpx 0 22rpx` 改为 `padding: 14rpx 24rpx 22rpx`
```css
.result-chat-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
min-height: 104rpx;
display: flex;
align-items: flex-end;
gap: 12rpx;
padding: 14rpx 24rpx 22rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(5, 2, 13, 0), rgba(5, 2, 13, 0.9) 30%, rgba(5, 2, 13, 0.96));
}
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 底部输入框区域增加左右边距"
```
---
### Task 3: 语音按钮改为麦克风图标
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:266-275`
- Modify: `mini-program/src/pages/main/ScriptView.vue:3036-3056`
- [ ] **Step 1: 修改语音按钮内容**
将:
```vue
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text>语音</text>
</view>
```
改为:
```vue
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text class="voice-icon">🎤</text>
</view>
```
- [ ] **Step 2: 增加语音图标样式**
`.chat-voice-btn` 样式后增加:
```css
.voice-icon {
font-size: 32rpx;
line-height: 1;
}
```
并修改按下状态的颜色继承:
```css
.chat-voice-btn.pressing,
.chat-voice-btn.recognizing {
color: #fff;
background: linear-gradient(145deg, #934dff, #4d1ccb);
box-shadow: 0 0 30rpx rgba(168, 85, 247, 0.38);
}
```
`.chat-voice-btn` 本身已有 `color: #e8ccff;`,图标会继承该颜色;按下时父元素颜色变为 `#fff`,图标也会变白。
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 语音按钮改为麦克风图标"
```
---
### Task 4: 编译与 H5 验证
**Files:**
- 无新增/修改文件
- [ ] **Step 1: 运行 H5 生产构建**
```bash
cd mini-program
npm run build:h5
```
期望输出包含 `DONE Build complete.`,无编译错误。
- [ ] **Step 2: 启动 H5 开发服务器**
确保端口未被占用,然后:
```bash
cd mini-program
npm run dev:h5
```
- [ ] **Step 3: 验证顶部按钮固定**
1. 登录后进入爽文生成页。
2. 从历史列表点击一个已有剧本,进入对话结果页。
3. 上下滚动剧本内容,确认左上角「历史」按钮和右上角「×」按钮始终保持在原位,不随内容滚动。
- [ ] **Step 4: 验证底部输入框样式**
1. 确认底部输入框区域左右与屏幕边缘有 24rpx 边距。
2. 确认语音按钮显示为麦克风图标,默认颜色 `#e8ccff`,按下时变为白色。
3. 确认发送按钮大小不变,可正常点击发送消息。
- [ ] **Step 5: 检查浏览器 Console**
确认没有新增的 JavaScript 错误。
---
## Self-Review
1. **Spec coverage:**
- 顶部按钮固定 → Task 1
- 底部边距 → Task 2
- 麦克风图标 → Task 3
- 编译与验证 → Task 4
2. **Placeholder scan:** 无 TBD、TODO、"implement later" 或模糊描述。
3. **Type consistency:** 所有 class 名、事件名与源码保持一致。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,679 @@
# 小程序添加标签按钮简化与自定义弹窗 实现计划
> **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:** 简化编辑资料页"添加"按钮文字、收紧字数限制到 4 字、标签不换行省略号截断、用自定义深色太空主题弹窗组件替换 `uni.showModal`
**Architecture:** 新建 `TagDialog.vue` 组件支持 input / confirm 双模式,样式与小程序深色太空主题一致;编辑资料页和管理页分别引入该组件,重构 `addCustomTag``confirmDelete``uni.showModal` 切换到自定义弹窗;标签样式增加 `white-space: nowrap` 三件套防止换行。
**Tech Stack:** UniApp 3 + Vue 3 Composition API (`<script setup>`),样式用 rpx,深色太空主题(glass-card 毛玻璃 + 紫色渐变 + 金色强调)。
**Spec 文档:** `docs/superpowers/specs/2026-06-24-tag-add-button-simplify-design.md`
---
## 文件结构总览
### 新增文件
| 文件 | 职责 |
|---|---|
| `mini-program/src/components/TagDialog.vue` | 自定义弹窗组件,input / confirm 双模式,深色太空主题样式 |
### 修改文件
| 文件 | 修改内容 |
|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 按钮文字简化为" 添加"、引入 TagDialog、重构 addCustomTag、字数限制 8→4、toast 文案、`.tag-choice` 不换行 |
| `mini-program/src/pages/onboarding/tag-manage.vue` | 引入 TagDialog、重构 confirmDelete、`.tag-text` 不换行 |
---
## Task 1: 新建 TagDialog.vue 组件
**目标:** 创建自定义弹窗组件,支持 input(输入)和 confirm(确认)两种模式,样式与小程序深色太空主题一致。
**Files:**
- Create: `mini-program/src/components/TagDialog.vue`
### Step 1: 创建组件文件
创建 `mini-program/src/components/TagDialog.vue`,完整内容:
```vue
<template>
<view v-if="visible" class="tag-dialog-mask" @click="onMaskClick">
<view class="tag-dialog-card glass-card" @click.stop>
<view class="tag-dialog-title-row">
<text class="tag-dialog-gold">✦</text>
<text class="tag-dialog-title">{{ title }}</text>
</view>
<input
v-if="mode === 'input'"
class="tag-dialog-input"
:value="modelValue"
:placeholder="placeholder"
placeholder-class="tag-dialog-placeholder"
:maxlength="4"
@input="onInput"
@confirm="onInputConfirm"
/>
<text v-if="mode === 'confirm'" class="tag-dialog-content">{{ content }}</text>
<view class="tag-dialog-actions">
<view class="tag-dialog-btn tag-dialog-btn-cancel" @click="onCancel">
<text>取消</text>
</view>
<view class="tag-dialog-btn tag-dialog-btn-confirm" @click="onConfirm">
<text>确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
visible: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'input'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['confirm', 'cancel', 'update:modelValue', 'update:visible'])
const onInput = (event) => {
const value = event.detail.value
emit('update:modelValue', value)
}
const onInputConfirm = () => {
emit('confirm', props.modelValue)
}
const onConfirm = () => {
if (props.mode === 'input') {
emit('confirm', props.modelValue)
} else {
emit('confirm')
}
}
const onCancel = () => {
emit('cancel')
emit('update:visible', false)
}
const onMaskClick = () => {
emit('cancel')
emit('update:visible', false)
}
</script>
<style scoped>
.tag-dialog-mask {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(3, 2, 13, 0.72);
}
.tag-dialog-card {
width: 560rpx;
box-sizing: border-box;
padding: 36rpx 32rpx 28rpx;
border-radius: 22rpx;
border: 1rpx solid rgba(155, 110, 255, 0.14);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.1), transparent 34%),
rgba(10, 13, 43, 0.54);
box-shadow: inset 0 0 30rpx rgba(123, 60, 255, 0.05), 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.tag-dialog-title-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
}
.tag-dialog-gold {
color: #ffd58c;
font-size: 26rpx;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.45);
}
.tag-dialog-title {
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.tag-dialog-input {
width: 100%;
box-sizing: border-box;
height: 80rpx;
margin-bottom: 28rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid rgba(151, 111, 255, 0.22);
background: rgba(10, 12, 40, 0.66);
color: #fff;
font-size: 26rpx;
}
.tag-dialog-placeholder {
color: rgba(214, 204, 235, 0.42);
}
.tag-dialog-content {
display: block;
margin-bottom: 32rpx;
color: rgba(239, 232, 255, 0.86);
font-size: 26rpx;
line-height: 1.6;
}
.tag-dialog-actions {
display: flex;
gap: 20rpx;
}
.tag-dialog-btn {
flex: 1;
height: 72rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
}
.tag-dialog-btn-cancel {
color: rgba(239, 232, 255, 0.82);
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
}
.tag-dialog-btn-confirm {
color: #fff;
border: 1rpx solid rgba(206, 82, 255, 0.95);
background: linear-gradient(180deg, rgba(169, 61, 255, 0.62), rgba(107, 41, 206, 0.5));
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.52), inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
</style>
```
### Step 2: 提交
```bash
git add mini-program/src/components/TagDialog.vue
git commit -m "feat:新增 TagDialog 自定义弹窗组件,深色太空主题样式"
```
---
## Task 2: 编辑资料页改动(index.vue
**目标:** 简化添加按钮文字、引入 TagDialog、重构 addCustomTag 用自定义弹窗、字数限制收紧到 4、标签不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue`
### Step 1: 模板 — 简化按钮文字
修改 `mini-program/src/pages/onboarding/index.vue`
找到性格标签 panel 末尾占位(约第 146 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加标签</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加</text>
```
找到兴趣爱好 panel 末尾占位(约第 167 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加兴趣</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加</text>
```
### Step 2: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 185-186 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 3: script — 引入组件
找到 import 区(约第 189-193 行):
```javascript
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 4: script — 新增弹窗响应式状态
找到 `const saving = ref(false)` 这一行(约第 198 行附近,在 `const isEdit = ref(false)` 之后),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input')
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null)
```
### Step 5: script — 重构 addCustomTag 并新增 handleAddTag / onDialogConfirm / onDialogCancel
找到现有的 `addCustomTag` 函数(约第 410-449 行,从 `/**` 注释到 `}` 结束):
```javascript
/**
* 新增自定义标签(性格标签 / 兴趣爱好通用)
* @param {string} type 'personality' 或 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
const title = isPersonality ? '添加性格标签' : '添加兴趣爱好'
uni.showModal({
title,
editable: true,
placeholderText: '请输入标签(最多8个字符)',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm) return
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 8) {
uni.showToast({ title: '标签最多8个字符', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
// 新标签插入到库数组最前面
library.unshift(value)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
})
}
```
完整替换为以下四个函数:
```javascript
/**
* 打开添加标签弹窗
* @param {string} type 'personality' 或 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
dialogMode.value = 'input'
dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
dialogPlaceholder.value = '请输入标签(最多4个字)'
dialogInput.value = ''
dialogAction.value = { kind: 'add', type }
dialogVisible.value = true
}
/**
* 处理添加标签的实际逻辑(字数限制 4 字)
*/
const handleAddTag = (type, rawValue) => {
const value = String(rawValue || '').trim()
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 4) {
uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
// 新标签插入到库数组最前面
library.unshift(value)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
}
dialogAction.value = null
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
```
### Step 6: 样式 — .tag-choice 不换行
找到 `.tag-choice` 样式(约第 1008-1011 行):
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
}
```
改为:
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 7: 提交
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "feat:编辑页添加按钮简化为添加,字数限制4字,标签不换行,接入自定义弹窗"
```
---
## Task 3: 管理页改动(tag-manage.vue
**目标:** 引入 TagDialog、重构 confirmDelete 用自定义弹窗、标签文字不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/tag-manage.vue`
### Step 1: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 44-46 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model:visible="dialogVisible"
mode="confirm"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 2: script — 引入组件
找到 import 区(约第 48-51 行):
```javascript
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 3: script — 新增弹窗响应式状态
找到 `const type = ref('personality')` 这一行(约第 56 行),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```
### Step 4: script — 重构 confirmDelete 并新增 onDialogConfirm / onDialogCancel
找到现有的 `confirmDelete` 函数(约第 127-139 行):
```javascript
/**
* 删除确认
*/
const confirmDelete = (tag) => {
uni.showModal({
title: '确认删除',
content: `确定删除「${tag}」吗?删除后不可恢复`,
success: (res) => {
if (!res.confirm) return
deleteTag(tag)
}
})
}
```
完整替换为以下三个函数:
```javascript
/**
* 打开删除确认弹窗
*/
const confirmDelete = (tag) => {
pendingTag.value = tag
dialogContent.value = `确定删除「${tag}」吗?删除后不可恢复`
dialogVisible.value = true
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = () => {
if (pendingTag.value) {
deleteTag(pendingTag.value)
pendingTag.value = null
}
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
pendingTag.value = null
dialogVisible.value = false
}
```
### Step 5: 样式 — .tag-text 不换行
找到 `.tag-text` 样式(约第 311-314 行):
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
}
```
改为:
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 6: 提交
```bash
git add mini-program/src/pages/onboarding/tag-manage.vue
git commit -m "feat:管理页删除确认接入自定义弹窗,标签文字不换行"
```
---
## Task 4: H5 浏览器验证
**目标:** 在 H5 模式下验证 spec 的 8 条验收标准。
### Step 1: 确认 H5 dev server 运行
```bash
python dev-services.py status
```
确认 `Emotion-museum-uniapp 前端` 状态为 `运行中`(端口 5175)。若未运行:
```bash
cd mini-program
npm run dev:h5
```
### Step 2: 逐条验证验收标准
在浏览器中打开 `http://localhost:5175`,登录后进入编辑资料页(`?edit=1`):
| # | 验收标准 | 验证方法 |
|---|---|---|
| 1 | 添加按钮只显示"+ 添加" | 观察性格标签、兴趣爱好 panel 末尾占位文字 |
| 2 | 点击"+ 添加"弹出深色太空主题弹窗 | 点击按钮,验证弹窗为毛玻璃卡片 + 紫色渐变按钮 + 金色 ✦,非白底蓝按钮 |
| 3 | 输入 5 字 toast 提示"内容不能超过4个字" | 输入 5 个字点确定,验证 toast 文案 |
| 4 | 输入 4 字以内新标签,确认后出现在最前 | 输入"测试"点确定,验证标签出现在列表第一位 |
| 5 | 标签过长时省略号截断不换行 | 输入 4 字标签,观察窄屏下是否单行省略号 |
| 6 | 管理页删除弹窗为深色主题 | 点"管理"进入管理页,点 × 或左滑,验证弹窗样式与文案 |
| 7 | 取消按钮和遮罩层点击都能关闭弹窗 | 分别测试取消按钮、点击遮罩层,验证弹窗关闭且不触发动作 |
| 8 | 弹窗 H5 下样式与深色主题一致 | 整体视觉检查 |
### Step 3: 验收通过后报告
所有 8 条通过后,确认工作区干净:
```bash
git status
```
---
## 执行顺序建议
1. Task 1(新建组件)→ Task 2(编辑页接入)→ Task 3(管理页接入)→ Task 4(验证)
2. Task 1 必须先完成,Task 2/3 依赖组件存在。
3. Task 2/3 完成后,H5 dev server 热加载自动生效,可直接进入 Task 4 验证。
@@ -0,0 +1,467 @@
---
author: claude
created_at: 2026-06-27
purpose: AI 消息卡片功能按钮统一实现计划
---
# AI 消息卡片功能按钮统一实现计划
> **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:** 统一小程序实现心愿页面中所有 AI 消息卡片的功能按钮,并修复 story-card 展开失效 bug
**Architecture:** 在现有单文件组件 `ScriptView.vue` 内修复 + 扩展,不提取独立组件,保持现有文件结构
**Tech Stack:** Vue 3, UniApp, 微信小程序
---
## 文件结构
**修改:**
- `mini-program/src/pages/main/ScriptView.vue` - 主页面组件(模板 + 脚本 + 样式)
---
### Task 1: 修复 story-card 展开失效 bug
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:192-210`
- [ ] **Step 1: 移除 resultHasScriptReplies 条件包裹**
打开 `mini-program/src/pages/main/ScriptView.vue`,找到第 192 行的 `<template v-if="!resultHasScriptReplies">`,删除这个条件包裹标签和对应的 `</template>` 闭合标签(第 210 行)。
修改前:
```vue
<template v-if="!resultHasScriptReplies">
<scroll-view
v-if="storyCollapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">
<text class="collapse-row-text">{{ storyCollapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</view>
</template>
```
修改后:
```vue
<scroll-view
v-if="storyCollapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">
<text class="collapse-row-text">{{ storyCollapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</view>
```
- [ ] **Step 2: 验证修复**
在微信开发者工具中编译运行,进入「实现心愿」页面:
1. 输入心愿并生成故事
2. 点击「继续生成」创建 script 类型消息
3. 点击 story-card 的展开/收起按钮,验证故事正文能正确展开和收起
4. 确认控制台无报错
- [ ] **Step 3: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复 story-card 展开失效 bug
移除 resultHasScriptReplies 条件包裹,让故事正文始终由 storyCollapsed 控制展开/收起"
```
---
### Task 2: 添加新方法支持消息卡片功能
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue` (script 部分)
- [ ] **Step 1: 添加 copyMessageContent 方法**
`toggleMessageCollapse` 方法之后(约第 450 行),添加复制消息内容的方法:
```javascript
const copyMessageContent = (message) => {
const content = message?.content || ''
if (!content.trim()) {
uni.showToast({ title: '暂无可复制内容', icon: 'none' })
return
}
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
analytics.track('script_message_copy_click', {
message_id: message?.id || '',
content_length: content.length
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 2: 添加 playMessageTts 方法**
`copyMessageContent` 方法之后,添加播放 TTS 的方法:
```javascript
const playMessageTts = (message) => {
const scriptId = currentResult.value?.id || ''
ttsPlayer.playSource(scriptId)
analytics.track('script_message_tts_click', {
message_id: message?.id || '',
script_id: scriptId
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 3: 修改 toggleMessageCollapse 方法添加埋点**
找到 `toggleMessageCollapse` 方法(约第 442-450 行),在方法开头添加埋点代码:
修改前:
```javascript
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value[message.id] = !isMessageCollapsed(message)
}
```
修改后:
```javascript
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value[message.id] = !isMessageCollapsed(message)
analytics.track('script_message_collapse_toggle', {
message_id: message?.id || '',
collapsed: collapsedMessageIds.value[message.id]
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 4: 验证方法添加**
在微信开发者工具中编译运行,确认控制台无语法错误。新方法暂时不会被调用,后续任务会在模板中使用。
- [ ] **Step 5: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 添加消息卡片功能方法
- copyMessageContent: 复制消息内容到剪贴板
- playMessageTts: 播放故事 TTS 音频
- toggleMessageCollapse: 添加展开/收起埋点"
```
---
### Task 3: 在 result-chat-list 中添加功能按钮组
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:245-270`
- [ ] **Step 1: 在 assistant 消息中添加功能按钮组**
找到 `result-chat-list` 部分(约第 245-270 行),在消息内容和展开/收起按钮之后、时间戳之前,添加功能按钮组。
修改前:
```vue
<view
v-for="(message, index) in resultMessages"
:key="message.id"
class="result-chat-list"
>
<view
v-if="isAssistantMessage(message)"
class="chat-bubble assistant"
>
<text class="message-content">{{ getMessageContent(message) }}</text>
<view class="message-toggle" @click="toggleMessageCollapse(message)">
<text>{{ isMessageCollapsed(message) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(message) }">
<view></view>
<view></view>
</view>
</view>
<text class="message-time">{{ formatMessageTime(message.createdAt) }}</text>
</view>
<!-- user message -->
</view>
```
修改后:
```vue
<view
v-for="(message, index) in resultMessages"
:key="message.id"
class="result-chat-list"
>
<view
v-if="isAssistantMessage(message)"
class="chat-bubble assistant"
>
<text class="message-content">{{ getMessageContent(message) }}</text>
<view class="message-toggle" @click="toggleMessageCollapse(message)">
<text>{{ isMessageCollapsed(message) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(message) }">
<view></view>
<view></view>
</view>
</view>
<view class="message-actions">
<view class="action-btn" @click="copyMessageContent(message)">
<text>复制</text>
</view>
<view class="action-btn" @click="changeDirection">
<text>换个方向</text>
</view>
<view class="action-btn" @click="notLikeMe">
<text>不像我</text>
</view>
<view class="action-btn" @click="continueInChat">
<text>继续生成</text>
</view>
<view class="action-btn primary" @click="playMessageTts(message)">
<text>▶ 播放</text>
</view>
</view>
<text class="message-time">{{ formatMessageTime(message.createdAt) }}</text>
</view>
<!-- user message -->
</view>
```
**说明:**
- 按钮组使用 `.message-actions` 容器,2 列网格布局
- 复制、换个方向、不像我、继续生成使用 `.action-btn` 样式
- 播放按钮使用 `.action-btn.primary` 样式(主色调突出)
- 所有按钮都调用 Task 2 中定义的方法或复用现有方法
- [ ] **Step 2: 验证按钮组显示**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面,生成故事
2. 点击「继续生成」创建 assistant 消息
3. 验证每条 assistant 消息下方显示 5 个功能按钮(复制、换个方向、不像我、继续生成、播放)
4. 验证按钮布局为 2 列网格
- [ ] **Step 3: 验证按钮功能**
逐个测试按钮功能:
1. **复制**:点击后应复制消息内容,显示「已复制」提示
2. **换个方向**:点击后应进入「换方向」修订确认流程
3. **不像我**:点击后应进入「不像我」修订确认流程
4. **继续生成**:点击后应收起故事并聚焦输入框
5. **播放**:点击后应播放故事 TTS 音频
确认控制台无报错,所有功能正常。
- [ ] **Step 4: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 在 result-chat-list 中添加功能按钮组
为每条 assistant 消息添加完整功能按钮:
- 复制:复制消息内容到剪贴板
- 换个方向:进入换方向修订确认
- 不像我:进入不像我修订确认
- 继续生成:收起故事并聚焦输入框
- 播放:播放故事 TTS 音频"
```
---
### Task 4: 添加消息按钮组样式
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue` (style 部分)
- [ ] **Step 1: 添加 .message-actions 样式**
`<style scoped>` 部分(约第 1500 行之后),找到 `.message-toggle` 样式附近,添加按钮组样式:
```css
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
margin-top: 24rpx;
}
.message-actions .action-btn {
padding: 16rpx 24rpx;
background: rgba(255, 255, 255, 0.08);
border: 2rpx solid rgba(255, 255, 255, 0.15);
border-radius: 16rpx;
color: #e0e0e0;
font-size: 24rpx;
text-align: center;
transition: all 0.2s;
}
.message-actions .action-btn:active {
background: rgba(255, 255, 255, 0.12);
transform: scale(0.98);
}
.message-actions .action-btn.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: transparent;
color: #ffffff;
font-weight: 500;
}
.message-actions .action-btn.primary:active {
opacity: 0.85;
}
```
- [ ] **Step 2: 验证样式效果**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面,生成故事
2. 点击「继续生成」创建 assistant 消息
3. 验证按钮样式:
- 按钮组为 2 列网格布局
- 普通按钮为半透明白色背景,圆角边框
- 播放按钮为主色调渐变(紫色)
- 点击时有缩放反馈效果
4. 验证按钮在不同屏幕尺寸下的响应式表现
- [ ] **Step 3: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: 添加消息按钮组样式
- .message-actions: 2 列网格布局,间距 16rpx
- .action-btn: 半透明白色背景,圆角边框,点击反馈
- .action-btn.primary: 主色调渐变(紫色),白色文字"
```
---
### Task 5: 最终验证和边界情况测试
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 测试 story-card 展开/收起**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面
2. 输入心愿并生成故事
3. 点击 story-card 的「展开全文」按钮,验证故事正文展开
4. 点击「收起全文」按钮,验证故事正文收起
5. 点击「继续生成」创建 script 类型消息
6. 再次点击 story-card 的展开/收起按钮,验证功能正常(之前会失效)
**预期结果:** 所有展开/收起操作正常工作
- [ ] **Step 2: 测试 assistant 消息功能按钮**
继续在上一步的基础上:
1. 验证每条 assistant 消息下方显示 5 个功能按钮
2. 点击「复制」按钮,验证复制成功提示
3. 点击「换个方向」按钮,验证进入修订确认流程
4. 点击「不像我」按钮,验证进入修订确认流程
5. 点击「继续生成」按钮,验证故事收起且输入框获得焦点
6. 点击「播放」按钮,验证 TTS 音频播放
**预期结果:** 所有按钮功能正常
- [ ] **Step 3: 测试边界情况**
测试以下边界场景:
**场景 1:空内容的 assistant 消息**
- 手动构造一条空内容的 assistant 消息(或等待 AI 返回空内容)
- 点击「复制」按钮
- **预期:** 显示「暂无可复制内容」提示
**场景 2pending 状态的 assistant 消息**
- 生成故事后,快速点击「继续生成」
- 观察正在加载的 assistant 消息(pending 状态)
- **预期:** pending 消息不显示功能按钮组
**场景 3user 消息**
- 查看用户发送的消息
- **预期:** user 消息不显示功能按钮组
**场景 4:无 currentResult 时的播放**
- 如果可能,测试在未生成故事时点击播放按钮
- **预期:** 按钮仍可点击,TTS 播放器内部处理空 id
- [ ] **Step 4: 验证埋点**
打开微信开发者工具的控制台,查看埋点日志:
1. 点击 assistant 消息的展开/收起按钮,验证 `script_message_collapse_toggle` 埋点
2. 点击「复制」按钮,验证 `script_message_copy_click` 埋点
3. 点击「播放」按钮,验证 `script_message_tts_click` 埋点
**预期结果:** 所有埋点正确触发,包含正确的参数
- [ ] **Step 5: 检查控制台错误**
在整个测试过程中,持续关注微信开发者工具的控制台:
- 确认无 Vue 警告
- 确认无 JavaScript 错误
- 确认无网络请求错误
**预期结果:** 控制台干净,无任何错误
- [ ] **Step 6: 最终提交(如有修改)**
如果在前面的测试中发现了需要修复的问题,修复后提交:
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复最终验证中发现的问题
[描述修复的具体问题]"
```
如果测试全部通过,无需额外提交。
- [ ] **Step 7: 创建 Pull Request(可选)**
如果项目使用 PR 流程,创建 PR 合并这些更改:
```bash
git push origin HEAD
```
然后在代码托管平台创建 PR,标题:
```
feat: 统一 AI 消息卡片功能按钮并修复 story-card 展开失效
```
PR 描述包含:
- 修复 story-card 展开失效 bug
- 为每条 assistant 消息添加完整功能按钮组
- 添加相应的样式和埋点
- 所有功能已通过测试
@@ -0,0 +1,462 @@
# backend-single 重命名为 server 实施计划
> **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:** 将 `backend-single` 目录重命名为 `server`,同步更新 Maven artifactId、部署脚本、配置文件、项目文档和历史文档中的所有引用。
**Architecture:** 分 10 个任务按依赖顺序执行。先移动目录(Task 1),再更新构建配置(Task 2-3),然后更新根级脚本(Task 4-5),接着更新文档(Task 6-8),最后批量替换历史文档(Task 9)并全面验证(Task 10)。
**Tech Stack:** Git, Maven, Python (deploy scripts), Bash, sed (batch replace)
---
### Task 1: 目录重命名
**Files:**
- Move: `backend-single/``server/`
- [ ] **Step 1: 使用 git mv 重命名目录**
```bash
cd /g/IdeaProjects/emotion-museun
git mv backend-single server
```
Expected: 目录从 `backend-single/` 重命名为 `server/`Git 历史完整保留。
- [ ] **Step 2: 确认目录已移动**
```bash
ls -d server/
ls server/pom.xml
```
Expected: 两个命令都成功,无报错。
- [ ] **Step 3: 提交目录移动**
```bash
git commit -m "refactor: 重命名 backend-single 目录为 server"
```
---
### Task 2: 更新 Maven pom.xml
**Files:**
- Modify: `server/pom.xml:8,12`
- [ ] **Step 1: 修改 artifactId 和 name**
`server/pom.xml` 中,将第 8 行和第 12 行的 `backend-single` 替换为 `server`
```xml
<!-- 第 8 行 -->
<artifactId>server</artifactId>
<!-- 第 12 行 -->
<name>server</name>
```
- [ ] **Step 2: 验证 pom.xml 修改**
```bash
grep -n "backend-single" server/pom.xml
```
Expected: 无任何输出(无残留引用)。
- [ ] **Step 3: 提交 pom.xml 修改**
```bash
git add server/pom.xml
git commit -m "refactor: 更新 Maven artifactId 为 server"
```
---
### Task 3: 更新后端部署脚本 JAR 名称
**Files:**
- Modify: `server/deploy.py:22`
- Modify: `server/deploy.sh:10`
- [ ] **Step 1: 修改 server/deploy.py 中的 JAR_NAME**
`server/deploy.py` 第 22 行:
```python
JAR_NAME = "backend-single-1.0.0.jar"
```
改为:
```python
JAR_NAME = "server-1.0.0.jar"
```
- [ ] **Step 2: 修改 server/deploy.sh 中的 JAR_NAME**
`server/deploy.sh` 第 10 行:
```bash
JAR_NAME="backend-single-1.0.0.jar"
```
改为:
```bash
JAR_NAME="server-1.0.0.jar"
```
- [ ] **Step 3: 验证后端部署脚本无残留**
```bash
grep -n "backend-single" server/deploy.py server/deploy.sh
```
Expected: 无任何输出。
- [ ] **Step 4: 提交后端部署脚本修改**
```bash
git add server/deploy.py server/deploy.sh
git commit -m "refactor: 更新后端部署脚本 JAR 名称为 server-1.0.0.jar"
```
---
### Task 4: 更新根目录部署脚本
**Files:**
- Modify: `deploy.py:398,405,413`
- Modify: `deploy.sh:77,81,82,86`
- [ ] **Step 1: 修改根目录 deploy.py**
`deploy.py` 中 3 处 `backend-single` 替换为 `server`
第 398 行注释:
```python
# 后端 - 调用 server/deploy.sh remote
```
第 405 行路径拼接:
```python
deploy_script = PROJECT_DIR / "server" / "deploy.py"
```
第 413 行 cwd
```python
cwd=str(PROJECT_DIR / "server"),
```
- [ ] **Step 2: 修改根目录 deploy.sh**
`deploy.sh` 中 4 处 `backend-single` 替换为 `server`
第 77 行注释:
```bash
# 后端 - 调用 server/deploy.sh
```
第 81 行存在性检查:
```bash
if [ ! -f "server/deploy.sh" ]; then
```
第 82 行错误信息:
```bash
log_error "后端部署脚本不存在: server/deploy.sh"
```
第 86 行 cd 命令:
```bash
cd server
```
- [ ] **Step 3: 验证根部署脚本无残留**
```bash
grep -n "backend-single" deploy.py deploy.sh
```
Expected: 无任何输出。
- [ ] **Step 4: 提交根部署脚本修改**
```bash
git add deploy.py deploy.sh
git commit -m "refactor: 更新根部署脚本中的后端目录引用为 server"
```
---
### Task 5: 更新 .gitignore
**Files:**
- Modify: `.gitignore:386`
- [ ] **Step 1: 修改 .gitignore**
`.gitignore` 第 386 行:
```
backend-single/backend.err
```
改为:
```
server/backend.err
```
- [ ] **Step 2: 验证并提交**
```bash
grep -n "backend-single" .gitignore
git add .gitignore
git commit -m "refactor: 更新 .gitignore 中的后端日志路径"
```
Expected: grep 无输出,提交成功。
---
### Task 6: 更新核心项目文档
**Files:**
- Modify: `CLAUDE.md` (4 处)
- Modify: `AGENTS.md` (4 处)
- Modify: `README.md` (1 处)
- [ ] **Step 1: 修改 CLAUDE.md**
`CLAUDE.md` 中所有 `backend-single` 替换为 `server`
第 23 行项目结构树:
```
├── server/ # Spring Boot 单体后端服务
```
第 37 行标题:
```
### 后端 (server)
```
第 41 行命令:
```
cd server
```
第 50 行 JAR 名称:
```
java -jar target/server-1.0.0.jar
```
- [ ] **Step 2: 修改 AGENTS.md**
`AGENTS.md` 中所有 `backend-single` 替换为 `server`(与 CLAUDE.md 相同的 4 处位置和内容)。
- [ ] **Step 3: 修改 README.md**
`README.md` 第 73 行:
```
├── backend-single/ # SpringBoot单体后端服务
```
改为:
```
├── server/ # SpringBoot单体后端服务
```
- [ ] **Step 4: 验证并提交**
```bash
grep -n "backend-single" CLAUDE.md AGENTS.md README.md
git add CLAUDE.md AGENTS.md README.md
git commit -m "docs: 更新核心文档中的后端目录引用为 server"
```
Expected: grep 无输出,提交成功。
---
### Task 7: 更新部署相关文档
**Files:**
- Modify: `server/部署说明.md` (1 处)
- Modify: `快速部署参考.md` (4 处)
- Modify: `一键部署说明.md` (5 处)
- [ ] **Step 1: 修改 server/部署说明.md**
`server/部署说明.md` 第 14 行:
```
# 在 backend-single 目录下执行
```
改为:
```
# 在 server 目录下执行
```
- [ ] **Step 2: 修改 快速部署参考.md**
`快速部署参考.md` 中所有 `backend-single` 替换为 `server`4 处:第 71、83、134、136 行)。
- [ ] **Step 3: 修改 一键部署说明.md**
`一键部署说明.md` 中所有 `backend-single` 替换为 `server`5 处:第 78、152、156、301、304 行)。注意第 156 行 `backend-single-1.0.0.jar` 变为 `server-1.0.0.jar`
- [ ] **Step 4: 验证并提交**
```bash
grep -n "backend-single" "server/部署说明.md" "快速部署参考.md" "一键部署说明.md"
git add "server/部署说明.md" "快速部署参考.md" "一键部署说明.md"
git commit -m "docs: 更新部署文档中的后端目录引用为 server"
```
Expected: grep 无输出,提交成功。
---
### Task 8: 更新前端代码和其他文档
**Files:**
- Modify: `web/src/types/auth.ts:5`
- Modify: `web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md:284-287`
- [ ] **Step 1: 修改 web/src/types/auth.ts 注释**
将第 5 行:
```ts
// 登录请求(对齐 backend-single:手机号 + 短信验证码)
```
改为:
```ts
// 登录请求(对齐 server:手机号 + 短信验证码)
```
- [ ] **Step 2: 修改 web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md**
将文件中 4 处 `backend-single/src/main/java/` 替换为 `server/src/main/java/`(第 284-287 行)。
- [ ] **Step 3: 验证并提交**
```bash
grep -rn "backend-single" web/ web-admin/
git add "web/src/types/auth.ts" "web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md"
git commit -m "docs: 更新前端代码和文档中的后端目录引用为 server"
```
Expected: grep 无输出(在这两个目录中),提交成功。
---
### Task 9: 批量替换历史文档
**Files:**
- Modify: `docs/superpowers/` 下约 37 个 `.md` 文件(排除本次新建的设计文档)
- [ ] **Step 1: 使用 sed 批量替换**
在 Windows Git Bash 中执行:
```bash
cd /g/IdeaProjects/emotion-museun
find docs/superpowers -name "*.md" \
! -name "2026-06-27-backend-single-rename-to-server-design.md" \
-exec sed -i 's/backend-single/server/g' {} +
```
说明:
- `sed -i` 原地替换
- `! -name` 排除本次设计文档自身(避免修改自己的文件名引用)
- `s/backend-single/server/g` 全局替换
- [ ] **Step 2: 验证 docs/superpowers/ 中无残留**
```bash
grep -rn "backend-single" docs/superpowers/ \
--include="*.md" \
! --include="2026-06-27-backend-single-rename-to-server-design.md"
```
Expected: 无任何输出。
- [ ] **Step 3: 提交历史文档更新**
```bash
git add docs/superpowers/
git commit -m "docs: 批量更新历史文档中的 backend-single 引用为 server"
```
---
### Task 10: 全面验证
**Files:** 无新增修改
- [ ] **Step 1: 全项目 grep 验证无残留**
```bash
grep -rn "backend-single" --include="*.md" --include="*.py" --include="*.sh" \
--include="*.xml" --include="*.ts" --include="*.java" --include="*.yml" \
--include="*.yaml" --include="*.json" --include="*.properties" .
```
Expected: **无任何输出**。如果有残留,逐一修复。
**注意**:以下文件中出现 `backend-single` 是允许的(不需要修改):
- `docs/superpowers/specs/2026-06-27-backend-single-rename-to-server-design.md`(本次设计文档自身)
- `docs/superpowers/plans/2026-06-27-backend-single-rename-to-server.md`(本次计划文档自身)
- [ ] **Step 2: Maven 编译验证**
```bash
cd /g/IdeaProjects/emotion-museun/server
mvn clean install -DskipTests
```
Expected: BUILD SUCCESS,生成 `target/server-1.0.0.jar`
- [ ] **Step 3: 验证 JAR 文件名称**
```bash
ls -lh server/target/server-1.0.0.jar
```
Expected: 文件存在,名称正确。
- [ ] **Step 4: 验证 Git 历史保留**
```bash
git log --follow --oneline server/pom.xml | head -5
```
Expected: 能看到旧的重命名之前的提交历史。
- [ ] **Step 5: 验证 .gitignore 生效**
```bash
git status
```
Expected: 工作区干净(无意外未跟踪文件)。
- [ ] **Step 6: 最终提交(如有修复)**
如果 Step 1 中发现了残留并修复了,执行:
```bash
git add -A
git commit -m "fix: 修复遗漏的 backend-single 引用"
```
@@ -0,0 +1,606 @@
---
author: claude
created_at: 2026-06-27
purpose: 本地服务管理规则优化实现计划
---
# 本地服务管理规则优化实现计划
> **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:** 优化 dev-services.py,强制使用脚本管理本地服务,固定前端/H5 端口从 5178 开始累加,并在无必须重启的变更时禁止 restart
**Architecture:** 在 dev-services.py 中内置固定前端端口映射并强制使用,修改各前端项目 vite 配置和 env 文件同步端口,新增 restart 命令热加载保护,更新 CLAUDE.md 规则
**Tech Stack:** Python, Vite, UniApp, Spring Boot
---
## 文件结构
**修改:**
- `dev-services.py` - 添加固定端口映射、强制端口分配、热加载保护
- `web/vite.config.ts` - 端口改为 5178
- `web/.env.development` - 添加 `VITE_PORT=5178`
- `web-admin/vite.config.ts` - 端口改为 5179
- `web-admin/.env.development` - `VITE_APP_PORT` 改为 5179
- `mini-program/vite.config.js` - 端口改为 5180
- `mini-program/.env.development` - 添加 `VITE_PORT=5180`
- `life-script/vite.config.js` - 端口改为 5181
- `life-script/.env.development` - 添加 `VITE_PORT=5181`
- `CLAUDE.md` - 更新本地服务管理规则
---
### Task 1: 修改 dev-services.py 固定前端端口并保护端口分配
**Files:**
- Modify: `dev-services.py`
- [ ] **Step 1: 在常量区添加固定前端端口映射**
找到 `DEFAULT_PORTS` 定义(约第 58-68 行),在其后添加:
```python
# 前端/H5 服务固定端口映射(按项目目录名匹配)
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
```
- [ ] **Step 2: 修改 `_update_service_port()` 函数以支持 Vite 端口**
当前 `_update_service_port()` 已处理 `--port``--server.port` 参数,还需要处理 Vite 的 `server.port` 配置更新。函数本身不需要修改,因为端口值更新后,`assign_unique_ports()` 会通过 `_service_has_explicit_port()` 判断。
但需要注意:对于 Vite/UniApp/Taro 服务,如果 `start_cmd` 中没有 `--port` 参数(当前 UniApp 的 `dev:h5` 脚本就没有),需要在启动命令前注入 `VITE_PORT` 环境变量或在命令中追加 `--port` 参数。
修改 `_update_service_port()` 函数,使其在更新 `start_cmd` 时,如果命令中没有 `--port` 参数,则在命令末尾追加 `--port {port}`
修改前:
```python
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
svc.start_cmd = updated
```
修改后:
```python
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
has_port_arg = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port":
has_port_arg = True
if index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
has_port_arg = True
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
# Vite/UniApp/Taro/Next 服务如果没有 --port 参数,追加 --port
if not has_port_arg and svc.project_type in {"vite", "uniapp", "taro", "next"}:
updated.extend(["--port", str(port)])
svc.start_cmd = updated
```
- [ ] **Step 3: 重写 `assign_unique_ports()` 强制前端固定端口**
修改前:
```python
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
```
修改后:
```python
def assign_unique_ports(services: list[Service]) -> list[Service]:
# 按目录名匹配固定前端端口
dir_name_to_service = {}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
dir_name_to_service[dir_name] = svc
# 先为固定前端服务分配端口
for dir_name, svc in dir_name_to_service.items():
fixed_port = FIXED_FRONTEND_PORTS[dir_name]
if svc.port != fixed_port:
log(f"{svc.name} 使用固定前端端口 {fixed_port}", "INFO")
_update_service_port(svc, fixed_port)
# 检查固定端口冲突
used_ports = {}
for svc in services:
port = svc.port
if port in used_ports:
other = used_ports[port]
log(f"端口冲突: {svc.name} ({port}) 与 {other.name} ({port}) 冲突", "ERROR")
sys.exit(1)
used_ports[port] = svc
# 非固定前端服务按原有逻辑处理冲突
reserved = {svc.port for svc in services}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
continue # 固定前端服务已处理
port = svc.port
if port in used_ports and used_ports[port] is not svc:
while port in used_ports:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
return services
```
- [ ] **Step 4: 验证 dev-services.py 语法**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:成功列出所有发现的服务,且前端服务端口为 5178-5181
- [ ] **Step 5: 提交**
```bash
git add dev-services.py
git commit -m "feat: dev-services.py 强制前端服务使用固定端口
- 新增 FIXED_FRONTEND_PORTS 映射
- 前端/H5 端口从 5178 开始固定分配
- 端口冲突时不再自动递增前端服务端口,改为报错"
```
---
### Task 2: 为 dev-services.py 添加 restart 热加载保护
**Files:**
- Modify: `dev-services.py`
- [ ] **Step 1: 添加"必须重启"文件模式常量**
`RESTART_REQUIRED_PATTERNS` 常量区(与 `FIXED_FRONTEND_PORTS` 一起)添加:
```python
# 必须重启才能生效的文件模式
RESTART_REQUIRED_PATTERNS = [
re.compile(r'vite\.config\.(ts|js|mjs|cjs)$'),
re.compile(r'tsconfig\.json$'),
re.compile(r'\.env(\.[^/]*)?$'),
re.compile(r'package\.json$'),
re.compile(r'application.*\.ya?ml$'),
re.compile(r'pom\.xml$'),
re.compile(r'build\.gradle(\.kts)?$'),
]
```
注意:需要确保文件顶部已导入 `re``sys`
- [ ] **Step 2: 添加 `_should_restart_for_changes()` 函数**
`assign_unique_ports()` 函数附近添加:
```python
def _should_restart_for_changes(service_dir: Path) -> tuple[bool, list[str]]:
"""
检查指定服务目录下是否有必须重启才能生效的变更。
返回: (是否需要重启, 已修改文件列表)
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--", str(service_dir)],
capture_output=True,
text=True,
cwd=SCRIPT_DIR,
check=True,
)
changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
except subprocess.CalledProcessError:
# 非 git 仓库或 git 命令失败,默认允许重启
return True, []
except FileNotFoundError:
return True, []
if not changed_files:
return False, []
for file_path in changed_files:
for pattern in RESTART_REQUIRED_PATTERNS:
if pattern.search(file_path):
return True, changed_files
return False, changed_files
```
- [ ] **Step 3: 修改 restart 命令解析支持 --force**
找到 `main()` 函数中的 `restart` 子命令解析部分(约文件末尾),添加 `--force` 参数。
当前 restart 解析可能类似:
```python
restart_parser = subparsers.add_parser("restart", help="重启所有或指定服务")
restart_parser.add_argument("service", nargs="?", help="指定服务名")
```
修改后:
```python
restart_parser = subparsers.add_parser("restart", help="重启所有或指定服务")
restart_parser.add_argument("service", nargs="?", help="指定服务名")
restart_parser.add_argument(
"--force",
action="store_true",
help="强制重启,忽略热加载保护"
)
```
- [ ] **Step 4: 在 restart 执行前加入热加载保护**
找到 `cmd_restart()` 函数,在其开头添加检测逻辑。
假设 `cmd_restart()` 函数签名当前为:
```python
def cmd_restart(args):
services = discover_services()
services = _filter_services(services, args.service)
```
修改后:
```python
def cmd_restart(args):
services = discover_services()
services = _filter_services(services, args.service)
if not args.force:
blocked = []
for svc in services:
should_restart, changed_files = _should_restart_for_changes(svc.project_dir)
if not should_restart:
if changed_files:
blocked.append((svc.name, changed_files))
else:
blocked.append((svc.name, []))
if blocked:
log("检测到以下服务无需重启(当前修改支持热加载):", "WARN")
for name, files in blocked:
if files:
log(f" - {name}: 修改文件 {', '.join(files)}", "WARN")
else:
log(f" - {name}: 未检测到代码变更", "WARN")
log("如需强制重启,请使用: python dev-services.py restart --force", "WARN")
sys.exit(0)
# 原有 restart 逻辑继续...
```
注意:实际代码中 `cmd_restart()` 的函数名和参数可能不同,请根据实际代码调整。
- [ ] **Step 5: 验证热加载保护**
1. 修改一个 `.vue` 文件(例如 `mini-program/src/pages/main/ScriptView.vue`
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:提示无需重启,并建议使用 `--force`
3. 运行:
```bash
python dev-services.py restart mini-program --force
```
预期:强制重启
- [ ] **Step 6: 提交**
```bash
git add dev-services.py
git commit -m "feat: dev-services.py restart 命令新增热加载保护
- 无必须重启的变更时禁止 restart
- 支持 --force 参数强制重启
- 避免无意义重启前端热加载服务"
```
---
### Task 3: 同步各前端项目端口配置
**Files:**
- Modify: `web/vite.config.ts`, `web/.env.development`
- Modify: `web-admin/vite.config.ts`, `web-admin/.env.development`
- Modify: `mini-program/vite.config.js`, `mini-program/.env.development`
- Modify: `life-script/vite.config.js`, `life-script/.env.development`
- [ ] **Step 1: 修改 web/ 端口为 5178**
修改 `web/vite.config.ts` 中的 `server.port`
```typescript
server: {
port: 5178,
// ... 其他配置
}
```
修改/创建 `web/.env.development`,添加:
```
VITE_PORT=5178
```
- [ ] **Step 2: 修改 web-admin/ 端口为 5179**
修改 `web-admin/vite.config.ts` 中的 `server.port`
```typescript
server: {
port: 5179,
// ... 其他配置
}
```
修改 `web-admin/.env.development`,将 `VITE_APP_PORT` 改为:
```
VITE_APP_PORT=5179
```
- [ ] **Step 3: 修改 mini-program/ 端口为 5180**
修改 `mini-program/vite.config.js` 中的 `server.port`
```javascript
server: {
port: 5180,
// ... 其他配置
}
```
修改/创建 `mini-program/.env.development`,添加:
```
VITE_PORT=5180
```
- [ ] **Step 4: 修改 life-script/ 端口为 5181**
修改 `life-script/vite.config.js` 中的 `server.port`
```javascript
server: {
port: 5181,
// ... 其他配置
}
```
修改/创建 `life-script/.env.development`,添加:
```
VITE_PORT=5181
```
- [ ] **Step 5: 验证端口配置**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:
- web 服务端口为 5178
- web-admin 服务端口为 5179
- mini-program 服务端口为 5180
- life-script 服务端口为 5181
- [ ] **Step 6: 提交**
```bash
git add web/ web-admin/ mini-program/ life-script/
git commit -m "chore: 统一前端/H5 服务固定端口
- web: 5178
- web-admin: 5179
- mini-program: 5180
- life-script: 5181"
```
---
### Task 4: 更新 CLAUDE.md 规则文档
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: 添加/更新本地服务管理规则章节**
`CLAUDE.md` 中找到现有的"本地服务管理规则"或"热加载规则"章节,合并更新为统一的"本地服务管理规则(强制)":
```markdown
## 本地服务管理规则(强制)
**启动、重启、停止本地前后端服务时,必须使用项目根目录下的 `dev-services.py` 脚本。**
### 强制要求
- ✅ 必须使用:`python dev-services.py [start|stop|restart|status|discover] [服务名]`
- ❌ 禁止直接使用 `npm run dev` / `pnpm run dev` 启动前端
- ❌ 禁止直接使用 `npm run dev:h5` 启动小程序 H5
- ❌ 禁止直接使用 `mvn spring-boot:run` 启动后端
- ❌ 禁止无意义重启支持热加载的服务
### 前端/H5 固定端口
| 服务 | 目录 | 固定端口 |
|---|---|---|
| web | `web/` | 5178 |
| web-admin | `web-admin/` | 5179 |
| mini-program | `mini-program/` | 5180 |
| life-script | `life-script/` | 5181 |
### 热加载规则
1. **不需要重启的场景**(热加载自动生效)
- 前端:修改 `.vue``.tsx``.ts``.js``.css``.scss` 等源码文件
- 样式、模板、静态资源等修改
2. **需要重启的场景**
- 前端:修改 `vite.config.ts``tsconfig.json``.env``package.json`、别名配置等
- 后端:按现有后端规则处理(本地不启动后端)
3. **禁止无意义重启**
- 执行 `python dev-services.py restart` 时,脚本会自动检测是否有必须重启才能生效的变更
- 如果只有支持热加载的源码文件变更,脚本会拒绝重启并提示:
> 当前修改只涉及支持热加载的源码文件,无需重启。如需强制重启,请使用 `python dev-services.py restart --force`
- 确需强制重启时,使用 `--force` 参数
```
- [ ] **Step 2: 删除冲突的旧规则**
检查 CLAUDE.md 中是否有与上述新规则冲突的旧规则(如旧的前端端口说明、热加载规则),删除或更新它们,确保文档内部一致。
- [ ] **Step 3: 提交**
```bash
git add CLAUDE.md
git commit -m "docs: 更新本地服务管理规则
- 强制使用 dev-services.py
- 明确前端/H5 固定端口
- 新增禁止无意义重启规则"
```
---
### Task 5: 最终验证
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 验证 dev-services.py 发现服务端口正确**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:
- web 端口 5178
- web-admin 端口 5179
- mini-program 端口 5180
- life-script 端口 5181
- 后端服务保持原有端口
- [ ] **Step 2: 验证热加载保护**
1. 修改 `mini-program/src/pages/main/ScriptView.vue` 中任意一处(例如加一个空行)
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:提示无需重启
3. 运行:
```bash
python dev-services.py restart mini-program --force
```
预期:强制重启
- [ ] **Step 3: 验证必须重启的场景**
1. 修改 `mini-program/vite.config.js` 中任意一处
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:正常允许重启
- [ ] **Step 4: 验证端口冲突检测**
1. 手动启动一个占用 5178 端口的进程(例如另一个 node 服务)
2. 运行:
```bash
python dev-services.py start web
```
预期:报错,提示端口 5178 被占用
3. 终止占用进程
- [ ] **Step 5: 检查 CLAUDE.md 一致性**
快速浏览 `CLAUDE.md`,确认:
- 没有重复的本地服务管理规则
- 前端端口说明与新的固定端口表一致
- 热加载规则与 dev-services.py 行为一致
- [ ] **Step 6: 提交验证修复(如有)**
如果在验证中修复了问题:
```bash
git add dev-services.py CLAUDE.md
git commit -m "fix: 修复本地服务管理规则验证中的问题
[描述具体修复]"
```
如果全部通过,无需额外提交。
---
## 自审检查
- **Spec 覆盖:**
- ✅ dev-services.py 固定前端端口 → Task 1
- ✅ 热加载保护 → Task 2
- ✅ 前端项目端口配置同步 → Task 3
- ✅ CLAUDE.md 规则更新 → Task 4
- ✅ 验证 → Task 5
- **无占位符:** 计划中没有 TBD/TODO
- **类型一致:** `FIXED_FRONTEND_PORTS``RESTART_REQUIRED_PATTERNS``_should_restart_for_changes` 命名和用法一致
@@ -0,0 +1,658 @@
---
author: claude
created_at: 2026-06-27
purpose: MessageCard 组件统一提取实现计划
---
# MessageCard 组件统一实现计划
> **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:** 提取 MessageCard.vue 组件,统一 story-card 和 result-chat-list 中 AI 消息卡片的样式,并按 100 字阈值自动切换短消息/完整卡片模式
**Architecture:** 新建 `MessageCard.vue` 组件封装长消息卡片结构,ScriptView.vue 中 story-card 和 result-chat-list 都复用该组件,短消息直接渲染简洁气泡
**Tech Stack:** Vue 3, UniApp, 微信小程序
---
## 文件结构
**创建:**
- `mini-program/src/components/MessageCard.vue` - 通用 AI 消息卡片组件
**修改:**
- `mini-program/src/pages/main/ScriptView.vue` - 替换 story-card 和 result-chat-list 中的 assistant 消息渲染,并迁移相关样式
---
### Task 1: 创建 MessageCard.vue 组件(短消息模式 + 基础结构)
**Files:**
- Create: `mini-program/src/components/MessageCard.vue`
- Modify: 无
- [ ] **Step 1: 创建组件文件并写入基础模板**
`mini-program/src/components/MessageCard.vue` 写入以下内容:
```vue
<template>
<view v-if="isShortMessage" class="chat-bubble system">
<text>{{ content }}</text>
</view>
<view v-else class="story-card" :class="{ collapsed }">
<view class="story-head">
<view class="story-title-wrap">
<text class="story-title">{{ title || '我的人生剧本' }}</text>
<view v-if="tags?.length" class="tag-row">
<text v-for="tag in tags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
<view class="story-head-actions">
<button class="collapse-icon" @click="$emit('toggle-collapse')">
<text class="collapse-icon-text">{{ collapsed ? '展开' : '收起' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</button>
<button class="copy-card-btn" @click="$emit('copy')">复制</button>
</view>
</view>
<scroll-view
v-if="collapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
<view class="collapse-row" @click="$emit('toggle-collapse')">
<text class="collapse-row-text">{{ collapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</view>
<view class="result-actions">
<button class="action-btn" @click="$emit('change-direction')">换个方向</button>
<button class="action-btn" @click="$emit('not-like-me')">不像我</button>
<button class="action-btn" @click="$emit('continue')">继续生成</button>
<button class="action-btn primary" @click="$emit('play-tts')">
<text class="action-icon">{{ ttsIcon || '▶' }}</text>
<text>{{ ttsText || '播放' }}</text>
</button>
</view>
</view>
</template>
<script setup>
const props = defineProps({
content: { type: String, required: true },
title: { type: String, default: '' },
tags: { type: Array, default: () => [] },
collapsed: { type: Boolean, default: false },
contentLength: { type: Number, required: true },
isShortMessage: { type: Boolean, required: true },
ttsIcon: { type: String, default: '▶' },
ttsText: { type: String, default: '播放' }
})
defineEmits([
'toggle-collapse',
'copy',
'change-direction',
'not-like-me',
'continue',
'play-tts'
])
</script>
<style scoped>
.chat-bubble {
max-width: 86%;
display: flex;
flex-direction: column;
gap: 8rpx;
padding: 24rpx 28rpx;
border-radius: 36rpx;
font-size: 34rpx;
line-height: 52rpx;
}
.chat-bubble.system {
align-self: flex-start;
background: rgba(16, 8, 34, 0.28);
border: 1rpx solid rgba(192, 132, 252, 0.3);
color: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.story-card {
border-radius: 52rpx;
padding: 34rpx;
background: rgba(12, 5, 28, 0.34);
border: 1rpx solid rgba(192, 132, 252, 0.46);
box-shadow: 0 0 48rpx rgba(125, 55, 205, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.story-card.collapsed {
box-shadow: 0 0 42rpx rgba(125, 55, 205, 0.14);
}
.story-head {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.story-head-actions {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 12rpx;
}
.story-title-wrap {
flex: 1;
min-width: 0;
}
.story-title {
display: block;
font-size: 52rpx;
font-weight: 700;
line-height: 1.35;
}
.collapse-icon {
width: 120rpx;
min-width: 120rpx;
height: 58rpx;
min-height: 58rpx;
line-height: 1;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: rgba(255, 244, 255, 0.95);
background:
linear-gradient(135deg, rgba(134, 72, 255, 0.44), rgba(39, 15, 76, 0.42)),
rgba(255, 255, 255, 0.05);
border: 1rpx solid rgba(216, 180, 254, 0.46);
box-shadow:
0 14rpx 30rpx rgba(61, 18, 113, 0.22),
inset 0 1rpx 0 rgba(255, 255, 255, 0.26),
inset 0 -10rpx 18rpx rgba(33, 9, 73, 0.16);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
}
.collapse-icon::after {
border: 0;
}
.collapse-icon:active,
.copy-card-btn:active,
.collapse-row:active {
transform: scale(0.98);
opacity: 0.92;
}
.copy-card-btn {
width: 120rpx;
min-width: 120rpx;
height: 50rpx;
min-height: 50rpx;
line-height: 1;
padding: 0;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 244, 255, 0.9);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
background:
radial-gradient(circle at 50% -30%, rgba(255, 255, 255, 0.18), transparent 50%),
rgba(88, 28, 135, 0.26);
border: 1rpx solid rgba(216, 180, 254, 0.32);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 10rpx 22rpx rgba(61, 18, 113, 0.12);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.copy-card-btn::after {
border: 0;
}
.collapse-icon-text,
.collapse-row-text {
line-height: 1;
white-space: nowrap;
}
.collapse-chevron {
position: relative;
width: 20rpx;
height: 16rpx;
flex-shrink: 0;
transition: transform 0.18s ease;
}
.collapse-chevron.down {
transform: rotate(180deg);
}
.collapse-chevron view {
position: absolute;
top: 7rpx;
width: 12rpx;
height: 4rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #fff3b0, #ffd86b);
box-shadow: 0 0 12rpx rgba(255, 216, 107, 0.5);
}
.collapse-chevron view:first-child {
left: 0;
transform: rotate(-38deg);
transform-origin: right center;
}
.collapse-chevron view:last-child {
right: 0;
transform: rotate(38deg);
transform-origin: left center;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.tag {
padding: 6rpx 16rpx;
border-radius: 999rpx;
color: #d18aff;
font-size: 24rpx;
background: rgba(168, 85, 247, 0.22);
}
.story-body {
display: block;
margin-top: 28rpx;
font-size: 32rpx;
font-weight: 400;
line-height: 1.78;
color: rgba(255, 255, 255, 0.92);
white-space: pre-wrap;
}
.story-body-scroll {
max-height: 420rpx;
height: auto;
margin-top: 28rpx;
padding-right: 8rpx;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.story-body-scroll .story-body {
margin-top: 0;
}
.collapse-row {
position: relative;
height: 62rpx;
margin-top: 22rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
border-radius: 999rpx;
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 800;
overflow: hidden;
background:
radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.2), transparent 46%),
linear-gradient(135deg, rgba(98, 40, 174, 0.3), rgba(24, 8, 58, 0.24));
border: 1rpx solid rgba(192, 132, 252, 0.36);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 12rpx 28rpx rgba(61, 18, 113, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.collapse-row::before {
content: '';
position: absolute;
left: 34rpx;
right: 34rpx;
top: 0;
height: 1rpx;
background: linear-gradient(90deg, transparent, rgba(255, 236, 180, 0.6), transparent);
}
.result-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 26rpx;
}
.action-btn {
height: 72rpx;
min-height: 72rpx;
padding: 0 8rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e8ccff;
font-size: 26rpx;
font-weight: 700;
line-height: 1.15;
text-align: center;
white-space: normal;
box-sizing: border-box;
background: rgba(88, 28, 135, 0.18);
border: 1rpx solid rgba(192, 132, 252, 0.35);
}
.action-btn.primary {
color: #fff;
background: linear-gradient(145deg, #8c44f2, #5f1db8);
}
.action-icon {
margin-right: 8rpx;
font-size: 24rpx;
font-weight: 900;
line-height: 1;
}
.action-btn::after {
border: 0;
}
</style>
```
- [ ] **Step 2: 验证组件文件创建成功**
确认文件路径:`mini-program/src/components/MessageCard.vue` 存在。
```bash
ls "G:/IdeaProjects/emotion-museun/mini-program/src/components/MessageCard.vue"
```
- [ ] **Step 3: 提交组件创建**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/components/MessageCard.vue
git commit -m "feat: 创建 MessageCard 组件
封装长消息/故事卡片结构,支持短消息简洁气泡模式"
```
---
### Task 2: 在 ScriptView.vue 中注册并使用 MessageCard 组件
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- [ ] **Step 1: 导入组件**
`ScriptView.vue``<script setup>` 顶部,导入 MessageCard 组件:
修改前:
```javascript
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
```
修改后:
```javascript
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import MessageCard from '../../components/MessageCard.vue'
```
- [ ] **Step 2: 替换 story-card 模板**
找到第 172-219 行的 `story-card` 模板,替换为 MessageCard 组件。
修改前:
```vue
<view class="story-card" :class="{ collapsed: storyCollapsed, dialogMode: resultHasScriptReplies }">
...(原有 story-card 全部内容)...
</view>
```
修改后:
```vue
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
```
- [ ] **Step 3: 替换 result-chat-list 中的 assistant 消息**
找到第 221-254 行的 `result-chat-list` 模板,替换为使用 MessageCard。
修改前:
```vue
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
class="chat-bubble"
:class="{ user: message.role === 'user', system: message.role === 'assistant', pending: message.pending }"
>
<text selectable user-select>{{ getMessageDisplayContent(message) }}</text>
<view v-if="message.pending" class="thinking-dots">
<view></view>
<view></view>
<view></view>
</view>
<view
v-if="isAssistantMessage(message)"
class="message-toggle"
@click.stop="toggleMessageCollapse(message)"
>
<text>{{ isMessageCollapsed(message) ? '展开' : '收起' }}</text>
...
</view>
<view v-if="isAssistantMessage(message)" class="message-actions">
...
</view>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
```
修改后:
```vue
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
>
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
</view>
```
- [ ] **Step 4: 提交使用组件**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: ScriptView 复用 MessageCard 组件
- story-card 使用 MessageCard
- result-chat-list 中的 assistant 消息按内容长度自动切换短消息/完整卡片模式"
```
---
### Task 3: 迁移并清理样式
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- [ ] **Step 1: 从 ScriptView.vue 中删除已迁移到 MessageCard 的样式**
删除以下 CSS 类(从 `<style scoped>` 中移除):
- `.story-card``.story-card.collapsed`
- `.story-head``.story-head-actions``.story-title-wrap`
- `.story-title``.tag-row``.tag`
- `.collapse-icon``.copy-card-btn``.collapse-row`
- `.collapse-chevron``.story-body``.story-body-scroll`
- `.result-actions``.action-btn`
- `.message-actions`(上一轮临时添加的,不再使用)
- `.message-toggle`(不再使用)
保留 `ScriptView.vue` 中仍然需要的样式,如:
- `.chat-bubble.user`
- `.bubble-time`
- `.result-chat-list`
- `.thinking-dots`
- 页面整体布局相关样式
- [ ] **Step 2: 验证样式清理后无遗漏**
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run build:h5 2>&1 | tail -20
```
预期输出:`DONE Build complete.`
- [ ] **Step 3: 提交样式清理**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor: 迁移 story-card 相关样式到 MessageCard
删除 ScriptView.vue 中已迁移到 MessageCard 的样式类"
```
---
### Task 4: 最终验证
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 构建验证**
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run build:h5 2>&1 | tail -20
```
预期输出:`DONE Build complete.`
- [ ] **Step 2: 功能验证(在 H5 浏览器中)**
启动 H5 开发服务器并验证:
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run dev:h5
```
打开浏览器访问显示的端口(默认 `http://localhost:5173`),执行以下验证:
1. **第一次生成故事**:确认 story-card 样式、标题、标签、展开/收起、复制、功能按钮组均正常
2. **重新生成(生成 script 类型长消息)**
- 内容 ≥ 100 字:显示与 story-card 完全一致的卡片样式
- 内容 < 100 字:显示简洁气泡,无任何功能按钮
3. **普通聊天 assistant 消息**
- 内容 ≥ 100 字:显示完整卡片
- 内容 < 100 字:显示简洁气泡
4. **用户消息**:保持原有 `.chat-bubble.user` 样式不变
5. **控制台**:确认无 Vue 警告和 JavaScript 错误
- [ ] **Step 3: 提交验证结果(如有修复)**
如果在验证中修复了问题,提交:
```bash
git add mini-program/src/components/MessageCard.vue mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复 MessageCard 组件验证中发现的问题
[描述具体修复内容]"
```
如果验证全部通过,无需额外提交。
---
## 自审检查
- **Spec 覆盖:**
- ✅ 创建 MessageCard.vue 组件 → Task 1
- ✅ ScriptView.vue 复用组件 → Task 2
- ✅ 100 字阈值判断 → Task 2 (`:is-short-message="message.content.length < 100"`)
- ✅ 样式迁移和清理 → Task 3
- ✅ 验证 → Task 4
- **无占位符:** 计划中没有 TBD/TODO
- **类型一致:** props 名称 `contentLength` 与模板中使用一致,事件名称在组件和父组件中一致
File diff suppressed because it is too large Load Diff
@@ -42,7 +42,7 @@ purpose: 设计并优化情绪博物馆跨平台服务管理脚本,修复硬
services:
backend:
name: "后端服务"
dir: "backend-single"
dir: "server"
type: "java"
port: 19089
health_check_path: "/actuator/health"
@@ -33,10 +33,10 @@ The current mini program already has the core functional modules:
The backend already has script generation support under:
- `backend-single/src/main/java/com/emotion/controller/EpicScriptController.java`
- `backend-single/src/main/java/com/emotion/service/EpicScriptService.java`
- `backend-single/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- `backend-single/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- `server/src/main/java/com/emotion/controller/EpicScriptController.java`
- `server/src/main/java/com/emotion/service/EpicScriptService.java`
- `server/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- `server/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
The missing product capability is a complete inspiration-mode flow. The backend currently exposes generic script creation, but it does not expose dedicated endpoints for inspiration recommendations, random inspiration, or one-sentence inspiration generation.
@@ -45,7 +45,7 @@ The missing product capability is a complete inspiration-mode flow. The backend
1. Redesign the mini-program pages to match the new prototype visual direction: dark cosmic background, purple glass panels, glowing primary actions, rounded bottom navigation, and stronger card hierarchy.
2. Keep the existing product functions largely unchanged.
3. Complete the functional flow from life events to script generation to script detail to path realization.
4. Add backend inspiration-mode APIs in `backend-single`.
4. Add backend inspiration-mode APIs in `server`.
5. Make the implementation modular enough to land in batches without leaving broken flows.
## Non-Goals
@@ -11,7 +11,7 @@ Add two complete but independently shippable capabilities:
The implementation should fit the current project shape:
- `backend-single`: Spring Boot 2.7, MyBatis Plus, MySQL, Redis.
- `server`: Spring Boot 2.7, MyBatis Plus, MySQL, Redis.
- `mini-program`: uni-app/Vue.
- `web-admin`: Vue, Element Plus, ECharts.
- Deployment target: `101.200.208.45` with passwordless SSH.
@@ -13,7 +13,7 @@
现有实现包含:
- `web-admin/src/views/aiconfig/AiConfigList.vue`:AI 配置列表、编辑、测试、测试后保存。
- `backend-single/src/main/java/com/emotion/entity/AiConfig.java`:对应 `t_ai_config`
- `server/src/main/java/com/emotion/entity/AiConfig.java`:对应 `t_ai_config`
- `AiConfigController``AiConfigService``AiConfigServiceImpl`:配置 CRUD、启用禁用、默认配置、测试后更新。
- `AiChatServiceImpl`:包含大量 Coze 专用调用、请求组装、SSE 解析、工作流调用和日志记录逻辑。
- `docs/dify平台接口.md`:Dify 平台接口文档,当前重点使用 `/chat-messages`
@@ -382,10 +382,10 @@ async function submitEndpointStreamTest() {
| 操作 | 文件 |
|------|------|
| 修改 | `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` |
| 修改 | `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java` |
| 修改 | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` |
| 修改 | `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` |
| 修改 | `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` |
| 修改 | `server/src/main/java/com/emotion/service/AiRuntimeService.java` |
| 修改 | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` |
| 修改 | `server/src/main/java/com/emotion/controller/AiRoutingController.java` |
| 修改 | `web-admin/src/types/aiconfig.ts` |
| 修改 | `web-admin/src/api/aiconfig.ts` |
| 修改 | `web-admin/src/views/aiconfig/AiRoutingList.vue` |
@@ -121,7 +121,7 @@ if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
|------|------|
| 修改 | `web-admin/src/views/aiconfig/AiRoutingList.vue` |
| 修改 | `web-admin/src/types/aiconfig.ts` |
| 修改 | `backend-single/src/main/java/com/emotion/service/ai/AiTemplateRenderer.java` |
| 修改 | `server/src/main/java/com/emotion/service/ai/AiTemplateRenderer.java` |
## 4. 风险
@@ -8,7 +8,7 @@ purpose: 为所有 Controller 及关联 DTO 补全 OpenAPI 中文注解
## 1. 目标
`backend-single/src/main/java/com/emotion/controller/` 下所有 39 个 Controller 及其关联的 Request/Response DTO 补全 OpenAPI 注解,使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
`server/src/main/java/com/emotion/controller/` 下所有 39 个 Controller 及其关联的 Request/Response DTO 补全 OpenAPI 注解,使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
## 2. 注解规范
@@ -31,7 +31,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
**保持不变的项**
- URL 路径(如 `/emotion-museum-admin/`)— 保持兼容
- 目录名(`web/``web-admin/``backend-single/`)— 避免构建断裂
- 目录名(`web/``web-admin/``server/`)— 避免构建断裂
- 数据库表名前缀(如 `emotion_record` → 保持)— 避免 Entity/Table 映射断裂
## 第 1 步:文档层(无风险)
@@ -43,7 +43,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- `README.md`
- `docs/superpowers/` 下所有 .md
- 各模块下的 `部署说明.md``技术方案.md``后端代码规范.md`
- `backend-single/部署说明.md``backend-single/后端项目结构.md`
- `server/部署说明.md``server/后端项目结构.md`
**操作**:批量 sed 替换中文 `情绪博物馆``开心星球`,英文 `Emotion Museum``Happy Planet``emotion-museum``happy-planet`
@@ -73,13 +73,13 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
**影响范围**:配置文件、部署脚本、nginx、systemd service
### 3.1 应用配置
- `backend-single/src/main/resources/application.yml``spring.application.name`
- `backend-single/src/main/resources/application-*.yml` — 同上
- `server/src/main/resources/application.yml``spring.application.name`
- `server/src/main/resources/application-*.yml` — 同上
- 日志路径、应用描述等配置中的 `emotion-museum`
### 3.2 部署脚本
- `deploy.sh``deploy.py`
- `backend-single/deploy.py``backend-single/deploy.sh`
- `server/deploy.py``server/deploy.sh`
- `web/deploy.sh``web/deploy.py`
- `web-admin/deploy.sh``web-admin/deploy.py`
- 各脚本中的路径引用、日志路径、提示文案
@@ -91,8 +91,8 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- jar 文件名、日志路径引用中的 `emotion-museum`
### 3.4 systemd Service
- `backend-single/asr-service/emotion-museum-asr.service``happy-planet-asr.service`
- `backend-single/tts-service/emotion-museum-tts.service``happy-planet-tts.service`
- `server/asr-service/emotion-museum-asr.service``happy-planet-asr.service`
- `server/tts-service/emotion-museum-tts.service``happy-planet-tts.service`
- 内部路径和描述中的 `emotion-museum`
### 3.5 工具脚本
@@ -118,7 +118,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
### 4.1 Java 包名重命名
**操作**
1. 移动目录:`backend-single/src/main/java/com/emotion/``backend-single/src/main/java/com/happyplanet/`
1. 移动目录:`server/src/main/java/com/emotion/``server/src/main/java/com/happyplanet/`
2. 更新 `pom.xml` 中的包名引用
3. 用 sed 批量替换所有 `.java` 文件中 `package com.emotion``package com.happyplanet`
4. 用 sed 批量替换所有 `.java` 文件中 `import com.emotion``import com.happyplanet`
@@ -126,7 +126,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
6. 处理编译错误(可能有遗漏的引用)
**测试目录**
- `backend-single/src/test/java/com/emotion/` 同样需要迁移
- `server/src/test/java/com/emotion/` 同样需要迁移
- `.java` 文件中的 `package``import` 语句
### 4.2 SQL 文件
@@ -139,7 +139,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- `USE emotion_museum``USE happy_planet`
### 4.3 其他底层引用
- `backend-single/create_api_tables.sql` 中的数据库名
- `server/create_api_tables.sql` 中的数据库名
- `.gitignore` 中的路径引用(如有)
**验证**
@@ -27,7 +27,7 @@ purpose: 设计接口管理页面的中文描述补全和测试面板表单化
### 3.1 Controller 注解补全
`backend-single/src/main/java/com/emotion/controller/` 下所有 Controller 补全注解:
`server/src/main/java/com/emotion/controller/` 下所有 Controller 补全注解:
**类级别注解:**
```java
@@ -247,9 +247,9 @@ path: '/api' + testForm.value.path // 例如 /api + /auth/login = /api/auth/log
| `web-admin/src/views/endpoint/EndpointList.vue` | 操作列加"测试"按钮,新增 `showTest` 方法 |
| `web-admin/src/views/endpoint/EndpointDetailDialog.vue` | 新增 `defaultTab` prop,测试面板改造:参数表单 + JSON 预填充 + 请求头展示 |
| `web-admin/src/api/endpoint.ts` | 类型定义无需修改(`operationId` 已存在) |
| `backend-single/src/main/java/com/emotion/controller/*.java` | 补全 @Tag/@Operation/@Parameter/@Schema 注解(按 4 个批次) |
| `backend-single/src/main/java/com/emotion/dto/request/**/*.java` | 补全 @Schema 注解(请求体参数描述来源) |
| `backend-single/src/main/java/com/emotion/dto/response/**/*.java` | 补全 @Schema 注解(响应体参数描述来源,可选) |
| `server/src/main/java/com/emotion/controller/*.java` | 补全 @Tag/@Operation/@Parameter/@Schema 注解(按 4 个批次) |
| `server/src/main/java/com/emotion/dto/request/**/*.java` | 补全 @Schema 注解(请求体参数描述来源) |
| `server/src/main/java/com/emotion/dto/response/**/*.java` | 补全 @Schema 注解(响应体参数描述来源,可选) |
## 5. 实施顺序
@@ -202,7 +202,7 @@ public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLog
### Request 对象
存放路径:`backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
存放路径:`server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
```java
@Schema(description = "AI 调用日志查询请求")
@@ -14,7 +14,7 @@ purpose: 在管理后台调用日志列表中增加"调用用户"列,显示用
### 1. 后端 — 实体新增字段
**文件**: `backend-single/src/main/java/com/emotion/entity/AiCallLog.java`
**文件**: `server/src/main/java/com/emotion/entity/AiCallLog.java`
`userId` 字段下方新增:
@@ -31,7 +31,7 @@ ALTER TABLE t_ai_call_log ADD COLUMN user_name VARCHAR(100) COMMENT '用户昵
### 3. 后端 — 日志保存时写入 userName
**文件**: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
**文件**: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
两处日志创建位置(第 68-73 行 `invokeStream` 方法、第 219-225 行 `invokeEndpointStream` 方法),在 `callLog.setUserId(...)` 之后补充:
@@ -27,7 +27,7 @@ The mini program currently supports phone number plus SMS code login only. The n
- `mini-program/src/pages/login/index.vue` contains the existing phone/SMS login UI and routes to main or onboarding based on profile presence.
- `mini-program/src/stores/app.js` already centralizes login, logout, token restore, and profile fetch behavior.
- `mini-program/src/services/auth.js` stores `access_token` and `refresh_token` from `AuthResponse`.
- `backend-single/src/main/java/com/emotion/controller/AuthController.java` exposes `/auth/login`, `/auth/refreshToken`, and token validation endpoints.
- `server/src/main/java/com/emotion/controller/AuthController.java` exposes `/auth/login`, `/auth/refreshToken`, and token validation endpoints.
- `t_user` already has `phone`, `avatar`, `nickname`, `third_party_id`, and `third_party_type`, which can support WeChat binding without a required schema change.
- The current backend issues JWT plus Redis-backed access/refresh tokens through `AuthServiceImpl`.
- The mini program manifest already has a WeChat appid: `wxaf2eaba72d28f0e4`.
@@ -167,7 +167,7 @@ if '//' in remote_conf or '..' in remote_conf:
### 不包含(明确排除)
- 不修改其他 deploy.py 子脚本(backend-single/web/web-admin/life-script
- 不修改其他 deploy.py 子脚本(server/web/web-admin/life-script
- 不改 `ssh_command()` / `run_ssh_args()` 底层
- 不增加 `--dry-run` 模式
- 不改 nginx 配置文件本身
@@ -51,7 +51,7 @@ alter table emotion_museum.t_user_profile
### 2. 实体类扩展
文件:`backend-single/src/main/java/com/emotion/entity/UserProfile.java`
文件:`server/src/main/java/com/emotion/entity/UserProfile.java`
新增字段(与数据库列名通过 `@TableField` 映射):
@@ -67,15 +67,15 @@ alter table emotion_museum.t_user_profile
以下三个文件新增相同 5 个字段:
- `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- `server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
`UserProfileResponse` 中的 `birthday` 使用 `@JsonFormat(pattern = "yyyy-MM-dd")` 注解,与现有日期字段保持一致。
### 4. Service 层
文件:`backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java`
文件:`server/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java`
无需改动。`BeanUtils.copyProperties``BeanUtil.copyProperties(..., CopyOptions.create().setIgnoreNullValue(true))` 会自动映射同名字段。
@@ -186,10 +186,10 @@ registrationData: {
| 文件 | 改动类型 | 说明 |
|------|----------|------|
| `sql/emotion_museum_ddl.sql` | 追加 | 5 个 ALTER TABLE 语句 |
| `backend-single/.../entity/UserProfile.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../request/.../UserProfileCreateRequest.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../request/.../UserProfileUpdateRequest.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../response/.../UserProfileResponse.java` | 修改 | 新增 5 个字段 |
| `server/.../entity/UserProfile.java` | 修改 | 新增 5 个字段 |
| `server/.../request/.../UserProfileCreateRequest.java` | 修改 | 新增 5 个字段 |
| `server/.../request/.../UserProfileUpdateRequest.java` | 修改 | 新增 5 个字段 |
| `server/.../response/.../UserProfileResponse.java` | 修改 | 新增 5 个字段 |
| `mini-program/src/services/userProfile.js` | 修改 | 双向转换增加 5 个字段 |
| `mini-program/src/stores/app.js` | 修改 | `registrationData` 扩展 |
| `mini-program/src/pages/main/RecordView.vue` | 修改 | 去除写死默认值和 sampleEvents |
@@ -90,14 +90,14 @@ purpose: 修复微信小程序登录 text/plain 错误(Spring RestTemplate 默
| # | 文件 | 操作 | 行数估算 | 目的 |
|---|---|---|---|---|
| 1 | `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java` | **重写** | 40 行 | 改用 `RestTemplateBuilder` + `BufferingClientHttpRequestFactory`:设置 User-Agent、超时、加 StringHttpMessageConverter、加 LoggingInterceptor、加 ErrorHandler |
| 2 | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | **重写** | 90 行 | `code2Session` 改用 `ResponseEntity<String>` + Jackson 二次解析;捕获所有失败场景的 raw body |
| 3 | `backend-single/src/main/resources/application.yml` | **修改** | 2 行 | 删除 `app-id``app-secret` 硬编码默认值(`${ENV_VAR:}` 空兜底) |
| 4 | `backend-single/src/main/java/com/emotion/exception/WechatApiException.java` | **新增** | 30 行 | 业务异常类(code 用 HTTP 风格状态码 + rawBody |
| 5 | `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | **新增** | 60 行 | ClientHttpRequestInterceptor:记录 method/URL/statusINFO+ 脱敏 bodyDEBUG |
| 6 | `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | **新增** | 30 行 | 4xx/5xx 不抛 RestClientException,重写 `hasError()` 显式声明 |
| 7 | `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | **修改** | 25 行 | 新增 `@ExceptionHandler(WechatApiException.class)` 返回脱敏的友好提示 |
| 8 | `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | **不动** | 0 行 | **保持纯 DTO**rawBody 不放在 DTO,只在异常对象和日志中) |
| 1 | `server/src/main/java/com/emotion/config/RestTemplateConfig.java` | **重写** | 40 行 | 改用 `RestTemplateBuilder` + `BufferingClientHttpRequestFactory`:设置 User-Agent、超时、加 StringHttpMessageConverter、加 LoggingInterceptor、加 ErrorHandler |
| 2 | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | **重写** | 90 行 | `code2Session` 改用 `ResponseEntity<String>` + Jackson 二次解析;捕获所有失败场景的 raw body |
| 3 | `server/src/main/resources/application.yml` | **修改** | 2 行 | 删除 `app-id``app-secret` 硬编码默认值(`${ENV_VAR:}` 空兜底) |
| 4 | `server/src/main/java/com/emotion/exception/WechatApiException.java` | **新增** | 30 行 | 业务异常类(code 用 HTTP 风格状态码 + rawBody |
| 5 | `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | **新增** | 60 行 | ClientHttpRequestInterceptor:记录 method/URL/statusINFO+ 脱敏 bodyDEBUG |
| 6 | `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | **新增** | 30 行 | 4xx/5xx 不抛 RestClientException,重写 `hasError()` 显式声明 |
| 7 | `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | **修改** | 25 行 | 新增 `@ExceptionHandler(WechatApiException.class)` 返回脱敏的友好提示 |
| 8 | `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | **不动** | 0 行 | **保持纯 DTO**rawBody 不放在 DTO,只在异常对象和日志中) |
**总计**:2 重写 + 3 新增 + 3 修改,约 280 行 Java + 2 行 YAML。
@@ -472,7 +472,7 @@ GlobalExceptionHandler.handleWechatApiException
| 步骤 | 命令 / 操作 | 期望 |
|---|---|---|
| 1. mvn 编译 | `mvn clean install -pl :backend-single -am -DskipTests` | 退出码 0 |
| 1. mvn 编译 | `mvn clean install -pl :server -am -DskipTests` | 退出码 0 |
| 2. 部署 | `python deploy.py backend` | 部署成功,重启 backend |
| 3. **WAF 验证**(修后 P1-7 | `tail -f /data/logs/emotion-museum/emotion-single.log \| grep "WeChatAPI"` | 看到 `[WeChatAPI] ---> GET https://api.weixin.qq.com/sns/jscode2session ...` 请求日志;`<--- 200` 响应日志 |
| 4. **UA 验证** | `grep "User-Agent" /data/logs/emotion-museum/emotion-single.log` | 看到 `Mozilla/5.0`(不再是 `Java/17.0.x` |
@@ -0,0 +1,67 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 明确小程序编辑资料页面在昵称未填时给出 Toast 提示的优化方案
---
# 小程序编辑资料页昵称必填提示优化
## 背景
当前小程序的编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)在昵称未填写时,点击保存按钮会直接静默返回,没有任何提示。用户无法判断是按钮失效、网络问题还是字段未填,体验较差。
## 目标
- 当用户点击保存按钮时,如果昵称未填写,必须给出明确的 Toast 提示「请填写昵称」。
- 保持改动最小,不影响现有保存流程和其他字段行为。
## 当前问题定位
`saveProfile` 方法中存在以下逻辑:
```javascript
if (!form.nickname.trim() || saving.value) return
```
`form.nickname` 为空或仅包含空白字符时,条件成立,方法直接 `return`,没有触发任何提示。
## 方案
采用「保存时校验 + Toast 提示」的轻量方案。
### 具体改动
**文件**`mini-program/src/pages/onboarding/index.vue`
**修改 `saveProfile` 方法**
1. 先判断 `saving.value`,避免重复提交。
2. 再判断昵称是否为空,为空时调用 `uni.showToast({ title: '请填写昵称', icon: 'none' })` 后返回。
改动后逻辑示例:
```javascript
const saveProfile = async () => {
if (saving.value) return
if (!form.nickname.trim()) {
uni.showToast({ title: '请填写昵称', icon: 'none' })
return
}
saving.value = true
// 原有保存逻辑保持不变
}
```
### 不改动的部分
- 不修改其他字段的校验规则。
- 不调整提示方式为字段高亮、弹窗或自动聚焦。
- 不新增失焦校验或实时输入校验。
## 验证标准
- 进入编辑资料页面,清空昵称后点击保存,页面顶部弹出 Toast「请填写昵称」。
- 填写昵称后点击保存,能正常提交并显示「已保存」。
- 浏览器 Console 无报错。
@@ -0,0 +1,81 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 去除编辑资料页默认昵称和剧本列表中的 mock/兜底数据
---
# 去除编辑资料页与剧本列表中的 mock/兜底数据
## 背景
项目代码中存在两处违反「禁止 mock 与兜底规则」的问题:
1. 编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)顶部预览卡片在没有昵称时,默认显示假名「Zoey」。
2. 剧本列表页面(`mini-program/src/pages/main/ScriptLibraryView.vue`)在 `store.scripts` 为空时,使用 `fallbackScripts` 数组展示 6 条写死 demo 数据;同时多个工具函数在无数据时返回固定的默认标签、字数、日期、进度等假值。
## 目标
- 没有昵称时,编辑资料页面顶部显示「未设置昵称」。
- 剧本列表没有数据时,不显示任何假数据,直接展示空状态。
- 清理剧本列表工具函数中的兜底默认值,确保「没有就是没有」。
## 改动点
### 1. 编辑资料页昵称空状态
**文件**`mini-program/src/pages/onboarding/index.vue:22`
**修改前**
```vue
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
```
**修改后**
```vue
<text class="hero-name">{{ form.nickname || '未设置昵称' }}</text>
```
### 2. 剧本列表删除 mock 数据与兜底值
**文件**`mini-program/src/pages/main/ScriptLibraryView.vue`
#### 2.1 删除 `fallbackScripts` 数组
删除第 178249 行的 `fallbackScripts` 常量及其 6 条 demo 数据。
#### 2.2 修改 `scripts` computed
**修改前**
```javascript
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
```
**修改后**
```javascript
const scripts = computed(() => store.scripts || [])
```
#### 2.3 清理工具函数默认值
| 函数 | 修改前 | 修改后 |
|---|---|---|
| `getTags` | `return [script.style || '逆袭成长', '都市', '事业', '热血']` | `return []` |
| `getWordCount` | `if (!count) return '3.1万字'` | 删除该分支,直接走后续逻辑(返回 `'0字'` |
| `getDateText` | 各分支使用 `|| '2025.05.10'``|| '2025.05.08'``|| '今天 21:30'` | 去掉兜底日期,返回空字符串 `''` |
| `getProgress` | `Number(script.progress || 28)` | `Number(script.progress || 0)` |
| `getChapterCount` | `script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))` | `script.chapterCount || script.chapters || 0` |
## 验证标准
1. 进入编辑资料页面,昵称输入框为空时,顶部预览卡片显示「未设置昵称」。
2. 剧本列表为空时,页面显示「还没有人生剧本」空状态,不展示任何 demo 剧本。
3. 剧本列表中的真实剧本数据展示不受影响(有数据时仍正常显示标题、标签、字数、日期、进度等)。
4. H5 模式下浏览器 Console 无新增报错。
5. 运行 `npm run build:h5` 编译通过。
@@ -0,0 +1,133 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 固定 ScriptView.vue 结果页顶部按钮,调整底部输入框边距,语音按钮改为麦克风图标
---
# ScriptView 对话结果页 UI 调整
## 背景
`ScriptView.vue`(剧本生成/对话结果页)中,结果页顶部左侧的「历史」按钮和右上角的「×」关闭按钮会随着页面内容滚动而移动,影响操作。底部输入框区域左右贴近屏幕边缘,视觉拥挤;语音按钮使用「语音」文字,不够简洁。
## 目标
1. 结果页顶部的历史按钮和关闭按钮在页面滚动时保持固定,不随内容滚动。
2. 底部输入框区域左右增加 24rpx 边距,输入框宽度适当压缩。
3. 语音按钮文字改为麦克风图标,颜色与当前主题一致。
4. 发送按钮保持当前大小和样式。
## 改动点
### 1. 顶部按钮固定
**文件**`mini-program/src/pages/main/ScriptView.vue`
将结果页顶部包裹层改为固定定位,并为下方内容增加顶部 padding 避免遮挡。
**结构示例**
```vue
<view class="result-header">
<view class="history-button" @click="openScriptLibrary">...</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
```
**样式示例**
```css
.result-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: 100rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
background: rgba(15, 7, 26, 0.85);
backdrop-filter: blur(18rpx);
-webkit-backdrop-filter: blur(18rpx);
}
```
同时给结果页内容区域增加顶部 padding(如 `padding-top: 100rpx` 或更大,以避开固定顶部栏和安全区)。
### 2. 底部输入框边距与压缩
**文件**`mini-program/src/pages/main/ScriptView.vue`
`.result-chat-bar` 增加左右内边距:
```css
.result-chat-bar {
/* 其他样式保持不变 */
padding: 0 24rpx;
}
```
输入框 `.result-input-shell` 保持 `flex: 1`,通过整体边距自然压缩可用宽度。如需进一步压缩,可增加左右 `margin: 0 10rpx`
语音按钮(86rpx × 76rpx)和发送按钮(92rpx × 76rpx)保持当前尺寸不变。
### 3. 语音按钮改为麦克风图标
**文件**`mini-program/src/pages/main/ScriptView.vue`
**修改前**
```vue
<view class="chat-voice-btn" ...>
<text>语音</text>
</view>
```
**修改后**
```vue
<view class="chat-voice-btn" ...>
<text class="voice-icon">🎤</text>
</view>
```
**样式**
```css
.chat-voice-btn {
/* 保持原有尺寸、背景、边框 */
width: 86rpx;
height: 76rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 26rpx;
background: rgba(88, 28, 135, 0.32);
border: 1rpx solid rgba(192, 132, 252, 0.3);
}
.voice-icon {
color: #e8ccff;
font-size: 32rpx;
}
.chat-voice-btn.pressing .voice-icon,
.chat-voice-btn.recognizing .voice-icon {
color: #fff;
}
```
如项目规范不允许使用 emoji,可改用 CSS 绘制的麦克风图标。
## 验证标准
1. 进入 ScriptView.vue 的剧本对话结果页,上下滚动内容,顶部历史按钮和关闭按钮始终保持在固定位置。
2. 底部输入框区域左右与屏幕边缘有 24rpx 边距。
3. 语音按钮显示为麦克风图标,默认颜色 `#e8ccff`,按下/识别时变为白色。
4. 发送按钮大小不变。
5. 输入框可以正常输入和发送消息。
6. 浏览器 Console 无新增报错。
7. 运行 `npm run build:h5` 编译通过。
@@ -0,0 +1,240 @@
---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"性格标签"与"兴趣爱好"两个模块的扩展设计:支持用户新增自定义标签、统一管理标签库、删除标签,并保证编辑页标签容器自适应撑开展示所有标签。
---
# 小程序编辑资料页:性格标签 / 兴趣爱好 标签库扩展设计
## 背景
当前小程序的编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)已具备性格标签和兴趣爱好的基础选择功能:
- 性格标签预设 11 个(理性、感性、乐观、独立、有创造力、坚韧、细腻、好奇、内敛、冒险、自由)。
- 兴趣爱好预设 10 个(阅读、旅行、音乐、写作、摄影、电影、运动、绘画、咖啡、游戏)。
- 兴趣爱好模块支持通过弹窗输入自定义标签。
- 性格标签模块 UI 上已有"+ 添加标签"占位,但未绑定事件。
- 两个模块均不支持删除标签,也没有统一的管理入口。
- 后端 `UserProfile` 实体只保存"用户已选中的标签"两个 JSON 字段(`personality_tags` / `hobbies`),不区分预设与自定义,也没有完整标签库的概念。
用户新增需求:性格标签和兴趣爱好两个模块均支持**用户自由新增自定义标签、保存、回显、删除**;编辑资料页的标签容器要**自适应撑开**,保证所有标签都能展示;**新增的标签展示在最前面**。
## 目标
1. 性格标签和兴趣爱好两个模块支持用户新增自定义标签、保存、回显、删除。
2. 新增的自定义标签展示在列表最前面。
3. 编辑资料页标签容器自适应撑开,不限行数,展示所有标签。
4. 提供独立的"管理标签"页面,支持统一删除(含预设与自定义)。
5. 老用户数据平滑迁移,不破坏现有已选标签。
6. 输入校验完整,避免重复、空值、超长。
## 非目标
- 不引入标签的分类、排序、分组等高级管理能力。
- 不做标签的全局共享、推荐、热门度等运营功能。
- 不修改后端存储架构(继续用 JSON 字段,不拆子表)。
- 不在编辑资料页做长按手势删除,统一收敛到管理页。
## 关键设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 新增标签输入方式 | `uni.showModal` 弹窗输入 | 与现有"自定义兴趣"交互保持一致,改动成本最低 |
| 删除入口 | 独立的"管理标签"页面 | 统一删除操作入口,避免编辑页交互过载;预设与自定义标签都能被删除 |
| 删除后的可见性 | 删除后从当前用户账号彻底移除 | 用户希望不再看到的标签就不会再出现 |
| 数据存储 | `UserProfile` 新增 `personality_tag_library` / `hobby_library` 两个 JSON 字段 | 后端需区分"用户已选中"与"用户可见的完整标签库",原有字段语义保留不变 |
| 预设 vs 自定义标签 | 前端维护一份内置预设数组;库里存的是用户专属合并后的字符串数组 | 预设标签库存在代码中便于维护,用户库字段只关心"当前账号可见的全部标签" |
| 编辑页标签顺序 | 新增的自定义标签 → 已选中的其他标签 → 其余未选中标签 | 严格保证"新增的标签展示在最前面",其他已选中标签紧随其后 |
## 后端改动
### 实体与数据库
`server` 模块:
- `UserProfile` 实体新增两个字段:
| 字段名 | Java 类型 | 数据库列 | 类型 | 说明 |
|---|---|---|---|---|
| `personalityTagLibrary` | `String` | `personality_tag_library` | `TEXT` | 用户的完整性格标签库,JSON 字符串(字符串数组) |
| `hobbyLibrary` | `String` | `hobby_library` | `TEXT` | 用户的完整兴趣爱好库,JSON 字符串(字符串数组) |
- 原 `personalityTags` / `hobbies` 字段保持不变,继续表示"用户已选中的标签"。
- 新增数据库迁移 SQL
```sql
ALTER TABLE user_profile
ADD COLUMN personality_tag_library TEXT DEFAULT NULL COMMENT '用户完整性格标签库(JSON数组)',
ADD COLUMN hobby_library TEXT DEFAULT NULL COMMENT '用户完整兴趣爱好库(JSON数组)';
```
### 接口变更
`UserProfileController` / `UserProfileService` 无需新增接口,复用现有:
- `PUT /user-profile/update`:入参 DTO 增加 `personalityTagLibrary` / `hobbyLibrary`(均为 `String`),直接持久化。
- `GET /user-profile/me`:返回体增加上述两个字段。
- `POST /user-profile/create`:同上。
### 数据兼容性
- 老用户(`personalityTagLibrary` 为空或 `null`)查询接口返回 `null`,由前端做兜底初始化。
- 后端不做兜底填充,严格遵守"没有就是没有"的禁止 mock 规则。
## 前端改动
### 数据层(`stores/app.js``services/userProfile.js`
- 请求返回的 `userProfile` 新增 `personalityTagLibrary` / `hobbyLibrary` 字段解析(JSON.parse)。
- `saveUserProfile` 提交时把两个库字段 `JSON.stringify` 后一并提交。
### 编辑资料页(`mini-program/src/pages/onboarding/index.vue`
#### 标签区结构(性格标签与兴趣爱好同构)
```
┌──────────────────────────────────────┐
│ 😊 性格标签 管理 > │ ← 标题行,右侧"管理"按钮跳转管理页
│ (最多选择5个) │
│ ┌────┐┌────┐┌────┐┌────┐ │
│ │ 理性 ││ 感性 ││ 乐观 ││ 独立 │... │ ← 标签网格,flex-wrap 自适应
│ └────┘└────┘└────┘└────┘ │
│ ┌───────┐ │
│ │ + 添加 │ │ ← 末尾占位,点击打开弹窗
│ └───────┘ │
└──────────────────────────────────────┘
```
- 容器使用 `display: flex; flex-wrap: wrap;`**高度随内容自适应撑开**。
- 标签顺序规则(严格保证"新增的标签展示在最前面"):
1. **新增的自定义标签**排在最前面(按创建时间倒序)。
2. **已选中的其他标签**(含预设与早期自定义)紧随其后。
3. **其余未选中的预设标签**排在最后。
- 样式沿用现有 `.tag-choice` / `.tag-choice.active`
- 点击标签:沿用 `toggleList(list, tag, 5)` 逻辑,超限 toast 提示。
#### 新增标签弹窗
- 点击" 添加标签"触发 `uni.showModal``editable: true`,占位符"请输入标签"。
- 校验规则:
- 非空、去除首尾空格。
- 长度 ≤ 8 个字符。
- 不能和当前库中任何标签完全相等(字符串完全一致比对)。
- 校验失败通过 `uni.showToast` 提示原因,不静默忽略。
- 校验通过后:
1. 新标签**插入到对应库数组的最前面**。
2. 若已选未满 5 个,自动加入已选数组;满则 toast 提示"已达上限,请先进入管理页删除其他标签",仍把新标签加入库,但不加入已选。
#### 保存流程
- 点击"保存"按钮时,把以下数据一起提交:
- `personalityTags`(已选中数组)
- `personalityTagLibrary`(完整库数组)
- `hobbies`(已选中数组)
- `hobbyLibrary`(完整库数组)
- 保存失败:toast 提示后端返回的真实错误信息,页面状态保留不变。
- 保存成功:toast 提示"已保存"350ms 后 `uni.navigateBack()`
### 管理页(新增独立页面)
路径:`mini-program/src/pages/onboarding/tag-manage.vue`
通过 `pages.json` 注册路由:`/pages/onboarding/tag-manage`
#### 入口与参数
- 编辑资料页的"管理"按钮跳转:`/pages/onboarding/tag-manage?type=personality``?type=hobby`
- 页面标题:根据 type 显示"管理性格标签"或"管理兴趣爱好"。
#### 页面结构
- 列表渲染当前库中全部标签(含剩余预设 + 自定义)。
- 每行结构:
```
┌────────────────────────────────────┐
│ 理性 × │
├────────────────────────────────────┤
│ 感性 × │
├────────────────────────────────────┤
│ ... │
└────────────────────────────────────┘
```
- **左滑删除**:使用 UniApp 的 `uni-swipe-action` 组件或自定义实现,向右滑动显示"删除"按钮。
- **点击 × 按钮**:同样触发删除。
- 删除前 `uni.showModal` 二次确认,提示"确定删除该标签吗?删除后不可恢复"。
- 删除后:
- 从库数组中移除该标签。
- 若该标签同时在已选数组中,一并移除。
- 页面列表立即刷新。
- 删除接口调用:删除动作本身不单独调用后端,仅在用户返回编辑资料页点击"保存"时统一提交。
#### 返回与数据同步
- 管理页的库数据存储在页面局部响应式变量中。
- 返回编辑资料页时,通过 `onUnload` 生命周期或全局 store 的 `updateRegistration` 把最新库数据回传给编辑资料页。
- 推荐方案:使用 `uni.$emit('tag-library-updated', { type, library })` 事件,编辑资料页在 `onShow` 中监听并合并。
## 数据初始化兼容方案
编辑资料页在 `onLoad` / `onShow` 时执行:
```
if (后端返回的 personalityTagLibrary 为空) {
库 = [...内置预设数组]
if (后端返回的 personalityTags 非空) {
// 把已选中的标签也合入库(老用户首次编辑时保留)
库 = 去重合并(库, 已选数组)
}
}
```
兴趣爱好同理。此逻辑保证老用户数据不丢失,新用户直接拿预设库。
## 错误处理
- 接口请求失败:按现有 `request.js` 约定,`try/catch` 返回 `{ success: false, error: 真实错误 }`,页面 toast 提示真实错误信息。**禁止本地 mock 成功响应、禁止兜底默认值**。
- 管理页删除:若保存时接口失败,本地已删除的标签状态**回滚**(恢复删除前的库内容),toast 提示失败原因。
- 网络超时:按 `request.js` 的现有超时策略处理。
## 文件清单
### 新增文件
- `mini-program/src/pages/onboarding/tag-manage.vue`:管理标签独立页面。
### 修改文件
**后端(`server/src/main/java/com/emotion/`):**
- `entity/UserProfile.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `controller/UserProfileController.java`:无需新增接口,入参 DTO 通过 `UserProfileUpdateRequest` / `UserProfileCreateRequest` 已支持新字段透传。
- `dto/request/userprofile/UserProfileUpdateRequest.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `dto/request/userprofile/UserProfileCreateRequest.java`:同上。
- `dto/response/userprofile/UserProfileResponse.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `service/UserProfileService.java``service/impl/UserProfileServiceImpl.java`:无业务逻辑改动,字段通过 MyBatis-Plus 自动映射持久化。
- 数据库迁移脚本:`server/src/main/resources/db/migration/2026-06-24-user-profile-tag-library.sql`,遵循现有 `YYYY-MM-DD-描述.sql` 命名约定。
**前端(`mini-program/`):**
- `src/pages/onboarding/index.vue`:重构标签区,新增管理入口、弹窗新增逻辑、自适应样式。
- `src/services/userProfile.js`:请求体与响应解析增加两个库字段。
- `src/stores/app.js``saveUserProfile` 提交时携带库字段。
- `src/pages.json`:注册 `tag-manage` 路由。
**不修改的文件:**
- `src/pages/main/MineView.vue`:展示区只读"已选中的标签"(沿用 `hobbies` 字段),不需要感知完整库,**无需修改**。
## 验收标准
1. 进入编辑资料页,性格标签、兴趣爱好两个区域的标签容器高度随内容自适应撑开,无截断。
2. 点击" 添加标签"或"+ 自定义兴趣",弹窗输入后,新标签出现在列表最前面。
3. 重复输入同名标签,toast 提示"标签已存在"。
4. 空输入、超长输入(>8 字符)分别 toast 提示对应原因。
5. 点击"管理"按钮,跳转到管理页,列表展示全部标签(预设 + 自定义)。
6. 管理页左滑或点 × 删除标签,二次确认后标签从列表消失。
7. 删除已在"已选"中的标签,返回编辑资料页后已选列表同步移除。
8. 保存后重新进入编辑资料页,新增的标签仍展示在最前面。
9. 老用户(无新字段)首次进入编辑资料页,已选标签保留,预设标签可见。
10. 接口失败时,页面保留原状态并 toast 真实错误,不静默成功。
@@ -0,0 +1,289 @@
---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"添加"按钮文字简化、字数限制收紧到4字、标签不换行、弹窗替换为深色太空主题自定义组件的改动设计。
---
# 小程序添加标签按钮简化与自定义弹窗设计
## 背景
当前小程序编辑资料页(`mini-program/src/pages/onboarding/index.vue`)和管理标签页(`mini-program/src/pages/onboarding/tag-manage.vue`)的"添加"按钮和弹窗存在以下问题:
- 编辑页性格标签的添加按钮文字是"+ 添加标签",兴趣爱好是"+ 添加兴趣",文字偏长,与紧凑的标签网格不协调。
- 添加标签的字数上限是 8 个字符,偏宽松,容易生成过长的标签破坏网格布局。
- 标签 `.tag-choice` / `.tag-text` 没有设置 `white-space: nowrap`,长标签会自动换行撑高单元格。
- 添加和删除确认都使用 `uni.showModal`,在 H5 模式下是 uni-app 默认的白底蓝按钮样式,与小程序整体的深色太空主题(深紫底、毛玻璃、紫色渐变、金色强调)严重不一致。
## 目标
1. 编辑页两个添加按钮文字统一简化为"+ 添加"。
2. 添加标签字数限制收紧到 4 个字。
3. 标签文字过长时不换行,用省略号截断。
4. 添加和删除确认弹窗替换为自定义弹窗组件,样式与深色太空主题保持一致。
## 非目标
- 不改标签的选择/取消选中逻辑、最多 5 个的限制。
- 不改后端接口或数据结构。
- 不改管理页的左滑删除手势。
- 不引入第三方 UI 组件库。
## 关键设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 添加按钮文字 | " 添加"(去掉"标签"/"兴趣"后缀) | 用户明确要求只保留"添加"两字 |
| 弹窗 title | 保持 `添加性格标签` / `添加兴趣爱好` | 按钮简、弹窗清,分开处理 |
| 字数限制 | 4 个字 | 用户明确要求 |
| toast 文案 | "内容不能超过4个字" | 用户指定 |
| 不换行实现 | `white-space: nowrap` + `overflow: hidden` + `text-overflow: ellipsis` | 标准三件套,标签固定高度不撑高 |
| 弹窗实现 | 新建自定义组件 `TagDialog.vue`,两种模式(input / confirm | 替换 `uni.showModal`,样式可控、深色主题统一 |
## 新增组件
### `mini-program/src/components/TagDialog.vue`
**职责**:统一的弹窗组件,支持输入模式和确认模式,样式与深色太空主题一致。
**Props**
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `visible` | `Boolean` | `false` | 是否显示弹窗 |
| `mode` | `String` | `'input'` | `'input'``'confirm'` |
| `title` | `String` | `''` | 弹窗标题 |
| `content` | `String` | `''` | confirm 模式的提示文案 |
| `placeholder` | `String` | `''` | input 模式的输入框占位符 |
| `modelValue` | `String` | `''` | input 模式的输入值(支持 v-model) |
**Emits**
| 事件 | 参数 | 说明 |
|---|---|---|
| `confirm` | input 模式:当前输入值;confirm 模式:无 | 点击确定按钮 |
| `cancel` | 无 | 点击取消按钮或遮罩层 |
| `update:modelValue` | 新值 | 输入框内容变化 |
| `update:visible` | `false` | 关闭弹窗 |
**模板结构**
```
┌────────────────────────────────┐
│ 遮罩层(半透明深色) │
│ ┌──────────────────────────┐ │
│ │ ✦ 添加性格标签 │ │ ← 标题 + 金色 ✦
│ │ ┌────────────────────┐ │ │
│ │ │ 输入框(深色底紫边)│ │ │ ← input 模式才有
│ │ └────────────────────┘ │ │
│ │ 确定删除「xxx」吗?... │ │ ← confirm 模式才有
│ │ │ │
│ │ [ 取消 ] [ 确定 ] │ │ ← 按钮
│ └──────────────────────────┘ │
└────────────────────────────────┘
```
**样式**(与小程序深色太空主题一致):
- 遮罩层:`rgba(3, 2, 13, 0.72)` 全屏覆盖
- 弹窗卡片:`glass-card` 毛玻璃 + `rgba(155, 110, 255, 0.14)` 紫色边框 + `22rpx` 圆角
- 标题:白色 `#fff` 加粗,配金色 `✦``#ffd58c`
- 输入框:深色底 `rgba(10, 12, 40, 0.66)` + 紫色边框 `rgba(151, 111, 255, 0.22)`
- 取消按钮:透明底 + 紫色边框
- 确定按钮:紫色渐变 `linear-gradient(180deg, #a855ff, #6b29ce)`
- 所有尺寸用 `rpx`
## 编辑资料页改动(`mini-program/src/pages/onboarding/index.vue`
### 模板
- 性格标签 panel 末尾占位:` 添加标签`` 添加`
- 兴趣爱好 panel 末尾占位:` 添加兴趣`` 添加`
- 在 `<scroll-view>` 之外、`</view>` 之前挂载 `<TagDialog>` 组件:
```html
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
```
### script setup
- 引入组件:`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态:
```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input') // 'input' 或 'confirm'
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null) // 记录当前弹窗对应的动作:{ kind: 'add'|'delete', type: 'personality'|'hobby', tag?: string }
```
- 重构 `addCustomTag`:改为打开弹窗(input 模式),不再调 `uni.showModal`
```javascript
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
dialogMode.value = 'input'
dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
dialogPlaceholder.value = '请输入标签(最多4个字)'
dialogInput.value = ''
dialogAction.value = { kind: 'add', type }
dialogVisible.value = true
}
```
- 新增 `onDialogConfirm`:处理弹窗确认(add / delete 两种动作):
```javascript
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
} else if (action.kind === 'delete') {
handleDeleteTag(action.type, action.tag)
}
dialogVisible.value = false
}
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
```
- 抽出 `handleAddTag`(原 `addCustomTag` 的 success 回调逻辑,字数限制改为 4):
```javascript
const handleAddTag = (type, rawValue) => {
const value = String(rawValue || '').trim()
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 4) {
uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
library.unshift(value)
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
```
### 样式
`.tag-choice` 增加:
```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```
## 管理页改动(`mini-program/src/pages/onboarding/tag-manage.vue`
### 模板
挂载 `<TagDialog>` 组件(confirm 模式):
```html
<TagDialog
v-model:visible="dialogVisible"
:mode="'confirm'"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
```
### script setup
- 引入组件:`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态:
```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```
- 重构 `confirmDelete`:改为打开弹窗(confirm 模式):
```javascript
const confirmDelete = (tag) => {
pendingTag.value = tag
dialogContent.value = `确定删除「${tag}」吗?删除后不可恢复`
dialogVisible.value = true
}
const onDialogConfirm = () => {
if (pendingTag.value) {
deleteTag(pendingTag.value)
pendingTag.value = null
}
dialogVisible.value = false
}
const onDialogCancel = () => {
pendingTag.value = null
dialogVisible.value = false
}
```
- `deleteTag` 函数保持不变(已经在 Task 6 实现)。
### 样式
`.tag-text` 增加:
```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```
## 文件清单
### 新增文件
- `mini-program/src/components/TagDialog.vue`:自定义弹窗组件。
### 修改文件
- `mini-program/src/pages/onboarding/index.vue`:按钮文字简化、字数限制 4、不换行样式、引入 TagDialog、重构 addCustomTag。
- `mini-program/src/pages/onboarding/tag-manage.vue`:引入 TagDialog、重构 confirmDelete、不换行样式。
## 验收标准
1. 编辑页性格标签和兴趣爱好的添加按钮只显示"+ 添加"。
2. 点击"+ 添加"弹出深色太空主题弹窗(毛玻璃卡片、紫色渐变、金色 ✦),不再是白底蓝按钮。
3. 输入 5 个字时 toast 提示"内容不能超过4个字"。
4. 输入 4 个字以内的新标签,确认后标签出现在列表最前面。
5. 标签文字过长时(如 4 字标签在窄屏),单元格内省略号截断,不换行撑高。
6. 管理页点 × 或左滑删除时,弹出深色主题确认弹窗,文案"确定删除「xxx」吗?删除后不可恢复"。
7. 弹窗的取消按钮和遮罩层点击都能关闭弹窗,不触发任何动作。
8. 弹窗在 H5 模式下样式与小程序深色太空主题一致。
@@ -0,0 +1,125 @@
---
author: claude
created_at: 2026-06-27
purpose: 统一小程序实现心愿页面中所有 AI 消息卡片的功能按钮,并修复 story-card 展开失效 bug
---
# AI 消息卡片功能按钮统一设计
## 背景
在小程序「实现心愿」页面(`mini-program/src/pages/main/ScriptView.vue`)中:
1. **第一个 AI 回复卡片**`story-card`)拥有完整的功能按钮:展开/收起、复制、换个方向、不像我、继续生成、播放。
2. **后续 AI 消息**`result-chat-list`)只有基础的展开/收起,缺少复制、换个方向、不像我、继续生成、播放。
3. **story-card 展开失效**:当用户触发过重新生成(`resultHasScriptReplies``true`)后,`<template v-if="!resultHasScriptReplies">` 会隐藏整个故事正文和底部展开按钮,导致点击展开无效果。
## 目标
- 每条 AI 回复消息卡片都拥有完整的功能按钮(展开/收起、复制、换个方向、不像我、继续生成、播放)
- 修复 story-card 展开失效的 bug
- 所有按钮功能正常可用
## 方案选择
采用 **方案 A:在现有单文件组件内修复 + 扩展**
- 不提取独立组件,保持现有文件结构
- 改动最小、风险低、不引入新文件
- 后续如有更复杂的差异化需求再升级
## 详细设计
### 1. 修复 story-card 展开失效
**根因**`ScriptView.vue` 第 192 行的条件包裹
```vue
<template v-if="!resultHasScriptReplies">
<scroll-view v-if="storyCollapsed" ...>
<text class="story-body">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">...</view>
</template>
```
`resultHasScriptReplies``true`(有 `kind: 'script'` 的消息)时,整个故事正文和底部展开按钮被隐藏。
**修复**:去掉 `<template v-if="!resultHasScriptReplies">` 条件包裹,让故事正文始终由 `storyCollapsed` 控制展开/收起。
**副作用**story-card 始终显示完整故事正文,与 result-chat-list 中的 script 消息内容重复。这是预期行为——两个卡片独立操作各自内容。
### 2. 给每条 assistant 消息加功能按钮组
**布局**:在每条 assistant 消息的内容 + 展开/收起按钮之后,追加一行网格按钮组,2 列布局(与 story-card 的 `.result-actions` 一致)。
**按钮清单**(按顺序):
| 按钮 | 作用 | 实现方式 |
|---|---|---|
| 复制 | 复制该消息正文到剪贴板 | `uni.setClipboardData` |
| 换个方向 | 进入「换方向」修订确认 | 复用现有 `changeDirection()` |
| 不像我 | 进入「不像我」修订确认 | 复用现有 `notLikeMe()` |
| 继续生成 | 收起故事 + 聚焦输入框 | 复用现有 `continueInChat()` |
| 播放 | 播放 TTS 音频 | 复用 `ttsPlayer.playSource`,传 `currentResult.id` |
**按钮条件**
- 展开/收起、复制:所有 assistant 消息都有
- 换个方向、不像我、继续生成、播放:所有 assistant 消息都有(本质上是全局操作,作用在 `currentResult` 上)
- 正在 `pending` 的 assistant 消息不显示按钮组(保持现有逻辑,`isAssistantMessage` 已排除 `pending`
### 3. 新增/修改的事件处理器
1. **`copyMessageContent(message)`**(新增):
- 复制 `message.content` 到剪贴板
- 空内容时 `showToast` 提示「暂无可复制内容」
- 埋点 `script_message_copy_click`
2. **`playMessageTts(message)`**(新增):
- 调用 `ttsPlayer.playSource(currentResult.value?.id || '')`
- 所有消息都播放同一个故事的 TTS(TTS 基于 `currentResult` 生成)
- 埋点 `script_message_tts_click`
3. **`toggleMessageCollapse(message)`**(修改):
- 补充埋点 `script_message_collapse_toggle`(与 `toggleStoryCollapse` 对齐)
### 4. 样式
为消息卡片内的按钮组新建 `.message-actions` 样式,复用 `.result-actions``.action-btn` 的视觉风格:
```css
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 16rpx;
}
```
按钮直接复用 `.action-btn` 类名,确保视觉与主卡片一致。展开/收起按钮(`.message-toggle`)保留现有金色调样式,与功能按钮组做视觉区分。
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| 空内容的 assistant 消息 | 复制按钮 `showToast` 提示「暂无可复制内容」;其他按钮正常可用 |
| pending 中的消息 | 不显示按钮组(现有逻辑) |
| user 消息 | 不显示按钮组 |
| 无 `currentResult` 时的播放 | 按钮仍可点击,`playSource` 会走内部兜底(传空 id |
## 修改范围
仅修改一个文件:`mini-program/src/pages/main/ScriptView.vue`
### 模板修改
1. 去掉第 192 行 `<template v-if="!resultHasScriptReplies">` 条件包裹
2. 在 `result-chat-list` 的 assistant 消息中添加功能按钮组
### 脚本修改
1. 新增 `copyMessageContent(message)` 方法
2. 新增 `playMessageTts(message)` 方法
3. 修改 `toggleMessageCollapse(message)` 补充埋点
### 样式修改
1. 新增 `.message-actions` 样式
@@ -0,0 +1,74 @@
---
author: peanu
created_at: 2026-06-27
purpose: 将 backend-single 目录重命名为 server 的设计方案
---
# 设计:backend-single 重命名为 server
## 背景
项目中的后端服务目录当前名为 `backend-single`,该名称是早期"单体服务"阶段的遗留命名。随着项目演进,这个名称已不再准确反映实际情况,且在日常开发中较长不便。将其重命名为更简洁的 `server` 可提升可读性和一致性。
## 决策
- **目录重命名**`backend-single/``server/`,使用 `git mv` 保留 Git 历史
- **Maven artifactId**`backend-single``server`JAR 文件随之变为 `server-1.0.0.jar`
- **历史文档**`docs/superpowers/` 下的历史设计和计划文档也全部更新,保持一致性
## 变更范围
### 第一层:目录和构建
| 操作 | 目标 |
|------|------|
| `git mv backend-single server` | 目录重命名 |
| `server/pom.xml` artifactId | `backend-single``server` |
| `server/pom.xml` name | `backend-single``server` |
| `server/deploy.py` JAR_NAME | `backend-single-1.0.0.jar``server-1.0.0.jar` |
| `server/deploy.sh` JAR_NAME | 同上 |
### 第二层:部署脚本
| 文件 | 变更内容 |
|------|---------|
| 根目录 `deploy.py` | 3 处 `backend-single``server`(注释、路径拼接、cwd |
| 根目录 `deploy.sh` | 4 处 `backend-single``server`(注释、存在性检查、cd |
| `.gitignore` | `backend-single/backend.err``server/backend.err` |
### 第三层:项目文档
| 文件 | 说明 |
|------|------|
| `CLAUDE.md` | 项目结构树(1 处)+ 后端命令段落(3 处) |
| `AGENTS.md` | 同 CLAUDE.md 的结构和命令段落 |
| `README.md` | 项目结构树(1 处) |
| `server/部署说明.md` | 自引用路径 |
| `快速部署参考.md` | 4 处部署路径引用 |
| `一键部署说明.md` | 5 处部署路径引用 |
### 第四层:前端和其他代码
| 文件 | 说明 |
|------|------|
| `web/src/types/auth.ts` | 第 5 行注释引用 |
| `web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md` | 4 处后端文件路径引用 |
### 第五层:历史文档批量替换
约 20 个 `docs/superpowers/specs/``docs/superpowers/plans/` 下的 `.md` 文件,将其中所有 `backend-single` 替换为 `server`
## 不在范围内
- **Java 包名**`com.emotion` 保持不变,不在本次重命名范围内
- **数据库名/表名**:不涉及数据库变更
- **`dev-services.py`**:该脚本自动发现 Spring Boot 服务,不直接引用 `backend-single` 字符串,无需修改
- **`deploy-server.sh`**:已存在的文件,不包含 `backend-single` 引用
## 验证标准
1. `mvn clean install -DskipTests``server/` 目录下编译成功
2. `server/deploy.py backend` 能正确找到 JAR 并部署
3. 根目录 `deploy.py` 能正确调用 `server/deploy.py`
4. 项目中不再有任何 `backend-single` 字符串残留(grep 验证)
5. Git 历史完整保留(`git log --follow server/pom.xml` 能看到旧历史)
@@ -0,0 +1,156 @@
---
author: claude
created_at: 2026-06-27
purpose: 规范本地服务管理:强制使用 dev-services.py 控制前后端服务,固定前端/H5 端口从 5178 开始累加,优化热加载避免无意义重启
---
# 本地服务管理规则优化设计
## 背景
当前项目使用 `dev-services.py` 管理本地前后端服务,但存在以下问题:
1. 前端/H5 端口分散配置在各自 `vite.config.*` 中,未统一管理
2. `dev-services.py` 端口冲突时自动递增,导致端口号不稳定
3. 用户已习惯使用 `dev-services.py`,但缺少"禁止无意义重启"的强制约束
4. 热加载场景下,开发者可能误重启支持热更新的前端服务,浪费时间
## 目标
1. 强制:启动/重启/停止本地前后端服务必须使用 `dev-services.py`
2. 固定:前端和 H5 服务端口从 5178 开始累加分配
3. 优化:`dev-services.py restart` 在无必须重启的变更时禁止重启
4. 一致:更新 CLAUDE.md 规则文档,与脚本行为对齐
## 方案选择
采用 **方案 Adev-services.py 内置固定端口表 + 修改各项目 vite 配置**
- 端口控制集中且与项目配置一致
- 与现有 `dev-services.py` 架构兼容
- 便于开发者直接查看配置文件了解端口
## 详细设计
### 1. 固定端口分配表
| 服务 | 目录 | 类型 | 固定端口 |
|---|---|---|---|
| Emotion-museum-web | `web/` | Vite | 5178 |
| Emotion-museum-admin | `web-admin/` | Vite | 5179 |
| Emotion-museum-uniapp | `mini-program/` | UniApp H5 | 5180 |
| Life-script 前端 | `life-script/` | Vite | 5181 |
后端服务端口保持现有配置不变。
### 2. dev-services.py 修改
#### 2.1 新增固定端口映射
```python
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
```
按项目目录名匹配。如果目录名不在映射中,仍按原有逻辑处理。
#### 2.2 修改端口分配逻辑
`assign_unique_ports()` 中:
- 对 `FIXED_FRONTEND_PORTS` 中定义的服务,强制使用固定端口
- 如果固定端口被非前端服务占用,报错退出
- 如果固定端口被另一个前端服务占用,报错退出(说明映射配置错误)
- 不再对前端服务自动递增端口
#### 2.3 新增 restart 热加载保护
定义"必须重启才能生效"的文件模式:
```python
RESTART_REQUIRED_PATTERNS = [
r'vite\.config\.(ts|js|mjs|cjs)$',
r'tsconfig\.json$',
r'\.env(\.[^/]*)?$',
r'package\.json$',
r'application.*\.ya?ml$',
r'pom\.xml$',
r'build\.gradle(\.kts)?$',
]
```
`restart` 命令执行前:
1. 获取 git 工作区中已修改的文件列表:`git diff --name-only`
2. 如果没有修改,提示"未检测到代码变更,无需重启"
3. 如果修改文件匹配 `RESTART_REQUIRED_PATTERNS`,允许正常 restart
4. 如果修改文件不匹配,拒绝 restart 并提示:
> 当前修改只涉及支持热加载的源码文件,无需重启。如需强制重启,请使用 `python dev-services.py restart --force`
#### 2.4 支持 `--force` 参数
`restart` 子命令解析中增加 `--force` 选项,允许用户绕过热加载保护强制重启。
### 3. 前端项目配置修改
| 文件 | 修改内容 |
|---|---|
| `web/vite.config.ts` | `server.port` 改为 5178 |
| `web/.env.development` | 添加 `VITE_PORT=5178` |
| `web-admin/vite.config.ts` | `server.port` 改为 5179 |
| `web-admin/.env.development` | `VITE_APP_PORT` 改为 5179 |
| `mini-program/vite.config.js` | `server.port` 改为 5180 |
| `mini-program/.env.development` | 添加 `VITE_PORT=5180` |
| `life-script/vite.config.js` | `server.port` 改为 5181 |
| `life-script/.env.development` | 添加 `VITE_PORT=5181`(不存在则创建) |
### 4. CLAUDE.md 规则更新
新增"本地服务管理规则"章节:
1. **必须使用 dev-services.py**
- 启动、重启、停止本地前后端服务必须使用 `python dev-services.py [命令] [服务名]`
- 禁止直接使用 `npm run dev``npm run dev:h5``mvn spring-boot:run` 等命令
2. **前端/H5 固定端口**
- `web`: 5178
- `web-admin`: 5179
- `mini-program`: 5180
- `life-script`: 5181
3. **禁止无意义重启**
- 只有修改必须重启才能生效的文件时,才允许执行 `restart`
- 修改源码文件(`.vue``.tsx``.ts``.js``.css``.scss` 等)时禁止 restart
- 确需强制重启时,使用 `python dev-services.py restart --force`
4. **热加载规则**
- 前端修改源码文件优先利用热加载
- 后端修改代码按现有规则处理(本地不启动后端,使用服务器验收)
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| 固定端口被占用 | `dev-services.py` 报错,要求先停止占用该端口的服务 |
| 无代码变更执行 restart | 提示无需重启 |
| 只修改 `.vue` 文件执行 restart | 拒绝重启,提示使用 `--force` 或无需重启 |
| 修改 `vite.config.ts` 执行 restart | 允许正常 restart |
| 多个服务同时 restart | 每个服务独立检测,符合规则的重启,不符合的跳过 |
| 用户传 `--force` | 跳过所有检测,强制重启 |
## 修改范围
| 文件 | 操作 |
|---|---|
| `dev-services.py` | 修改:添加固定端口映射、优化端口分配、新增热加载保护 |
| `web/vite.config.ts` | 修改:端口改为 5178 |
| `web/.env.development` | 修改/创建:添加 `VITE_PORT=5178` |
| `web-admin/vite.config.ts` | 修改:端口改为 5179 |
| `web-admin/.env.development` | 修改:`VITE_APP_PORT` 改为 5179 |
| `mini-program/vite.config.js` | 修改:端口改为 5180 |
| `mini-program/.env.development` | 修改/创建:添加 `VITE_PORT=5180` |
| `life-script/vite.config.js` | 修改:端口改为 5181 |
| `life-script/.env.development` | 修改/创建:添加 `VITE_PORT=5181` |
| `CLAUDE.md` | 修改:更新本地服务管理规则 |
@@ -0,0 +1,158 @@
---
author: claude
created_at: 2026-06-27
purpose: 提取 MessageCard 组件,统一所有 AI 消息卡片样式,按内容长度阈值(100 字)决定是否显示完整功能按钮
---
# MessageCard 组件统一设计
## 背景
在上一轮实现中,result-chat-list 中的 assistant 消息使用了 `.chat-bubble.system` 气泡样式,与 `story-card` 的卡片样式不一致。用户要求:
1. 所有 AI 回复消息卡片的样式、功能、布局、按钮等,都与第一次生成剧本的 `story-card` 完全一致
2. 内容 < 100 字的 assistant 消息:只显示消息内容,无功能按钮
3. 内容 ≥ 100 字的 assistant 消息:必须与 `story-card` 完全一致
## 目标
- 提取独立的 `MessageCard.vue` 组件
- story-card 和 result-chat-list 都复用该组件
- 按 100 字阈值自动切换显示模式
## 方案选择
采用 **方案 B:提取独立 MessageCard.vue 组件**
- 代码复用,维护性好
- 改动范围:新建 1 个组件 + 修改 ScriptView.vue
## 详细设计
### 1. 组件接口
**文件**`mini-program/src/components/MessageCard.vue`
**Props**
| Prop | 类型 | 必填 | 说明 |
|---|---|---|---|
| `content` | String | 是 | 消息/故事正文内容 |
| `title` | String | 否 | 卡片标题,默认 `'我的人生剧本'` |
| `tags` | Array | 否 | 标签列表 |
| `collapsed` | Boolean | 是 | 当前折叠状态 |
| `contentLength` | Number | 是 | 内容字数(用于阈值判断) |
| `isShortMessage` | Boolean | 是 | `contentLength < 100` 时为 true |
| `ttsIcon` | String | 否 | TTS 按钮图标(`▶``Ⅱ` |
| `ttsText` | String | 否 | TTS 按钮文字(`播放``暂停` |
**Events**
| 事件 | 说明 |
|---|---|
| `toggle-collapse` | 展开/收起 |
| `copy` | 复制内容 |
| `change-direction` | 换个方向 |
| `not-like-me` | 不像我 |
| `continue` | 继续生成 |
| `play-tts` | 播放 TTS |
### 2. 组件内部结构
**短消息(`isShortMessage = true`**
```vue
<view class="chat-bubble system">
<text>{{ content }}</text>
</view>
```
只显示纯文本气泡,无标题、无标签、无按钮。
**长消息(`isShortMessage = false`**
完整复用现有 `story-card` 的 HTML 结构:
- 标题(`.story-title`+ 标签(`.tag-row`
- 头部操作区:展开/收起按钮 + 复制按钮
- 正文:展开时用 scroll-view,收起时用 text
- 底部:展开全文/收起全文按钮
- 功能按钮组(2 列网格):换个方向、不像我、继续生成、播放
### 3. ScriptView.vue 中的使用
**替换现有 story-card**
```vue
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
```
**替换 result-chat-list 中的 assistant 消息**
```vue
<view v-for="message in resultMessages" :key="message.id">
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
```
### 4. 样式迁移
将以下 CSS 类从 `ScriptView.vue` 迁移到 `MessageCard.vue``<style scoped>` 中:
- `.story-card``.story-card.collapsed`
- `.story-head``.story-head-actions``.story-title-wrap`
- `.story-title``.tag-row``.tag`
- `.collapse-icon``.copy-card-btn``.collapse-row`
- `.collapse-chevron``.story-body``.story-body-scroll`
- `.result-actions``.action-btn`
`ScriptView.vue` 中删除这些已迁移的样式。
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| content 刚好 100 字 | `isShortMessage = false`,显示完整卡片 |
| content 为空 | 显示空卡片(与 story-card 现有行为一致) |
| pending 中的消息 | 不使用 MessageCard,保持 thinking-dots 渲染 |
| user 消息 | 不使用 MessageCard,保持 `.chat-bubble.user` 气泡 |
## 修改范围
| 文件 | 操作 |
|---|---|
| `mini-program/src/components/MessageCard.vue` | 新建 |
| `mini-program/src/pages/main/ScriptView.vue` | 修改(模板 + 脚本 + 样式) |
@@ -0,0 +1,207 @@
---
author: system
created_at: 2026-06-27
purpose: 短信登录开关与通用系统配置功能设计
---
# 短信登录开关与通用系统配置设计
## 背景
当前系统未接入真实短信服务商,短信验证码使用硬编码 `123456`。小程序登录页同时展示「微信一键登录」和「手机号+验证码登录」两种方式,SMS 相关元素始终可见。
需要在管理后台增加系统配置管理功能,通过「短信登录开关」控制小程序端是否展示手机号短信登录入口。短信未启用时,小程序登录页只显示微信授权登录。
## 方案选择
采用 **方案 AKey-Value 通用配置表**
- 新建 `t_system_config` 表,key-value 结构
- 通用性强,后续加新配置只需 INSERT 一行数据,不用改表结构
- 复杂度可控,管理后台根据 `value_type` 动态渲染对应控件
## 详细设计
### 一、数据库设计
#### 新建表 `t_system_config`
```sql
CREATE TABLE t_system_config (
id VARCHAR(64) PRIMARY KEY,
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置唯一标识',
config_value VARCHAR(500) NOT NULL DEFAULT '' COMMENT '配置值',
value_type VARCHAR(20) NOT NULL DEFAULT 'string' COMMENT '值类型: boolean/string/number/json',
config_group VARCHAR(50) NOT NULL DEFAULT 'system' COMMENT '配置分组: system/login/notification',
config_name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '显示名称',
description VARCHAR(300) DEFAULT '' COMMENT '配置说明',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序号',
is_visible TINYINT NOT NULL DEFAULT 1 COMMENT '是否在管理后台可见: 0-隐藏, 1-显示',
create_by VARCHAR(64) DEFAULT NULL,
create_time DATETIME DEFAULT NULL,
update_by VARCHAR(64) DEFAULT NULL,
update_time DATETIME DEFAULT NULL,
is_deleted TINYINT NOT NULL DEFAULT 0
) COMMENT = '系统配置表';
```
#### 初始数据
```sql
INSERT INTO t_system_config (id, config_key, config_value, value_type, config_group, config_name, description, sort_order, is_visible, create_time, update_time, is_deleted)
VALUES (
REPLACE(UUID(), '-', ''),
'sms_login_enabled',
'false',
'boolean',
'login',
'短信登录',
'是否启用手机号+短信验证码登录方式',
1,
1,
NOW(),
NOW(),
0
);
```
### 二、后端设计
#### 新增文件
| 层 | 文件 | 说明 |
|---|---|---|
| Entity | `SystemConfig.java` | 继承 `BaseEntity`,映射 `t_system_config` |
| Mapper | `SystemConfigMapper.java` | MyBatis-Plus 基础 CRUD |
| Service | `SystemConfigService.java` | 接口定义 |
| Service | `SystemConfigServiceImpl.java` | 业务逻辑实现 |
| Controller | `SystemConfigController.java` | 管理端接口,需 admin 登录 |
| DTO | `SystemConfigUpdateRequest.java` | 更新请求体(key + value |
| DTO | `LoginConfigResponse.java` | 返回给小程序的登录方式配置 |
#### 接口设计
**管理端接口(需要 admin token):**
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/admin/systemConfig/list` | 获取所有可见配置列表(`is_visible=1` |
| PUT | `/admin/systemConfig/update` | 批量更新配置值,传入 `List<SystemConfigUpdateRequest>` |
**公开接口(不需要登录,小程序登录页调用):**
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/auth/loginConfig` | 返回当前启用的登录方式 |
`/auth/loginConfig` 返回格式:
```json
{
"code": 200,
"data": {
"wechatLoginEnabled": true,
"smsLoginEnabled": false
}
}
```
#### 核心逻辑
1. `SystemConfigService` 提供 `getConfigValue(String key)` 方法
2. 内部做 Redis 缓存,缓存 key 为 `system_config:{config_key}`
3. 更新配置时同步清除对应缓存
4. `/auth/loginConfig` 读取 `sms_login_enabled` 配置值,组装 `LoginConfigResponse` 返回
5. 管理端更新接口接收 `List<SystemConfigUpdateRequest>`,支持一次提交多个配置变更
6. `/auth/loginConfig``WebMvcConfig` 白名单中放行(与 `/auth/login``/auth/wechat/login` 同级)
### 三、管理后台设计
#### 路由配置
`web-admin/src/router/index.ts` 中新增:
```ts
{
path: '/system',
component: Layout,
redirect: '/system/settings',
meta: { title: '系统设置', icon: 'Setting' },
children: [
{
path: 'settings',
name: 'SystemSettings',
component: () => import('@/views/system/SystemSettings.vue'),
meta: { title: '基础设置' }
}
]
}
```
#### 页面设计
**文件**`web-admin/src/views/system/SystemSettings.vue`
**页面结构**
- 顶部:页面标题「基础设置」+ 保存按钮
- 内容区:按 `config_group` 分组展示配置项
- 每个配置项根据 `value_type` 渲染不同控件:
- `boolean``el-switch` 开关
- `string``el-input` 输入框
- `number``el-input-number` 数字输入框
- 当前只有一个配置项:「短信登录」开关,下方显示描述文字
- 点击「保存」调用批量更新接口
#### API 封装
新增 `web-admin/src/api/systemConfig.ts`
- `getSystemConfigList()` → GET `/admin/systemConfig/list`
- `updateSystemConfig(list)` → PUT `/admin/systemConfig/update`
### 四、小程序登录页设计
#### 数据获取
在登录页 `onMounted` 时调用 `/auth/loginConfig` 接口:
```js
const loginConfig = ref({ wechatLoginEnabled: true, smsLoginEnabled: false })
onMounted(async () => {
try {
const res = await getLoginConfig()
loginConfig.value = res.data
} catch (e) {
// 接口失败时默认只显示微信登录
loginConfig.value = { wechatLoginEnabled: true, smsLoginEnabled: false }
}
})
```
#### 条件渲染
`v-if="loginConfig.smsLoginEnabled"` 控制以下元素的显示:
- 分隔线(「或使用手机号登录」)
- 手机号输入框
- 验证码输入框 + 获取验证码按钮
- 「开启旅程」手机号登录按钮
始终显示的元素:
- 品牌标题区域
- 「微信一键登录」按钮
- 底部协议文字
#### 代码处理
- 新增 `services/auth.js` 中的 `getLoginConfig()` 方法
- 短信相关的 `ref``phone``code``countdown` 等)和方法保留不动,只是 `v-if` 不渲染
- 不删除任何现有短信登录代码,后续启用短信时只需将配置改为 `true` 即可
## 验收标准
1. 管理后台出现「系统设置」菜单,可看到「短信登录」开关
2. 开关默认关闭(`false`
3. 小程序登录页在短信关闭时只显示微信登录按钮,无手机号表单
4. 管理后台将开关打开后,小程序刷新登录页可看到手机号+验证码表单
5. 后端编译通过,部署到服务器后验收正常
6. 浏览器 Console 无报错
+1
View File
@@ -1,2 +1,3 @@
# 开发环境配置
VITE_API_BASE_URL=http://localhost:19089/api
VITE_PORT=5181
+1 -1
View File
@@ -8,6 +8,6 @@ export default defineConfig(({ mode }) => ({
// 生产环境使用 /life-script/ 路径,开发环境使用根路径
base: mode === 'production' ? '/life-script/' : '/',
server: {
port: 3000
port: 5181
}
}))
+1 -1
View File
@@ -4,7 +4,7 @@
services:
backend:
name: "后端服务"
dir: "backend-single"
dir: "server"
type: "java"
port: 19089
health_check_path: "/actuator/health"
+1
View File
@@ -10,3 +10,4 @@ VITE_APP_ENV=dev
VITE_API_BASE_URL=https://lifescript.happylifeos.com/api
VITE_WS_URL=wss://lifescript.happylifeos.com/ws
VITE_DEBUG=true
VITE_PORT=5180
+373
View File
@@ -0,0 +1,373 @@
<template>
<view v-if="isShortMessage" class="chat-bubble system">
<text>{{ content }}</text>
</view>
<view v-else class="story-card" :class="{ collapsed }">
<view class="story-head">
<view class="story-title-wrap">
<text class="story-title">{{ title || '我的人生剧本' }}</text>
<view v-if="tags?.length" class="tag-row">
<text v-for="tag in tags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
<view class="story-head-actions">
<button class="collapse-icon" @click="$emit('toggle-collapse')">
<text class="collapse-icon-text">{{ collapsed ? '展开' : '收起' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</button>
<button class="copy-card-btn" @click="$emit('copy')">复制</button>
</view>
</view>
<scroll-view
v-if="collapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
<view class="collapse-row" @click="$emit('toggle-collapse')">
<text class="collapse-row-text">{{ collapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</view>
<view class="result-actions">
<button class="action-btn" @click="$emit('change-direction')">换个方向</button>
<button class="action-btn" @click="$emit('not-like-me')">不像我</button>
<button class="action-btn" @click="$emit('continue')">继续生成</button>
<button class="action-btn primary" @click="$emit('play-tts')">
<text class="action-icon">{{ ttsIcon || '▶' }}</text>
<text>{{ ttsText || '播放' }}</text>
</button>
</view>
</view>
</template>
<script setup>
const props = defineProps({
content: { type: String, required: true },
title: { type: String, default: '' },
tags: { type: Array, default: () => [] },
collapsed: { type: Boolean, default: false },
contentLength: { type: Number, required: true },
isShortMessage: { type: Boolean, required: true },
ttsIcon: { type: String, default: '▶' },
ttsText: { type: String, default: '播放' }
})
defineEmits([
'toggle-collapse',
'copy',
'change-direction',
'not-like-me',
'continue',
'play-tts'
])
</script>
<style scoped>
/* 短消息气泡样式 */
.chat-bubble {
max-width: 86%;
display: flex;
flex-direction: column;
gap: 8rpx;
padding: 24rpx 28rpx;
border-radius: 36rpx;
font-size: 34rpx;
line-height: 52rpx;
}
.chat-bubble.system {
align-self: flex-start;
background: rgba(16, 8, 34, 0.28);
border: 1rpx solid rgba(192, 132, 252, 0.3);
color: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
/* 故事卡片容器 */
.story-card {
border-radius: 52rpx;
padding: 34rpx;
background: rgba(12, 5, 28, 0.34);
border: 1rpx solid rgba(192, 132, 252, 0.46);
box-shadow: 0 0 48rpx rgba(125, 55, 205, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.story-card.collapsed {
box-shadow: 0 0 42rpx rgba(125, 55, 205, 0.14);
}
/* 卡片头部区域 */
.story-head {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.story-head-actions {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 12rpx;
}
.story-title-wrap {
flex: 1;
min-width: 0;
}
.story-title {
display: block;
font-size: 52rpx;
font-weight: 700;
line-height: 1.35;
}
/* 展开/收起按钮 */
.collapse-icon {
width: 120rpx;
min-width: 120rpx;
height: 58rpx;
min-height: 58rpx;
line-height: 1;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: rgba(255, 244, 255, 0.95);
background:
linear-gradient(135deg, rgba(134, 72, 255, 0.44), rgba(39, 15, 76, 0.42)),
rgba(255, 255, 255, 0.05);
border: 1rpx solid rgba(216, 180, 254, 0.46);
box-shadow:
0 14rpx 30rpx rgba(61, 18, 113, 0.22),
inset 0 1rpx 0 rgba(255, 255, 255, 0.26),
inset 0 -10rpx 18rpx rgba(33, 9, 73, 0.16);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
}
.collapse-icon::after {
border: 0;
}
.collapse-icon:active,
.copy-card-btn:active,
.collapse-row:active {
transform: scale(0.98);
opacity: 0.92;
}
/* 复制按钮 */
.copy-card-btn {
width: 120rpx;
min-width: 120rpx;
height: 50rpx;
min-height: 50rpx;
line-height: 1;
padding: 0;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 244, 255, 0.9);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
background:
radial-gradient(circle at 50% -30%, rgba(255, 255, 255, 0.18), transparent 50%),
rgba(88, 28, 135, 0.26);
border: 1rpx solid rgba(216, 180, 254, 0.32);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 10rpx 22rpx rgba(61, 18, 113, 0.12);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.copy-card-btn::after {
border: 0;
}
.collapse-icon-text,
.collapse-row-text {
line-height: 1;
white-space: nowrap;
}
/* 箭头图标 */
.collapse-chevron {
position: relative;
width: 20rpx;
height: 16rpx;
flex-shrink: 0;
transition: transform 0.18s ease;
}
.collapse-chevron.down {
transform: rotate(180deg);
}
.collapse-chevron view {
position: absolute;
top: 7rpx;
width: 12rpx;
height: 4rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #fff3b0, #ffd86b);
box-shadow: 0 0 12rpx rgba(255, 216, 107, 0.5);
}
.collapse-chevron view:first-child {
left: 0;
transform: rotate(-38deg);
transform-origin: right center;
}
.collapse-chevron view:last-child {
right: 0;
transform: rotate(38deg);
transform-origin: left center;
}
/* 标签行 */
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.tag {
padding: 6rpx 16rpx;
border-radius: 999rpx;
color: #d18aff;
font-size: 24rpx;
background: rgba(168, 85, 247, 0.22);
}
/* 正文内容 */
.story-body {
display: block;
margin-top: 28rpx;
font-size: 32rpx;
font-weight: 400;
line-height: 1.78;
color: rgba(255, 255, 255, 0.92);
white-space: pre-wrap;
}
.story-body-scroll {
max-height: 420rpx;
height: auto;
margin-top: 28rpx;
padding-right: 8rpx;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.story-body-scroll .story-body {
margin-top: 0;
}
/* 底部展开/收起行 */
.collapse-row {
position: relative;
height: 62rpx;
margin-top: 22rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
border-radius: 999rpx;
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 800;
overflow: hidden;
background:
radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.2), transparent 46%),
linear-gradient(135deg, rgba(98, 40, 174, 0.3), rgba(24, 8, 58, 0.24));
border: 1rpx solid rgba(192, 132, 252, 0.36);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 12rpx 28rpx rgba(61, 18, 113, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.collapse-row::before {
content: '';
position: absolute;
left: 34rpx;
right: 34rpx;
top: 0;
height: 1rpx;
background: linear-gradient(90deg, transparent, rgba(255, 236, 180, 0.6), transparent);
}
/* 操作按钮区域 */
.result-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 26rpx;
}
.action-btn {
height: 72rpx;
min-height: 72rpx;
padding: 0 8rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e8ccff;
font-size: 26rpx;
font-weight: 700;
line-height: 1.15;
text-align: center;
white-space: normal;
box-sizing: border-box;
background: rgba(88, 28, 135, 0.18);
border: 1rpx solid rgba(192, 132, 252, 0.35);
}
.action-btn.primary {
color: #fff;
background: linear-gradient(145deg, #8c44f2, #5f1db8);
}
.action-icon {
margin-right: 8rpx;
font-size: 24rpx;
font-weight: 900;
line-height: 1;
}
.action-btn::after {
border: 0;
}
</style>
+189
View File
@@ -0,0 +1,189 @@
<template>
<view v-if="visible" class="tag-dialog-mask" @click="onMaskClick">
<view class="tag-dialog-card" @click.stop>
<view class="tag-dialog-title-row">
<text class="tag-dialog-gold"></text>
<text class="tag-dialog-title">{{ title }}</text>
</view>
<input
v-if="mode === 'input'"
class="tag-dialog-input"
:value="modelValue"
:placeholder="placeholder"
placeholder-class="tag-dialog-placeholder"
:maxlength="4"
@input="onInput"
@confirm="onInputConfirm"
/>
<text v-if="mode === 'confirm'" class="tag-dialog-content">{{ content }}</text>
<view class="tag-dialog-actions">
<view class="tag-dialog-btn tag-dialog-btn-cancel" @click="onCancel">
<text>取消</text>
</view>
<view class="tag-dialog-btn tag-dialog-btn-confirm" @click="onConfirm">
<text>确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
visible: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'input'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['confirm', 'cancel', 'update:modelValue', 'update:visible'])
const onInput = (event) => {
const value = event.detail.value
emit('update:modelValue', value)
}
const onInputConfirm = () => {
emit('confirm', props.modelValue)
}
const onConfirm = () => {
if (props.mode === 'input') {
emit('confirm', props.modelValue)
} else {
emit('confirm')
}
}
const onCancel = () => {
emit('cancel')
emit('update:visible', false)
}
const onMaskClick = () => {
emit('cancel')
emit('update:visible', false)
}
</script>
<style scoped>
.tag-dialog-mask {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(3, 2, 13, 0.72);
}
.tag-dialog-card {
width: 560rpx;
box-sizing: border-box;
padding: 36rpx 32rpx 28rpx;
border-radius: 22rpx;
border: 1rpx solid rgba(155, 110, 255, 0.14);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.1), transparent 34%),
rgba(10, 13, 43, 0.54);
box-shadow: inset 0 0 30rpx rgba(123, 60, 255, 0.05), 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.tag-dialog-title-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
}
.tag-dialog-gold {
color: #ffd58c;
font-size: 26rpx;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.45);
}
.tag-dialog-title {
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.tag-dialog-input {
width: 100%;
box-sizing: border-box;
height: 80rpx;
margin-bottom: 28rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid rgba(151, 111, 255, 0.22);
background: rgba(10, 12, 40, 0.66);
color: #fff;
font-size: 26rpx;
}
.tag-dialog-placeholder {
color: rgba(214, 204, 235, 0.42);
}
.tag-dialog-content {
display: block;
margin-bottom: 32rpx;
color: rgba(239, 232, 255, 0.86);
font-size: 26rpx;
line-height: 1.6;
}
.tag-dialog-actions {
display: flex;
gap: 20rpx;
}
.tag-dialog-btn {
flex: 1;
height: 72rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
}
.tag-dialog-btn-cancel {
color: rgba(239, 232, 255, 0.82);
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
}
.tag-dialog-btn-confirm {
color: #fff;
border: 1rpx solid rgba(206, 82, 255, 0.95);
background: linear-gradient(180deg, rgba(169, 61, 255, 0.62), rgba(107, 41, 206, 0.5));
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.52), inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
</style>
+7
View File
@@ -18,6 +18,13 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/onboarding/tag-manage",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "管理标签"
}
},
{
"path": "pages/main/index",
"style": {
+23 -10
View File
@@ -30,13 +30,13 @@
<text v-else>微信一键登录</text>
</view>
<view class="login-divider">
<view class="login-divider" v-if="loginConfig.smsLoginEnabled">
<view></view>
<text>或使用手机号登录</text>
<view></view>
</view>
<view class="form">
<view class="form" v-if="loginConfig.smsLoginEnabled">
<view class="input-group">
<text class="input-label">手机号码</text>
<input
@@ -73,6 +73,7 @@
</view>
<view
v-if="loginConfig.smsLoginEnabled"
class="btn-primary login-btn"
:class="{ disabled: !canSubmit || loading }"
@click="handleLogin"
@@ -92,7 +93,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { getSmsCode } from '../../services/auth.js'
import { getSmsCode, getLoginConfig } from '../../services/auth.js'
const store = useAppStore()
@@ -100,13 +101,6 @@ const statusBarHeight = ref(uni.getStorageSync('statusBarHeight') || 20)
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
onMounted(() => {
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 20
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
})
const phone = ref('')
const code = ref('')
const loading = ref(false)
@@ -114,6 +108,25 @@ const activeMethod = ref('')
const countdown = ref(60)
const isCountingDown = ref(false)
const loginConfig = ref({ wechatLoginEnabled: true, smsLoginEnabled: false })
onMounted(async () => {
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 20
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
try {
const res = await getLoginConfig()
if (res.data) {
loginConfig.value = res.data
}
} catch (e) {
//
loginConfig.value = { wechatLoginEnabled: true, smsLoginEnabled: false }
}
})
const canSubmit = computed(() => {
return phone.value.length === 11 && code.value.length === 6
})
@@ -175,83 +175,7 @@ const statusFilters = [
{ label: '收藏夹', value: 'favorite' }
]
const fallbackScripts = [
{
id: 'demo-1',
title: '逆袭人生:从低谷到巅峰',
length: 'long',
status: 'progress',
tags: ['逆袭成长', '都市', '事业', '热血'],
summary: '从被分手、被否定的低谷开始,凭借天赋、努力与智慧,一步步逆袭成为行业巅峰,收获事业、财富...',
chapterCount: 28,
wordCount: 128000,
updatedAt: '今天 21:30',
progress: 28
},
{
id: 'demo-2',
title: '如果那年我没有放弃',
length: 'long',
status: 'done',
tags: ['成长治愈', '校园', '爱情', '温暖'],
summary: '重回十八岁,弥补遗憾,勇敢追梦,守护那些曾经错过的人和事。',
chapterCount: 36,
wordCount: 156000,
completedAt: '2025.05.10',
isFavorite: true
},
{
id: 'demo-3',
title: '重生之我在未来等你',
length: 'long',
status: 'progress',
tags: ['重生', '科幻', '爱情', '未来'],
summary: '一觉醒来,回到十年前的那一天。这一次,我不仅要改变自己的人生,还要找到你。',
chapterCount: 18,
wordCount: 83000,
updatedAt: '昨天 18:47',
progress: 46
},
{
id: 'demo-4',
title: '天才作曲家的璀璨之路',
length: 'long',
status: 'draft',
tags: ['音乐', '励志', '天赋', '梦想'],
summary: '从默默无闻到享誉全球,用音符征服世界,写下属于自己的传奇乐章。',
chapterCount: 9,
wordCount: 31000,
createdAt: '2025.05.08'
},
{
id: 'demo-5',
title: '咖啡馆里的奇遇',
length: 'short',
status: 'done',
tags: ['生活', '治愈', '奇幻', '温暖'],
summary: '一杯咖啡,一次奇遇,改变了我平凡的生活,也让我遇见了最特别的你。',
chapterCount: 1,
wordCount: 23000,
completedAt: '2025.05.01',
isFavorite: true
},
{
id: 'demo-6',
title: '赛博时代的追光者',
length: 'long',
status: 'draft',
tags: ['科幻', '未来', '冒险', '热血'],
summary: '在数据与代码构建的世界里,我追寻光明,也在黑暗中寻找真正的自由。',
chapterCount: 3,
wordCount: 12000,
createdAt: '2025.05.12'
}
]
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
const scripts = computed(() => store.scripts || [])
const visibleScripts = computed(() => {
const filtered = scripts.value.filter(script => {
@@ -296,25 +220,24 @@ const getLengthLabel = (length) => {
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return [script.style || '逆袭成长', '都市', '事业', '热血']
return []
}
const getChapterCount = (script) => script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))
const getChapterCount = (script) => script.chapterCount || script.chapters || 0
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (!count) return '3.1万字'
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || '2025.05.10'}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || '2025.05.08'}`
return `最近更新:${script.updatedAt || script.date || '今天 21:30'}`
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || ''}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || ''}`
return `最近更新:${script.updatedAt || script.date || ''}`
}
const getProgress = (script) => Math.max(1, Math.min(99, Number(script.progress || 28)))
const getProgress = (script) => Math.max(0, Math.min(99, Number(script.progress || 0)))
const getInitial = (script) => (script.title || '剧').slice(0, 1)
+87 -371
View File
@@ -133,6 +133,18 @@
</view>
<view v-else class="result-page">
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
<view class="history-lines">
<view></view>
<view></view>
<view></view>
</view>
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
<scroll-view
class="result-view"
scroll-y
@@ -145,17 +157,6 @@
>
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
<view class="history-lines">
<view></view>
<view></view>
<view></view>
</view>
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
<view class="conversation compact">
<view class="chat-bubble user">
@@ -168,79 +169,47 @@
</view>
</view>
<view class="story-card" :class="{ collapsed: storyCollapsed, dialogMode: resultHasScriptReplies }">
<view class="story-head">
<view class="story-title-wrap">
<text class="story-title">{{ currentResult?.title || '我的人生剧本' }}</text>
<view class="tag-row">
<text v-for="tag in resultTags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
<view class="story-head-actions">
<button class="collapse-icon" @click="toggleStoryCollapse">
<text class="collapse-icon-text">{{ storyCollapsed ? '展开' : '收起' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</button>
<button class="copy-card-btn" @click="copyResultContent">复制</button>
</view>
</view>
<template v-if="!resultHasScriptReplies">
<scroll-view
v-if="storyCollapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">
<text class="collapse-row-text">{{ storyCollapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</view>
</template>
<view class="result-actions">
<button class="action-btn" @click="changeDirection">换个方向</button>
<button class="action-btn" @click="notLikeMe">不像我</button>
<button class="action-btn" @click="continueInChat">继续生成</button>
<button class="action-btn primary" @click="trackTtsClick">
<text class="action-icon">{{ ttsActionIcon }}</text>
<text>{{ ttsActionText }}</text>
</button>
</view>
</view>
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
class="chat-bubble"
:class="{ user: message.role === 'user', system: message.role === 'assistant', pending: message.pending }"
>
<text selectable user-select>{{ getMessageDisplayContent(message) }}</text>
<view v-if="message.pending" class="thinking-dots">
<view></view>
<view></view>
<view></view>
</view>
<view
<MessageCard
v-if="isAssistantMessage(message)"
class="message-toggle"
@click.stop="toggleMessageCollapse(message)"
>
<text>{{ isMessageCollapsed(message) ? '展开' : '收起' }}</text>
<text class="message-toggle-icon" :class="{ open: !isMessageCollapsed(message) }"></text>
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
@@ -271,7 +240,7 @@
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text>语音</text>
<text class="voice-icon">🎤</text>
</view>
<view class="result-input-shell" :class="{ twoLine: resultInputLevel === 'two', expanded: resultInputLevel === 'multi' }">
<text v-if="!resultChatInput" class="custom-input-placeholder result-input-placeholder">继续提修改建议或确认后重新生成</text>
@@ -303,6 +272,7 @@ import { useTtsPlayer } from '../../composables/useTtsPlayer.js'
import { useTypewriterStream } from '../../composables/useTypewriterStream.js'
import { transcribeAudio } from '../../services/asr.js'
import { streamAiScene } from '../../services/aiRuntime.js'
import MessageCard from '../../components/MessageCard.vue'
const store = useAppStore()
const pagePath = '/pages/main/ScriptView'
@@ -445,6 +415,37 @@ const toggleMessageCollapse = (message) => {
[message.id]: !isMessageCollapsed(message)
}
nextTick(measureResultViewport)
analytics.track('script_message_collapse_toggle', {
message_id: message?.id || '',
collapsed: collapsedMessageIds.value[message.id]
}, { eventType: 'script', pagePath })
}
const copyMessageContent = (message) => {
const content = String(message?.content || '')
if (!content.trim()) {
uni.showToast({ title: '暂无可复制内容', icon: 'none' })
return
}
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
analytics.track('script_message_copy_click', {
message_id: message?.id || '',
content_length: content.length
}, { eventType: 'script', pagePath })
}
const playMessageTts = (message) => {
const scriptId = currentResult.value?.id || ''
ttsPlayer.playSource(scriptId)
analytics.track('script_message_tts_click', {
message_id: message?.id || '',
script_id: scriptId
}, { eventType: 'script', pagePath })
}
const visibleStreamContent = computed(() => streamWriter.visibleText.value)
@@ -2456,31 +2457,6 @@ onUnmounted(() => {
border-color: rgba(255, 216, 107, 0.28);
}
.message-toggle {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 18rpx;
border-radius: 999rpx;
color: #ffd86b;
font-size: 24rpx;
line-height: 32rpx;
font-weight: 700;
background: rgba(140, 68, 242, 0.18);
border: 1rpx solid rgba(255, 216, 107, 0.18);
}
.message-toggle-icon {
font-size: 22rpx;
line-height: 28rpx;
transition: transform 0.2s ease;
}
.message-toggle-icon.open {
transform: rotate(180deg);
}
.thinking-dots {
display: flex;
align-items: center;
@@ -2714,271 +2690,6 @@ onUnmounted(() => {
border: 1rpx solid rgba(168, 85, 247, 0.32);
}
.story-card {
border-radius: 52rpx;
padding: 34rpx;
background: rgba(12, 5, 28, 0.34);
border: 1rpx solid rgba(192, 132, 252, 0.46);
box-shadow: 0 0 48rpx rgba(125, 55, 205, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.story-card.collapsed {
box-shadow: 0 0 42rpx rgba(125, 55, 205, 0.14);
}
.story-head {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.story-head-actions {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 12rpx;
}
.story-title-wrap {
flex: 1;
min-width: 0;
}
.story-title {
display: block;
font-size: 52rpx;
font-weight: 700;
line-height: 1.35;
}
.collapse-icon {
width: 120rpx;
min-width: 120rpx;
height: 58rpx;
min-height: 58rpx;
line-height: 1;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: rgba(255, 244, 255, 0.95);
background:
linear-gradient(135deg, rgba(134, 72, 255, 0.44), rgba(39, 15, 76, 0.42)),
rgba(255, 255, 255, 0.05);
border: 1rpx solid rgba(216, 180, 254, 0.46);
box-shadow:
0 14rpx 30rpx rgba(61, 18, 113, 0.22),
inset 0 1rpx 0 rgba(255, 255, 255, 0.26),
inset 0 -10rpx 18rpx rgba(33, 9, 73, 0.16);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
}
.collapse-icon::after {
border: 0;
}
.collapse-icon:active,
.copy-card-btn:active,
.collapse-row:active {
transform: scale(0.98);
opacity: 0.92;
}
.copy-card-btn {
width: 120rpx;
min-width: 120rpx;
height: 50rpx;
min-height: 50rpx;
line-height: 1;
padding: 0;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 244, 255, 0.9);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
background:
radial-gradient(circle at 50% -30%, rgba(255, 255, 255, 0.18), transparent 50%),
rgba(88, 28, 135, 0.26);
border: 1rpx solid rgba(216, 180, 254, 0.32);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 10rpx 22rpx rgba(61, 18, 113, 0.12);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.copy-card-btn::after {
border: 0;
}
.collapse-icon-text,
.collapse-row-text {
line-height: 1;
white-space: nowrap;
}
.collapse-chevron {
position: relative;
width: 20rpx;
height: 16rpx;
flex-shrink: 0;
transition: transform 0.18s ease;
}
.collapse-chevron.down {
transform: rotate(180deg);
}
.collapse-chevron view {
position: absolute;
top: 7rpx;
width: 12rpx;
height: 4rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #fff3b0, #ffd86b);
box-shadow: 0 0 12rpx rgba(255, 216, 107, 0.5);
}
.collapse-chevron view:first-child {
left: 0;
transform: rotate(-38deg);
transform-origin: right center;
}
.collapse-chevron view:last-child {
right: 0;
transform: rotate(38deg);
transform-origin: left center;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.tag {
padding: 6rpx 16rpx;
border-radius: 999rpx;
color: #d18aff;
font-size: 24rpx;
background: rgba(168, 85, 247, 0.22);
}
.story-body {
display: block;
margin-top: 28rpx;
font-size: 32rpx;
font-weight: 400;
line-height: 1.78;
color: rgba(255, 255, 255, 0.92);
white-space: pre-wrap;
}
.story-body-scroll {
max-height: 420rpx;
height: auto;
margin-top: 28rpx;
padding-right: 8rpx;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.story-body-scroll .story-body {
margin-top: 0;
}
.collapse-row {
position: relative;
height: 62rpx;
margin-top: 22rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
border-radius: 999rpx;
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 800;
overflow: hidden;
background:
radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.2), transparent 46%),
linear-gradient(135deg, rgba(98, 40, 174, 0.3), rgba(24, 8, 58, 0.24));
border: 1rpx solid rgba(192, 132, 252, 0.36);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 12rpx 28rpx rgba(61, 18, 113, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.collapse-row::before {
content: '';
position: absolute;
left: 34rpx;
right: 34rpx;
top: 0;
height: 1rpx;
background: linear-gradient(90deg, transparent, rgba(255, 236, 180, 0.6), transparent);
}
.result-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 26rpx;
}
.action-btn {
height: 72rpx;
min-height: 72rpx;
padding: 0 8rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e8ccff;
font-size: 26rpx;
font-weight: 700;
line-height: 1.15;
text-align: center;
white-space: normal;
box-sizing: border-box;
background: rgba(88, 28, 135, 0.18);
border: 1rpx solid rgba(192, 132, 252, 0.35);
}
.action-btn.primary {
color: #fff;
background: linear-gradient(145deg, #8c44f2, #5f1db8);
}
.action-icon {
margin-right: 8rpx;
font-size: 24rpx;
font-weight: 900;
line-height: 1;
}
.action-btn::after {
border: 0;
}
.result-chat-list {
display: flex;
flex-direction: column;
@@ -3028,19 +2739,19 @@ onUnmounted(() => {
display: flex;
align-items: flex-end;
gap: 12rpx;
padding: 14rpx 0 22rpx;
padding: 14rpx 24rpx 22rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(5, 2, 13, 0), rgba(5, 2, 13, 0.9) 30%, rgba(5, 2, 13, 0.96));
}
.chat-voice-btn {
width: 86rpx;
height: 76rpx;
width: 72rpx;
height: 68rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 26rpx;
border-radius: 22rpx;
color: #e8ccff;
font-size: 24rpx;
font-weight: 800;
@@ -3055,6 +2766,11 @@ onUnmounted(() => {
box-shadow: 0 0 30rpx rgba(168, 85, 247, 0.38);
}
.voice-icon {
font-size: 24rpx;
line-height: 1;
}
.result-input-shell {
position: relative;
flex: 1;
+217 -23
View File
@@ -19,7 +19,7 @@
</view>
<view class="hero-info">
<view class="hero-name-row">
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
<text class="hero-name">{{ form.nickname || '未设置昵称' }}</text>
<text class="gold hero-star"></text>
</view>
<text class="hero-sub">正在成为更清晰的自己</text>
@@ -127,20 +127,23 @@
</view>
<view class="panel glass-card">
<view class="section-head">
<view class="section-icon smile-title-icon"></view>
<text class="section-title">性格标签</text>
<text class="section-hint">最多选择5个</text>
<view class="section-head with-action">
<view class="section-title-wrap">
<view class="section-icon smile-title-icon"></view>
<text class="section-title">性格标签</text>
<text class="section-hint">最多选择5个</text>
</view>
<text class="manage-link" @click="goTagManage('personality')">管理</text>
</view>
<view class="tag-grid">
<text
v-for="tag in personalityTags"
v-for="tag in displayPersonalityTags"
:key="tag"
class="tag-choice"
:class="{ active: form.personalityTags.includes(tag) }"
@click="toggleList(form.personalityTags, tag, 5)"
>{{ tag }}</text>
<text class="tag-choice dashed"> 添加标签</text>
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加</text>
</view>
</view>
@@ -151,16 +154,17 @@
<text class="section-title">兴趣爱好</text>
<text class="section-hint">最多选择5个</text>
</view>
<text class="custom" @click="addCustomHobby"> 自定义兴趣</text>
<text class="manage-link" @click="goTagManage('hobby')">管理</text>
</view>
<view class="tag-grid">
<text
v-for="tag in hobbyOptions"
v-for="tag in displayHobbyTags"
:key="tag"
class="tag-choice"
:class="{ active: form.hobbies.includes(tag) }"
@click="toggleList(form.hobbies, tag, 5)"
>{{ tag }}</text>
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加</text>
</view>
</view>
@@ -179,18 +183,38 @@
<text class="bio-count">{{ (form.future.ideal || '').length }}/200</text>
</view>
</scroll-view>
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
import TagDialog from '../../components/TagDialog.vue'
const store = useAppStore()
const { capsuleTopReservePx } = useMenuButtonSafeArea({ extraTopPx: 2 })
const isEdit = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
const dialogMode = ref('input')
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null)
const birthday = ref('')
const avatarLocal = ref('')
const bioPlaceholder = '热爱阅读和旅行,喜欢用文字和镜头记录生活。\n相信真诚和努力能让世界变得更美好。'
@@ -225,6 +249,8 @@ const form = reactive({
company: '',
personalityTags: [],
hobbies: [],
personalityTagLibrary: [], //
hobbyLibrary: [], //
childhood: { date: '', text: '' },
joy: { date: '', text: '' },
low: { date: '', text: '' },
@@ -243,8 +269,32 @@ const birthdayDisplay = computed(() => {
return `${year}${month}${day}`
})
/**
* 合并预设数组与已选数组去重返回完整库
*/
const buildInitialLibrary = (preset, selected) => {
const library = [...preset]
if (Array.isArray(selected)) {
selected.forEach((tag) => {
if (!library.includes(tag)) library.push(tag)
})
}
return library
}
const syncFromStore = () => {
const source = store.userProfile || store.registrationData || {}
// +
const personalityTagLibrary = Array.isArray(source.personalityTagLibrary) && source.personalityTagLibrary.length > 0
? [...source.personalityTagLibrary]
: buildInitialLibrary(personalityTags, source.personalityTags)
// +
const hobbyLibrary = Array.isArray(source.hobbyLibrary) && source.hobbyLibrary.length > 0
? [...source.hobbyLibrary]
: buildInitialLibrary(hobbyOptions, source.hobbies)
Object.assign(form, {
nickname: source.nickname || '',
gender: source.gender || '',
@@ -256,6 +306,8 @@ const syncFromStore = () => {
company: source.company || '',
personalityTags: Array.isArray(source.personalityTags) ? [...source.personalityTags] : [],
hobbies: Array.isArray(source.hobbies) ? [...source.hobbies] : [],
personalityTagLibrary,
hobbyLibrary,
childhood: { date: source.childhood?.date || '', text: source.childhood?.text || '' },
joy: { date: source.joy?.date || '', text: source.joy?.text || '' },
low: { date: source.low?.date || '', text: source.low?.text || '' },
@@ -264,6 +316,78 @@ const syncFromStore = () => {
birthday.value = source.birthday || ''
}
/**
* 处理管理页发回的库更新事件
*/
const handleTagLibraryUpdated = (payload) => {
if (!payload || !payload.type) return
if (payload.type === 'personality') {
if (Array.isArray(payload.library)) {
form.personalityTagLibrary = [...payload.library]
//
form.personalityTags = form.personalityTags.filter(
(tag) => payload.library.includes(tag)
)
}
} else if (payload.type === 'hobby') {
if (Array.isArray(payload.library)) {
form.hobbyLibrary = [...payload.library]
form.hobbies = form.hobbies.filter(
(tag) => payload.library.includes(tag)
)
}
}
}
onShow(() => {
// onShow
uni.$off('tag-library-updated', handleTagLibraryUpdated)
uni.$on('tag-library-updated', handleTagLibraryUpdated)
})
onUnmounted(() => {
uni.$off('tag-library-updated', handleTagLibraryUpdated)
})
/**
* 性格标签有序展示列表
* 1. 新增的自定义标签不在预设中按库顺序排在最前
* 2. 已选中的其他标签紧随其后
* 3. 其余未选中的预设标签排在最后
*/
const displayPersonalityTags = computed(() => {
const selectedSet = new Set(form.personalityTags)
const presetSet = new Set(personalityTags)
const customTags = form.personalityTagLibrary.filter((tag) => !presetSet.has(tag))
const selectedPreset = form.personalityTagLibrary.filter(
(tag) => presetSet.has(tag) && selectedSet.has(tag)
)
const unselectedPreset = form.personalityTagLibrary.filter(
(tag) => presetSet.has(tag) && !selectedSet.has(tag)
)
return [...customTags, ...selectedPreset, ...unselectedPreset]
})
/**
* 兴趣爱好有序展示列表逻辑同上
*/
const displayHobbyTags = computed(() => {
const selectedSet = new Set(form.hobbies)
const presetSet = new Set(hobbyOptions)
const customTags = form.hobbyLibrary.filter((tag) => !presetSet.has(tag))
const selectedPreset = form.hobbyLibrary.filter(
(tag) => presetSet.has(tag) && selectedSet.has(tag)
)
const unselectedPreset = form.hobbyLibrary.filter(
(tag) => presetSet.has(tag) && !selectedSet.has(tag)
)
return [...customTags, ...selectedPreset, ...unselectedPreset]
})
const onBirthday = (event) => {
birthday.value = event.detail.value
}
@@ -293,22 +417,89 @@ const chooseAvatar = () => {
})
}
const addCustomHobby = () => {
uni.showModal({
title: '自定义兴趣',
editable: true,
placeholderText: '输入兴趣名称',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm || !value) return
if (!hobbyOptions.includes(value)) hobbyOptions.push(value)
toggleList(form.hobbies, value, 5)
}
/**
* 跳转到管理标签页面
*/
const goTagManage = (type) => {
uni.navigateTo({
url: `/pages/onboarding/tag-manage?type=${type}`
})
}
/**
* 打开添加标签弹窗
* @param {string} type 'personality' 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
dialogMode.value = 'input'
dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
dialogPlaceholder.value = '请输入标签(最多4个字)'
dialogInput.value = ''
dialogAction.value = { kind: 'add', type }
dialogVisible.value = true
}
/**
* 处理添加标签的实际逻辑字数限制 4
*/
const handleAddTag = (type, rawValue) => {
const value = String(rawValue || '').trim()
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 4) {
uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
//
library.unshift(value)
// 5
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
}
dialogAction.value = null
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
const saveProfile = async () => {
if (!form.nickname.trim() || saving.value) return
if (saving.value) return
if (!form.nickname.trim()) {
uni.showToast({ title: '请填写昵称', icon: 'none' })
return
}
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
@@ -861,6 +1052,9 @@ onMounted(async () => {
.tag-choice {
height: 40rpx;
font-size: 21rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-choice.dashed {
@@ -868,7 +1062,7 @@ onMounted(async () => {
color: #c06dff;
}
.custom {
.manage-link {
color: #c06dff;
font-size: 23rpx;
}
@@ -0,0 +1,387 @@
<template>
<view class="manage-page">
<view class="space-bg"></view>
<view class="status-space" :style="{ height: capsuleTopReservePx + 'px' }"></view>
<view class="topbar">
<text class="back" @click="goBack"></text>
<text class="title">{{ pageTitle }}</text>
<text class="placeholder-slot"></text>
</view>
<scroll-view class="scroll" scroll-y :show-scrollbar="false">
<view class="panel glass-card">
<view class="section-head">
<text class="section-title">全部标签</text>
<text class="section-hint"> {{ library.length }} </text>
</view>
<view v-if="library.length === 0" class="empty-tip">
<text>暂无标签</text>
</view>
<view
v-for="(tag, index) in library"
:key="tag"
class="tag-row"
@touchstart="onTouchStart($event, index)"
@touchmove="onTouchMove($event, index)"
@touchend="onTouchEnd($event, index)"
>
<view class="tag-row-inner" :style="{ transform: `translateX(${swipeOffsets[index] || 0}rpx)` }">
<view class="tag-row-main">
<text class="tag-text">{{ tag }}</text>
</view>
<view class="tag-row-delete" @click.stop="confirmDelete(tag)">
<text class="delete-text">删除</text>
</view>
</view>
<view class="tag-row-close" @click.stop="confirmDelete(tag)">
<text class="close-icon">×</text>
</view>
</view>
</view>
</scroll-view>
<TagDialog
v-model:visible="dialogVisible"
mode="confirm"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
import TagDialog from '../../components/TagDialog.vue'
const store = useAppStore()
const { capsuleTopReservePx } = useMenuButtonSafeArea({ extraTopPx: 2 })
const type = ref('personality')
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
const library = ref([])
const selected = ref([])
const swipeOffsets = reactive({})
//
const personalityTags = ['理性', '感性', '乐观', '独立', '有创造力', '坚韧', '细腻', '好奇', '内敛', '冒险', '自由']
const hobbyOptions = ['阅读', '旅行', '音乐', '写作', '摄影', '电影', '运动', '绘画', '咖啡', '游戏']
const pageTitle = computed(() => (type.value === 'personality' ? '管理性格标签' : '管理兴趣爱好'))
/**
* store 初始化库数据与编辑页 syncFromStore 同逻辑
*/
const buildInitialLibrary = (preset, selectedArr) => {
const lib = [...preset]
if (Array.isArray(selectedArr)) {
selectedArr.forEach((tag) => {
if (!lib.includes(tag)) lib.push(tag)
})
}
return lib
}
const initFromStore = () => {
const source = store.userProfile || store.registrationData || {}
const isPersonality = type.value === 'personality'
const preset = isPersonality ? personalityTags : hobbyOptions
const selectedArr = isPersonality ? source.personalityTags : source.hobbies
const libraryArr = isPersonality ? source.personalityTagLibrary : source.hobbyLibrary
const finalLibrary = Array.isArray(libraryArr) && libraryArr.length > 0
? [...libraryArr]
: buildInitialLibrary(preset, selectedArr)
library.value = finalLibrary
selected.value = Array.isArray(selectedArr) ? [...selectedArr] : []
}
/**
* 左滑手势处理
*/
const touchState = ref({ startX: 0, startY: 0, currentIndex: -1, moved: false })
const DELETE_THRESHOLD = -60
const onTouchStart = (event, index) => {
const touch = event.touches[0]
touchState.value = { startX: touch.clientX, startY: touch.clientY, currentIndex: index, moved: false }
}
const onTouchMove = (event, index) => {
const touch = event.touches[0]
const deltaX = touch.clientX - touchState.value.startX
const deltaY = touch.clientY - touchState.value.startY
//
if (Math.abs(deltaX) < Math.abs(deltaY)) return
touchState.value.moved = true
const offset = Math.min(0, Math.max(deltaX, -100))
swipeOffsets[index] = offset
}
const onTouchEnd = (event, index) => {
const currentOffset = swipeOffsets[index] || 0
if (currentOffset < DELETE_THRESHOLD) {
swipeOffsets[index] = -80
} else {
swipeOffsets[index] = 0
}
}
/**
* 打开删除确认弹窗
*/
const confirmDelete = (tag) => {
pendingTag.value = tag
dialogContent.value = `确定删除「${tag}」吗?删除后不可恢复`
dialogVisible.value = true
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = () => {
if (pendingTag.value) {
deleteTag(pendingTag.value)
pendingTag.value = null
}
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
pendingTag.value = null
dialogVisible.value = false
}
/**
* 执行删除从库数组移除若同时在已选数组也一并移除
*/
const deleteTag = (tag) => {
const idx = library.value.indexOf(tag)
if (idx >= 0) library.value.splice(idx, 1)
const selectedIdx = selected.value.indexOf(tag)
if (selectedIdx >= 0) selected.value.splice(selectedIdx, 1)
// store.registrationData
const isPersonality = type.value === 'personality'
const key = isPersonality ? 'personalityTagLibrary' : 'hobbyLibrary'
const selectedKey = isPersonality ? 'personalityTags' : 'hobbies'
store.updateRegistration({
[key]: [...library.value],
[selectedKey]: [...selected.value]
})
//
uni.$emit('tag-library-updated', {
type: type.value,
library: [...library.value],
selected: [...selected.value]
})
}
const goBack = () => {
uni.navigateBack()
}
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
type.value = currentPage?.options?.type === 'hobby' ? 'hobby' : 'personality'
initFromStore()
})
</script>
<style scoped>
.manage-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050615;
}
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 84% 3%, rgba(87, 122, 255, 0.12), transparent 28%),
radial-gradient(circle at 14% 26%, rgba(130, 71, 255, 0.16), transparent 30%),
linear-gradient(180deg, #05081b 0%, #07031a 52%, #03020d 100%);
}
.status-space,
.topbar,
.scroll {
position: relative;
z-index: 1;
}
.status-space,
.topbar {
flex-shrink: 0;
}
.topbar {
height: 72rpx;
display: grid;
grid-template-columns: 76rpx 1fr 76rpx;
align-items: center;
padding: 0 28rpx;
}
.back {
color: #fff;
font-size: 56rpx;
line-height: 1;
}
.title {
text-align: center;
color: #fff;
font-size: 32rpx;
font-weight: 900;
}
.placeholder-slot {
width: 1px;
}
.scroll {
flex: 1;
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 24rpx 28rpx;
margin-top: -6rpx;
}
.glass-card {
position: relative;
overflow: hidden;
border: 1rpx solid rgba(155, 110, 255, 0.14);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.1), transparent 34%),
rgba(10, 13, 43, 0.54);
box-shadow: inset 0 0 30rpx rgba(123, 60, 255, 0.05), 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.panel {
border-radius: 22rpx;
margin-bottom: 18rpx;
padding: 22rpx 24rpx;
}
.section-head {
display: flex;
align-items: center;
min-height: 44rpx;
gap: 10rpx;
}
.section-title {
color: rgba(239, 232, 255, 0.9);
font-size: 24rpx;
font-weight: 800;
}
.section-hint {
color: rgba(222, 211, 240, 0.54);
font-size: 22rpx;
}
.empty-tip {
padding: 40rpx 0;
text-align: center;
color: rgba(222, 211, 240, 0.5);
font-size: 24rpx;
}
.tag-row {
position: relative;
overflow: hidden;
border-top: 1rpx solid rgba(180, 139, 255, 0.12);
}
.tag-row-inner {
position: relative;
z-index: 2;
display: flex;
align-items: center;
min-height: 72rpx;
background: transparent;
transition: transform 0.2s ease;
}
.tag-row-main {
flex: 1;
min-width: 0;
padding: 16rpx 8rpx;
}
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-row-delete {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 140rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ff4d6d, #c9184a);
z-index: 1;
}
.delete-text {
color: #fff;
font-size: 24rpx;
font-weight: 700;
}
.tag-row-close {
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
width: 44rpx;
height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 77, 109, 0.16);
z-index: 3;
}
.close-icon {
color: #ff4d6d;
font-size: 32rpx;
line-height: 1;
font-weight: 700;
}
</style>
+10
View File
@@ -16,6 +16,15 @@ export const getSmsCode = async (phone) => {
return response
}
/**
* 获取登录方式配置
* @returns {Promise<Object>} 登录方式配置
*/
export const getLoginConfig = async () => {
const response = await get('/auth/loginConfig')
return response
}
/**
* 用户登录手机号 + 验证码
* @param {Object} params - 登录参数
@@ -134,6 +143,7 @@ export const checkPhone = async (phone) => {
}
export default {
getLoginConfig,
getSmsCode,
login,
wechatLogin,
+8
View File
@@ -78,6 +78,8 @@ const transformToBackendFormat = (frontendData) => {
industry,
company,
personalityTags,
personalityTagLibrary,
hobbyLibrary,
birthday
} = frontendData
@@ -107,6 +109,8 @@ const transformToBackendFormat = (frontendData) => {
industry: industry ?? '',
company: company ?? '',
personalityTags: Array.isArray(personalityTags) ? JSON.stringify(personalityTags) : personalityTags,
personalityTagLibrary: Array.isArray(personalityTagLibrary) ? JSON.stringify(personalityTagLibrary) : personalityTagLibrary,
hobbyLibrary: Array.isArray(hobbyLibrary) ? JSON.stringify(hobbyLibrary) : hobbyLibrary,
birthday: birthday || null
}
}
@@ -140,6 +144,8 @@ export const transformToFrontendFormat = (backendData) => {
industry,
company,
personalityTags,
personalityTagLibrary,
hobbyLibrary,
birthday
} = backendData
@@ -177,6 +183,8 @@ export const transformToFrontendFormat = (backendData) => {
industry: industry || '',
company: company || '',
personalityTags: parseJsonArray(personalityTags),
personalityTagLibrary: parseJsonArray(personalityTagLibrary),
hobbyLibrary: parseJsonArray(hobbyLibrary),
birthday: normalizeDate(birthday)
}
}
+2
View File
@@ -31,6 +31,8 @@ const state = reactive({
industry: '',
company: '',
personalityTags: [],
personalityTagLibrary: [],
hobbyLibrary: [],
birthday: ''
}
})
+1 -1
View File
@@ -42,7 +42,7 @@ export default defineConfig(({ command }) => {
envDir: __dirname,
publicDir: resolve(__dirname, 'static'),
server: {
port: 5175,
port: 5180,
watch: {
usePolling: true,
interval: 100

Some files were not shown because too many files have changed in this diff Show More