Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05ea918ee1 | |||
| 50f1bdba38 | |||
| 74fa304cd9 | |||
| 9d352f40a0 | |||
| 674188f471 | |||
| 62ca1730b9 | |||
| 0d27d1a072 | |||
| d74d8b22c6 | |||
| 489131a04b | |||
| 43ac037f5b | |||
| b20f30f91b | |||
| 7e52bb9ffb | |||
| 8a2001af3d | |||
| 35b4f97527 | |||
| f5b26c2fc7 | |||
| cb965fadff | |||
| cafa8717ab | |||
| 283914690a | |||
| 04ffa2dceb | |||
| 0a9ea60795 | |||
| 118d20914d | |||
| b194b343ad | |||
| 7a0e729ab4 | |||
| f07817aaff | |||
| 30ecf9f4a3 | |||
| f6ef0222b3 | |||
| e109e59f1a | |||
| e41266e6f4 | |||
| b4fca0abfa | |||
| 84b6a77632 | |||
| c0adb48949 | |||
| 25d8a9f8f2 | |||
| b2df7d4de0 | |||
| 30e2836742 | |||
| c1293c28ac | |||
| d9a061a6bc | |||
| e313086d9b | |||
| 1b566d574c | |||
| 0190941df7 | |||
| 34f443939e | |||
| b3a2f3a3d3 | |||
| ab13ac258b | |||
| 00f17d2f47 | |||
| c06b04869b | |||
| 02c58767bd | |||
| 7018f5780a | |||
| 031d824dd0 | |||
| 8f96239b56 | |||
| 21ce56c6da | |||
| ce244f9ad5 | |||
| 99a3cb883e | |||
| 2c778fabbb | |||
| 6aeb5319e3 | |||
| 3bb6716e0f | |||
| 86543df75d | |||
| 1073123360 | |||
| 3829a3ef92 | |||
| f45fee7cce | |||
| a713e8823c | |||
| 4a7914eefe | |||
| 88ff280b78 | |||
| 912fc42860 | |||
| 7fb90299d1 | |||
| 6ae721eb89 | |||
| 808a590b8d | |||
| 19337b9371 | |||
| e3eb1afbb0 | |||
| caaeb94477 | |||
| bd3ccbd757 | |||
| bf6f2797ad | |||
| 6109202a26 | |||
| 3b9a8222de | |||
| 8537c27015 | |||
| a66a78a0f3 | |||
| 57f1e311b6 | |||
| 8618b8b80f | |||
| b11a3463b2 | |||
| e252dc5fd2 | |||
| 0520f30d84 | |||
| 752fd30686 | |||
| 56369fdeb2 | |||
| 88d586bab1 | |||
| 0149bac281 | |||
| 42d1350aff | |||
| 43622be5b0 | |||
| 74183ccb30 | |||
| 06b2ee58e2 | |||
| 83c379d5d5 | |||
| 15fce9eadf | |||
| 0ba2960c1b | |||
| 0e2cc42a41 | |||
| 6bc00863fa | |||
| 1b39d873ab | |||
| 75de32828a | |||
| 4dc3d4cfdc | |||
| a43bb1a555 | |||
| 5b31845b33 | |||
| 8cd52f76ae | |||
| c2ed051de0 | |||
| cbf5157dcf | |||
| 8dc2454beb | |||
| f7dba6ba60 | |||
| 847e80aa0e | |||
| d724cab581 | |||
| 0db434c21f | |||
| 9b3006d128 | |||
| 944f6a0a13 | |||
| e014450aa3 | |||
| 2198781c68 | |||
| 8e6b69f6c7 | |||
| 591f70a2f6 | |||
| 5586dc5eac | |||
| 8f51701f54 | |||
| 6cb9496abb | |||
| 0d77d76858 | |||
| 8daf51301a | |||
| e3b21cac3e | |||
| 9131203d1c | |||
| ed741ac8bf | |||
| 4b235fa5d3 | |||
| 7679e973d0 | |||
| f11fb142e1 | |||
| 57a42474d7 | |||
| bd0f4d6bea | |||
| 637334adb8 | |||
| 1900ba6efc | |||
| 11f1638eef | |||
| 843ddd4c69 | |||
| 53e895797e | |||
| 2600cb2164 | |||
| c7e84f5a13 | |||
| 7dc75cbfb4 | |||
| 071af02482 | |||
| a446848d8e | |||
| 341b08c02e | |||
| 433ba4c498 | |||
| 1cdee47f3a | |||
| 3cd7de1f55 | |||
| 1c45689f03 | |||
| 99a06fcc28 | |||
| 4cb6d6de9b | |||
| 6b6af757d0 | |||
| ae1463e400 | |||
| 70434c1325 | |||
| 04b184a3c1 | |||
| e7531f86cf | |||
| a00ce0bf95 | |||
| 0eaf44ed93 | |||
| eb50358e3d | |||
| 41ed8257f6 | |||
| 2c69da46e5 | |||
| 42543ff8a6 | |||
| c744d92f38 | |||
| 04cdd5585e | |||
| a62257b21a | |||
| 816e166e08 | |||
| b9bacb36e7 | |||
| ffa77400c9 | |||
| d1e223ad23 | |||
| 83d3f87cf6 | |||
| 16cee44a85 | |||
| 49f73679d0 | |||
| 74a2c15b6b | |||
| cb3c5e6724 | |||
| e021fe8443 | |||
| d710f529fc | |||
| 00a93b66d8 | |||
| e2a02e0d5f | |||
| 675311a1be | |||
| d4088069dd | |||
| e55c3acc6a | |||
| 059e7f8f52 | |||
| b784942079 | |||
| b0a82dea13 | |||
| f7180eb83d | |||
| 1e2de85f2e | |||
| 10653edc52 | |||
| 55f06e8caf | |||
| 1eb9ef67a4 | |||
| a6dcc0843d | |||
| 8fce9ff87a | |||
| 0669af0203 | |||
| 8ded0cc3ad | |||
| e23eb32c1d | |||
| 2413b2bf58 | |||
| 76d4d7465c | |||
| 012fa46d3a | |||
| 9c785a675c | |||
| 4452d0301c | |||
| 35baa6c958 | |||
| 070e7dfbd9 | |||
| 82c308b40a | |||
| 13f773f3f0 | |||
| 5631f22566 | |||
| 76bc979cfe | |||
| 820ff93dc7 | |||
| 35834b121e | |||
| b124fe78af | |||
| a6df8dcb92 | |||
| a649357650 | |||
| a277a7325f | |||
| 85ba685bba | |||
| 78e97f6fee | |||
| f6b3ba2d8c | |||
| 2189ed24e6 | |||
| 280c3d5e7d | |||
| 9f829acb0e |
@@ -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级别日志
|
||||
@@ -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
|
||||
@@ -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 A–E 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 follow‑ups (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?"
|
||||
@@ -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 multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
|
||||
- A one-word / short‑phrase 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 multiple‑choice 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 short‑answer 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
|
||||
@@ -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 non‑negotiable 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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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级别日志
|
||||
+5
-1
@@ -383,7 +383,11 @@ OpenClaw*.md
|
||||
aiconfig-*.png
|
||||
|
||||
# 运行时错误日志
|
||||
backend-single/backend.err
|
||||
server/backend.err
|
||||
|
||||
# Windows 临时空文件
|
||||
nul
|
||||
|
||||
# 服务器日志下载(tools/download-server-log.py 产物)
|
||||
logs/server/*.log
|
||||
logs/server/*.bak
|
||||
|
||||
Generated
-10
@@ -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
|
||||
Generated
-10
File diff suppressed because one or more lines are too long
Generated
-29
@@ -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>
|
||||
Generated
-82
@@ -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>
|
||||
Generated
-61
@@ -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&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&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&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&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&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&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>
|
||||
Generated
-105
@@ -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>
|
||||
Generated
-65
@@ -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>
|
||||
Generated
-12
@@ -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>
|
||||
Generated
-16
@@ -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>
|
||||
Generated
-8
@@ -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>
|
||||
Generated
-8
@@ -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
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_152f89055ffej3OExy0HNx26un",
|
||||
"updatedAt": "2026-06-10T11:59:35.517Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-06-10T11:59:35.517Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_15f343272ffe65miVsUT3gxmR0",
|
||||
"updatedAt": "2026-06-07T06:37:08.712Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-06-07T06:37:08.712Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_17cc33154ffe9MqmbYyg3bghu8",
|
||||
"updatedAt": "2026-06-02T23:41:53.619Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-06-02T23:41:53.619Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
|
||||
情绪博物馆 (Emotion Museum) 是一款基于 AI 技术的心理健康应用,通过智能对话、情绪分析、个性化成长方案等功能,帮助用户建立健康的情绪管理习惯。
|
||||
|
||||
**生产环境地址**: `lifescript.happylifeos.com`
|
||||
- 用户前端:https://lifescript.happylifeos.com/
|
||||
- 管理后台:https://lifescript.happylifeos.com/emotion-museum-admin/
|
||||
- 后端 API:https://lifescript.happylifeos.com/api
|
||||
- WebSocket:wss://lifescript.happylifeos.com/ws
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
.
|
||||
├── server/ # Spring Boot 单体后端服务
|
||||
├── web/ # 用户前端 (Vue3 + TS + Vite)
|
||||
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
|
||||
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
|
||||
├── mini-program/ # 小程序项目
|
||||
├── course-web/ # 课程 Web 项目
|
||||
├── life-script/ # 生活脚本工具
|
||||
└── tools/ # 工具脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 后端 (server)
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd server
|
||||
|
||||
# 编译打包
|
||||
mvn clean package -DskipTests
|
||||
|
||||
# 本地运行
|
||||
mvn spring-boot:run
|
||||
|
||||
# 或直接运行 JAR
|
||||
java -jar target/server-1.0.0.jar
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 运行单个测试类
|
||||
mvn test -Dtest=ClassNameTest
|
||||
```
|
||||
|
||||
### 用户前端 (web)
|
||||
|
||||
```bash
|
||||
# 进入前端目录
|
||||
cd web
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器 (端口 5173)
|
||||
npm run dev
|
||||
|
||||
# 类型检查
|
||||
npm run type-check
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 运行测试
|
||||
npm run test
|
||||
|
||||
# E2E 测试
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### 管理后台 (web-admin)
|
||||
|
||||
```bash
|
||||
# 进入目录
|
||||
cd web-admin
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器 (端口 5174)
|
||||
npm run dev
|
||||
|
||||
# 类型检查
|
||||
npm run type-check
|
||||
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
```
|
||||
|
||||
### UniApp/小程序
|
||||
|
||||
```bash
|
||||
# 进入目录
|
||||
cd UniApp
|
||||
|
||||
# 开发微信小程序
|
||||
npm run dev:mp-weixin
|
||||
|
||||
# 构建微信小程序
|
||||
npm run build:mp-weixin
|
||||
|
||||
# 开发 H5
|
||||
npm run dev:h5
|
||||
|
||||
# 构建 H5
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
### mini-program(推荐开发模式)
|
||||
|
||||
**H5 开发模式**(日常开发,推荐):
|
||||
```bash
|
||||
cd mini-program
|
||||
# 或使用启动脚本
|
||||
start-h5-dev.bat
|
||||
```
|
||||
访问 `http://localhost:5173`,登录态保留,支持热更新。
|
||||
|
||||
**微信小程序开发模式**(仅用于小程序特性验证):
|
||||
```bash
|
||||
cd mini-program
|
||||
npm run dev:mp-weixin
|
||||
```
|
||||
在微信开发者工具中打开 `unpackage/dist/dev/mp-weixin`。
|
||||
|
||||
**开发工作流说明**:详见 [小程序开发工作流程设计](docs/superpowers/specs/2026-04-07-mini-program-dev-workflow-design.md)
|
||||
|
||||
### 一键部署
|
||||
|
||||
```bash
|
||||
# 部署所有服务(后端 + 前端 + 管理后台)到生产服务器
|
||||
bash deploy-all.sh
|
||||
|
||||
# 仅部署后端
|
||||
bash deploy-all.sh backend
|
||||
|
||||
# 仅部署前端
|
||||
bash deploy-all.sh frontend
|
||||
|
||||
# 仅部署管理后台
|
||||
bash deploy-all.sh admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
- **框架**: Spring Boot 2.7.18
|
||||
- **ORM**: MyBatis-Plus 3.5.3.1
|
||||
- **数据库**: MySQL 8.0.33
|
||||
- **缓存**: Redis 7.0+
|
||||
- **JWT**: io.jsonwebtoken 0.11.5
|
||||
- **构建**: Maven 3.6+
|
||||
- **JDK**: 17
|
||||
|
||||
### 前端
|
||||
- **框架**: Vue 3.4.x + TypeScript 5.x
|
||||
- **构建**: Vite 5.x
|
||||
- **UI**: Element Plus 2.4.x
|
||||
- **样式**: Tailwind CSS 3.4.x
|
||||
- **状态管理**: Pinia 2.1.x
|
||||
- **路由**: Vue Router 4.2.x
|
||||
- **HTTP**: Axios 1.6.x
|
||||
- **实时通信**: @stomp/stompjs + SockJS
|
||||
- **图表**: ECharts 5.4.x
|
||||
- **包管理**: npm 9+
|
||||
|
||||
### 移动端
|
||||
- **框架**: UniApp (Vue 3)
|
||||
- **平台**: 微信小程序、H5
|
||||
|
||||
---
|
||||
|
||||
## 架构规范
|
||||
|
||||
### 后端分层架构
|
||||
- **Controller 层**: 接口定义,参数校验,禁止业务逻辑
|
||||
- **Service 层**: 业务逻辑实现
|
||||
- **Mapper 层**: 数据访问层
|
||||
- **Entity 层**: 数据实体
|
||||
- **Request/Response 层**: 入参出参封装
|
||||
- **Config 层**: 配置类
|
||||
|
||||
### 接口设计规范
|
||||
- Controller 层路由禁止添加 `/api` 前缀
|
||||
- 使用 `@RequestParam` 传递路径参数,禁止使用 `@PathVariable`
|
||||
- 接口方法参数不超过两个,超出时使用 Request/DTO 对象封装
|
||||
- 使用项目已有的 `Result` 作为统一接口返回
|
||||
- 禁止使用枚举类型作为接口入参
|
||||
|
||||
### 前端架构
|
||||
- 使用 Vue 3 Composition API (`<script setup>`)
|
||||
- 使用 TypeScript 进行类型检查
|
||||
- 组件通信优先使用 Pinia 状态管理
|
||||
- 前端访问后端通过网关统一调用
|
||||
|
||||
---
|
||||
|
||||
## 服务器端验收强制规则(最高优先级,不可违反)
|
||||
|
||||
**所有前端功能验收(小程序、用户端 web、管理后台 admin)必须使用服务器上部署的后端服务,禁止使用本地后端服务验收。**
|
||||
|
||||
### 强制流程
|
||||
|
||||
修改任意后端代码后,必须按以下顺序操作:
|
||||
|
||||
1. **编译验证**:本地 `mvn clean install -DskipTests -am` 必须通过,零报错。
|
||||
2. **部署到服务器**:执行 `python deploy.py backend`(或其他对应模块名),确保部署成功、无报错。
|
||||
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 面板查看)
|
||||
- 业务功能按预期工作
|
||||
- 只有验收通过才算任务完成
|
||||
|
||||
---
|
||||
|
||||
## 热加载规则(强制)
|
||||
|
||||
**修改前后端代码后,优先利用热加载,避免不必要的重启**
|
||||
|
||||
### 判断逻辑
|
||||
|
||||
1. **不需要重启的场景**(热加载自动生效)
|
||||
- 前端:修改 `.vue`、`.tsx`、`.ts`、`.js`、`.css`、`.scss` 等源码文件
|
||||
- 后端:修改业务逻辑、API 接口、组件代码等支持热更新的文件
|
||||
- 样式、模板、静态资源等修改
|
||||
|
||||
2. **需要重启的场景**(热加载无法生效)
|
||||
- 前端:修改 `vite.config.ts`、`tsconfig.json`、`.env`、`package.json`、别名配置等
|
||||
- 后端:修改启动配置、数据库连接、中间件注册、环境变量等
|
||||
- 新增或删除依赖包后
|
||||
|
||||
### 操作原则
|
||||
|
||||
- 修改后先观察终端输出,确认热加载是否成功
|
||||
- 只有修改了必须重启才能生效的配置时,才执行重启操作
|
||||
- 重启前后端服务前,需告知用户
|
||||
|
||||
---
|
||||
|
||||
## 语言规则(强制)
|
||||
|
||||
- 所有对话和文档使用**中文**
|
||||
- 代码内容(变量名、函数名、注释)使用英文
|
||||
- 技术术语保持英文(API、HTTP、JSON 等)
|
||||
|
||||
---
|
||||
|
||||
## Git 提交规范
|
||||
|
||||
- 使用中文提交
|
||||
- 格式:`类型:描述`
|
||||
- 类型包括:feat, fix, docs, style, refactor, test, chore
|
||||
|
||||
---
|
||||
|
||||
## 开发规范(来自 Cursor Rules)
|
||||
|
||||
### 基础设置
|
||||
- 保持对话语言为中文
|
||||
- 不允许在未经允许的情况下删除代码和文件
|
||||
- 不允许破坏正常的业务代码
|
||||
- 执行终端命令时要关注执行情况,避免无效等待
|
||||
|
||||
### 代码规范
|
||||
- 生成代码时必须添加类级和函数级注释
|
||||
- 使用 `import` 导包,禁止使用全限定名称引用类
|
||||
- 禁止使用枚举类型作为 Entity、Request、Response、DTO 对象的字段
|
||||
- 禁止以枚举类作为方法的入参
|
||||
- 新增数据的 id 使用已存在的雪花算法生成器生成
|
||||
|
||||
### 架构规范
|
||||
- Controller 层禁止添加业务逻辑
|
||||
- 使用全局异常处理,禁止使用 try-catch
|
||||
- 前端接口访问尽可能走网关调用
|
||||
|
||||
### 接口设计规范
|
||||
- Controller 层接口定义:
|
||||
- 入参使用 request 封装传递到 service 层
|
||||
- service 层方法命名与 controller 层保持一致
|
||||
- 出参使用 response 封装由 service 层传递到 controller 层
|
||||
- 禁止在 controller 层做 entity 与 request/response 的转换
|
||||
- 使用项目已有的 `Result` 做接口返回
|
||||
- 接口和方法参数不允许超过两个,超过时使用 request 或 DTO 对象封装
|
||||
- Controller 层路由禁止添加 `/api` 前缀
|
||||
- Controller 层 `@RequestMapping` 的 value 属性值不允许重复且不允许为空
|
||||
- 必须明确指定 value 属性值且使用驼峰结构命名,避免使用下划线
|
||||
- 禁止使用 `@GetMapping()` 或 `@PostMapping` 等空注解形式
|
||||
- 用户相关接口禁止直接传递用户 id,需要后端根据 token 获取当前登录用户信息
|
||||
- 禁止使用 `/{param}` 格式的路径参数,避免网关路由冲突
|
||||
- 路径参数统一使用 `@RequestParam` 而非 `@PathVariable`
|
||||
- 接口路径命名应具有明确的语义,避免使用通用词汇如 `/get`、`/list` 等
|
||||
- 批量操作接口应使用专门的 Request 对象封装参数,而非直接传递 List
|
||||
- 接口路径应避免层级过深,建议不超过 3 级路径结构
|
||||
|
||||
### 数据库规范
|
||||
- 所有数据表必须包含 `create_time` 和 `update_time` 字段
|
||||
- 删除操作优先使用逻辑删除,添加 `deleted` 字段标识
|
||||
- 数据库字段命名使用下划线分隔,Java 实体类使用驼峰命名
|
||||
- 优先使用 `LambdaQueryWrapper` 构造条件查询
|
||||
- 使用 Lambda 表达式引用实体类属性,提高代码可维护性
|
||||
- 复杂查询条件应使用 `LambdaQueryWrapper` 的链式调用
|
||||
|
||||
### 安全规范
|
||||
- 所有外部输入必须进行参数校验
|
||||
- 敏感信息不得在日志中输出
|
||||
- 数据库操作必须使用参数化查询,防止 SQL 注入
|
||||
|
||||
### 性能规范
|
||||
- 避免 N+1 查询问题,合理使用批量查询
|
||||
- 大数据量查询必须分页处理
|
||||
- 缓存策略要考虑数据一致性问题
|
||||
|
||||
### 日志规范
|
||||
- 关键业务操作必须记录操作日志
|
||||
- 异常信息要包含足够的上下文信息
|
||||
- 生产环境禁止输出 debug 级别日志
|
||||
|
||||
---
|
||||
|
||||
## 操作系统命令执行规则(强制)
|
||||
|
||||
**执行任何 Shell 命令前,必须根据当前操作系统(`win32` / `darwin` / `linux`)选择合适的命令语法。**
|
||||
|
||||
### 后台启动长期运行进程(dev server 等)
|
||||
|
||||
不同操作系统后台启动进程的方式完全不同,选错会导致命令阻塞或失败:
|
||||
|
||||
| 操作系统 | 正确方式 | 错误方式(禁止) |
|
||||
|---------|---------|----------------|
|
||||
| **Windows** | `Start-Process -FilePath "npm" -ArgumentList "run","dev" -WorkingDirectory "..." -WindowStyle Hidden` | `cmd /c "start /B npm run dev > log 2>&1"`(`cmd /c` 会阻塞等待重定向完成) |
|
||||
| **macOS/Linux** | `nohup npm run dev > log 2>&1 &` | |
|
||||
|
||||
**核心原则:**
|
||||
- Windows 下 `cmd /c "start /B ... > log 2>&1"` **绝对禁止**用于启动长期运行的后台服务。`cmd /c` 会等待重定向管道关闭,导致命令无限阻塞。
|
||||
- Windows 下启动后台进程必须使用 PowerShell 的 `Start-Process` cmdlet,并通过 `-WindowStyle Hidden` 隐藏窗口。
|
||||
|
||||
### 启动前检查(强制)
|
||||
|
||||
**启动任何开发服务器前,必须先检查目标端口是否已被占用:**
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
netstat -ano | findstr "端口号"
|
||||
# macOS/Linux
|
||||
lsof -i :端口号
|
||||
```
|
||||
|
||||
如果端口已被占用,必须先终止旧进程再启动新进程,**禁止重复启动**导致端口递增。
|
||||
|
||||
### 热加载优先原则
|
||||
|
||||
Vite / Webpack 等现代构建工具支持热更新(HMR):
|
||||
- **启动一次即可**:`npm run dev` 启动后,修改源码文件会自动热更新
|
||||
- **禁止频繁重启**:除非修改了 `vite.config.ts`、`.env`、`package.json` 等配置文件,否则不需要重启 dev server
|
||||
- 重启前必须告知用户原因
|
||||
@@ -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,125 @@ 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 面板查看)
|
||||
- 业务功能按预期工作
|
||||
- 只有验收通过才算任务完成
|
||||
|
||||
---
|
||||
|
||||
## 热加载规则(强制)
|
||||
## 部署强制规则(最高优先级,不可违反)
|
||||
|
||||
**修改前后端代码后,优先利用热加载,避免不必要的重启**
|
||||
**本地开发完成、验证通过、验收没有问题后,必须执行 `deploy.py` 部署脚本,将应用部署到服务器上。**
|
||||
|
||||
### 判断逻辑
|
||||
### 强制流程
|
||||
|
||||
每次功能开发或 bug 修复完成后,必须按以下顺序操作:
|
||||
|
||||
1. **本地验证**:在本地开发环境中完成功能开发和自测,确认无报错。
|
||||
2. **验收通过**:按"服务器端验收强制规则"完成验收,确认功能正常。
|
||||
3. **执行部署**:验收通过后,立即执行部署脚本将应用部署到服务器:
|
||||
- 后端:`python deploy.py backend`
|
||||
- 前端:`python deploy.py frontend`
|
||||
- 管理后台:`python deploy.py admin`
|
||||
- 全部:`python deploy.py all`
|
||||
4. **部署验证**:部署完成后,在服务器上验证功能正常,无报错。
|
||||
|
||||
### 禁止行为
|
||||
|
||||
- ❌ 禁止本地开发完成后不部署到服务器就认为任务完成
|
||||
- ❌ 禁止跳过验收直接部署
|
||||
- ❌ 禁止部署后不验证就结束任务
|
||||
- ❌ 禁止在编译或验收有报错的情况下部署
|
||||
|
||||
### 允许的唯一例外
|
||||
|
||||
- ✅ 纯文档修改(不涉及代码变更)
|
||||
- ✅ 仅在本地运行的脚本工具(不涉及线上服务)
|
||||
|
||||
---
|
||||
|
||||
## 本地服务管理规则(强制)
|
||||
|
||||
**启动、重启、停止本地前后端服务时,必须使用项目根目录下的 `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`、别名配置等
|
||||
- 后端:修改启动配置、数据库连接、中间件注册、环境变量等
|
||||
- 新增或删除依赖包后
|
||||
|
||||
### 操作原则
|
||||
|
||||
- 修改后先观察终端输出,确认热加载是否成功
|
||||
- 只有修改了必须重启才能生效的配置时,才执行重启操作
|
||||
- 重启前后端服务前,需告知用户
|
||||
- 后端:按现有后端规则处理(本地不启动后端,使用服务器验收)
|
||||
|
||||
---
|
||||
|
||||
@@ -338,3 +415,53 @@ bash deploy-all.sh admin
|
||||
- 关键业务操作必须记录操作日志
|
||||
- 异常信息要包含足够的上下文信息
|
||||
- 生产环境禁止输出 debug 级别日志
|
||||
|
||||
---
|
||||
|
||||
## 日志排查规则(强制)
|
||||
|
||||
**遇到"对话不存在或无权限"错误时,按以下顺序排查:**
|
||||
|
||||
### 1. 检查 conversationId 是否为空
|
||||
|
||||
- **前端检查**:在浏览器 DevTools Network 面板查看请求参数 `conversationId`
|
||||
- **后端检查**:查看 `UserContextHolder.getCurrentUserId()` 是否返回有效值
|
||||
- **数据库检查**:执行 `SELECT * FROM t_conversation WHERE id = '<conversationId>'` 确认对话是否存在
|
||||
|
||||
### 2. 检查权限匹配
|
||||
|
||||
- **前端问题**:当前登录用户的 userId 与对话的 userId 不一致
|
||||
- **后端逻辑**:`ScriptMessageServiceImpl.listByConversation` 中会校验 `conversation.getUserId().equals(currentUserId)`
|
||||
- **修复方案**:确认前端传递的是正确的 conversationId,或重新创建对话
|
||||
|
||||
### 3. 常见原因
|
||||
|
||||
| 原因 | 解决方案 |
|
||||
|------|----------|
|
||||
| conversationId 为空字符串 | 前端增加短路保护,显示"暂无对话记录"提示 |
|
||||
| 对话不存在(已删除) | 引导用户创建新对话 |
|
||||
| 用户未登录 | 检查 Token 是否有效,重新登录 |
|
||||
| 对话属于其他用户 | 确认前端传递的 conversationId 正确 |
|
||||
|
||||
### 4. 快速定位方法
|
||||
|
||||
使用 `tools/download-server-log.py` 脚本下载服务器日志:
|
||||
|
||||
```bash
|
||||
# 下载完整日志
|
||||
python tools/download-server-log.py latest
|
||||
|
||||
# 只看最近 50 条错误
|
||||
python tools/download-server-log.py errors 50
|
||||
|
||||
# 搜索特定关键词(如 listByConversation)
|
||||
python tools/download-server-log.py grep "listByConversation" 20
|
||||
```
|
||||
|
||||
日志位置:`logs/server/` 目录
|
||||
|
||||
### 5. 防御性编程原则
|
||||
|
||||
- **Service 层**:对空参数做防御性检查,返回空列表而非抛异常
|
||||
- **Controller 层**:参数校验失败时返回友好错误提示
|
||||
- **前端**:接口调用前做空值检查,避免发送无效请求
|
||||
|
||||
@@ -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/ # 早期网页版本
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
+33
-113
@@ -1,26 +1,12 @@
|
||||
# Emotion Museum HTTPS Nginx 配置
|
||||
# 配置路径:/www/server/panel/vhost/nginx/emotion-museum.conf
|
||||
# 域名:lifescript.happylifeos.com
|
||||
|
||||
# HTTP 服务器 - 强制跳转 HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name lifescript.happylifeos.com;
|
||||
|
||||
# 强制跳转 HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS 服务器
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name lifescript.happylifeos.com;
|
||||
|
||||
# SSL 证书配置
|
||||
ssl_certificate /etc/letsencrypt/live/lifescript.happylifeos.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/lifescript.happylifeos.com/privkey.pem;
|
||||
|
||||
# SSL 优化配置
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# WeChat mini program/Cronet compatibility: keep TLS 1.2 enabled with broad modern suites.
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
|
||||
@@ -29,97 +15,73 @@ server {
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
add_header Strict-Transport-Security max-age=31536000 always;
|
||||
|
||||
# HSTS (可选,生产环境建议开启)
|
||||
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# ACME 挑战目录 - 用于 SSL 证书申请和续期
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
root /data/www/acme-challenge;
|
||||
allow all;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# 根路径 - 用户前端应用
|
||||
location = / {
|
||||
return 404;
|
||||
rewrite ^ /emotion-museum/ permanent;
|
||||
}
|
||||
|
||||
location / {
|
||||
location /emotion-museum/ {
|
||||
alias /data/www/emotion-museum/;
|
||||
autoindex off;
|
||||
|
||||
# 处理 Vue Router 的 history 模式
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# HTML 文件不缓存
|
||||
location ~ \.html?$ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# 静态资源缓存 1 年
|
||||
location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
# 静态资源直接返回文件,不经过 try_files
|
||||
location ~* ^/emotion-museum/(assets|static)/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
expires 1y;
|
||||
try_files $uri =404;
|
||||
}
|
||||
# SPA 路由兜底
|
||||
try_files $uri $uri/ /emotion-museum/index.html;
|
||||
}
|
||||
|
||||
location = /emotion-museum {
|
||||
rewrite ^(.*)$ $1/ permanent;
|
||||
}
|
||||
|
||||
# 管理后台应用路径
|
||||
location /emotion-museum-admin/ {
|
||||
alias /data/www/emotion-museum-admin/;
|
||||
autoindex off;
|
||||
|
||||
# 处理 Vue Router 的 history 模式
|
||||
try_files $uri $uri/ /emotion-museum-admin/index.html;
|
||||
|
||||
# HTML 文件不缓存
|
||||
location ~ \.html?$ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# 静态资源缓存 1 年
|
||||
location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
expires 1y;
|
||||
}
|
||||
}
|
||||
|
||||
location = /emotion-museum-admin {
|
||||
rewrite ^(.*)$ $1/ permanent;
|
||||
}
|
||||
|
||||
# Life-Script 应用路径
|
||||
location /course-of-life/ {
|
||||
alias /data/www/course-of-life/;
|
||||
autoindex off;
|
||||
try_files $uri $uri/ /course-of-life/index.html;
|
||||
}
|
||||
|
||||
location = /course-of-life {
|
||||
rewrite ^ /course-of-life/ last;
|
||||
}
|
||||
|
||||
# life-script React 应用
|
||||
location /life-script/ {
|
||||
alias /data/www/life-script/;
|
||||
autoindex off;
|
||||
|
||||
# 处理 React Router 的 history 模式
|
||||
try_files $uri $uri/ /life-script/index.html;
|
||||
|
||||
# HTML 文件不缓存
|
||||
location ~ \.html?$ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# 静态资源缓存 1 年
|
||||
location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
# 静态资源直接返回文件
|
||||
location ~* ^/life-script/(assets|static)/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
expires 1y;
|
||||
try_files $uri =404;
|
||||
}
|
||||
# SPA 路由兜底
|
||||
try_files $uri $uri/ /life-script/index.html;
|
||||
}
|
||||
|
||||
location = /life-script {
|
||||
rewrite ^ /life-script/ last;
|
||||
}
|
||||
|
||||
# 后端 API 代理
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:19089;
|
||||
proxy_set_header Host $host;
|
||||
@@ -131,50 +93,6 @@ server {
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Swagger UI / Knife4j API 文档代理
|
||||
# 使用 ^~ 前缀匹配,防止被 / 的 try_files 拦截
|
||||
location ^~ /swagger-ui {
|
||||
proxy_pass http://127.0.0.1:19089;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ^~ /v3/api-docs {
|
||||
proxy_pass http://127.0.0.1:19089;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /doc.html {
|
||||
proxy_pass http://127.0.0.1:19089;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ^~ /webjars {
|
||||
proxy_pass http://127.0.0.1:19089;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Knife4j 4.x 额外需要的路径(swagger-resources 等)
|
||||
location ^~ /swagger-resources {
|
||||
proxy_pass http://127.0.0.1:19089;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# WebSocket 代理
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:19089;
|
||||
proxy_http_version 1.1;
|
||||
@@ -189,21 +107,23 @@ server {
|
||||
proxy_read_timeout 7d;
|
||||
}
|
||||
|
||||
# 健康检查端点
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# 隐藏文件访问控制
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# 日志配置
|
||||
access_log /www/wwwlogs/ssl-access.log;
|
||||
error_log /www/wwwlogs/ssl-error.log;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name lifescript.happylifeos.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ Purpose: 统一部署所有服务到生产服务器,替代原有的多个 shel
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
@@ -35,6 +37,70 @@ if hasattr(sys.stdout, 'buffer'):
|
||||
# 当前 Python 可执行文件(Windows 用 python.exe,Linux/Mac 用 python3)
|
||||
PYTHON = sys.executable
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 环境变量修复(Windows 专用)
|
||||
# ============================================================================
|
||||
def _build_full_path():
|
||||
"""构造包含所有关键目录的完整 PATH 字符串。
|
||||
|
||||
Windows cmd.exe 启动时会用自己的"标准 PATH"(只有 2 段用户 PATH),
|
||||
完全忽略父进程传入的 PATH 变量。解决办法是在每个 cmd.exe 命令前
|
||||
用 `set "PATH=..."` 显式设置。这个函数返回要设置的完整 PATH。
|
||||
"""
|
||||
if sys.platform != 'win32':
|
||||
return None
|
||||
parts = [
|
||||
r'C:\Windows\system32',
|
||||
r'C:\Windows',
|
||||
r'C:\Windows\System32\Wbem',
|
||||
r'C:\Windows\System32\OpenSSH',
|
||||
]
|
||||
# 从已知环境变量自动推断关键目录(不硬编码)
|
||||
if os.environ.get('MAVEN_HOME'):
|
||||
parts.append(os.environ['MAVEN_HOME'] + r'\bin')
|
||||
if os.environ.get('JAVA_HOME'):
|
||||
parts.append(os.environ['JAVA_HOME'] + r'\bin')
|
||||
# Node.js 常见安装位置自动检测
|
||||
for d in [r'C:\Program Files\nodejs', r'F:\Program Files\nodejs',
|
||||
r'C:\Program Files (x86)\nodejs', r'F:\Programs\nvm']:
|
||||
if Path(d).exists():
|
||||
parts.append(d)
|
||||
# 保留用户已有的 PATH(不丢失用户自定义工具)
|
||||
user_path = os.environ.get('PATH', '')
|
||||
for p in user_path.split(';'):
|
||||
if p and p not in parts:
|
||||
parts.append(p)
|
||||
return ';'.join(parts)
|
||||
|
||||
|
||||
def _wrap_cmd_for_windows(cmd):
|
||||
"""为 Windows cmd.exe 命令包装 set PATH 头部,解决 PATH 截断问题。
|
||||
|
||||
原因:cmd.exe 启动后 PATH 只有 2 段(pnpm/npm),找不到 mvn/node/npm。
|
||||
修复:在每个命令前用 `set "PATH=完整路径" &&` 显式设置 PATH。
|
||||
"""
|
||||
if sys.platform != 'win32':
|
||||
return cmd
|
||||
full_path = _build_full_path()
|
||||
return f'set "PATH={full_path}" && {cmd}'
|
||||
|
||||
|
||||
def _ensure_env():
|
||||
"""修复 Windows 子进程编码问题 + 准备 PATH 工具。
|
||||
|
||||
Windows cmd.exe 启动时强制用 GBK 编码输出(即使父进程是 UTF-8),
|
||||
设置 PYTHONIOENCODING/PYTHONUTF8 强制 Python 子进程使用 UTF-8。
|
||||
实际 PATH 修复在 _wrap_cmd_for_windows() 中按需包装每个命令。
|
||||
"""
|
||||
if sys.platform != 'win32':
|
||||
return
|
||||
# 强制 Python 子进程使用 UTF-8 编码
|
||||
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
|
||||
os.environ.setdefault('PYTHONUTF8', '1')
|
||||
os.environ.setdefault('PYTHONLEGACYWINDOWSSTDIO', '0')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 配置
|
||||
# ============================================================================
|
||||
@@ -84,21 +150,23 @@ def log_section(msg):
|
||||
|
||||
|
||||
def run_command(cmd, cwd=None, timeout=120, capture=True):
|
||||
"""执行本地命令"""
|
||||
"""执行本地命令(自动处理 Windows cmd.exe PATH 截断)"""
|
||||
# 设置 PYTHONUNBUFFERED=1 确保 Python 子进程不缓冲 stdout
|
||||
env = os.environ.copy()
|
||||
env['PYTHONUNBUFFERED'] = '1'
|
||||
# Windows 上包装 set PATH 前缀,解决 cmd.exe PATH 截断问题
|
||||
effective_cmd = _wrap_cmd_for_windows(cmd)
|
||||
try:
|
||||
if capture:
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, cwd=cwd, env=env,
|
||||
effective_cmd, shell=True, cwd=cwd, env=env,
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace',
|
||||
timeout=timeout
|
||||
)
|
||||
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
|
||||
else:
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, cwd=cwd, env=env, timeout=timeout
|
||||
effective_cmd, shell=True, cwd=cwd, env=env, timeout=timeout
|
||||
)
|
||||
return result.returncode == 0, "", ""
|
||||
except subprocess.TimeoutExpired:
|
||||
@@ -158,6 +226,137 @@ def check_ssh():
|
||||
log_info("SSH 连接正常")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Nginx 站点启用(防御性诊断 + 清理)
|
||||
# ============================================================================
|
||||
|
||||
# 变量安全校验正则
|
||||
_SAFE_DOMAIN = re.compile(r'^[a-zA-Z0-9.-]+$')
|
||||
_SAFE_REMOTE_CONF = re.compile(r'^[a-zA-Z0-9./_-]+$')
|
||||
|
||||
|
||||
def _validate_nginx_inputs(remote_conf: str, domain: str) -> bool:
|
||||
"""校验 domain 和 remote_conf 防止 shell 注入和路径遍历。
|
||||
|
||||
Returns: True 合法;False 非法(已 log_error)
|
||||
"""
|
||||
if not domain or not _SAFE_DOMAIN.match(domain):
|
||||
log_error(f"非法 domain 名称: {domain!r}")
|
||||
return False
|
||||
if domain.startswith('-') or domain.endswith('-') \
|
||||
or domain.startswith('.') or domain.endswith('.') \
|
||||
or '..' in domain:
|
||||
log_error(f"非法 domain 结构: {domain!r} (首尾连字符/点 或 连续点非法)")
|
||||
return False
|
||||
if not remote_conf or not _SAFE_REMOTE_CONF.match(remote_conf):
|
||||
log_error(f"非法 remote_conf 路径: {remote_conf!r}")
|
||||
return False
|
||||
if '//' in remote_conf or '..' in remote_conf:
|
||||
log_error(f"remote_conf 包含可疑路径片段: {remote_conf!r}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def enable_nginx_site(remote_conf: str, domain: str) -> bool:
|
||||
"""幂等地启用 nginx 站点(sites-enabled symlink)。
|
||||
|
||||
流程:
|
||||
1. 诊断目标路径当前状态(symlink/dir/file/missing)
|
||||
2. 清理异常状态(目录/文件/断链);失败短路
|
||||
3. 二次检查 + 创建 symlink
|
||||
4. 移除 default 站点(失败仅 warning)
|
||||
5. 验证 symlink + default 状态
|
||||
|
||||
Returns: True 成功;False 失败(stderr 已打印)
|
||||
"""
|
||||
if not _validate_nginx_inputs(remote_conf, domain):
|
||||
return False
|
||||
|
||||
target = f"/etc/nginx/sites-enabled/{domain}.conf"
|
||||
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
|
||||
|
||||
# ===== 步骤 1: 诊断目标路径状态 =====
|
||||
log_info("诊断步骤: 检查目标路径...")
|
||||
diagnose_cmd = (
|
||||
f'test -e {target} && '
|
||||
f'(test -L {target} && echo "symlink" || '
|
||||
f'(test -d {target} && echo "dir" || echo "file")) || '
|
||||
f'echo "missing"'
|
||||
)
|
||||
ok, status, err = ssh_command(diagnose_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"诊断步骤失败: {err}")
|
||||
return False
|
||||
status = status.strip()
|
||||
log_info(f"诊断结果: {status}")
|
||||
|
||||
# ===== 步骤 2: 清理异常状态 =====
|
||||
log_info("清理步骤: 处理异常状态...")
|
||||
cleanup_cmd = (
|
||||
f'case "{status}" in '
|
||||
f'symlink) unlink {target} ;; '
|
||||
f'dir) rm -rf {target} ;; '
|
||||
f'file) rm -f {target} ;; '
|
||||
f'missing) ;; '
|
||||
f'esac'
|
||||
)
|
||||
ok, _, err = ssh_command(cleanup_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"清理步骤失败: {err}")
|
||||
log_error("上下文: 目标 = " + target)
|
||||
log_error("提示: 权限可能不足(检查 /etc/nginx/sites-enabled 写权限)")
|
||||
return False
|
||||
log_info("清理步骤完成")
|
||||
|
||||
# ===== 步骤 3: 建链(两段式:先二次检查,再 ln -snf) =====
|
||||
log_info("建链步骤: 创建 symlink...")
|
||||
# 第一段:二次检查 target 不存在(防并发进程干扰)
|
||||
guard_cmd = f'if [ -e {target} ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi'
|
||||
ok, _, err = ssh_command(guard_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"建链步骤失败 - 二次检查: {err}")
|
||||
log_error("提示: 并发进程干扰(其他进程在步骤 2 后、步骤 3 前创建了同名项)")
|
||||
log_error("上下文: 目标 = " + target)
|
||||
return False
|
||||
# 第二段:执行 ln -snf(独立 SSH 调用,错误信息不会被二次检查误报)
|
||||
link_cmd = f'ln -snf {remote_conf} {target}'
|
||||
ok, _, err = ssh_command(link_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"建链步骤失败 - ln 执行: {err}")
|
||||
log_error("提示: 确认 scp_file 是否成功上传了 sites-available 下的 conf 文件")
|
||||
log_error("上下文: 目标 = " + target)
|
||||
return False
|
||||
log_info("建链步骤完成")
|
||||
|
||||
# ===== 步骤 4: 清理 default 站点(失败仅 warning) =====
|
||||
log_info("清默认步骤: 移除 default 站点...")
|
||||
ok, _, err = ssh_command("rm -f /etc/nginx/sites-enabled/default", timeout=30)
|
||||
if not ok:
|
||||
log_warn(f"清默认步骤失败(不阻塞): {err}")
|
||||
log_warn("提示: default 文件可能仍存在,nginx 可能有端口冲突;步骤 5 验证会确认")
|
||||
else:
|
||||
log_info("清默认步骤完成")
|
||||
|
||||
# ===== 步骤 5: 验证结果 =====
|
||||
log_info("验证步骤: 检查 symlink 和 default 状态...")
|
||||
verify_cmd = (
|
||||
f'readlink {target} && '
|
||||
f'(test ! -e /etc/nginx/sites-enabled/default && echo "default:OK" || echo "default:STILL_EXISTS") && '
|
||||
f'ls -la /etc/nginx/sites-enabled/'
|
||||
)
|
||||
ok, stdout, err = ssh_command(verify_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"验证步骤失败: {err}")
|
||||
log_error(f"实际输出: {stdout}")
|
||||
return False
|
||||
log_info("验证结果:")
|
||||
for line in stdout.split('\n'):
|
||||
if line.strip():
|
||||
log_info(f" {line}")
|
||||
log_info("启用站点配置完成")
|
||||
return True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Nginx 配置
|
||||
# ============================================================================
|
||||
@@ -181,12 +380,8 @@ def deploy_nginx():
|
||||
return False
|
||||
|
||||
log_info("启用站点配置...")
|
||||
ok, _, err = ssh_command(
|
||||
f"ln -snf {remote_conf} /etc/nginx/sites-enabled/{DOMAIN}.conf "
|
||||
f"&& rm -f /etc/nginx/sites-enabled/default"
|
||||
)
|
||||
if not ok:
|
||||
log_error(f"启用站点失败: {err}")
|
||||
if not enable_nginx_site(remote_conf, DOMAIN):
|
||||
log_error("启用站点失败(详见 enable_nginx_site 内部日志)")
|
||||
return False
|
||||
|
||||
log_info("验证 Nginx 配置...")
|
||||
@@ -201,14 +396,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
|
||||
@@ -216,7 +411,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
|
||||
)
|
||||
@@ -330,8 +525,10 @@ def verify_deploy():
|
||||
]
|
||||
|
||||
for url, label in endpoints:
|
||||
null_device = 'NUL' if sys.platform == 'win32' else '/dev/null'
|
||||
safe_url = shlex.quote(url)
|
||||
ok, stdout, _ = run_command(
|
||||
f'curl -k -s -o /dev/null -w "HTTP %{{http_code}}" {url}',
|
||||
f'curl -k -s -o {null_device} -w "HTTP %{{http_code}}" {safe_url}',
|
||||
timeout=30
|
||||
)
|
||||
if ok and stdout:
|
||||
@@ -365,6 +562,9 @@ def print_usage():
|
||||
|
||||
|
||||
def main():
|
||||
# 修复 Windows cmd.exe 子进程环境块截断问题
|
||||
_ensure_env()
|
||||
|
||||
deploy_type = sys.argv[1] if len(sys.argv) > 1 else "all"
|
||||
|
||||
if deploy_type in ("--help", "-h", "help"):
|
||||
|
||||
@@ -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
@@ -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 SUCCESS(endpointId 字段新增不破坏任何编译,因为这是新增字段)
|
||||
|
||||
- [ ] **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 字段"
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,508 @@
|
||||
# deploy.py nginx 站点启用超时修复 — 实施计划
|
||||
|
||||
> **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:** 修复 `python deploy.py` 中 nginx 站点启用命令 30s 超时问题,通过将链式 `ln -snf && rm -f` 拆为 5 步独立 SSH 调用 + 前置诊断清理。
|
||||
|
||||
**Architecture:** 在 `deploy.py` 中新增 `enable_nginx_site(remote_conf, domain)` 函数(5 步骤:诊断→清理→建链→清默认→验证),替换 `deploy_nginx` 第 250-253 行的链式命令。每步独立 `ssh_command(timeout=30)` 调用,失败立即停止;变量入口校验防御 shell 注入。
|
||||
|
||||
**Tech Stack:** Python 3.10+, subprocess (ssh/scp), bash test/case/ln 内置命令
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
**修改**:`G:\IdeaProjects\emotion-museun\deploy.py`
|
||||
- 新增函数 `enable_nginx_site`(在 `deploy_nginx` 之前,约 70 行)
|
||||
- 修改 `deploy_nginx`(第 250-256 行,替换链式命令为函数调用)
|
||||
|
||||
**不修改**:
|
||||
- `ssh_command()` / `run_ssh_args()` / `scp_file()` 底层
|
||||
- `conf/emotion-museum.conf` nginx 配置文件
|
||||
- 其他 deploy.py 子脚本
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 添加 `enable_nginx_site` 函数骨架 + 变量安全校验
|
||||
|
||||
**Files:**
|
||||
- Modify: `G:\IdeaProjects\emotion-museun\deploy.py:230-266`(在 `deploy_nginx` 前插入新函数)
|
||||
|
||||
- [ ] **Step 1: 在 `deploy_nginx` 之前插入新函数骨架**
|
||||
|
||||
**Step 1a**:先在文件顶部 imports 区(第 23-28 行附近)追加 `import re`:
|
||||
|
||||
将 `deploy.py` 第 23-28 行:
|
||||
```python
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
```
|
||||
|
||||
改为:
|
||||
```python
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
```
|
||||
|
||||
**Step 1b**:在 `deploy.py` 第 230 行(`# Nginx 配置` 注释之前)插入以下代码:
|
||||
|
||||
```python
|
||||
# ============================================================================
|
||||
# Nginx 站点启用(防御性诊断 + 清理)
|
||||
# ============================================================================
|
||||
|
||||
# 变量安全校验正则
|
||||
_SAFE_DOMAIN = re.compile(r'^[a-zA-Z0-9.-]+$')
|
||||
_SAFE_REMOTE_CONF = re.compile(r'^[a-zA-Z0-9./_-]+$')
|
||||
|
||||
|
||||
def _validate_nginx_inputs(remote_conf: str, domain: str) -> bool:
|
||||
"""校验 domain 和 remote_conf 防止 shell 注入和路径遍历。
|
||||
|
||||
Returns: True 合法;False 非法(已 log_error)
|
||||
"""
|
||||
if not domain or not _SAFE_DOMAIN.match(domain):
|
||||
log_error(f"非法 domain 名称: {domain!r}")
|
||||
return False
|
||||
if domain.startswith('-') or domain.endswith('-') \
|
||||
or domain.startswith('.') or domain.endswith('.') \
|
||||
or '..' in domain:
|
||||
log_error(f"非法 domain 结构: {domain!r} (首尾连字符/点 或 连续点非法)")
|
||||
return False
|
||||
if not remote_conf or not _SAFE_REMOTE_CONF.match(remote_conf):
|
||||
log_error(f"非法 remote_conf 路径: {remote_conf!r}")
|
||||
return False
|
||||
if '//' in remote_conf or '..' in remote_conf:
|
||||
log_error(f"remote_conf 包含可疑路径片段: {remote_conf!r}")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def enable_nginx_site(remote_conf: str, domain: str) -> bool:
|
||||
"""幂等地启用 nginx 站点(sites-enabled symlink)。
|
||||
|
||||
流程:
|
||||
1. 诊断目标路径当前状态(symlink/dir/file/missing)
|
||||
2. 清理异常状态(目录/文件/断链);失败短路
|
||||
3. 二次检查 + 创建 symlink
|
||||
4. 移除 default 站点(失败仅 warning)
|
||||
5. 验证 symlink + default 状态
|
||||
|
||||
Returns: True 成功;False 失败(stderr 已打印)
|
||||
"""
|
||||
if not _validate_nginx_inputs(remote_conf, domain):
|
||||
return False
|
||||
|
||||
target = f"/etc/nginx/sites-enabled/{domain}.conf"
|
||||
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
|
||||
return True # 后续 Task 替换为完整 5 步实现
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 Python 语法正确**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"`
|
||||
Expected: 退出码 0,无输出(语法正确)
|
||||
|
||||
- [ ] **Step 3: 验证变量校验逻辑**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "from deploy import _validate_nginx_inputs; print(_validate_nginx_inputs('/etc/nginx/sites-available/lifescript.happylifeos.com.conf', 'lifescript.happylifeos.com'))"`
|
||||
Expected: 输出 `True`
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "from deploy import _validate_nginx_inputs; print(_validate_nginx_inputs('/etc/nginx/sites-available/test.conf', '-evil.com'))"`
|
||||
Expected: 输出 `False`(同时 stderr 出现 `[ERROR] 非法 domain 结构`)
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "from deploy import _validate_nginx_inputs; print(_validate_nginx_inputs('/etc/nginx/../etc/passwd', 'example.com'))"`
|
||||
Expected: 输出 `False`(同时 stderr 出现 `[ERROR] remote_conf 包含可疑路径片段`)
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd G:\IdeaProjects\emotion-museun
|
||||
git add deploy.py
|
||||
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 添加 enable_nginx_site 函数骨架 + 变量安全校验"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 实现步骤 1(诊断)+ 步骤 2(清理,case 注入 status)
|
||||
|
||||
**Files:**
|
||||
- Modify: `G:\IdeaProjects\emotion-museun\deploy.py:enable_nginx_site` 函数体
|
||||
|
||||
- [ ] **Step 1: 替换 `enable_nginx_site` 末尾的 `return True` 为 5 步骤实现(部分)**
|
||||
|
||||
将 `enable_nginx_site` 函数末尾的:
|
||||
```python
|
||||
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
|
||||
return True # 后续 Task 替换为完整 5 步实现
|
||||
```
|
||||
|
||||
替换为:
|
||||
```python
|
||||
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
|
||||
|
||||
# ===== 步骤 1: 诊断目标路径状态 =====
|
||||
log_info("诊断步骤: 检查目标路径...")
|
||||
diagnose_cmd = (
|
||||
f'test -e {target} && '
|
||||
f'(test -L {target} && echo "symlink" || '
|
||||
f'(test -d {target} && echo "dir" || echo "file")) || '
|
||||
f'echo "missing"'
|
||||
)
|
||||
ok, status, err = ssh_command(diagnose_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"诊断步骤失败: {err}")
|
||||
return False
|
||||
status = status.strip()
|
||||
log_info(f"诊断结果: {status}")
|
||||
|
||||
# ===== 步骤 2: 清理异常状态 =====
|
||||
log_info("清理步骤: 处理异常状态...")
|
||||
cleanup_cmd = (
|
||||
f'case "{status}" in '
|
||||
f'symlink) unlink {target} ;; '
|
||||
f'dir) rm -rf {target} ;; '
|
||||
f'file) rm -f {target} ;; '
|
||||
f'missing) ;; '
|
||||
f'esac'
|
||||
)
|
||||
ok, _, err = ssh_command(cleanup_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"清理步骤失败: {err}")
|
||||
log_error("上下文: 目标 = " + target)
|
||||
log_error("提示: 权限可能不足(检查 /etc/nginx/sites-enabled 写权限)")
|
||||
return False
|
||||
log_info("清理步骤完成")
|
||||
return True # Task 3 替换为完整建链+清默认
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 Python 语法**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"`
|
||||
Expected: 退出码 0
|
||||
|
||||
- [ ] **Step 3: 验证 SSH 命令构造正确(不执行 SSH)**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "
|
||||
from deploy import _validate_nginx_inputs
|
||||
target = '/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf'
|
||||
status = 'dir'
|
||||
diagnose_cmd = f'test -e {target} && (test -L {target} && echo symlink || (test -d {target} && echo dir || echo file)) || echo missing'
|
||||
cleanup_cmd = f'case \"{status}\" in symlink) unlink {target} ;; dir) rm -rf {target} ;; file) rm -f {target} ;; missing) ;; esac'
|
||||
print('DIAGNOSE:', diagnose_cmd)
|
||||
print('CLEANUP:', cleanup_cmd)
|
||||
"`
|
||||
Expected:
|
||||
```
|
||||
DIAGNOSE: test -e /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && (test -L /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && echo symlink || (test -d /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && echo dir || echo file)) || echo missing
|
||||
CLEANUP: case "dir" in symlink) unlink /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf ;; dir) rm -rf /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf ;; file) rm -f /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf ;; missing) ;; esac
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd G:\IdeaProjects\emotion-museun
|
||||
git add deploy.py
|
||||
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 实现 enable_nginx_site 步骤 1(诊断)+ 步骤 2(清理)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 实现步骤 3(建链,两段式)+ 步骤 4(清 default)
|
||||
|
||||
**Files:**
|
||||
- Modify: `G:\IdeaProjects\emotion-museun\deploy.py:enable_nginx_site` 函数体
|
||||
|
||||
- [ ] **Step 1: 在清理步骤后插入步骤 3 和 4**
|
||||
|
||||
将 `enable_nginx_site` 函数末尾的 `return True # Task 3 替换为完整建链+清默认` 替换为:
|
||||
|
||||
```python
|
||||
log_info("清理步骤完成")
|
||||
|
||||
# ===== 步骤 3: 建链(两段式:先二次检查,再 ln -snf) =====
|
||||
log_info("建链步骤: 创建 symlink...")
|
||||
# 第一段:二次检查 target 不存在(防并发进程干扰)
|
||||
guard_cmd = f'if [ -e {target} ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi'
|
||||
ok, _, err = ssh_command(guard_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"建链步骤失败 - 二次检查: {err}")
|
||||
log_error("提示: 并发进程干扰(其他进程在步骤 2 后、步骤 3 前创建了同名项)")
|
||||
log_error("上下文: 目标 = " + target)
|
||||
return False
|
||||
# 第二段:执行 ln -snf(独立 SSH 调用,错误信息不会被二次检查误报)
|
||||
link_cmd = f'ln -snf {remote_conf} {target}'
|
||||
ok, _, err = ssh_command(link_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"建链步骤失败 - ln 执行: {err}")
|
||||
log_error("提示: 确认 scp_file 是否成功上传了 sites-available 下的 conf 文件")
|
||||
log_error("上下文: 目标 = " + target)
|
||||
return False
|
||||
log_info("建链步骤完成")
|
||||
|
||||
# ===== 步骤 4: 清理 default 站点(失败仅 warning) =====
|
||||
log_info("清默认步骤: 移除 default 站点...")
|
||||
ok, _, err = ssh_command("rm -f /etc/nginx/sites-enabled/default", timeout=30)
|
||||
if not ok:
|
||||
log_warn(f"清默认步骤失败(不阻塞): {err}")
|
||||
log_warn("提示: default 文件可能仍存在,nginx 可能有端口冲突;步骤 5 验证会确认")
|
||||
else:
|
||||
log_info("清默认步骤完成")
|
||||
return True # Task 4 替换为完整 5 步(含验证)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 Python 语法**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"`
|
||||
Expected: 退出码 0
|
||||
|
||||
- [ ] **Step 3: 验证 SSH 命令构造(两段式)**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "
|
||||
target = '/etc/nginx/sites-enabled/test.com.conf'
|
||||
remote_conf = '/etc/nginx/sites-available/test.com.conf'
|
||||
guard_cmd = f'if [ -e {target} ]; then echo \"二次检查失败:target 仍存在\" >&2; exit 1; fi'
|
||||
link_cmd = f'ln -snf {remote_conf} {target}'
|
||||
print('GUARD:', guard_cmd)
|
||||
print('LINK:', link_cmd)
|
||||
"`
|
||||
Expected:
|
||||
```
|
||||
GUARD: if [ -e /etc/nginx/sites-enabled/test.com.conf ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi
|
||||
LINK: ln -snf /etc/nginx/sites-available/test.com.conf /etc/nginx/sites-enabled/test.com.conf
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
cd G:\IdeaProjects\emotion-museun
|
||||
git add deploy.py
|
||||
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 实现 enable_nginx_site 步骤 3(两段式建链)+ 步骤 4(清 default)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 实现步骤 5(验证 symlink + default)+ 集成到 deploy_nginx
|
||||
|
||||
**Files:**
|
||||
- Modify: `G:\IdeaProjects\emotion-museun\deploy.py:enable_nginx_site` 函数体(末尾)
|
||||
- Modify: `G:\IdeaProjects\emotion-museun\deploy.py:249-256`(deploy_nginx 内部)
|
||||
|
||||
- [ ] **Step 1: 在 `enable_nginx_site` 末尾插入步骤 5**
|
||||
|
||||
将 `enable_nginx_site` 函数末尾的 `return True # Task 4 替换为完整 5 步(含验证)` 替换为:
|
||||
|
||||
```python
|
||||
else:
|
||||
log_info("清默认步骤完成")
|
||||
|
||||
# ===== 步骤 5: 验证结果 =====
|
||||
log_info("验证步骤: 检查 symlink 和 default 状态...")
|
||||
verify_cmd = (
|
||||
f'readlink {target} && '
|
||||
f'(test ! -e /etc/nginx/sites-enabled/default && echo "default:OK" || echo "default:STILL_EXISTS") && '
|
||||
f'ls -la /etc/nginx/sites-enabled/'
|
||||
)
|
||||
ok, stdout, err = ssh_command(verify_cmd, timeout=30)
|
||||
if not ok:
|
||||
log_error(f"验证步骤失败: {err}")
|
||||
log_error(f"实际输出: {stdout}")
|
||||
return False
|
||||
log_info("验证结果:")
|
||||
for line in stdout.split('\n'):
|
||||
if line.strip():
|
||||
log_info(f" {line}")
|
||||
log_info("启用站点配置完成")
|
||||
return True
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 `deploy_nginx`,替换链式命令为函数调用**
|
||||
|
||||
将 `deploy.py` 第 249-256 行的:
|
||||
|
||||
```python
|
||||
log_info("启用站点配置...")
|
||||
ok, _, err = ssh_command(
|
||||
f"ln -snf {remote_conf} /etc/nginx/sites-enabled/{DOMAIN}.conf "
|
||||
f"&& rm -f /etc/nginx/sites-enabled/default"
|
||||
)
|
||||
if not ok:
|
||||
log_error(f"启用站点失败: {err}")
|
||||
return False
|
||||
```
|
||||
|
||||
替换为:
|
||||
|
||||
```python
|
||||
log_info("启用站点配置...")
|
||||
if not enable_nginx_site(remote_conf, DOMAIN):
|
||||
log_error("启用站点失败(详见 enable_nginx_site 内部日志)")
|
||||
return False
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 验证 Python 语法**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"`
|
||||
Expected: 退出码 0
|
||||
|
||||
- [ ] **Step 4: 验证函数可正常导入**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "from deploy import enable_nginx_site, _validate_nginx_inputs; print('import OK')"`
|
||||
Expected: 输出 `import OK`
|
||||
|
||||
- [ ] **Step 5: 验证函数签名和文档**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "
|
||||
from deploy import enable_nginx_site
|
||||
import inspect
|
||||
sig = inspect.signature(enable_nginx_site)
|
||||
print('Signature:', sig)
|
||||
print('Docstring (first line):', enable_nginx_site.__doc__.split(chr(10))[0])
|
||||
"`
|
||||
Expected:
|
||||
```
|
||||
Signature: (remote_conf: str, domain: str) -> bool
|
||||
Docstring (first line): 幂等地启用 nginx 站点(sites-enabled symlink)。
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd G:\IdeaProjects\emotion-museun
|
||||
git add deploy.py
|
||||
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 实现 enable_nginx_site 步骤 5(验证)+ 集成到 deploy_nginx"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 端到端验证(CLAUDE.md 强制规则)
|
||||
|
||||
**Files:**
|
||||
- 无文件修改
|
||||
|
||||
- [ ] **Step 1: SSH 连接预检**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python deploy.py verify 2>&1 | head -30`
|
||||
Expected: SSH 连接成功(看到 `[INFO] SSH 连接正常`)
|
||||
|
||||
若失败:
|
||||
- 检查 `~/.ssh/config` 是否有 `101.200.208.45` 免密配置
|
||||
- 手动 `ssh root@101.200.208.45 echo ok` 测试
|
||||
|
||||
- [ ] **Step 2: 执行 nginx 部署**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python deploy.py nginx 2>&1 | tee C:\Users\peanu\AppData\Local\Temp\opencode\nginx-deploy-validation.txt`
|
||||
Expected: 完整流程成功,无 30s 超时;看到:
|
||||
```
|
||||
[INFO] 部署 Nginx 配置
|
||||
[INFO] 上传 Nginx 配置文件...
|
||||
[INFO] 启用站点配置...
|
||||
[INFO] 启用 nginx 站点: /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf -> /etc/nginx/sites-available/lifescript.happylifeos.com.conf
|
||||
[INFO] 诊断步骤: 检查目标路径...
|
||||
[INFO] 诊断结果: <symlink|dir|file|missing>
|
||||
[INFO] 清理步骤: 处理异常状态...
|
||||
[INFO] 清理步骤完成
|
||||
[INFO] 建链步骤: 创建 symlink...
|
||||
[INFO] 建链步骤完成
|
||||
[INFO] 清默认步骤: 移除 default 站点...
|
||||
[INFO] 清默认步骤完成
|
||||
[INFO] 验证步骤: 检查 symlink 和 default 状态...
|
||||
[INFO] 验证结果:
|
||||
[INFO] <readlink 输出>
|
||||
[INFO] default:OK
|
||||
[INFO] <ls -la 输出>
|
||||
[INFO] 启用站点配置完成
|
||||
[INFO] 验证 Nginx 配置...
|
||||
[INFO] Nginx 配置重载完成
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 验证远程 nginx 状态**
|
||||
|
||||
Run: `cd G:\IdeaProjects\emotion-museun && python -c "from deploy import ssh_command; print(ssh_command('ls -la /etc/nginx/sites-enabled/', timeout=15))"`
|
||||
Expected: 输出 `/etc/nginx/sites-enabled/` 目录列表,包含有效 symlink(如 `lifescript.happylifeos.com.conf -> /etc/nginx/sites-available/lifescript.happylifeos.com.conf`),**不包含** `default`
|
||||
|
||||
- [ ] **Step 4: 验证 nginx 服务可访问**
|
||||
|
||||
Run: `curl -I https://lifescript.happylifeos.com/ 2>&1 | head -5`
|
||||
Expected: HTTP 200 或 301/302(站点可达)
|
||||
|
||||
- [ ] **Step 5: Commit 验证记录**
|
||||
|
||||
```bash
|
||||
cd G:\IdeaProjects\emotion-museun
|
||||
git add -A
|
||||
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "test: 端到端验证 nginx 部署成功(见 opencode temp 验证日志)" --allow-empty
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证场景(设计文档要求)
|
||||
|
||||
以下场景可在 Task 5 验证后**可选**执行(手动复现异常状态):
|
||||
|
||||
### 场景 A: 断链场景
|
||||
```bash
|
||||
ssh root@101.200.208.45 "ln -s /nonexistent /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
|
||||
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
|
||||
```
|
||||
Expected: 诊断结果 = `symlink`,步骤 2 用 `unlink` 清理,步骤 3 重建成功
|
||||
|
||||
### 场景 B: 目录场景
|
||||
```bash
|
||||
ssh root@101.200.208.45 "rm -f /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && mkdir /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
|
||||
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
|
||||
```
|
||||
Expected: 诊断结果 = `dir`,步骤 2 用 `rm -rf` 清理,步骤 3 重建成功
|
||||
|
||||
### 场景 C: 干净场景
|
||||
```bash
|
||||
ssh root@101.200.208.45 "rm -f /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
|
||||
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
|
||||
```
|
||||
Expected: 诊断结果 = `missing`,跳过清理,步骤 3 直接建链
|
||||
|
||||
### 场景 D: 正常 symlink 场景(幂等性)
|
||||
```bash
|
||||
ssh root@101.200.208.45 "ln -snf /etc/nginx/sites-available/lifescript.happylifeos.com.conf /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
|
||||
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
|
||||
```
|
||||
Expected: 诊断结果 = `symlink`,步骤 2 用 `unlink`,步骤 3 重建(**幂等**:重复执行结果相同)
|
||||
|
||||
---
|
||||
|
||||
## 回滚
|
||||
|
||||
如新代码有问题:
|
||||
```bash
|
||||
cd G:\IdeaProjects\emotion-museun
|
||||
git log --oneline deploy.py # 找到本次 4 个 commit
|
||||
git revert <commit-hash>..HEAD # 批量回滚
|
||||
# 或:
|
||||
git checkout HEAD~4 -- deploy.py # 恢复到本次修改前
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 风险提醒
|
||||
|
||||
1. **首次部署若远程 nginx 配置异常**:清理步骤可能误删用户手动创建的目录 → 建议首次部署前先 SSH 检查 `/etc/nginx/sites-enabled/` 状态
|
||||
2. **SSH 免密登录失效**:所有 ssh_command 调用都会失败,Task 5 Step 1 会捕获
|
||||
3. **网络慢导致 30s 超时**:单步 30s 对常规操作足够;如遇网络问题可临时改为 `timeout=60`(在调用处覆盖)
|
||||
|
||||
---
|
||||
|
||||
## 后续可选增强(不在本次范围)
|
||||
|
||||
- `disable_nginx_site()` 反向操作
|
||||
- 提取到独立模块 `deploy_nginx_helpers.py`
|
||||
- 增加 `--dry-run` 模式
|
||||
- 步骤级重试机制
|
||||
@@ -0,0 +1,167 @@
|
||||
# 登录路由跳转修复 Implementation Plan
|
||||
|
||||
> **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:** 修复小程序登录成功后错误进入编辑资料页的问题,确保登录后统一进入爽文生成页面。
|
||||
|
||||
**Architecture:** 移除登录流程中的 `hasProfile` 分支判断,将 `login/index.vue` 和 `splash/index.vue` 中的登录后路由固定指向 `/pages/main/index?tab=script`。编辑资料页面保留,仅通过"我的" tab 主动进入。
|
||||
|
||||
**Tech Stack:** Vue 3, uni-app, JavaScript
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 修复 login/index.vue 登录后跳转逻辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/login/index.vue:152-158`
|
||||
- Modify: `mini-program/src/pages/login/index.vue:187`
|
||||
- Modify: `mini-program/src/pages/login/index.vue:209`
|
||||
|
||||
**Context:** 当前 `routeAfterLogin` 函数根据 `profileReady` 参数分支:有资料去爽文生成页,无资料去编辑资料页。需要移除该判断,统一跳转至爽文生成页。
|
||||
|
||||
- [ ] **Step 1: 修改 `routeAfterLogin` 函数,移除 `profileReady` 参数和条件分支**
|
||||
|
||||
将:
|
||||
```javascript
|
||||
const routeAfterLogin = (profileReady) => {
|
||||
if (profileReady) {
|
||||
uni.redirectTo({ url: '/pages/main/index?tab=script' })
|
||||
} else {
|
||||
uni.redirectTo({ url: '/pages/onboarding/index' })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
改为:
|
||||
```javascript
|
||||
const routeAfterLogin = () => {
|
||||
uni.redirectTo({ url: '/pages/main/index?tab=script' })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 `handleWechatLogin` 中的调用,移除参数**
|
||||
|
||||
将:
|
||||
```javascript
|
||||
routeAfterLogin(result.hasProfile)
|
||||
```
|
||||
|
||||
改为:
|
||||
```javascript
|
||||
routeAfterLogin()
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 修改 `handleLogin` 中的调用,移除参数**
|
||||
|
||||
将:
|
||||
```javascript
|
||||
routeAfterLogin(result.hasProfile)
|
||||
```
|
||||
|
||||
改为:
|
||||
```javascript
|
||||
routeAfterLogin()
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/login/index.vue
|
||||
git commit -m "fix(login): remove profile check, always redirect to script page after login"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 修复 splash/index.vue 会话恢复后的跳转逻辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/splash/index.vue:54-56`
|
||||
|
||||
**Context:** `resolveInitialRoute` 在检测到已登录用户时,会根据 `session.hasProfile` 决定跳转到爽文生成页或编辑资料页。需要移除该分支。
|
||||
|
||||
- [ ] **Step 1: 修改已登录用户的跳转目标**
|
||||
|
||||
将:
|
||||
```javascript
|
||||
if (session.status === store.SESSION_STATUS.AUTHENTICATED) {
|
||||
const target = session.hasProfile ? '/pages/main/index?tab=script' : '/pages/onboarding/index'
|
||||
routeOnce(target, { reason: session.reason, hasProfile: session.hasProfile })
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
改为:
|
||||
```javascript
|
||||
if (session.status === store.SESSION_STATUS.AUTHENTICATED) {
|
||||
routeOnce('/pages/main/index?tab=script', { reason: session.reason })
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/splash/index.vue
|
||||
git commit -m "fix(splash): remove profile check on session restore, always go to script page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 验证修复
|
||||
|
||||
**Files:**
|
||||
- Read: `mini-program/src/pages/login/index.vue`
|
||||
- Read: `mini-program/src/pages/splash/index.vue`
|
||||
|
||||
- [ ] **Step 1: 验证 login/index.vue 中无残留的分支逻辑**
|
||||
|
||||
运行:
|
||||
```bash
|
||||
grep -n "onboarding" mini-program/src/pages/login/index.vue
|
||||
```
|
||||
|
||||
预期:无输出(`onboarding` 不再被 `login/index.vue` 引用)
|
||||
|
||||
- [ ] **Step 2: 验证 splash/index.vue 中无残留的分支逻辑**
|
||||
|
||||
运行:
|
||||
```bash
|
||||
grep -n "onboarding" mini-program/src/pages/splash/index.vue
|
||||
```
|
||||
|
||||
预期:无输出(`onboarding` 不再被 `splash/index.vue` 引用)
|
||||
|
||||
- [ ] **Step 3: 确认 `onboarding` 页面本身未被修改**
|
||||
|
||||
运行:
|
||||
```bash
|
||||
git diff --name-only
|
||||
```
|
||||
|
||||
预期:输出不包含 `mini-program/src/pages/onboarding/index.vue`
|
||||
|
||||
- [ ] **Step 4: 最终提交(如需要)**
|
||||
|
||||
```bash
|
||||
git log --oneline -3
|
||||
```
|
||||
|
||||
预期:显示两次提交,分别为 login 和 splash 的修复。
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
**1. Spec coverage:**
|
||||
- ✅ `login/index.vue` 的 `routeAfterLogin` 及两处调用 — Task 1
|
||||
- ✅ `splash/index.vue` 的会话恢复路由分支 — Task 2
|
||||
- ✅ `hasProfile` 和 `onboarding` 页面保留不动 — 确认无相关修改
|
||||
- ✅ 测试验证点 — Task 3
|
||||
|
||||
**2. Placeholder scan:**
|
||||
- ✅ 无 "TBD"/"TODO" 等占位符
|
||||
- ✅ 每步均含完整代码和命令
|
||||
- ✅ 无 "类似 Task N" 的引用
|
||||
|
||||
**3. Type consistency:**
|
||||
- ✅ 文件路径和函数名与 spec 及代码库一致
|
||||
@@ -0,0 +1,241 @@
|
||||
# 个人中心页面动态数据修复 Implementation Plan
|
||||
|
||||
> **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:** 修复"我的"页面显示写死信息的问题,改为从 store 动态读取用户头像、昵称、身份标签和统计数据。
|
||||
|
||||
**Architecture:** 仅修改 `MineView.vue` 一个文件。复用 `main/index.vue` 已加载的 store 数据(`userProfile` 和 `events`),通过新增 computed 属性生成头像 URL、觉醒深度等级和星历契合百分比,替换模板中的硬编码值。
|
||||
|
||||
**Tech Stack:** Vue 3, uni-app, JavaScript
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 替换头像为动态生成头像
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/MineView.vue:12-19`(模板)
|
||||
- Modify: `mini-program/src/pages/main/MineView.vue:64-78`(script)
|
||||
|
||||
**Context:** 当前头像区域用 `.person-head` 和 `.person-body` 两个 div 画了一个通用人形。需要替换为 `<image>` 组件显示 Dicebear 根据昵称生成的头像。
|
||||
|
||||
- [ ] **Step 1: 在 template 中替换 CSS 人形为 image 组件**
|
||||
|
||||
将:
|
||||
```html
|
||||
<view class="avatar-circle">
|
||||
<view class="person-head"></view>
|
||||
<view class="person-body"></view>
|
||||
</view>
|
||||
```
|
||||
|
||||
改为:
|
||||
```html
|
||||
<view class="avatar-circle">
|
||||
<image class="avatar-img" :src="avatarUrl" mode="aspectFill" />
|
||||
</view>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 在 script 中新增 `avatarUrl` computed**
|
||||
|
||||
在 `profile` computed 之后、`displayName` 之前添加:
|
||||
|
||||
```javascript
|
||||
const avatarUrl = computed(() => {
|
||||
const seed = profile.value.nickname || 'user'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(seed)}&backgroundColor=b982ff`
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 给 avatar-img 添加样式(在 style 区块末尾追加)**
|
||||
|
||||
```css
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/MineView.vue && git commit -m "fix(mine): replace static avatar with dynamic dicebear image"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 替换写死的用户信息与统计数据
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/MineView.vue:71-78`(displayName + metaLine)
|
||||
- Modify: `mini-program/src/pages/main/MineView.vue:24-33`(stats-grid 模板)
|
||||
|
||||
**Context:** `displayName` fallback 为 `'张义明'`,`metaLine` fallback 为 `'ENFJ · 狮子座 · 星民'`,统计数值 `Lv.4` 和 `98%` 硬编码。
|
||||
|
||||
- [ ] **Step 1: 修改 `displayName` 的 fallback**
|
||||
|
||||
将:
|
||||
```javascript
|
||||
const displayName = computed(() => profile.value.nickname || '张义明')
|
||||
```
|
||||
|
||||
改为:
|
||||
```javascript
|
||||
const displayName = computed(() => profile.value.nickname || '未设置昵称')
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 `metaLine` computed**
|
||||
|
||||
将:
|
||||
```javascript
|
||||
const metaLine = computed(() => {
|
||||
const mbti = profile.value.mbti || 'ENFJ'
|
||||
const zodiac = profile.value.zodiac || '狮子座'
|
||||
const identity = profile.value.profession || '星民'
|
||||
return `${mbti} · ${zodiac} · ${identity}`
|
||||
})
|
||||
```
|
||||
|
||||
改为:
|
||||
```javascript
|
||||
const metaLine = computed(() => {
|
||||
const mbti = profile.value.mbti
|
||||
const zodiac = profile.value.zodiac
|
||||
const identity = profile.value.profession
|
||||
if (!mbti && !zodiac && !identity) {
|
||||
return '点击设置个人档案'
|
||||
}
|
||||
return `${mbti || ''} · ${zodiac || ''} · ${identity || ''}`.replace(/(^ · | · $)/g, '').replace(/ · · /g, ' · ')
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 新增 `awakenLevel` computed**
|
||||
|
||||
在 `metaLine` 之后添加:
|
||||
|
||||
```javascript
|
||||
const awakenLevel = computed(() => {
|
||||
const count = store.events?.length || 0
|
||||
if (count >= 10) return 'Lv.5'
|
||||
if (count >= 6) return 'Lv.4'
|
||||
if (count >= 3) return 'Lv.3'
|
||||
if (count >= 1) return 'Lv.2'
|
||||
return 'Lv.1'
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 新增 `harmonyPercent` computed**
|
||||
|
||||
在 `awakenLevel` 之后添加:
|
||||
|
||||
```javascript
|
||||
const harmonyPercent = computed(() => {
|
||||
const hasLifeStory = profile.value.childhood?.text ||
|
||||
profile.value.joy?.text ||
|
||||
profile.value.low?.text ||
|
||||
profile.value.future?.ideal
|
||||
const fields = [
|
||||
profile.value.nickname,
|
||||
profile.value.gender,
|
||||
profile.value.zodiac,
|
||||
profile.value.mbti,
|
||||
profile.value.profession,
|
||||
profile.value.hobbies?.length,
|
||||
hasLifeStory
|
||||
]
|
||||
const filled = fields.filter(Boolean).length
|
||||
return Math.round((filled / 7) * 100) + '%'
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 替换模板中的硬编码统计值**
|
||||
|
||||
将:
|
||||
```html
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-label">觉醒深度</text>
|
||||
<text class="stat-value">Lv.4</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-label">星历契合</text>
|
||||
<text class="stat-value">98%</text>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
改为:
|
||||
```html
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-label">觉醒深度</text>
|
||||
<text class="stat-value">{{ awakenLevel }}</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-label">星历契合</text>
|
||||
<text class="stat-value">{{ harmonyPercent }}</text>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/MineView.vue && git commit -m "fix(mine): replace hardcoded user info and stats with dynamic store data"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 验证修复
|
||||
|
||||
**Files:**
|
||||
- Read: `mini-program/src/pages/main/MineView.vue`
|
||||
|
||||
- [ ] **Step 1: 确认模板中无硬编码用户信息**
|
||||
|
||||
运行:
|
||||
```bash
|
||||
grep -n "张义明\|ENFJ\|狮子座\|星民\|Lv\.4\|98%" mini-program/src/pages/main/MineView.vue
|
||||
```
|
||||
|
||||
预期:无输出(所有硬编码值已被替换)
|
||||
|
||||
- [ ] **Step 2: 确认 `<image>` 和 computed 属性存在**
|
||||
|
||||
运行:
|
||||
```bash
|
||||
grep -n "avatarUrl\|awakenLevel\|harmonyPercent" mini-program/src/pages/main/MineView.vue
|
||||
```
|
||||
|
||||
预期:输出包含 `avatarUrl`、`awakenLevel`、`harmonyPercent` 的定义和引用
|
||||
|
||||
- [ ] **Step 3: 查看提交记录**
|
||||
|
||||
运行:
|
||||
```bash
|
||||
git log --oneline -3
|
||||
```
|
||||
|
||||
预期:显示两次提交,分别为头像修复和统计数据修复
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
**1. Spec coverage:**
|
||||
- ✅ 头像替换为 Dicebear 动态生成 — Task 1
|
||||
- ✅ displayName fallback 改为 `'未设置昵称'` — Task 2 Step 1
|
||||
- ✅ metaLine 无资料时显示 `'点击设置个人档案'` — Task 2 Step 2
|
||||
- ✅ 觉醒深度基于 `store.events.length` — Task 2 Step 3
|
||||
- ✅ 星历契合基于资料完整度 — Task 2 Step 4
|
||||
- ✅ 模板硬编码值替换 — Task 2 Step 5
|
||||
- ✅ 验证步骤 — Task 3
|
||||
|
||||
**2. Placeholder scan:**
|
||||
- ✅ 无 "TBD"/"TODO" 等占位符
|
||||
- ✅ 每步均含完整代码和命令
|
||||
- ✅ 无 "类似 Task N" 的引用
|
||||
|
||||
**3. Type consistency:**
|
||||
- ✅ 字段名(`nickname`、`mbti`、`zodiac`、`profession`、`events`、`hobbies`、`childhood`、`joy`、`low`、`future`)与 store 和 spec 一致
|
||||
- ✅ `avatarUrl` 生成逻辑与 onboarding 中一致
|
||||
@@ -0,0 +1,692 @@
|
||||
# 小程序用户信息去写死 + 编辑资料保存生效 — Implementation Plan
|
||||
|
||||
> **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:** 去除小程序人生轨迹页和编辑资料页的所有硬编码用户信息,扩展后端 `t_user_profile` 表支持 `city`/`industry`/`company`/`personalityTags`/`birthday` 字段,确保编辑资料能完整保存和回显。
|
||||
|
||||
**Architecture:** 后端通过数据库 ALTER + Java 实体/DTO 扩展新增5个字段;前端同步扩展 Service 转换函数和 Store 数据结构,并移除两个页面中所有 `|| '硬编码默认值'` 回退和示例事件数据。
|
||||
|
||||
**Tech Stack:** Java 17 / Spring Boot / MyBatis-Plus / MySQL;uni-app (Vue 3) / JavaScript
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 文件 | 改动 | 职责 |
|
||||
|------|------|------|
|
||||
| `sql/emotion_museum_ddl.sql` | 追加 | 数据库迁移:新增5列 |
|
||||
| `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 |
|
||||
| `mini-program/src/pages/onboarding/index.vue` | 修改 | 去除硬编码默认值,空值友好展示 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 数据库迁移 — 新增5个字段
|
||||
|
||||
**Files:**
|
||||
- Modify: `sql/emotion_museum_ddl.sql`
|
||||
|
||||
- [ ] **Step 1: 追加 ALTER TABLE 语句**
|
||||
|
||||
在 `sql/emotion_museum_ddl.sql` 文件末尾追加:
|
||||
|
||||
```sql
|
||||
-- 用户档案扩展字段:城市、行业、公司、性格标签、生日
|
||||
alter table emotion_museum.t_user_profile
|
||||
add city varchar(100) null comment '所在城市',
|
||||
add industry varchar(100) null comment '行业',
|
||||
add company varchar(200) null comment '公司',
|
||||
add personality_tags json null comment '性格标签列表',
|
||||
add birthday date null comment '生日';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add sql/emotion_museum_ddl.sql
|
||||
git commit -m "db: add city, industry, company, personality_tags, birthday to t_user_profile"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 后端实体扩展 — UserProfile.java
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/main/java/com/emotion/entity/UserProfile.java`
|
||||
|
||||
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
|
||||
|
||||
找到 `idealLife` 字段定义(约第115行),在其后、`scripts` 字段前插入:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 所在城市
|
||||
*/
|
||||
@TableField("city")
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 行业
|
||||
*/
|
||||
@TableField("industry")
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 公司
|
||||
*/
|
||||
@TableField("company")
|
||||
private String company;
|
||||
|
||||
/**
|
||||
* 性格标签 (JSON字符串)
|
||||
*/
|
||||
@TableField("personality_tags")
|
||||
private String personalityTags;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
@TableField("birthday")
|
||||
private LocalDate birthday;
|
||||
```
|
||||
|
||||
注意:`LocalDate` 的 import 已在文件顶部存在(第12行),无需新增 import。
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/main/java/com/emotion/entity/UserProfile.java
|
||||
git commit -m "feat: extend UserProfile entity with city/industry/company/personalityTags/birthday"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 后端创建请求 DTO 扩展 — UserProfileCreateRequest.java
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
|
||||
|
||||
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
|
||||
|
||||
找到 `idealLife` 字段定义(约第86行),在其后、`scripts` 字段前插入:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 所在城市
|
||||
*/
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 行业
|
||||
*/
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 公司
|
||||
*/
|
||||
private String company;
|
||||
|
||||
/**
|
||||
* 性格标签 (JSON字符串)
|
||||
*/
|
||||
private String personalityTags;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
private LocalDate birthday;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 后端更新请求 DTO 扩展 — UserProfileUpdateRequest.java
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
|
||||
|
||||
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
|
||||
|
||||
找到 `idealLife` 字段定义(约第90行),在其后、`scripts` 字段前插入:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 所在城市
|
||||
*/
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 行业
|
||||
*/
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 公司
|
||||
*/
|
||||
private String company;
|
||||
|
||||
/**
|
||||
* 性格标签 (JSON字符串)
|
||||
*/
|
||||
private String personalityTags;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
private LocalDate birthday;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 后端响应 DTO 扩展 — UserProfileResponse.java
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
|
||||
|
||||
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
|
||||
|
||||
找到 `idealLife` 字段定义(约第105行),在其后、`scripts` 字段前插入:
|
||||
|
||||
```java
|
||||
/**
|
||||
* 所在城市
|
||||
*/
|
||||
private String city;
|
||||
|
||||
/**
|
||||
* 行业
|
||||
*/
|
||||
private String industry;
|
||||
|
||||
/**
|
||||
* 公司
|
||||
*/
|
||||
private String company;
|
||||
|
||||
/**
|
||||
* 性格标签 (JSON字符串)
|
||||
*/
|
||||
private String personalityTags;
|
||||
|
||||
/**
|
||||
* 生日
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private LocalDate birthday;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 前端 Service 扩展 — userProfile.js
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/services/userProfile.js`
|
||||
|
||||
- [ ] **Step 1: 扩展 transformToBackendFormat 的解构和返回对象**
|
||||
|
||||
原解构(第47-59行)新增5个字段:
|
||||
|
||||
```js
|
||||
const {
|
||||
id,
|
||||
nickname,
|
||||
gender,
|
||||
zodiac,
|
||||
profession,
|
||||
mbti,
|
||||
hobbies,
|
||||
childhood,
|
||||
joy,
|
||||
low,
|
||||
future,
|
||||
city,
|
||||
industry,
|
||||
company,
|
||||
personalityTags,
|
||||
birthday
|
||||
} = frontendData
|
||||
```
|
||||
|
||||
原返回对象(第61-83行)在 `idealLife` 后新增:
|
||||
|
||||
```js
|
||||
return {
|
||||
id,
|
||||
nickname,
|
||||
gender,
|
||||
zodiac,
|
||||
profession,
|
||||
mbti,
|
||||
// 兴趣爱好转为JSON字符串
|
||||
hobbies: Array.isArray(hobbies) ? JSON.stringify(hobbies) : hobbies,
|
||||
// 童年经历
|
||||
childhoodDate: childhood?.date || null,
|
||||
childhoodContent: childhood?.text || null,
|
||||
// 高光时刻(对应前端的joy)
|
||||
peakDate: joy?.date || null,
|
||||
peakContent: joy?.text || null,
|
||||
// 低谷时期(对应前端的low)
|
||||
valleyDate: low?.date || null,
|
||||
valleyContent: low?.text || null,
|
||||
// 未来期许
|
||||
futureVision: future?.vision || null,
|
||||
// 理想生活状态
|
||||
idealLife: future?.ideal || null,
|
||||
// 新增字段
|
||||
city: city || null,
|
||||
industry: industry || null,
|
||||
company: company || null,
|
||||
personalityTags: Array.isArray(personalityTags) ? JSON.stringify(personalityTags) : personalityTags,
|
||||
birthday: birthday || null
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 扩展 transformToFrontendFormat 的解构和返回对象**
|
||||
|
||||
原解构(第94-111行)新增5个字段:
|
||||
|
||||
```js
|
||||
const {
|
||||
id,
|
||||
userId,
|
||||
nickname,
|
||||
gender,
|
||||
zodiac,
|
||||
profession,
|
||||
mbti,
|
||||
hobbies,
|
||||
childhoodDate,
|
||||
childhoodContent,
|
||||
peakDate,
|
||||
peakContent,
|
||||
valleyDate,
|
||||
valleyContent,
|
||||
futureVision,
|
||||
idealLife,
|
||||
city,
|
||||
industry,
|
||||
company,
|
||||
personalityTags,
|
||||
birthday
|
||||
} = backendData
|
||||
```
|
||||
|
||||
原返回对象(第113-143行)在 `future` 对象后新增:
|
||||
|
||||
```js
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
nickname: nickname || '',
|
||||
gender: gender || '',
|
||||
zodiac: zodiac || '',
|
||||
profession: profession || '',
|
||||
mbti: mbti || '',
|
||||
// 兴趣爱好从JSON字符串解析
|
||||
hobbies: hobbies ? (typeof hobbies === 'string' ? JSON.parse(hobbies) : hobbies) : [],
|
||||
// 童年经历
|
||||
childhood: {
|
||||
date: childhoodDate || '',
|
||||
text: childhoodContent || ''
|
||||
},
|
||||
// 高光时刻
|
||||
joy: {
|
||||
date: peakDate || '',
|
||||
text: peakContent || ''
|
||||
},
|
||||
// 低谷时期
|
||||
low: {
|
||||
date: valleyDate || '',
|
||||
text: valleyContent || ''
|
||||
},
|
||||
// 未来期许
|
||||
future: {
|
||||
vision: futureVision || '',
|
||||
ideal: idealLife || ''
|
||||
},
|
||||
// 新增字段
|
||||
city: city || '',
|
||||
industry: industry || '',
|
||||
company: company || '',
|
||||
personalityTags: personalityTags ? (typeof personalityTags === 'string' ? JSON.parse(personalityTags) : personalityTags) : [],
|
||||
birthday: birthday || ''
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/services/userProfile.js
|
||||
git commit -m "feat: add city/industry/company/personalityTags/birthday to userProfile transforms"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 前端 Store 扩展 — app.js
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/stores/app.js`
|
||||
|
||||
- [ ] **Step 1: 扩展 registrationData 初始结构**
|
||||
|
||||
找到 `registrationData` 定义(第19-30行),在 `future` 对象后新增:
|
||||
|
||||
```js
|
||||
registrationData: {
|
||||
nickname: '',
|
||||
gender: '',
|
||||
mbti: '',
|
||||
zodiac: '',
|
||||
profession: '',
|
||||
hobbies: [],
|
||||
childhood: { date: '', text: '' },
|
||||
joy: { date: '', text: '' },
|
||||
low: { date: '', text: '' },
|
||||
future: { vision: '', ideal: '' },
|
||||
city: '',
|
||||
industry: '',
|
||||
company: '',
|
||||
personalityTags: [],
|
||||
birthday: ''
|
||||
}
|
||||
```
|
||||
|
||||
注意:此文件使用 CRLF 行尾(`\r\n`),使用 Edit 工具时请按 Read 工具输出的格式匹配。
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/stores/app.js
|
||||
git commit -m "feat: extend registrationData with city/industry/company/personalityTags/birthday"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 人生轨迹页去写死 — RecordView.vue
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/RecordView.vue`
|
||||
|
||||
- [ ] **Step 1: 删除 sampleEvents 数组**
|
||||
|
||||
删除第143-176行的 `sampleEvents` 数组定义。
|
||||
|
||||
- [ ] **Step 2: 修改 events computed 去除 sampleEvents 回退**
|
||||
|
||||
将第179-182行:
|
||||
```js
|
||||
const events = computed(() => {
|
||||
const list = store.events || []
|
||||
return list.length ? list : sampleEvents
|
||||
})
|
||||
```
|
||||
改为:
|
||||
```js
|
||||
const events = computed(() => store.events || [])
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 修改 heroTags computed 去除硬编码默认值**
|
||||
|
||||
将第183-187行:
|
||||
```js
|
||||
const heroTags = computed(() => {
|
||||
const hobbies = profile.value.hobbies
|
||||
if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4)
|
||||
return ['阅读', '旅行', '音乐', '创作']
|
||||
})
|
||||
```
|
||||
改为:
|
||||
```js
|
||||
const heroTags = computed(() => {
|
||||
const hobbies = profile.value.hobbies
|
||||
if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4)
|
||||
return []
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 修改 avatar computed 去除硬编码昵称**
|
||||
|
||||
将第189-192行:
|
||||
```js
|
||||
const avatar = computed(() => {
|
||||
const nickname = profile.value.nickname || 'Zoey'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
|
||||
})
|
||||
```
|
||||
改为:
|
||||
```js
|
||||
const avatar = computed(() => {
|
||||
const nickname = profile.value.nickname || 'User'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 修改模板中硬编码默认值**
|
||||
|
||||
将第13行:
|
||||
```vue
|
||||
<text class="profile-name">{{ profile.nickname || 'Zoey' }}</text>
|
||||
```
|
||||
改为:
|
||||
```vue
|
||||
<text class="profile-name">{{ profile.nickname || '未设置' }}</text>
|
||||
```
|
||||
|
||||
将第31行:
|
||||
```vue
|
||||
<text class="meta-value">{{ profile.zodiac || '巨蟹座' }}</text>
|
||||
```
|
||||
改为:
|
||||
```vue
|
||||
<text class="meta-value">{{ profile.zodiac || '未设置' }}</text>
|
||||
```
|
||||
|
||||
将第38行:
|
||||
```vue
|
||||
<text class="meta-value">{{ profile.mbti || 'ENTJ' }}</text>
|
||||
```
|
||||
改为:
|
||||
```vue
|
||||
<text class="meta-value">{{ profile.mbti || '未设置' }}</text>
|
||||
```
|
||||
|
||||
将第45行:
|
||||
```vue
|
||||
<text class="meta-value">{{ profile.profession || '产品经理' }}</text>
|
||||
```
|
||||
改为:
|
||||
```vue
|
||||
<text class="meta-value">{{ profile.profession || '未设置' }}</text>
|
||||
```
|
||||
|
||||
将第56行:
|
||||
```vue
|
||||
<text class="hobby-text">{{ heroTags.join(' · ') }}</text>
|
||||
```
|
||||
改为:
|
||||
```vue
|
||||
<text class="hobby-text">{{ heroTags.length ? heroTags.join(' · ') : '未设置' }}</text>
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/RecordView.vue
|
||||
git commit -m "fix: remove hardcoded defaults and sampleEvents from RecordView"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 编辑资料页去写死 — onboarding/index.vue
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/onboarding/index.vue`
|
||||
|
||||
- [ ] **Step 1: 修改 avatarUrl computed 去除硬编码昵称**
|
||||
|
||||
将第234-238行:
|
||||
```js
|
||||
const avatarUrl = computed(() => {
|
||||
if (avatarLocal.value) return avatarLocal.value
|
||||
const nickname = form.nickname || 'Zoey'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
|
||||
})
|
||||
```
|
||||
改为:
|
||||
```js
|
||||
const avatarUrl = computed(() => {
|
||||
if (avatarLocal.value) return avatarLocal.value
|
||||
const nickname = form.nickname || 'User'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 birthdayDisplay computed 去除硬编码日期**
|
||||
|
||||
将第240-244行:
|
||||
```js
|
||||
const birthdayDisplay = computed(() => {
|
||||
if (!birthday.value) return '1998年06月18日'
|
||||
const [year, month, day] = birthday.value.split('-')
|
||||
return `${year}年${month}月${day}日`
|
||||
})
|
||||
```
|
||||
改为:
|
||||
```js
|
||||
const birthdayDisplay = computed(() => {
|
||||
if (!birthday.value) return '请选择生日'
|
||||
const [year, month, day] = birthday.value.split('-')
|
||||
return `${year}年${month}月${day}日`
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 修改 syncFromStore 去除所有硬编码默认值**
|
||||
|
||||
将第246-265行的 `syncFromStore` 函数整体替换为:
|
||||
|
||||
```js
|
||||
const syncFromStore = () => {
|
||||
const source = store.userProfile || store.registrationData || {}
|
||||
Object.assign(form, {
|
||||
nickname: source.nickname || '',
|
||||
gender: source.gender || '',
|
||||
zodiac: source.zodiac || '',
|
||||
mbti: source.mbti || '',
|
||||
profession: source.profession || '',
|
||||
city: source.city || '',
|
||||
industry: source.industry || '',
|
||||
company: source.company || '',
|
||||
personalityTags: Array.isArray(source.personalityTags) ? [...source.personalityTags] : [],
|
||||
hobbies: Array.isArray(source.hobbies) ? [...source.hobbies] : [],
|
||||
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 || '' },
|
||||
future: { vision: source.future?.vision || '', ideal: source.future?.ideal || '' }
|
||||
})
|
||||
birthday.value = source.birthday || ''
|
||||
}
|
||||
```
|
||||
|
||||
注意:此文件使用 CRLF 行尾(`\r\n`),使用 Edit 工具时请按 Read 工具输出的格式匹配。
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/onboarding/index.vue
|
||||
git commit -m "fix: remove hardcoded defaults from profile edit page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 验证测试
|
||||
|
||||
**Files:**
|
||||
- 无需修改,纯验证
|
||||
|
||||
- [ ] **Step 1: 后端编译验证**
|
||||
|
||||
```bash
|
||||
cd server && mvn compile -q
|
||||
```
|
||||
|
||||
预期:编译成功,无错误。
|
||||
|
||||
- [ ] **Step 2: 前端语法检查(如有 linter)**
|
||||
|
||||
```bash
|
||||
cd mini-program && npm run lint 2>/dev/null || echo "No lint script, skipping"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 手动功能验证清单**
|
||||
|
||||
1. 启动后端服务,确认无启动错误。
|
||||
2. 在数据库执行 `DESCRIBE t_user_profile;`,确认 `city`、`industry`、`company`、`personality_tags`、`birthday` 五列已存在。
|
||||
3. 小程序登录后进入"人生轨迹"页:
|
||||
- 未设置资料时,昵称显示"未设置",星座/MBTI/职业显示"未设置",爱好显示"未设置"。
|
||||
- 无人生事件时,显示"还没有人生轨迹"空状态,无 sampleEvents。
|
||||
4. 点击"编辑资料":
|
||||
- 首次进入(无资料)时,所有输入框为空,生日显示"请选择生日",性格标签和爱好无选中项。
|
||||
- 填写所有字段(包括城市、行业、公司、性格标签、生日)后保存。
|
||||
- 保存成功后返回人生轨迹页,数据已更新。
|
||||
- 再次进入编辑资料页,所有字段正确回显。
|
||||
5. 已有用户(老数据,无新字段)登录后,页面正常展示,无报错,新字段显示为空。
|
||||
|
||||
- [ ] **Step 4: Commit(如测试通过)**
|
||||
|
||||
```bash
|
||||
git log --oneline -10
|
||||
```
|
||||
|
||||
预期看到 9 个 commits(Task 1-9 各一个)。
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
**1. Spec coverage:**
|
||||
- 数据库扩展5个字段 → Task 1 ✅
|
||||
- 后端实体扩展 → Task 2 ✅
|
||||
- 后端 DTO 扩展(Create/Update/Response)→ Task 3/4/5 ✅
|
||||
- 前端 Service 双向转换扩展 → Task 6 ✅
|
||||
- 前端 Store 扩展 → Task 7 ✅
|
||||
- 人生轨迹页去除写死数据和 sampleEvents → Task 8 ✅
|
||||
- 编辑资料页去除硬编码默认值 → Task 9 ✅
|
||||
- 测试验证 → Task 10 ✅
|
||||
|
||||
**2. Placeholder scan:**
|
||||
- 无 TBD/TODO/"implement later"/"fill in details" ✅
|
||||
- 无 "Add appropriate error handling" 等模糊描述 ✅
|
||||
- 无 "Similar to Task N" ✅
|
||||
- 每个代码步骤都包含完整代码 ✅
|
||||
|
||||
**3. Type consistency:**
|
||||
- 后端5个字段名在 Entity/CreateRequest/UpdateRequest/Response 中完全一致:`city`, `industry`, `company`, `personalityTags`, `birthday` ✅
|
||||
- 前端 Service 中 `transformToBackendFormat` 和 `transformToFrontendFormat` 字段名一致 ✅
|
||||
- Store 中 `registrationData` 字段名与 Service 一致 ✅
|
||||
- `personalityTags` 在前端始终作为数组处理,后端作为 JSON 字符串,转换逻辑与 `hobbies` 保持一致 ✅
|
||||
- `birthday` 前后端均为 `yyyy-MM-dd` 字符串格式 ✅
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,892 @@
|
||||
# Mini Program Life Event Detail Actions Implementation Plan
|
||||
|
||||
> **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:** 调整小程序人生轨迹详情页,把事件标题卡与事件描述卡合并,移除顶部悬浮操作 icon,并将收藏、分享、分析、编辑、收起和“聊聊”入口按新布局展示。
|
||||
|
||||
**Architecture:** 只修改 `mini-program/src/pages/life-event/detail.vue`。模板层用新的 `event-content-card` 替换 `floating-nav`、`hero-card`、`description-card` 组合;脚本层增加 `scrollTop`、`currentScrollTop`、`onContentScroll` 与 `scrollToAnalysis` 来支持分析按钮定位;样式层复用当前宇宙玻璃风格并新增局部按钮和 Markdown 折叠样式。
|
||||
|
||||
**Tech Stack:** UniApp, Vue 3 `<script setup>`, CSS scoped, existing `mini-program/src/components/Markdown.vue`, npm scripts from `mini-program/package.json`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `mini-program/src/pages/life-event/detail.vue`
|
||||
- Template: 合并事件标题区和事件描述区,迁移操作按钮,底部只保留居中的聊天主按钮。
|
||||
- Script: 删除 `useMenuButtonSafeArea` 使用,新增 `scrollTop`、`currentScrollTop`、`onContentScroll`、`scrollToAnalysis`,保留现有业务方法。
|
||||
- Style: 删除顶部悬浮导航样式,新增合并卡、正文 Markdown 折叠、内容区按钮和底部聊天按钮样式。
|
||||
|
||||
- Verify only: `mini-program/src/components/Markdown.vue`
|
||||
- 不改文件,仅复用 `<Markdown :content="eventDescription" />`。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Template Restructure
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/life-event/detail.vue`
|
||||
|
||||
- [ ] **Step 1: Remove the floating nav block**
|
||||
|
||||
Delete this block from the template:
|
||||
|
||||
```vue
|
||||
<view class="floating-nav" :style="floatingTopStyle">
|
||||
<view class="circle-btn" @click="goBack">
|
||||
<view class="back-icon"></view>
|
||||
</view>
|
||||
<view class="circle-btn" @click="openMore">
|
||||
<view class="more-dot"></view>
|
||||
<view class="more-dot"></view>
|
||||
<view class="more-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
Expected result: 页面左上角和右上角不再渲染悬浮 icon。
|
||||
|
||||
- [ ] **Step 2: Add scroll state binding to the page scroll-view**
|
||||
|
||||
Change the current scroll-view opening tag from:
|
||||
|
||||
```vue
|
||||
<scroll-view class="content" scroll-y :show-scrollbar="false">
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```vue
|
||||
<scroll-view
|
||||
class="content"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
:scroll-top="scrollTop"
|
||||
@scroll="onContentScroll"
|
||||
scroll-with-animation
|
||||
>
|
||||
```
|
||||
|
||||
Expected result: `scrollToAnalysis` can move the scroll-view to the AI 分析 section and account for the current scroll position.
|
||||
|
||||
- [ ] **Step 3: Replace hero-card and description-card with the merged event-content-card**
|
||||
|
||||
Replace the whole existing `hero-card` and `description-card` blocks with:
|
||||
|
||||
```vue
|
||||
<view class="event-content-card glass-card">
|
||||
<view class="event-hero-section">
|
||||
<view class="hero-timeline">
|
||||
<view class="timeline-glow"></view>
|
||||
<view class="timeline-node"></view>
|
||||
<view class="timeline-line"></view>
|
||||
</view>
|
||||
|
||||
<view class="hero-copy">
|
||||
<text class="year">{{ eventYear }}</text>
|
||||
<view class="date-row">
|
||||
<text class="date-range">{{ dateRange }}</text>
|
||||
<text class="type-chip">{{ primaryTag }}</text>
|
||||
</view>
|
||||
<text class="event-title">{{ displayEvent.title || '决定全职做自己热爱的AI产品' }}</text>
|
||||
<view class="location-row">
|
||||
<view class="pin-icon"></view>
|
||||
<text>{{ locationText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="memory-cover">
|
||||
<view class="cover-sky"></view>
|
||||
<view class="cover-window"></view>
|
||||
<view class="cover-desk"></view>
|
||||
<view class="cover-screen main"></view>
|
||||
<view class="cover-screen small left"></view>
|
||||
<view class="cover-screen small right"></view>
|
||||
</view>
|
||||
<view class="hero-star">★</view>
|
||||
</view>
|
||||
|
||||
<view class="event-body-section">
|
||||
<view class="event-body-toolbar">
|
||||
<view class="content-action favorite-action" @click="toggleFavorite">
|
||||
<view class="bookmark-icon" :class="{ active: isFavorite }"></view>
|
||||
<text>{{ isFavorite ? '已收藏' : '收藏' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="toolbar-right">
|
||||
<view class="analysis-pill" @click="scrollToAnalysis">
|
||||
<text>分析</text>
|
||||
</view>
|
||||
<view class="content-action share-action" @click="shareCurrent">
|
||||
<view class="share-icon">
|
||||
<view></view>
|
||||
<view></view>
|
||||
<view></view>
|
||||
</view>
|
||||
<text>分享</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title-row description-title-row">
|
||||
<view class="orbit-icon"></view>
|
||||
<text>事件描述</text>
|
||||
</view>
|
||||
|
||||
<view class="description-markdown" :class="{ collapsed: isDescriptionCollapsed }">
|
||||
<Markdown :content="eventDescription" />
|
||||
</view>
|
||||
|
||||
<view class="event-body-footer">
|
||||
<view class="content-action edit-action" @click="editEvent">
|
||||
<view class="edit-icon"></view>
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<text class="collapse-action" @click="isDescriptionCollapsed = !isDescriptionCollapsed">
|
||||
{{ isDescriptionCollapsed ? '展开' : '收起' }} ^
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
Expected result: 事件信息和事件描述在同一张卡内,正文通过 `Markdown` 组件渲染。
|
||||
|
||||
- [ ] **Step 4: Add the analysis anchor data attribute**
|
||||
|
||||
Change the current AI analysis opening block from:
|
||||
|
||||
```vue
|
||||
<view class="analysis-panel">
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```vue
|
||||
<view class="analysis-panel" id="analysisPanel">
|
||||
```
|
||||
|
||||
Expected result: `uni.createSelectorQuery()` can locate the AI 分析 block.
|
||||
|
||||
- [ ] **Step 5: Replace bottom action bar with centered chat action**
|
||||
|
||||
Replace the existing bottom action bar:
|
||||
|
||||
```vue
|
||||
<view class="bottom-actions" :style="{ paddingBottom: bottomInset + 'px' }">
|
||||
<view class="bottom-action" @click="editEvent">
|
||||
<view class="edit-icon"></view>
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="bottom-action" @click="toggleFavorite">
|
||||
<view class="bookmark-icon" :class="{ active: isFavorite }"></view>
|
||||
<text>{{ isFavorite ? '已收藏' : '收藏' }}</text>
|
||||
</view>
|
||||
<view class="chat-action" @click="chatEvent">
|
||||
<view class="chat-badge">AI</view>
|
||||
<text>和开开聊聊这段经历</text>
|
||||
</view>
|
||||
<view class="bottom-action" @click="shareCurrent">
|
||||
<view class="share-icon">
|
||||
<view></view>
|
||||
<view></view>
|
||||
<view></view>
|
||||
</view>
|
||||
<text>分享</text>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```vue
|
||||
<view class="bottom-actions" :style="{ paddingBottom: bottomInset + 'px' }">
|
||||
<view class="chat-action" @click="chatEvent">
|
||||
<view class="chat-badge">AI</view>
|
||||
<text>聊聊</text>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
Expected result: 底部只保留居中的紫色主按钮,文案为“聊聊”。
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Script State And Methods
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/life-event/detail.vue`
|
||||
|
||||
- [ ] **Step 1: Remove the unused safe-area composable import**
|
||||
|
||||
Change imports from:
|
||||
|
||||
```js
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import Markdown from '../../components/Markdown.vue'
|
||||
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
|
||||
import analytics from '../../services/analytics.js'
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```js
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import Markdown from '../../components/Markdown.vue'
|
||||
import analytics from '../../services/analytics.js'
|
||||
```
|
||||
|
||||
Expected result: 没有未使用的 `useMenuButtonSafeArea` import。
|
||||
|
||||
- [ ] **Step 2: Replace floating nav state with scroll state**
|
||||
|
||||
Change:
|
||||
|
||||
```js
|
||||
const store = useAppStore()
|
||||
const pagePath = '/pages/life-event/detail'
|
||||
const { floatingTopStyle } = useMenuButtonSafeArea()
|
||||
const safeAreaBottom = ref(0)
|
||||
const eventId = ref('')
|
||||
const cachedEvent = ref(null)
|
||||
const isFavorite = ref(false)
|
||||
const isDescriptionCollapsed = ref(false)
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```js
|
||||
const store = useAppStore()
|
||||
const pagePath = '/pages/life-event/detail'
|
||||
const safeAreaBottom = ref(0)
|
||||
const eventId = ref('')
|
||||
const cachedEvent = ref(null)
|
||||
const isFavorite = ref(false)
|
||||
const isDescriptionCollapsed = ref(false)
|
||||
const scrollTop = ref(0)
|
||||
const currentScrollTop = ref(0)
|
||||
```
|
||||
|
||||
Expected result: scroll-view has a reactive value available for animated positioning.
|
||||
|
||||
- [ ] **Step 3: Add scroll methods before editEvent**
|
||||
|
||||
Insert these methods before `const editEvent = () => {`:
|
||||
|
||||
```js
|
||||
const onContentScroll = (event) => {
|
||||
currentScrollTop.value = event.detail?.scrollTop || 0
|
||||
}
|
||||
|
||||
const scrollToAnalysis = () => {
|
||||
uni.createSelectorQuery()
|
||||
.select('#analysisPanel')
|
||||
.boundingClientRect((rect) => {
|
||||
if (!rect) return
|
||||
scrollTop.value = Math.max(0, currentScrollTop.value + rect.top - 24)
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: 点击“分析”后 scroll-view 会向 AI 分析卡移动。
|
||||
|
||||
- [ ] **Step 4: Keep existing business methods unchanged**
|
||||
|
||||
Do not change the bodies of these existing methods:
|
||||
|
||||
```js
|
||||
const editEvent = () => {
|
||||
if (!eventId.value) return
|
||||
uni.navigateTo({ url: `/pages/life-event/form?id=${eventId.value}` })
|
||||
}
|
||||
|
||||
const toggleFavorite = async () => {
|
||||
if (!eventId.value) return
|
||||
const next = !isFavorite.value
|
||||
const result = await store.favoriteEvent({ id: eventId.value, favorite: next })
|
||||
if (!result.success) {
|
||||
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
isFavorite.value = next
|
||||
if (next) uni.setStorageSync(`event_favorite_${eventId.value}`, '1')
|
||||
else uni.removeStorageSync(`event_favorite_${eventId.value}`)
|
||||
analytics.track('life_event_favorite', {
|
||||
life_event_id: eventId.value,
|
||||
favorite: next
|
||||
}, { eventType: 'life_event', pagePath })
|
||||
uni.showToast({ title: next ? '已收藏' : '已取消收藏', icon: 'success' })
|
||||
}
|
||||
|
||||
const chatEvent = async () => {
|
||||
const result = await store.chatAboutEvent({
|
||||
id: eventId.value,
|
||||
title: displayEvent.value.title,
|
||||
content: displayEvent.value.content
|
||||
})
|
||||
uni.showModal({
|
||||
title: '和开开聊聊',
|
||||
content: result.success ? result.data?.reply || '聊天能力已准备' : result.error || '聊天服务暂不可用',
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const shareCurrent = async () => {
|
||||
const result = await store.shareEvent({
|
||||
id: eventId.value,
|
||||
title: displayEvent.value.title,
|
||||
content: displayEvent.value.content
|
||||
})
|
||||
if (!result.success) {
|
||||
uni.showToast({ title: result.error || '分享失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
analytics.track('life_event_share', {
|
||||
life_event_id: eventId.value,
|
||||
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
|
||||
}, { eventType: 'life_event', pagePath })
|
||||
uni.setClipboardData({
|
||||
data: result.data?.shareText || `分享我的人生轨迹:${displayEvent.value.title || ''}`,
|
||||
success: () => uni.showToast({ title: '分享文案已复制', icon: 'success' })
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: 编辑、收藏、分享、聊天功能沿用现有行为。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Styles For Merged Card And Actions
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/life-event/detail.vue`
|
||||
|
||||
- [ ] **Step 1: Update z-index selector**
|
||||
|
||||
Change:
|
||||
|
||||
```css
|
||||
.floating-nav,
|
||||
.content,
|
||||
.bottom-actions {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```css
|
||||
.content,
|
||||
.bottom-actions {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: CSS no longer references removed `floating-nav`.
|
||||
|
||||
- [ ] **Step 2: Delete removed floating navigation styles**
|
||||
|
||||
Delete these style blocks:
|
||||
|
||||
```css
|
||||
.floating-nav {
|
||||
height: 108rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.circle-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5rpx;
|
||||
color: #fff;
|
||||
border: 1rpx solid rgba(118, 82, 225, 0.35);
|
||||
background: rgba(22, 18, 63, 0.72);
|
||||
box-shadow: inset 0 0 20rpx rgba(137, 91, 255, 0.1);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 23rpx;
|
||||
height: 23rpx;
|
||||
border-left: 6rpx solid #fff;
|
||||
border-bottom: 6rpx solid #fff;
|
||||
transform: rotate(45deg);
|
||||
margin-left: 7rpx;
|
||||
}
|
||||
|
||||
.more-dot {
|
||||
width: 7rpx;
|
||||
height: 7rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: no dead CSS for removed top icons.
|
||||
|
||||
- [ ] **Step 3: Adjust scroll content spacing**
|
||||
|
||||
Change:
|
||||
|
||||
```css
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0 32rpx 134rpx;
|
||||
}
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```css
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 36rpx 32rpx 154rpx;
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: merged card starts with breathing room after top safe area removal, and bottom content is not covered by the chat button.
|
||||
|
||||
- [ ] **Step 4: Replace hero-card styles with event-content-card and event-hero-section**
|
||||
|
||||
Replace:
|
||||
|
||||
```css
|
||||
.hero-card {
|
||||
position: relative;
|
||||
min-height: 264rpx;
|
||||
border-radius: 28rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 70rpx 1fr 148rpx;
|
||||
gap: 20rpx;
|
||||
padding: 38rpx 34rpx 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```css
|
||||
.event-content-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
|
||||
.event-hero-section {
|
||||
position: relative;
|
||||
min-height: 264rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 70rpx 1fr 148rpx;
|
||||
gap: 20rpx;
|
||||
padding: 40rpx 34rpx 34rpx;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(circle at 86% 12%, rgba(129, 76, 255, 0.22), transparent 36%),
|
||||
linear-gradient(145deg, rgba(6, 8, 35, 0.98) 0%, rgba(15, 13, 58, 0.96) 58%, rgba(23, 16, 72, 0.92) 100%);
|
||||
box-shadow: inset 0 -1rpx 0 rgba(164, 116, 255, 0.22), inset 0 0 42rpx rgba(63, 45, 148, 0.16);
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: top half of the merged card is visually deeper than the body area.
|
||||
|
||||
- [ ] **Step 5: Replace description-card styles with event body styles**
|
||||
|
||||
Replace:
|
||||
|
||||
```css
|
||||
.description-card,
|
||||
.tags-card {
|
||||
border-radius: 28rpx;
|
||||
margin-top: 18rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```css
|
||||
.event-body-section {
|
||||
position: relative;
|
||||
padding: 26rpx 30rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
background:
|
||||
radial-gradient(circle at 92% 0%, rgba(159, 98, 255, 0.12), transparent 36%),
|
||||
rgba(13, 16, 52, 0.62);
|
||||
}
|
||||
|
||||
.tags-card {
|
||||
border-radius: 28rpx;
|
||||
margin-top: 18rpx;
|
||||
padding: 28rpx 30rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: description body sits inside the merged card and related tags keep their card styling.
|
||||
|
||||
- [ ] **Step 6: Add content action toolbar styles before `.section-title-row`**
|
||||
|
||||
Insert:
|
||||
|
||||
```css
|
||||
.event-body-toolbar,
|
||||
.event-body-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.event-body-toolbar {
|
||||
min-height: 64rpx;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.content-action {
|
||||
min-height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
color: rgba(238, 231, 255, 0.88);
|
||||
font-size: 23rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.favorite-action,
|
||||
.share-action,
|
||||
.edit-action {
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.analysis-pill {
|
||||
min-width: 92rpx;
|
||||
height: 54rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0 18rpx 18rpx 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(231, 217, 255, 0.86);
|
||||
font-size: 23rpx;
|
||||
font-weight: 900;
|
||||
background: rgba(157, 104, 255, 0.18);
|
||||
border: 1rpx solid rgba(193, 154, 255, 0.24);
|
||||
box-shadow: inset 0 0 18rpx rgba(185, 124, 255, 0.08);
|
||||
}
|
||||
|
||||
.description-title-row {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: 收藏与分享同一水平线,分析按钮弱化并位于右上凹位。
|
||||
|
||||
- [ ] **Step 7: Replace description text styles with Markdown wrapper styles**
|
||||
|
||||
Replace:
|
||||
|
||||
```css
|
||||
.description-text {
|
||||
display: block;
|
||||
margin-top: 24rpx;
|
||||
color: rgba(227, 218, 246, 0.73);
|
||||
font-size: 25rpx;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.description-text.collapsed {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.collapse-action {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
text-align: right;
|
||||
color: #a970ff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```css
|
||||
.description-markdown {
|
||||
margin-top: 22rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.description-markdown.collapsed {
|
||||
max-height: 168rpx;
|
||||
}
|
||||
|
||||
.event-body-footer {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.collapse-action {
|
||||
min-height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: #a970ff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: Markdown content can collapse without changing `Markdown.vue`.
|
||||
|
||||
- [ ] **Step 8: Replace bottom action layout styles**
|
||||
|
||||
Replace the current `.bottom-actions`, `.bottom-action`, and `.chat-action` blocks:
|
||||
|
||||
```css
|
||||
.bottom-actions {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-height: 108rpx;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 0.76fr 0.76fr 2fr 0.76fr;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 14rpx 32rpx 0;
|
||||
border-radius: 36rpx 36rpx 0 0;
|
||||
border-top: 1rpx solid rgba(126, 87, 255, 0.32);
|
||||
background: rgba(13, 13, 43, 0.94);
|
||||
box-shadow: 0 -14rpx 44rpx rgba(0, 0, 0, 0.45), inset 0 0 28rpx rgba(143, 92, 255, 0.1);
|
||||
backdrop-filter: blur(28rpx);
|
||||
-webkit-backdrop-filter: blur(28rpx);
|
||||
}
|
||||
|
||||
.bottom-action,
|
||||
.chat-action {
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
color: rgba(238, 231, 255, 0.9);
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.bottom-action {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-action {
|
||||
border-radius: 999rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #b246ff, #742fff 58%, #5126ff);
|
||||
box-shadow: 0 0 28rpx rgba(168, 85, 247, 0.58), inset 0 1rpx 0 rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```css
|
||||
.bottom-actions {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-height: 108rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14rpx 32rpx 0;
|
||||
border-radius: 36rpx 36rpx 0 0;
|
||||
border-top: 1rpx solid rgba(126, 87, 255, 0.32);
|
||||
background: rgba(13, 13, 43, 0.94);
|
||||
box-shadow: 0 -14rpx 44rpx rgba(0, 0, 0, 0.45), inset 0 0 28rpx rgba(143, 92, 255, 0.1);
|
||||
backdrop-filter: blur(28rpx);
|
||||
-webkit-backdrop-filter: blur(28rpx);
|
||||
}
|
||||
|
||||
.chat-action {
|
||||
width: min(420rpx, 100%);
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, #b246ff, #742fff 58%, #5126ff);
|
||||
box-shadow: 0 0 28rpx rgba(168, 85, 247, 0.58), inset 0 1rpx 0 rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
```
|
||||
|
||||
Expected result: “聊聊” button is centered and visually primary.
|
||||
|
||||
- [ ] **Step 9: Confirm shared icon styles still work in new locations**
|
||||
|
||||
Keep the existing `.edit-icon`, `.bookmark-icon`, `.share-icon`, and `.chat-badge` blocks unchanged.
|
||||
|
||||
Expected result: content toolbar and footer reuse the existing drawn icons with no new asset dependency.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Build Verification
|
||||
|
||||
**Files:**
|
||||
- Verify: `mini-program/src/pages/life-event/detail.vue`
|
||||
- Verify: `mini-program/package.json`
|
||||
|
||||
- [ ] **Step 1: Run H5 production build**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd mini-program
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
Expected: command exits with code 0 and Vite/UniApp writes the H5 build output under `mini-program/dist/build/h5` or the configured UniApp output directory.
|
||||
|
||||
- [ ] **Step 2: Fix compile errors by editing only detail.vue**
|
||||
|
||||
If the build reports a compile error in `detail.vue`, fix the exact syntax issue in `mini-program/src/pages/life-event/detail.vue`, then rerun:
|
||||
|
||||
```bash
|
||||
cd mini-program
|
||||
npm run build:h5
|
||||
```
|
||||
|
||||
Expected: command exits with code 0.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Browser Verification
|
||||
|
||||
**Files:**
|
||||
- Verify: `mini-program/src/pages/life-event/detail.vue`
|
||||
|
||||
- [ ] **Step 1: Start H5 dev server**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd mini-program
|
||||
npm run dev:h5
|
||||
```
|
||||
|
||||
Expected: dev server prints a local URL, usually `http://localhost:5173`.
|
||||
|
||||
- [ ] **Step 2: Open the app in a browser**
|
||||
|
||||
Use the available browser automation tool to open:
|
||||
|
||||
```text
|
||||
http://localhost:5173
|
||||
```
|
||||
|
||||
Expected: app loads without a blank screen.
|
||||
|
||||
- [ ] **Step 3: Navigate to a life event detail page**
|
||||
|
||||
Use the app UI to reach 人生轨迹详情页. If the local dev data has no event entry, open a detail URL with the current route format after creating or selecting an event through the UI.
|
||||
|
||||
Expected: the detail page renders and uses local store/cached fallback data when available.
|
||||
|
||||
- [ ] **Step 4: Verify visual requirements**
|
||||
|
||||
Check these exact outcomes:
|
||||
|
||||
- The left-top edit icon and right-top more icon are absent.
|
||||
- Event title information and event description are inside one merged card.
|
||||
- The upper title area is visibly darker than the body area.
|
||||
- Event description is rendered through Markdown formatting.
|
||||
- 收藏 is at the body area's left top.
|
||||
- 分享 is horizontally aligned with 收藏.
|
||||
- 分析 is a lighter pill at the right-top side of the body area.
|
||||
- 编辑 is at the body area's left bottom.
|
||||
- 编辑 and 收起 or 展开 are horizontally aligned.
|
||||
- 分析 and 收起 or 展开 are vertically aligned on the right side.
|
||||
- The bottom button is centered and displays “聊聊”.
|
||||
- The page has no overlapping text at mobile width.
|
||||
|
||||
Expected: all checks pass.
|
||||
|
||||
- [ ] **Step 5: Verify button behavior and console health**
|
||||
|
||||
Click these controls:
|
||||
|
||||
- 收藏
|
||||
- 分享
|
||||
- 分析
|
||||
- 编辑
|
||||
- 收起 or 展开
|
||||
- 聊聊
|
||||
|
||||
Expected:
|
||||
|
||||
- 收藏 toggles visual state or shows the existing failure toast when unauthenticated.
|
||||
- 分享 copies or shows the existing failure toast.
|
||||
- 分析 scrolls to the AI 分析 section.
|
||||
- 编辑 navigates to the event form when `eventId` exists.
|
||||
- 收起/展开 toggles Markdown body height.
|
||||
- 聊聊 opens the existing chat modal or shows existing failure text.
|
||||
- Browser Console contains no new errors from `detail.vue`.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
Spec coverage:
|
||||
|
||||
- Top icons removed: Task 1 Step 1, Task 3 Step 1-2.
|
||||
- Title and content merged: Task 1 Step 3, Task 3 Step 4-5.
|
||||
- Darker title area: Task 3 Step 4.
|
||||
- Markdown description rendering: Task 1 Step 3, Task 3 Step 7.
|
||||
- Button placement: Task 1 Step 3 and Step 5, Task 3 Step 6-8.
|
||||
- Existing behavior preserved: Task 2 Step 4.
|
||||
- Browser verification required by repository rules: Task 5.
|
||||
|
||||
Placeholder scan:
|
||||
|
||||
- This plan contains no placeholder or incomplete implementation steps.
|
||||
|
||||
Type and property consistency:
|
||||
|
||||
- `scrollTop` is declared as `ref(0)` and bound as `:scroll-top="scrollTop"`.
|
||||
- `currentScrollTop` is declared as `ref(0)` and updated by `@scroll="onContentScroll"`.
|
||||
- `scrollToAnalysis` is referenced by the template and defined in script.
|
||||
- `analysisPanel` is the exact id used by selector query.
|
||||
@@ -0,0 +1,445 @@
|
||||
# 个人信息组件合并 实现计划
|
||||
|
||||
> **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:** 将人生轨迹页面(RecordView)的个人信息展示区域完整合并到我的页面(MineView),取并集展示所有字段,RecordView 移除个人信息区域。
|
||||
|
||||
**Architecture:** MineView 保留现有宇宙星空风格和居中头像布局,在 stats-grid 下方新增「个人档案」信息区块,使用三栏网格展示所有个人信息字段。RecordView 完全移除 profile-card 区域及相关代码。
|
||||
|
||||
**Tech Stack:** Vue 3 Composition API, Taro/UniApp 小程序, SCSS scoped styles
|
||||
|
||||
**设计文档:** `docs/superpowers/specs/2026-06-15-profile-merge-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 操作 | 文件路径 | 职责 |
|
||||
|------|---------|------|
|
||||
| 修改 | `mini-program/src/pages/main/MineView.vue` | 新增「个人档案」区块(computed + template + styles) |
|
||||
| 修改 | `mini-program/src/pages/main/RecordView.vue` | 移除 profile-card 区域及相关代码 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: MineView — 新增 computed 属性
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/MineView.vue:63-138`
|
||||
|
||||
- [ ] **Step 1: 在 `<script setup>` 中新增字段 computed 属性**
|
||||
|
||||
在 `harmonyPercent` computed 之后、`openProfileSettings` 方法之前,新增以下 computed:
|
||||
|
||||
```js
|
||||
const genderText = computed(() => {
|
||||
const map = { male: '男', female: '女', other: '其他' }
|
||||
return map[profile.value.gender] || null
|
||||
})
|
||||
|
||||
const age = computed(() => {
|
||||
const birthday = profile.value.birthday
|
||||
if (!birthday) return null
|
||||
const birthYear = Number(String(birthday).slice(0, 4))
|
||||
if (!birthYear) return null
|
||||
return new Date().getFullYear() - birthYear
|
||||
})
|
||||
|
||||
const hobbyText = computed(() => {
|
||||
const hobbies = profile.value.hobbies
|
||||
if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4).join(' · ')
|
||||
return null
|
||||
})
|
||||
|
||||
const goEditProfile = () => {
|
||||
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/MineView.vue
|
||||
git commit -m "feat:MineView 新增个人档案字段 computed 属性"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: MineView — 新增「个人档案」模板
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/MineView.vue:32-34`
|
||||
|
||||
- [ ] **Step 1: 在 stats-grid 和 menu-list 之间插入个人档案区块**
|
||||
|
||||
在第 32 行 `</view>`(stats-grid 结束标签)之后、第 34 行 `<view class="menu-list">` 之前,插入以下模板:
|
||||
|
||||
```html
|
||||
<view class="profile-section" @click="goEditProfile">
|
||||
<view class="section-header">
|
||||
<text class="section-star">✦</text>
|
||||
<text class="section-title">个人档案</text>
|
||||
</view>
|
||||
|
||||
<view class="profile-grid">
|
||||
<view class="grid-item">
|
||||
<text class="grid-icon">♋</text>
|
||||
<text class="grid-label">星座</text>
|
||||
<text class="grid-value">{{ profile.zodiac || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="grid-item">
|
||||
<text class="grid-icon">◉</text>
|
||||
<text class="grid-label">MBTI</text>
|
||||
<text class="grid-value">{{ profile.mbti || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="grid-item no-border">
|
||||
<text class="grid-icon">◻</text>
|
||||
<text class="grid-label">职业</text>
|
||||
<text class="grid-value">{{ profile.profession || '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-grid">
|
||||
<view class="grid-item">
|
||||
<text class="grid-icon">📍</text>
|
||||
<text class="grid-label">城市</text>
|
||||
<text class="grid-value">{{ profile.city || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="grid-item">
|
||||
<text class="grid-icon">🏢</text>
|
||||
<text class="grid-label">行业</text>
|
||||
<text class="grid-value">{{ profile.industry || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="grid-item no-border">
|
||||
<text class="grid-icon">🏗</text>
|
||||
<text class="grid-label">公司</text>
|
||||
<text class="grid-value">{{ profile.company || '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-grid">
|
||||
<view class="grid-item">
|
||||
<text class="grid-icon">🎂</text>
|
||||
<text class="grid-label">生日</text>
|
||||
<text class="grid-value">{{ profile.birthday ? String(profile.birthday).slice(5).replace('-', '.') : '未设置' }}</text>
|
||||
</view>
|
||||
<view class="grid-item">
|
||||
<text class="grid-icon">♂</text>
|
||||
<text class="grid-label">性别</text>
|
||||
<text class="grid-value">{{ genderText || '未设置' }}</text>
|
||||
</view>
|
||||
<view class="grid-item no-border">
|
||||
<text class="grid-icon">⏳</text>
|
||||
<text class="grid-label">年龄</text>
|
||||
<text class="grid-value">{{ age ? age + '岁' : '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="hobby-line">
|
||||
<text class="hobby-heart">♡</text>
|
||||
<text class="hobby-label">爱好</text>
|
||||
<text class="hobby-value">{{ hobbyText || '未设置' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/MineView.vue
|
||||
git commit -m "feat:MineView 新增个人档案区块模板"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: MineView — 新增「个人档案」样式
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/MineView.vue:141-435`
|
||||
|
||||
- [ ] **Step 1: 在 `<style scoped>` 中新增个人档案区块样式**
|
||||
|
||||
在 `.stat-value` 样式块之后、`.menu-list` 样式块之前,插入以下样式:
|
||||
|
||||
```css
|
||||
.profile-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 42rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.section-star {
|
||||
color: #ffd184;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6rpx;
|
||||
padding-right: 16rpx;
|
||||
border-right: 1rpx solid rgba(180, 139, 255, 0.22);
|
||||
}
|
||||
|
||||
.grid-item + .grid-item {
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
|
||||
.grid-item.no-border {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.grid-icon {
|
||||
font-size: 30rpx;
|
||||
line-height: 1;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.grid-label {
|
||||
color: rgba(219, 204, 247, 0.54);
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.grid-value {
|
||||
color: #fff;
|
||||
font-size: 23rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hobby-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid rgba(180, 139, 255, 0.22);
|
||||
}
|
||||
|
||||
.hobby-heart {
|
||||
color: #a855ff;
|
||||
font-size: 30rpx;
|
||||
text-shadow: 0 0 24rpx rgba(168, 85, 255, 0.7);
|
||||
}
|
||||
|
||||
.hobby-label {
|
||||
color: rgba(219, 204, 247, 0.54);
|
||||
font-size: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hobby-value {
|
||||
color: #fff;
|
||||
font-size: 23rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/MineView.vue
|
||||
git commit -m "feat:MineView 新增个人档案区块样式"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: RecordView — 移除 profile-card 模板
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/RecordView.vue:1-59`
|
||||
|
||||
- [ ] **Step 1: 移除 profile-card 区域的模板代码**
|
||||
|
||||
将模板中的第 3-59 行(从 `<view class="profile-card kos-card">` 到其闭合 `</view>`)完全删除。
|
||||
|
||||
删除后,模板应从 `<view class="record-view">` 直接开始,紧接 `<view class="section-head">`。
|
||||
|
||||
修改后的模板开头应为:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<view class="record-view">
|
||||
<view class="section-head">
|
||||
<view>
|
||||
<view class="title-line">
|
||||
<text class="section-title">人生轨迹</text>
|
||||
<text class="star gold">✦</text>
|
||||
</view>
|
||||
<text class="section-subtitle">你的成长之路,正在展开</text>
|
||||
</view>
|
||||
<view class="social-import-btn" @click="openSocialImport">
|
||||
<text>导入社交数据</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="filters" scroll-x :show-scrollbar="false">
|
||||
...(后续保持不变)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/RecordView.vue
|
||||
git commit -m "feat:RecordView 移除 profile-card 模板"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: RecordView — 移除相关 script 代码
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/RecordView.vue:126-247`
|
||||
|
||||
- [ ] **Step 1: 移除不再需要的 computed 和方法**
|
||||
|
||||
在 `<script setup>` 中删除以下内容:
|
||||
|
||||
1. 删除 `profile` computed(约第 143 行):
|
||||
```js
|
||||
const profile = computed(() => store.userProfile || store.registrationData || {})
|
||||
```
|
||||
|
||||
2. 删除 `heroTags` computed(约第 145-149 行):
|
||||
```js
|
||||
const heroTags = computed(() => {
|
||||
const hobbies = profile.value.hobbies
|
||||
if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4)
|
||||
return []
|
||||
})
|
||||
```
|
||||
|
||||
3. 删除 `avatar` computed(约第 151-154 行):
|
||||
```js
|
||||
const avatar = computed(() => {
|
||||
const nickname = profile.value.nickname || 'User'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
|
||||
})
|
||||
```
|
||||
|
||||
4. 删除 `editProfile` 方法(约第 223-225 行):
|
||||
```js
|
||||
const editProfile = () => {
|
||||
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
|
||||
}
|
||||
```
|
||||
|
||||
5. `getAgeText` 方法中仍使用 `profile.value.birthYear`,需要保留 `profile` computed 或改为直接引用。由于 `getAgeText` 仍需要 profile 数据,**保留 `profile` computed**,只删除 `heroTags`、`avatar`、`editProfile`。
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/RecordView.vue
|
||||
git commit -m "feat:RecordView 移除无用的 computed 和方法"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: RecordView — 移除相关样式
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/RecordView.vue:249-784`
|
||||
|
||||
- [ ] **Step 1: 移除 profile-card 相关的所有样式**
|
||||
|
||||
删除以下样式规则:
|
||||
- `.profile-card` 及其 `::after` 伪元素
|
||||
- `.profile-top`, `.avatar-wrap`, `.avatar`, `.avatar-edit`, `.edit-pen`, `.tiny-pen`
|
||||
- `.profile-info`, `.name-row`, `.profile-name`, `.star`(注意:`.star` 在 section-head 中也有使用,只删除 profile 相关的,保留 `.star.gold`)
|
||||
- `.profile-subtitle`, `.edit-profile`
|
||||
- `.profile-divider`, `.profile-divider.small`
|
||||
- `.meta-grid`, `.meta-item`, `.meta-icon`, `.meta-label`, `.meta-value`
|
||||
- `.person-icon`, `.job-icon` 及其伪元素
|
||||
- `.hobby-row`, `.hobby-text`
|
||||
- `.heart`
|
||||
|
||||
**注意**:`.star` 样式**不要删除**,因为 section-head 中的 `<text class="star gold">✦</text>` 仍在使用它(提供 `font-size: 26rpx`)。
|
||||
|
||||
保留的样式(不要删除):
|
||||
- `.record-view`
|
||||
- `.section-head`, `.title-line`, `.section-title`, `.star`, `.gold`, `.section-subtitle`
|
||||
- `.social-import-btn`
|
||||
- `.filters`, `.filter-row`, `.filter-chip`, `.add-filter`
|
||||
- `.timeline` 及所有子样式
|
||||
- `.event-card` 及所有子样式
|
||||
- `.empty-card`, `.empty-title`, `.empty-text`
|
||||
- `.create-fab`, `.plus-core`
|
||||
- `.star.gold`(section-head 中使用)
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/RecordView.vue
|
||||
git commit -m "feat:RecordView 移除 profile-card 相关样式"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 浏览器验证
|
||||
|
||||
- [ ] **Step 1: 启动 H5 开发服务器**
|
||||
|
||||
```bash
|
||||
cd mini-program
|
||||
npm run dev:h5
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证 MineView(我的页面)**
|
||||
|
||||
打开浏览器访问 `http://localhost:5173`,切换到「我的」tab:
|
||||
|
||||
检查项:
|
||||
- [ ] 头像和昵称正常显示
|
||||
- [ ] 觉醒深度和星历契合正常显示
|
||||
- [ ] 「个人档案」区块正常显示,包含:
|
||||
- 星座、MBTI、职业三栏
|
||||
- 城市、行业、公司三栏
|
||||
- 生日、性别、年龄三栏
|
||||
- 爱好行
|
||||
- [ ] 未设置的字段显示"未设置"
|
||||
- [ ] 点击「个人档案」区块可跳转到编辑页
|
||||
- [ ] 菜单列表和退出按钮正常
|
||||
- [ ] 整体星空风格无变化
|
||||
|
||||
- [ ] **Step 3: 验证 RecordView(人生轨迹页面)**
|
||||
|
||||
切换到「人生轨迹」tab:
|
||||
|
||||
检查项:
|
||||
- [ ] 个人信息卡片已完全移除
|
||||
- [ ] "人生轨迹"标题和"导入社交数据"按钮正常
|
||||
- [ ] 筛选器正常
|
||||
- [ ] 事件时间线正常显示
|
||||
- [ ] 创建 FAB 按钮正常
|
||||
- [ ] 页面无 Console 报错
|
||||
|
||||
- [ ] **Step 4: 最终提交**
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat:完成个人信息组件合并,RecordView 移除个人信息区域,MineView 新增完整个人档案区块"
|
||||
```
|
||||
+125
@@ -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`,与源码保持一致。
|
||||
+218
@@ -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: 查看当前代码**
|
||||
|
||||
确认第 178–249 行为 `fallbackScripts` 数组:
|
||||
|
||||
```javascript
|
||||
const fallbackScripts = [
|
||||
{
|
||||
id: 'demo-1',
|
||||
title: '逆袭人生:从低谷到巅峰',
|
||||
// ...
|
||||
},
|
||||
// ... 共 6 条
|
||||
]
|
||||
```
|
||||
|
||||
确认第 251–254 行为 `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 "fix:ScriptView 结果页顶部操作栏固定不随滚动"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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 "fix:ScriptView 底部输入框区域增加左右边距"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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 "fix:ScriptView 语音按钮改为麦克风图标"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 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 返回空内容)
|
||||
- 点击「复制」按钮
|
||||
- **预期:** 显示「暂无可复制内容」提示
|
||||
|
||||
**场景 2:pending 状态的 assistant 消息**
|
||||
- 生成故事后,快速点击「继续生成」
|
||||
- 观察正在加载的 assistant 消息(pending 状态)
|
||||
- **预期:** pending 消息不显示功能按钮组
|
||||
|
||||
**场景 3:user 消息**
|
||||
- 查看用户发送的消息
|
||||
- **预期:** 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
@@ -0,0 +1,225 @@
|
||||
# Chat 视图底部遮挡架构层修复实施计划
|
||||
|
||||
> **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 chat 视图 outline 输入框 placeholder 和修改/确认大纲按钮被主页 bottom-nav 遮挡的问题。`.chat-page` 从 `height: 100vh` 改为 `height: 100%`,让父级 `.content`(`padding-bottom: 132rpx`)自然生效;同时把 `.chat-scroll-content` 的 `padding-bottom` 从 120rpx 改回 40rpx,避免双重 padding。
|
||||
|
||||
**Architecture:** 架构层修正而非 CSS 遮罩。让 chat-page 正确填充父级已预留 bottom-nav 空间的内容区,chat 滚动区底部自然避开 bottom-nav。iPhone 安全区用 `env(safe-area-inset-bottom)` 处理。
|
||||
|
||||
**Tech Stack:** UniApp Vue 3 `<script setup>` + mp-weixin 编译 + rpx 单位 + `env(safe-area-inset-bottom)` 安全区适配
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 修改 .chat-page 从 height: 100vh 改为 height: 100%
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue:3589-3598`
|
||||
|
||||
- [ ] **Step 1: 修改 .chat-page 样式**
|
||||
|
||||
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3589-3598 行的 `.chat-page` 样式块:
|
||||
|
||||
```css
|
||||
.chat-page {
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #13091f;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
```
|
||||
|
||||
改为(`height: 100vh` → `height: 100%`,删除旧版 iOS 兼容的 `constant()`):
|
||||
|
||||
```css
|
||||
.chat-page {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #13091f;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "refactor: .chat-page 改为 height: 100% 让父级 padding 生效"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 修改 .chat-scroll-content 改回 padding-bottom: 40rpx
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue:3617-3619`
|
||||
|
||||
- [ ] **Step 1: 修改 .chat-scroll-content 样式**
|
||||
|
||||
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3617-3619 行的 `.chat-scroll-content` 样式块:
|
||||
|
||||
```css
|
||||
.chat-scroll-content {
|
||||
padding: 0 24rpx 120rpx;
|
||||
}
|
||||
```
|
||||
|
||||
改为(`padding-bottom` 从 120rpx 改回 40rpx,避免与父级 `.content` 的 `padding-bottom: 132rpx` 双重叠加):
|
||||
|
||||
```css
|
||||
.chat-scroll-content {
|
||||
padding: 0 24rpx 40rpx;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "fix: .chat-scroll-content padding-bottom 改回 40rpx 避免与父级 padding 叠加"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: H5 浏览器验证修复效果
|
||||
|
||||
**Files:**
|
||||
- Test: `http://localhost:5180` (H5 开发服务器,端口 5180 由 `dev-services.py` 管理)
|
||||
|
||||
- [ ] **Step 1: 确认 H5 开发服务器运行中**
|
||||
|
||||
```bash
|
||||
python dev-services.py status
|
||||
```
|
||||
Expected: `mini-program` 服务在端口 5180 上运行。如未运行,执行:
|
||||
```bash
|
||||
python dev-services.py start mini-program
|
||||
```
|
||||
等待 `VITE` 编译完成,确认输出包含 `Local: http://localhost:5180/`。
|
||||
|
||||
- [ ] **Step 2: 浏览器打开页面并进入 chat 视图**
|
||||
|
||||
1. 用浏览器打开 `http://localhost:5180`
|
||||
2. 确认已登录(如未登录,使用手机号验证码 123456 登录)
|
||||
3. 点击底部导航「爽文生成」
|
||||
4. 点击任意一条灵感推荐填充输入框,点击「发送」
|
||||
|
||||
- [ ] **Step 3: 检查 .chat-page 计算样式**
|
||||
|
||||
在浏览器 DevTools Console 中执行:
|
||||
|
||||
```javascript
|
||||
() => {
|
||||
const el = document.querySelector('.chat-page');
|
||||
if (!el) return 'chat-page not found';
|
||||
const styles = window.getComputedStyle(el);
|
||||
const parent = el.parentElement;
|
||||
const parentStyles = parent ? window.getComputedStyle(parent) : null;
|
||||
return {
|
||||
chatPage: {
|
||||
height: styles.height,
|
||||
paddingBottom: styles.paddingBottom
|
||||
},
|
||||
parent: parent ? {
|
||||
tag: parent.tagName,
|
||||
className: parent.className,
|
||||
paddingBottom: parentStyles.paddingBottom
|
||||
} : null
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Expected: `chatPage.height` 应小于视口高度(不再 100vh),`parent.paddingBottom` 应为 `66px` (= 132rpx @ 750rpx 设计稿/2 = 实际 132rpx / 2 = 66px in 375px viewport)。
|
||||
|
||||
- [ ] **Step 4: 等待 SSE 返回澄清卡片或 outline**
|
||||
|
||||
在 chat 视图等待 10-15 秒:
|
||||
- 若出现澄清卡片(ClarificationCard):向下滚动,确认「提交」按钮在 bottom-nav 上方完整可见
|
||||
- 若出现 outline(带 beats 列表):向下滚动,确认 placeholder "如需修改请输入意见" 完整可见、「修改大纲」「确认大纲」按钮在 bottom-nav 上方完整可点击
|
||||
|
||||
- [ ] **Step 5: 验证 chat-scroll-content 实际高度**
|
||||
|
||||
在 Console 中执行:
|
||||
|
||||
```javascript
|
||||
() => {
|
||||
const scrollContent = document.querySelector('.chat-scroll-content');
|
||||
const scroll = document.querySelector('.chat-scroll');
|
||||
if (!scrollContent || !scroll) return 'not found';
|
||||
return {
|
||||
scrollContentPaddingBottom: window.getComputedStyle(scrollContent).paddingBottom,
|
||||
scrollHeight: scroll.offsetHeight,
|
||||
scrollClientHeight: scroll.clientHeight,
|
||||
scrollScrollHeight: scroll.scrollHeight
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Expected: `scrollContentPaddingBottom` = `20px` (= 40rpx / 2 in 375px viewport),`scrollScrollHeight > scrollClientHeight` 表示有内容可滚动。
|
||||
|
||||
- [ ] **Step 6: 验证 home 视图不受影响**
|
||||
|
||||
点击 chat 视图右上角「×」关闭按钮返回 home 视图,确认:
|
||||
- ✅ 首页正常渲染
|
||||
- ✅ 灵感推荐、输入框、语音球布局完整
|
||||
- ✅ bottom-nav 与 home 内容无重叠
|
||||
|
||||
---
|
||||
|
||||
### Task 4: mp-weixin 编译验证
|
||||
|
||||
**Files:**
|
||||
- Build: `mini-program/unpackage/dist/build/mp-weixin/`
|
||||
|
||||
- [ ] **Step 1: 执行 mp-weixin 编译**
|
||||
|
||||
```bash
|
||||
cd mini-program
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
Expected: 编译成功,无 error 输出。编译产物在 `unpackage/dist/build/mp-weixin/`。
|
||||
|
||||
- [ ] **Step 2: 验证编译产物样式已更新**
|
||||
|
||||
```bash
|
||||
grep -o '.chat-page[^}]*}' mini-program/unpackage/dist/build/mp-weixin/pages/main/ScriptView.wxss
|
||||
grep -o '.chat-scroll-content[^}]*}' mini-program/unpackage/dist/build/mp-weixin/pages/main/ScriptView.wxss
|
||||
```
|
||||
|
||||
Expected:
|
||||
- `.chat-page` 包含 `height:100%` 和 `padding-bottom:env(safe-area-inset-bottom)`
|
||||
- `.chat-page` 不再包含 `height:100vh` 或 `constant(safe-area-inset-bottom)`
|
||||
- `.chat-scroll-content` 的 padding 为 `0 24rpx 40rpx`(不再是 120rpx)
|
||||
|
||||
- [ ] **Step 3: 提交(如有微调)**
|
||||
|
||||
如有需要,执行:
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "style: mp-weixin 编译产物验证通过"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 通知用户在微信开发者工具中验证
|
||||
|
||||
- [ ] **Step 1: 提示用户操作**
|
||||
|
||||
告知用户:
|
||||
> mp-weixin 编译产物已更新(`mini-program/unpackage/dist/build/mp-weixin/`)。请在**微信开发者工具**中:
|
||||
> 1. 打开 `unpackage/dist/build/mp-weixin` 目录
|
||||
> 2. 点击「编译」按钮重新编译
|
||||
> 3. 进入心愿实现页面,触发对话流直至出现 outline 消息(带 beats 列表)
|
||||
> 4. 向下滚动,确认:
|
||||
> - outline 输入框的 placeholder "如需修改请输入意见" 完整可见(不被截断)
|
||||
> - 「修改大纲」「确认大纲」按钮在底部导航栏上方完整可见可点击
|
||||
> 5. 在 iPhone 真机/模拟器上验证底部 home indicator 区域不遮挡内容
|
||||
>
|
||||
> 如有问题,截图反馈。
|
||||
@@ -0,0 +1,188 @@
|
||||
# Chat 视图底部内边距修复实施计划
|
||||
|
||||
> **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 chat 视图 ClarificationCard 提交按钮被主页 bottom-nav(104rpx)遮挡导致用户无法滚动到可见的问题。
|
||||
|
||||
**Architecture:** 在 `ScriptView.vue` 样式中,给 `.chat-page` 增加 `env(safe-area-inset-bottom)` 适配 iPhone 安全区,给 `.chat-scroll-content` 的 `padding-bottom` 从 40rpx 增至 120rpx,使滚动内容底部留出 bottom-nav 高度(104rpx)+ 16rpx 呼吸间距。
|
||||
|
||||
**Tech Stack:** UniApp Vue 3 `<script setup>` + mp-weixin 编译 + rpx 单位 + `env(safe-area-inset-bottom)` 安全区适配
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 修改 .chat-page 增加 iPhone 安全区内边距
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue:3589-3596`
|
||||
|
||||
- [ ] **Step 1: 修改 .chat-page 样式**
|
||||
|
||||
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3589-3596 行的 `.chat-page` 样式块:
|
||||
|
||||
```css
|
||||
.chat-page {
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #13091f;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
```
|
||||
|
||||
改为(新增 `padding-bottom` 两行,与 `App.vue` 中 `.safe-area-bottom` 的写法保持一致,`constant()` 兼容 iOS 11.0-11.2,`env()` 覆盖 iOS 11.2+):
|
||||
|
||||
```css
|
||||
.chat-page {
|
||||
height: 100vh;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #13091f;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "style: .chat-page 增加 safe-area-inset-bottom 适配 iPhone 底部"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 修改 .chat-scroll-content 增加底部内边距
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue:3615-3617`
|
||||
|
||||
- [ ] **Step 1: 修改 .chat-scroll-content 样式**
|
||||
|
||||
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3615-3617 行的 `.chat-scroll-content` 样式块:
|
||||
|
||||
```css
|
||||
.chat-scroll-content {
|
||||
padding: 0 24rpx 40rpx;
|
||||
}
|
||||
```
|
||||
|
||||
改为(`padding-bottom` 从 40rpx 增至 120rpx = bottom-nav 104rpx + 16rpx 呼吸间距):
|
||||
|
||||
```css
|
||||
.chat-scroll-content {
|
||||
padding: 0 24rpx 120rpx;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "fix: .chat-scroll-content padding-bottom 增至 120rpx 避开 bottom-nav 遮挡"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: H5 浏览器验证修复效果
|
||||
|
||||
**Files:**
|
||||
- Test: `http://localhost:5180` (H5 开发服务器,端口 5180 由 `dev-services.py` 管理)
|
||||
|
||||
- [ ] **Step 1: 确认 H5 开发服务器运行中**
|
||||
|
||||
```bash
|
||||
python dev-services.py status
|
||||
```
|
||||
Expected: `mini-program` 服务在端口 5180 上运行。如未运行,执行:
|
||||
```bash
|
||||
python dev-services.py start mini-program
|
||||
```
|
||||
等待 `VITE` 编译完成,确认输出包含 `Local: http://localhost:5180/`。
|
||||
|
||||
- [ ] **Step 2: 浏览器打开页面并进入 chat 视图**
|
||||
|
||||
1. 用浏览器打开 `http://localhost:5180`
|
||||
2. 确认已登录(如未登录,使用手机号验证码 123456 登录)
|
||||
3. 点击底部导航「爽文生成」
|
||||
4. 点击任意一条灵感推荐(如「我想把最近一次低谷,改写成主角觉醒的开端。」)
|
||||
5. 点击「发送」按钮
|
||||
|
||||
- [ ] **Step 3: 验证澄清卡片提交按钮可见且可点击**
|
||||
|
||||
进入 chat 视图后,等待 SSE 返回澄清卡片。在浏览器中验证:
|
||||
- ✅ 澄清卡片完整渲染(标题、描述、选项、自定义输入框)
|
||||
- ✅ 向下滚动时「提交」按钮在 bottom-nav(「人生轨迹/爽文生成/我的」)上方,不被遮挡
|
||||
- ✅ 选中一个选项后「提交」按钮变为可点击状态(不再灰色/禁用)
|
||||
- ✅ 点击「提交」后流程正常继续(追加一条 user 消息气泡)
|
||||
|
||||
- [ ] **Step 4: 检查浏览器 Console 无报错**
|
||||
|
||||
打开浏览器 DevTools → Console 面板,确认:
|
||||
- ✅ 无新增红色 ERROR(`uni.getRecorderManager not supported` 是 H5 环境预期内的 warning,可忽略)
|
||||
- ✅ Network 面板中 `/shortNovel/followup` 请求返回正常响应
|
||||
|
||||
- [ ] **Step 5: 验证 home 视图不受影响**
|
||||
|
||||
点击 chat 视图右上角「×」关闭按钮,确认:
|
||||
- ✅ 返回首页(wish-home)正常渲染
|
||||
- ✅ 灵感推荐、输入框、语音球布局未受 `padding-bottom` 影响
|
||||
- ✅ 主页 bottom-nav 正常显示,与 home 视图内容无重叠
|
||||
|
||||
- [ ] **Step 6: 验证 chat-input-bar 出现时布局正确**
|
||||
|
||||
当 `generationPhase === 'done'` 时(即 novel_done 后或从历史记录打开已有剧本时):
|
||||
- ✅ 底部输入栏(`chat-input-bar`)在 bottom-nav 上方显示,无重叠
|
||||
- ✅ 输入栏与滚动内容之间无多余空隙
|
||||
- ✅ 输入栏内的 textarea 和「发送」按钮均可正常交互
|
||||
|
||||
---
|
||||
|
||||
### Task 4: mp-weixin 编译验证
|
||||
|
||||
**Files:**
|
||||
- Build: `mini-program/unpackage/dist/dev/mp-weixin/`
|
||||
|
||||
- [ ] **Step 1: 执行 mp-weixin 编译**
|
||||
|
||||
```bash
|
||||
cd mini-program
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
Expected: 编译成功,无 error 输出。编译产物在 `unpackage/dist/dev/mp-weixin/`。
|
||||
|
||||
- [ ] **Step 2: 验证编译产物样式已更新**
|
||||
|
||||
```bash
|
||||
grep "padding-bottom.*env" unpackage/dist/dev/mp-weixin/pages/main/ScriptView.wxss
|
||||
grep "120rpx" unpackage/dist/dev/mp-weixin/pages/main/ScriptView.wxss
|
||||
```
|
||||
Expected:
|
||||
- `padding-bottom: env(safe-area-inset-bottom);` 出现在 `ScriptView.wxss`
|
||||
- `.chat-scroll-content` 的 `padding-bottom` 值为 `120rpx`
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "style: mp-weixin 编译产物验证通过"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 通知用户在微信开发者工具中验证
|
||||
|
||||
- [ ] **Step 1: 提示用户操作**
|
||||
|
||||
告知用户:
|
||||
> mp-weixin 编译产物已更新(`unpackage/dist/dev/mp-weixin/`)。请在**微信开发者工具**中:
|
||||
> 1. 打开 `unpackage/dist/dev/mp-weixin` 目录
|
||||
> 2. 点击「编译」按钮重新编译
|
||||
> 3. 进入心愿实现页面,选择灵感推荐 → 发送
|
||||
> 4. 等待澄清卡片出现后,向下滚动,确认「提交」按钮在底部导航栏上方完整可见
|
||||
> 5. 选中一个选项,点击「提交」,确认流程正常继续
|
||||
> 6. 在 iPhone 真机/模拟器上验证底部 home indicator 区域不遮挡内容
|
||||
>
|
||||
> 如有问题,截图反馈。
|
||||
@@ -0,0 +1,874 @@
|
||||
# ScriptView 统一对话页改造实现计划
|
||||
|
||||
> **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 心愿实现页的澄清/大纲/小说生成从独立视图切换改为统一对话流,用户提交心愿后始终停留在同一对话页面
|
||||
|
||||
**Architecture:** viewState 简化为 home+chat;扩展 resultMessages 消息模型支持 kind: text/card/outline/novel + 交互态字段;handleShortNovelEvent 把 SSE 事件转为对话消息追加;卡片/大纲消息内嵌交互按钮,用户操作后转 user 消息并调 followupStream;novel_done 后底部输入栏按 generationPhase 切换到旧接口 streamScriptChat 做自由对话修改
|
||||
|
||||
**Tech Stack:** UniApp + Vue 3 Composition API(`<script setup>`),shortNovel.js(新接口 SSE),scriptChat.js(旧接口流式对话),H5 热加载验证
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
### 修改文件
|
||||
- `mini-program/src/pages/main/ScriptView.vue` — 核心改造文件(约 3300 行)
|
||||
- 扩展 `addResultMessage` 消息模型
|
||||
- 新增 `generationPhase` 状态
|
||||
- 改造 `handleShortNovelEvent` 为对话流追加消息
|
||||
- 改造 `runGeneration`/`submitClarification`/`confirmOutline`/`modifyOutline`/`resumeSession` 操作对话消息
|
||||
- 改造 `sendChat` 按 generationPhase 切换接口
|
||||
- 新增统一 chat 视图模板,删除 clarifying/outlining/novel-generating 独立视图
|
||||
|
||||
### 不变文件
|
||||
- `mini-program/src/components/ClarificationCard.vue` — 复用,由父组件控制 submitted 态
|
||||
- `mini-program/src/services/shortNovel.js` — 新接口不变
|
||||
- `mini-program/src/services/scriptChat.js` — 旧接口不变
|
||||
|
||||
### 验证方式
|
||||
本项目无前端单元测试框架,采用 **H5 热加载 + Playwright 浏览器验证**。每个任务结束后在浏览器确认页面行为。
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 扩展消息模型 + 新增 generationPhase 状态
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue`(addResultMessage 函数约 1283 行,新增 generationPhase ref 约 350 行附近)
|
||||
|
||||
- [ ] **Step 1: 扩展 addResultMessage 支持 card/outline/交互态字段**
|
||||
|
||||
找到 `addResultMessage` 函数(约 1283 行),当前实现:
|
||||
|
||||
```javascript
|
||||
const addResultMessage = ({ role, content, pending = false, kind = '' }) => {
|
||||
const message = {
|
||||
id: createMessageId(),
|
||||
role,
|
||||
kind,
|
||||
content,
|
||||
pending,
|
||||
time: formatMessageTime()
|
||||
}
|
||||
resultMessages.value.push(message)
|
||||
if (!pending) persistResultMessages()
|
||||
keepResultAtBottom()
|
||||
return message
|
||||
}
|
||||
```
|
||||
|
||||
替换为(新增 card/outline/submitted/confirmed/failed/answer/feedback 字段):
|
||||
|
||||
```javascript
|
||||
const addResultMessage = ({ role, content, pending = false, kind = 'text', card = null, outline = null, answer = '', feedback = '', failed = false }) => {
|
||||
const message = {
|
||||
id: createMessageId(),
|
||||
role,
|
||||
kind,
|
||||
content,
|
||||
pending,
|
||||
time: formatMessageTime(),
|
||||
card,
|
||||
outline,
|
||||
answer,
|
||||
feedback,
|
||||
failed,
|
||||
submitted: false,
|
||||
confirmed: false
|
||||
}
|
||||
resultMessages.value.push(message)
|
||||
if (!pending) persistResultMessages()
|
||||
keepResultAtBottom()
|
||||
return message
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 新增 generationPhase ref**
|
||||
|
||||
找到 `const resumeableSessionId = ref('')`(约 350 行),在其后新增一行:
|
||||
|
||||
```javascript
|
||||
const resumeableSessionId = ref('')
|
||||
const generationPhase = ref('idle')
|
||||
```
|
||||
|
||||
`generationPhase` 取值:`'idle'`(未开始)/`'generating'`(生成流程中,用新接口)/`'done'`(novel_done 后,用旧接口)。
|
||||
|
||||
- [ ] **Step 3: 验证热加载无编译错误**
|
||||
|
||||
H5 服务在后台运行(端口 5180),保存文件后 Vite 自动热加载。打开浏览器 http://localhost:5180 确认页面无白屏、Console 无语法错误。
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "refactor(mini-program): 扩展 addResultMessage 消息模型 + 新增 generationPhase"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 改造 handleShortNovelEvent 为对话流追加消息
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue`(handleShortNovelEvent 函数约 1682 行)
|
||||
|
||||
- [ ] **Step 1: 改造 handleShortNovelEvent**
|
||||
|
||||
找到 `handleShortNovelEvent` 函数(约 1682 行),当前实现是切换 viewState。替换为往 resultMessages 追加消息:
|
||||
|
||||
```javascript
|
||||
const handleShortNovelEvent = (event) => {
|
||||
const { type, session_id, payload = {} } = event
|
||||
if (session_id) novelSessionId.value = session_id
|
||||
|
||||
switch (type) {
|
||||
case 'status':
|
||||
// 仅更新加载提示,不追加独立消息
|
||||
generationStatus.value = payload.stage || 'processing'
|
||||
break
|
||||
case 'clarification_card':
|
||||
addResultMessage({ role: 'assistant', kind: 'card', card: payload.card || null })
|
||||
generationStatus.value = 'idle'
|
||||
break
|
||||
case 'outline_created':
|
||||
addResultMessage({ role: 'assistant', kind: 'outline', outline: payload.outline || null })
|
||||
generationStatus.value = 'idle'
|
||||
break
|
||||
case 'novel_start':
|
||||
addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true })
|
||||
generationStatus.value = 'streaming'
|
||||
break
|
||||
case 'novel_delta': {
|
||||
// 往最后一条 novel 消息追加 delta
|
||||
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
|
||||
if (lastNovel) {
|
||||
lastNovel.content += payload.delta || ''
|
||||
}
|
||||
keepResultAtBottom()
|
||||
break
|
||||
}
|
||||
case 'novel_done': {
|
||||
// 标记最后一条 novel 消息完成
|
||||
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel')
|
||||
if (lastNovel) {
|
||||
if (payload.full_text) lastNovel.content = payload.full_text
|
||||
lastNovel.pending = false
|
||||
}
|
||||
scriptId.value = payload.scriptId || ''
|
||||
conversationId.value = payload.conversationId || ''
|
||||
currentVersionMessageId.value = payload.currentVersionMessageId || ''
|
||||
generationPhase.value = 'done'
|
||||
generationStatus.value = 'idle'
|
||||
persistResultMessages()
|
||||
break
|
||||
}
|
||||
case 'error':
|
||||
if (payload.code === 'DAILY_GENERATION_IN_PROGRESS' && session_id) {
|
||||
resumeableSessionId.value = session_id
|
||||
markGenerationFailed(payload.message || '今天已有未完成的创作,可点击继续创作恢复')
|
||||
} else {
|
||||
markGenerationFailed(payload.message || '生成失败')
|
||||
addResultMessage({ role: 'assistant', kind: 'text', content: payload.message || '生成失败', failed: true })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键变化:
|
||||
- 不再切换 `viewState`(始终保持 `chat`)
|
||||
- `clarification_card` → 追加 kind:'card' 消息
|
||||
- `outline_created` → 追加 kind:'outline' 消息
|
||||
- `novel_start` → 追加 pending 的 kind:'novel' 消息
|
||||
- `novel_delta` → 往最后一条 pending novel 消息追加文本
|
||||
- `novel_done` → 标记 novel 消息完成,设置 `generationPhase='done'`
|
||||
|
||||
- [ ] **Step 2: 验证热加载无错误**
|
||||
|
||||
保存后浏览器确认无 Console 错误(此时 UI 还未改造,但逻辑应无语法错误)。
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "refactor(mini-program): handleShortNovelEvent 改为对话流追加消息"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 改造 runGeneration 和交互函数
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue`(runGeneration 约 1626 行,submitClarification/confirmOutline/modifyOutline/resumeSession 约 1727 行)
|
||||
|
||||
- [ ] **Step 1: 改造 runGeneration 进入 chat 视图并追加用户消息**
|
||||
|
||||
找到 `runGeneration` 函数(约 1626 行)。当前在函数中段设置 `viewState.value = 'generating'` 和 `generationDisplayText.value = display`。
|
||||
|
||||
把 `viewState.value = 'generating'` 改为 `viewState.value = 'chat'`,并在 `generationDisplayText.value = display` 后新增追加用户消息的逻辑:
|
||||
|
||||
找到这一段:
|
||||
```javascript
|
||||
streamWriter.reset()
|
||||
startGenerationFeedback()
|
||||
ttsPlayer.reset()
|
||||
viewState.value = 'generating'
|
||||
keepGenerationAtBottom()
|
||||
```
|
||||
|
||||
替换为:
|
||||
```javascript
|
||||
streamWriter.reset()
|
||||
startGenerationFeedback()
|
||||
ttsPlayer.reset()
|
||||
viewState.value = 'chat'
|
||||
generationPhase.value = 'generating'
|
||||
resultMessages.value = []
|
||||
addResultMessage({ role: 'user', kind: 'text', content: display })
|
||||
keepResultAtBottom()
|
||||
```
|
||||
|
||||
说明:进入 chat 视图,清空旧消息,把用户心愿作为第一条 user 消息追加。
|
||||
|
||||
- [ ] **Step 2: 改造 submitClarification 接收消息对象**
|
||||
|
||||
找到 `submitClarification` 函数(约 1727 行),当前签名 `const submitClarification = (answer) =>`。替换为接收消息对象 + answer:
|
||||
|
||||
```javascript
|
||||
const submitClarification = (msg, answer) => {
|
||||
if (answeringClarification.value) return
|
||||
if (msg) {
|
||||
msg.submitted = true
|
||||
msg.answer = answer
|
||||
}
|
||||
addResultMessage({ role: 'user', kind: 'text', content: answer })
|
||||
answeringClarification.value = true
|
||||
currentStreamTask.value = followupStream({
|
||||
sessionId: novelSessionId.value,
|
||||
action: 'answer_clarification',
|
||||
payload: { answer },
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => markGenerationFailed(msg)
|
||||
})
|
||||
setTimeout(() => { answeringClarification.value = false }, 200)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 改造 confirmOutline 接收消息对象**
|
||||
|
||||
找到 `confirmOutline` 函数(约 1740 行),替换为:
|
||||
|
||||
```javascript
|
||||
const confirmOutline = (msg) => {
|
||||
if (msg) msg.confirmed = true
|
||||
addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' })
|
||||
currentStreamTask.value = followupStream({
|
||||
sessionId: novelSessionId.value,
|
||||
action: 'confirm_outline',
|
||||
payload: null,
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => markGenerationFailed(msg)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 改造 modifyOutline 接收消息对象**
|
||||
|
||||
找到 `modifyOutline` 函数(约 1750 行),替换为:
|
||||
|
||||
```javascript
|
||||
const modifyOutline = (msg) => {
|
||||
const feedback = msg?.feedback || ''
|
||||
if (!feedback.trim()) {
|
||||
uni.showToast({ title: '请先填写修改意见', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (msg) msg.confirmed = true
|
||||
addResultMessage({ role: 'user', kind: 'text', content: feedback })
|
||||
currentStreamTask.value = followupStream({
|
||||
sessionId: novelSessionId.value,
|
||||
action: 'modify_outline',
|
||||
payload: { feedback },
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => markGenerationFailed(msg)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 改造 resumeSession 在对话流内恢复**
|
||||
|
||||
找到 `resumeSession` 函数(约 1761 行),替换为:
|
||||
|
||||
```javascript
|
||||
const resumeSession = () => {
|
||||
if (!resumeableSessionId.value || generating.value) return
|
||||
novelSessionId.value = resumeableSessionId.value
|
||||
resumeableSessionId.value = ''
|
||||
generating.value = true
|
||||
generationPhase.value = 'generating'
|
||||
generationStatus.value = 'waiting'
|
||||
generationError.value = ''
|
||||
addResultMessage({ role: 'user', kind: 'text', content: '继续之前的创作' })
|
||||
streamWriter.reset()
|
||||
startGenerationFeedback()
|
||||
keepResultAtBottom()
|
||||
|
||||
try {
|
||||
currentStreamTask.value = followupStream({
|
||||
sessionId: novelSessionId.value,
|
||||
action: 'retry',
|
||||
payload: null,
|
||||
onEvent: handleShortNovelEvent,
|
||||
onError: (msg) => {
|
||||
markGenerationFailed(msg)
|
||||
analytics.track('script_generate_fail', {
|
||||
source: 'resume',
|
||||
error: msg
|
||||
}, { eventType: 'script', pagePath })
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
markGenerationFailed(error.message || '恢复失败')
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键变化:不再切换 viewState(保持 chat),改为追加 user 消息"继续之前的创作"。
|
||||
|
||||
- [ ] **Step 6: 验证热加载无错误**
|
||||
|
||||
保存后浏览器确认无 Console 错误。
|
||||
|
||||
- [ ] **Step 7: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "refactor(mini-program): runGeneration+交互函数改为操作对话消息"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 新增统一 chat 视图模板 + 删除旧独立视图
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue`(template 部分,约 89-183 行的 clarifying/outlining 块 + 139-183 的 generating 块 + 185-302 的 result 块)
|
||||
|
||||
- [ ] **Step 1: 删除 clarifying 和 outlining 独立视图块**
|
||||
|
||||
找到 `clarifying` 视图块(约 89 行 `<view v-else-if="viewState === 'clarifying'"` 到 102 行 `</view>`)和 `outlining` 视图块(约 104 行 `<view v-else-if="viewState === 'outlining'"` 到 137 行 `</view>`),**整块删除**。
|
||||
|
||||
删除这两个块后,template 里不再有 viewState==='clarifying' 和 'outlining' 的分支。
|
||||
|
||||
- [ ] **Step 2: 改造 generating 视图为 chat 视图**
|
||||
|
||||
找到 generating 视图块(约 139 行 `<view v-else-if="viewState === 'generating"`,但由于 Step 1 删除了前面两个块,行号会前移)。把整个 generating 块替换为统一 chat 视图:
|
||||
|
||||
把 `<view v-else-if="viewState === 'generating'" class="generation-view">` 整块(到对应的 `</view>` 结束)替换为:
|
||||
|
||||
```vue
|
||||
<view v-else-if="viewState === 'chat'" class="chat-page">
|
||||
<view class="chat-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="chat-scroll"
|
||||
scroll-y
|
||||
:scroll-top="resultCommandScrollTop"
|
||||
:scroll-into-view="resultScrollTarget"
|
||||
:scroll-with-animation="true"
|
||||
:enhanced="true"
|
||||
:show-scrollbar="false"
|
||||
@scroll="handleResultScroll"
|
||||
>
|
||||
<view class="chat-scroll-content">
|
||||
<view id="result-scroll-top" class="result-scroll-top"></view>
|
||||
|
||||
<view class="conversation compact">
|
||||
<view
|
||||
v-for="msg in resultMessages"
|
||||
:key="msg.id"
|
||||
>
|
||||
<!-- user text -->
|
||||
<view v-if="msg.role === 'user' && msg.kind === 'text'" class="chat-bubble user">
|
||||
<text>{{ msg.content }}</text>
|
||||
<text class="bubble-time">{{ msg.time }}</text>
|
||||
</view>
|
||||
|
||||
<!-- assistant card -->
|
||||
<view v-else-if="msg.kind === 'card'" class="chat-bubble system">
|
||||
<ClarificationCard
|
||||
v-if="!msg.submitted && msg.card"
|
||||
:card="msg.card"
|
||||
@submit="(answer) => submitClarification(msg, answer)"
|
||||
/>
|
||||
<view v-else class="card-answered">
|
||||
<text>已回答:{{ msg.answer }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- assistant outline -->
|
||||
<view v-else-if="msg.kind === 'outline'" class="chat-bubble system">
|
||||
<view v-if="msg.outline">
|
||||
<view v-if="msg.outline.title" class="outline-title">
|
||||
<text>{{ msg.outline.title }}</text>
|
||||
</view>
|
||||
<view v-if="msg.outline.logline" class="outline-logline">
|
||||
<text>{{ msg.outline.logline }}</text>
|
||||
</view>
|
||||
<view v-if="Array.isArray(msg.outline.beats)" class="outline-beats">
|
||||
<view v-for="(beat, idx) in msg.outline.beats" :key="idx" class="beat-item">
|
||||
<text class="beat-order">{{ beat.order || idx + 1 }}</text>
|
||||
<view class="beat-content">
|
||||
<text class="beat-title">{{ beat.title || '' }}</text>
|
||||
<text v-if="beat.summary" class="beat-summary">{{ beat.summary }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="msg.outline.ending" class="outline-ending">
|
||||
<text class="ending-label">结局</text>
|
||||
<text class="ending-text">{{ msg.outline.ending }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!msg.confirmed" class="outline-actions">
|
||||
<input v-model="msg.feedback" placeholder="如需修改请输入意见" class="outline-feedback" />
|
||||
<view class="outline-buttons">
|
||||
<view class="btn-secondary" @click="modifyOutline(msg)">修改大纲</view>
|
||||
<view class="btn-primary" @click="confirmOutline(msg)">确认大纲</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- assistant novel -->
|
||||
<view v-else-if="msg.kind === 'novel'" class="chat-bubble system">
|
||||
<text class="novel-text">{{ msg.content }}<text v-if="msg.pending" class="typing-cursor">|</text></text>
|
||||
</view>
|
||||
|
||||
<!-- assistant text (status/error) -->
|
||||
<view v-else class="chat-bubble system">
|
||||
<text :class="{ 'generation-error': msg.failed }">{{ msg.content }}</text>
|
||||
<text class="bubble-time">{{ msg.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部 loading -->
|
||||
<view v-if="generating && generationStatus !== 'failed'" class="chat-loading">
|
||||
<view class="loading-orbit" :class="{ streaming: generationStatus === 'streaming' }">
|
||||
<view class="orbit-ring outer"></view>
|
||||
<view class="orbit-ring inner"></view>
|
||||
<view class="orbit-core"></view>
|
||||
</view>
|
||||
<text class="loading-copy">{{ generationCopy }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 失败继续创作入口 -->
|
||||
<view v-if="generationStatus === 'failed' && resumeableSessionId" class="resume-action">
|
||||
<button class="generation-action primary" @click="resumeSession">继续创作</button>
|
||||
</view>
|
||||
|
||||
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入栏:novel_done 后启用 -->
|
||||
<view v-if="generationPhase === 'done'" class="chat-input-bar">
|
||||
<view class="result-input-shell">
|
||||
<textarea
|
||||
class="result-chat-input"
|
||||
v-model="chatInput"
|
||||
:auto-height="true"
|
||||
:show-confirm-bar="false"
|
||||
maxlength="500"
|
||||
confirm-type="send"
|
||||
placeholder="输入修改建议,或让 AI 继续生成"
|
||||
@confirm="sendChat"
|
||||
/>
|
||||
</view>
|
||||
<view class="chat-send-btn" :class="{ disabled: !chatInput.trim() || generating }" @click="sendChat">发送</view>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 删除独立的 result 视图块**
|
||||
|
||||
找到原 result 视图块(`<view v-else class="result-page">`,约 185 行到 302 行)。由于 chat 视图已包含所有对话功能,**整块删除**原 result-page 块。
|
||||
|
||||
删除后 template 的 v-if/v-else-if 链为:`home` → `chat`。
|
||||
|
||||
- [ ] **Step 4: 新增 chat 视图相关样式**
|
||||
|
||||
在 `<style scoped>` 部分末尾添加 chat 视图样式(复用现有 result/generation 样式,新增缺失部分):
|
||||
|
||||
```scss
|
||||
.chat-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #13091f;
|
||||
}
|
||||
|
||||
.chat-top-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.chat-scroll {
|
||||
flex: 1;
|
||||
height: calc(100vh - 180rpx);
|
||||
}
|
||||
|
||||
.chat-scroll-content {
|
||||
padding: 0 24rpx 40rpx;
|
||||
}
|
||||
|
||||
.card-answered {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 26rpx;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.outline-title {
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.outline-logline {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.outline-beats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.beat-item {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.beat-order {
|
||||
flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: #087e8b;
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.beat-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.beat-title {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.beat-summary {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 26rpx;
|
||||
margin-top: 8rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.outline-ending {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.ending-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 24rpx;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ending-text {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.outline-actions {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.outline-feedback {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.outline-buttons {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.novel-text {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.typing-cursor {
|
||||
color: #087e8b;
|
||||
animation: blink 900ms steps(1) infinite;
|
||||
}
|
||||
|
||||
.chat-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.resume-action {
|
||||
padding: 24rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-input-bar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.result-input-shell {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.result-chat-input {
|
||||
width: 100%;
|
||||
min-height: 60rpx;
|
||||
padding: 12rpx 0;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.chat-send-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #087e8b;
|
||||
border-radius: 32rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.chat-send-btn.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 验证热加载渲染**
|
||||
|
||||
保存后浏览器打开 http://localhost:5180,进入心愿页输入心愿发送,确认:
|
||||
- 页面进入 chat 视图(对话页)
|
||||
- 用户心愿作为气泡显示
|
||||
- 澄清卡片作为 AI 消息出现在对话流
|
||||
- Console 无错误
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "feat(mini-program): 统一 chat 视图,删除 clarifying/outlining/result 独立视图"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 改造 sendChat 按 generationPhase 切换接口
|
||||
|
||||
**Files:**
|
||||
- Modify: `mini-program/src/pages/main/ScriptView.vue`(sendChat 函数约 701 行)
|
||||
|
||||
- [ ] **Step 1: 改造 sendChat 根据 generationPhase 切换接口**
|
||||
|
||||
找到 `sendChat` 函数(约 701 行)。当前实现用旧接口 `streamScriptChat` + `createMessage`。
|
||||
|
||||
在函数开头插入 generationPhase 判断:如果是 `'generating'`(生成流程中,理论上 novel_done 前底部输入栏不显示,但防御性处理),直接 return;如果是 `'done'`,走原有旧接口逻辑。
|
||||
|
||||
把 `sendChat` 函数替换为:
|
||||
|
||||
```javascript
|
||||
const sendChat = async () => {
|
||||
if (!chatInput.value.trim() || generating.value) return
|
||||
const content = chatInput.value.trim()
|
||||
chatInput.value = ''
|
||||
|
||||
// novel_done 前底部输入栏不显示,此处只处理 done 态的后续对话
|
||||
if (generationPhase.value !== 'done') return
|
||||
|
||||
const messageId = currentVersionMessageId.value
|
||||
|
||||
const userMessageRes = await createMessage({
|
||||
conversationId: conversationId.value,
|
||||
userId: getCurrentUserId(),
|
||||
content,
|
||||
senderType: 'user',
|
||||
contentType: 'chat'
|
||||
})
|
||||
const userMessageId = userMessageRes.data?.id
|
||||
|
||||
resultMessages.value.push({
|
||||
id: userMessageId || createMessageId(),
|
||||
role: 'user',
|
||||
kind: 'text',
|
||||
content,
|
||||
pending: false,
|
||||
time: formatMessageTime(),
|
||||
submitted: false,
|
||||
confirmed: false
|
||||
})
|
||||
keepResultAtBottom()
|
||||
|
||||
generating.value = true
|
||||
generationStatus.value = 'streaming'
|
||||
await streamScriptChat({
|
||||
operationType: 'chat',
|
||||
conversationId: conversationId.value,
|
||||
messageId,
|
||||
userMessageId,
|
||||
content,
|
||||
scriptId: scriptId.value,
|
||||
onDelta: (delta) => {
|
||||
// 往最后一条 AI 消息追加(如有)
|
||||
const lastAssistant = [...resultMessages.value].reverse().find(m => m.role === 'assistant' && m.kind === 'novel')
|
||||
if (lastAssistant) {
|
||||
lastAssistant.content += delta
|
||||
}
|
||||
},
|
||||
onDone: () => {
|
||||
generating.value = false
|
||||
generationStatus.value = 'idle'
|
||||
// 重新加载持久化消息以获取最新版本
|
||||
loadMessages()
|
||||
},
|
||||
onError: (error) => {
|
||||
generating.value = false
|
||||
generationStatus.value = 'idle'
|
||||
uni.showToast({ title: error || '生成失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
关键变化:
|
||||
- 只在 `generationPhase === 'done'` 时处理(底部输入栏此时才显示)
|
||||
- 用户消息追加到 `resultMessages`(统一对话流)
|
||||
- `onDelta` 往最后一条 AI 消息追加文本
|
||||
|
||||
- [ ] **Step 2: 验证热加载无错误**
|
||||
|
||||
保存后浏览器确认无 Console 错误。
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add mini-program/src/pages/main/ScriptView.vue
|
||||
git commit -m "feat(mini-program): sendChat 按 generationPhase 切换接口,消息进统一对话流"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 端到端浏览器验证
|
||||
|
||||
**Files:**
|
||||
- 无代码改动,纯浏览器验证
|
||||
|
||||
- [ ] **Step 1: 确认 H5 服务运行**
|
||||
|
||||
Run: `python dev-services.py status`
|
||||
Expected: mini-program 端口 5180 运行中
|
||||
|
||||
若未运行,启动:`cd mini-program && npm run dev:h5 -- --host 127.0.0.1 --port 5180`(后台运行)
|
||||
|
||||
- [ ] **Step 2: Playwright 验证完整对话流程**
|
||||
|
||||
使用 Playwright MCP 打开 http://localhost:5180:
|
||||
|
||||
1. 登录(手机号 13800138000 + 验证码 123456)
|
||||
2. 进入心愿页,输入"今天我想当 CEO",发送
|
||||
3. **验证**:进入 chat 对话页,用户心愿作为气泡显示在对话流
|
||||
4. **验证**:澄清卡片作为 AI 消息出现在对话流(不在独立页面)
|
||||
5. 选择选项,提交
|
||||
6. **验证**:卡片变为"已回答"态,user 消息追加,下一轮澄清或大纲出现
|
||||
7. 若进入大纲:**验证**大纲作为 AI 消息出现(带确认/修改按钮)
|
||||
8. 确认大纲
|
||||
9. **验证**:小说流式生成作为一条 AI 消息逐字出现
|
||||
10. novel_done 后:**验证**底部输入栏出现,可输入修改建议
|
||||
|
||||
- [ ] **Step 3: 验证继续创作(DAILY_GENERATION_IN_PROGRESS)**
|
||||
|
||||
若触发每日限制:
|
||||
1. **验证**:对话流内显示失败提示 + "继续创作"按钮
|
||||
2. 点击"继续创作"
|
||||
3. **验证**:恢复会话,新澄清/大纲作为消息追加到对话流
|
||||
|
||||
- [ ] **Step 4: 检查 Console 和 Network**
|
||||
|
||||
- Console:无业务错误(`uni.getRecorderManager not supported` 平台限制可忽略)
|
||||
- Network:`/shortNovel/stream` 和 `/shortNovel/followup` 返回 SSE 事件流,无 4xx/5xx
|
||||
|
||||
- [ ] **Step 5: 验收报告**
|
||||
|
||||
输出验收报告:每个步骤通过/失败 + 截图证据 + 最终结论
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 提交心愿后进入 chat 对话页,用户心愿作为气泡显示
|
||||
- [ ] 澄清卡片作为 AI 消息出现在对话流(非独立页面)
|
||||
- [ ] 卡片提交后变"已回答"态,user 消息追加
|
||||
- [ ] 大纲作为 AI 消息出现,确认/修改后按钮消失
|
||||
- [ ] 小说流式生成作为一条 AI 消息逐字出现
|
||||
- [ ] novel_done 后底部输入栏出现,继续修改用旧接口工作
|
||||
- [ ] 失败时对话流内显示"继续创作"入口
|
||||
- [ ] 整个过程不切换视图,用户始终在同一对话页
|
||||
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(...)` 之后补充:
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
# Mini Program WeChat Auth Design
|
||||
|
||||
## Problem
|
||||
|
||||
The mini program currently supports phone number plus SMS code login only. The new requirement is to add full WeChat authorization login while keeping the existing phone login available. WeChat login must use the free openid-based login flow as the primary path, and phone number authorization should be integrated when available so the backend can identify and bind existing phone users automatically.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add WeChat mini program login with `wx.login`/`uni.login` and backend `code2Session` exchange.
|
||||
- Keep the existing phone plus SMS code login flow unchanged as a supported fallback and alternate login method.
|
||||
- Use WeChat openid as the free, reliable identity for mini program login.
|
||||
- If WeChat phone authorization succeeds, use the returned phone number to identify an existing user and automatically bind the WeChat openid to that account.
|
||||
- Save nickname and avatar into existing `t_user.nickname` and `t_user.avatar` fields when the user provides them.
|
||||
- Reuse the current JWT response, token storage, session restore, onboarding routing, and profile flow.
|
||||
- Match the current mini program dark glass/cosmic visual style.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not remove, hide permanently, or degrade phone SMS login.
|
||||
- Do not introduce paid third-party login or SMS providers.
|
||||
- Do not require phone number authorization before a user can log in with WeChat openid.
|
||||
- Do not redesign unrelated onboarding, main tabs, or profile features.
|
||||
- Do not store WeChat `session_key` on the client.
|
||||
|
||||
## Current Findings
|
||||
|
||||
- `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`.
|
||||
- `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`.
|
||||
- `application.yml` security ignore URLs must include the new public WeChat login endpoint, while the phone binding endpoint must remain protected by JWT.
|
||||
- `UserInfoResponse` exposes phone, nickname, and avatar, but it does not expose whether the current account has a WeChat binding. The frontend needs a non-sensitive `wechatBound` boolean instead of receiving openid.
|
||||
- Binding an openid from a temporary WeChat user to an existing phone user must also transfer or preserve data created under the temporary user; otherwise early actions before phone binding can be stranded on the temporary account.
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
Use a two-track login page:
|
||||
|
||||
1. Primary action: WeChat login. The mini program calls `uni.login({ provider: 'weixin' })`, sends the returned code to the backend, and receives the same `AuthResponse` shape used by existing login.
|
||||
2. Secondary action: phone SMS login. The current phone/code form remains available and continues to call `/auth/login`.
|
||||
|
||||
Phone authorization is a post-login enhancement:
|
||||
|
||||
1. After WeChat login succeeds, show or surface a "bind phone" action using WeChat `open-type="getPhoneNumber"` when the platform supports it.
|
||||
2. The mini program sends the phone authorization `code` to the backend.
|
||||
3. The backend calls WeChat's phone-number API.
|
||||
4. If the phone already belongs to an existing user, bind the current openid to that existing user and return a fresh `AuthResponse` for that existing user.
|
||||
5. If the phone does not exist, write it to the current WeChat user and return updated user info.
|
||||
|
||||
This keeps WeChat openid login free and resilient while letting phone number authorization unify identities when available.
|
||||
|
||||
## Backend Design
|
||||
|
||||
### Configuration
|
||||
|
||||
Add configuration under `emotion.wechat.mini-program`:
|
||||
|
||||
- `app-id`
|
||||
- `app-secret`
|
||||
- `base-url`, default `https://api.weixin.qq.com`
|
||||
|
||||
Secrets must come from environment/profile configuration in deploy environments. `app-secret` must not be sent to the client or logged.
|
||||
|
||||
Add a database migration for WeChat identity integrity:
|
||||
|
||||
- Add a unique composite index on `t_user(third_party_type, third_party_id)`.
|
||||
- MySQL allows multiple `NULL` values in a unique index, so ordinary phone users without WeChat binding are unaffected.
|
||||
- Before transferring an openid from a temporary WeChat user to an existing phone user, clear the temporary user's third-party fields so the unique index remains valid.
|
||||
|
||||
### DTOs
|
||||
|
||||
Add request/response DTOs:
|
||||
|
||||
- `WechatLoginRequest`
|
||||
- `code`: required login code from `uni.login`.
|
||||
- `nickname`: optional user-provided nickname.
|
||||
- `avatar`: optional user-provided avatar URL.
|
||||
|
||||
- `WechatPhoneBindRequest`
|
||||
- `code`: required phone authorization code from `getPhoneNumber`.
|
||||
|
||||
- The response can reuse `AuthResponse` for login and phone binding so token handling stays consistent.
|
||||
- Extend `UserInfoResponse` with `wechatBound: boolean`. It must be computed from the server-side third-party fields and must not expose openid or session key.
|
||||
|
||||
### Service Boundaries
|
||||
|
||||
Add `WechatMiniProgramService` for WeChat API calls:
|
||||
|
||||
- `code2Session(code)` returns openid/session metadata.
|
||||
- `getStableAccessToken()` obtains and caches the mini program interface token in Redis.
|
||||
- `getPhoneNumber(phoneCode)` returns the verified phone number.
|
||||
- Translate WeChat API error codes into clear `BusinessException` messages.
|
||||
|
||||
Add `UserIdentityMergeService` for account convergence:
|
||||
|
||||
- `mergeTemporaryWechatUserIntoPhoneUser(String temporaryWechatUserId, String phoneUserId)` transfers user-owned rows from the temporary WeChat user to the canonical phone user.
|
||||
- The service updates the core mini-program tables that store `user_id`: `t_user_profile`, `t_life_event`, `t_epic_script`, `t_life_path`, `t_life_path_step`, `t_social_content_item`, `t_social_profile_insight`, `t_user_consent_log`, `t_tts_task`, and `t_analytics_event`.
|
||||
- It also updates user-facing shared data where appropriate: `t_conversation`, `t_message`, `t_coze_api_call`, `t_emotion_analysis`, `t_emotion_record`, `t_diary_post`, `t_diary_comment`, `t_community_post`, `t_comment`, `t_user_stats`, and `t_ai_call_log`.
|
||||
- If both users have a singleton row such as `t_user_profile` or `t_user_stats`, keep the existing phone user's row and only transfer rows that do not conflict. This avoids overwriting a mature phone account with sparse temporary data.
|
||||
|
||||
Extend `AuthService` with:
|
||||
|
||||
- `wechatLogin(WechatLoginRequest request)`
|
||||
- `bindWechatPhone(String currentUserId, WechatPhoneBindRequest request)`
|
||||
|
||||
Extend `UserService` with focused lookup helpers:
|
||||
|
||||
- `getByThirdParty(String thirdPartyType, String thirdPartyId)`
|
||||
- `getByPhone(String phone)` already exists and should be reused for phone identity matching.
|
||||
|
||||
The controller stays thin and delegates all identity decisions to the service layer.
|
||||
|
||||
### Identity Matching
|
||||
|
||||
For WeChat login:
|
||||
|
||||
1. Exchange login code for openid.
|
||||
2. Query user by `third_party_type = "wechat-mp"` and `third_party_id = openid`.
|
||||
3. If found and enabled, issue tokens for that user.
|
||||
4. If not found, create a user:
|
||||
- `account = "wx_" + openid`
|
||||
- `username =` generated username
|
||||
- `nickname =` request nickname or generated username
|
||||
- `avatar =` request avatar if present
|
||||
- `third_party_type = "wechat-mp"`
|
||||
- `third_party_id = openid`
|
||||
- `member_level = "free"`
|
||||
- `status = 1`
|
||||
|
||||
For phone binding:
|
||||
|
||||
1. Require a valid current JWT.
|
||||
2. Resolve the current WeChat user's openid from `third_party_id`.
|
||||
3. Exchange the phone authorization code for phone number.
|
||||
4. Query existing user by phone.
|
||||
5. If no phone user exists, set current user's `phone` and mark verified.
|
||||
6. If the phone user is the current user, ensure openid and phone are both present.
|
||||
7. If another enabled user owns the phone, treat that phone user as canonical.
|
||||
8. If the phone user already has a different WeChat openid, reject with a clear "phone already bound to another WeChat account" message.
|
||||
9. If the phone user can accept the binding, transfer user-owned rows from the temporary WeChat user to the phone user, clear the temporary user's third-party fields, bind the openid to the phone user, and issue new tokens for the phone user.
|
||||
|
||||
When the phone belongs to an existing account, the existing account is the canonical identity. The response must return tokens for that account so future openid login lands on the same user as phone SMS login.
|
||||
|
||||
## Frontend Design
|
||||
|
||||
### Login Page
|
||||
|
||||
Update `mini-program/src/pages/login/index.vue` while preserving the current visual language:
|
||||
|
||||
- Keep the dark background, glass card, purple accents, and safe-area handling.
|
||||
- Add a primary WeChat login button at the top of the card.
|
||||
- Keep the existing phone number and SMS code form as an alternate method below a subtle divider.
|
||||
- Use one `loading` state for the active login method and prevent duplicate taps.
|
||||
- On successful login, reuse current routing:
|
||||
- has profile -> `/pages/main/index?tab=script`
|
||||
- no profile -> `/pages/onboarding/index`
|
||||
|
||||
### Frontend Services And Store
|
||||
|
||||
Add to `mini-program/src/services/auth.js`:
|
||||
|
||||
- `wechatLogin({ code, nickname, avatar })`
|
||||
- `bindWechatPhone({ code })`
|
||||
|
||||
Both methods store returned tokens exactly like existing `login()` and `refreshToken()`.
|
||||
|
||||
Add to `mini-program/src/stores/app.js`:
|
||||
|
||||
- `loginWithWechat(profilePayload)`
|
||||
- `bindWechatPhone(phoneCode)`
|
||||
- `fetchCurrentUserInfo()`, called after login, token restore, and phone binding.
|
||||
- Keep `login(phone, smsCode)` unchanged.
|
||||
- After phone binding returns new tokens, refresh current profile/user state.
|
||||
|
||||
### Phone Authorization UI
|
||||
|
||||
Add a small phone-bind entry in the logged-in experience, preferably in `MineView.vue` or onboarding completion area:
|
||||
|
||||
- Show only when logged in, `userInfo.phone` is empty, and `userInfo.wechatBound` is true.
|
||||
- Use WeChat `button open-type="getPhoneNumber"` on mp-weixin.
|
||||
- On non-WeChat/H5 builds, show the existing phone SMS login/bind fallback.
|
||||
- If authorization succeeds and backend returns tokens for an existing phone user, overwrite local tokens and refresh profile.
|
||||
- If the user denies authorization, keep them logged in and show a non-blocking message.
|
||||
|
||||
### Nickname And Avatar
|
||||
|
||||
Use WeChat's current nickname/avatar collection pattern in profile or onboarding:
|
||||
|
||||
- Nickname input can populate existing `registrationData.nickname`.
|
||||
- Avatar selection can populate existing avatar fields.
|
||||
- Save through existing user/profile update endpoints where possible.
|
||||
- Do not block WeChat openid login on nickname or avatar.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Missing backend WeChat appid/secret: backend returns configuration error; frontend keeps phone login available.
|
||||
- `uni.login` failure: show a concise "WeChat login failed, please try again later" message and keep phone login visible.
|
||||
- `code2Session` failure or invalid code: show a concise login failure message.
|
||||
- Disabled user: reject login consistently with existing auth behavior.
|
||||
- Phone authorization denied: do not log out; show a non-blocking message that the user can bind a phone later in the profile center.
|
||||
- Phone API unavailable or permission not enabled: keep openid login active and route user normally.
|
||||
- Existing phone is bound to another openid: reject binding and ask user to use phone login or contact support.
|
||||
- Existing phone account has data while temporary WeChat account also has data: transfer non-conflicting temporary rows to the phone account before switching tokens.
|
||||
- Temporary WeChat account after successful merge: clear its `third_party_type` and `third_party_id` and disable it from future WeChat login matching.
|
||||
- Network failures: preserve current session restore behavior and avoid clearing tokens unless backend explicitly rejects auth.
|
||||
|
||||
## Data Flow
|
||||
|
||||
### WeChat Login
|
||||
|
||||
1. User taps WeChat login.
|
||||
2. Mini program calls `uni.login`.
|
||||
3. Mini program posts `{ code }` to `/auth/wechat/login`.
|
||||
4. Backend exchanges code for openid through WeChat.
|
||||
5. Backend finds or creates user by openid.
|
||||
6. Backend returns `AuthResponse`.
|
||||
7. Mini program stores tokens, fetches profile, and routes.
|
||||
|
||||
### Phone Auto-Binding
|
||||
|
||||
1. Logged-in user taps authorize phone.
|
||||
2. WeChat returns a phone authorization code.
|
||||
3. Mini program posts `{ code }` to `/auth/wechat/phone`.
|
||||
4. Backend exchanges code for phone number.
|
||||
5. Backend finds existing user by phone.
|
||||
6. Backend binds openid to the phone user or writes phone to current user.
|
||||
7. If an existing phone user is canonical, backend transfers non-conflicting temporary user data and clears the temporary WeChat identity.
|
||||
8. Backend returns `AuthResponse`.
|
||||
9. Mini program replaces tokens and refreshes user/profile state.
|
||||
|
||||
## Testing
|
||||
|
||||
Backend:
|
||||
|
||||
- WeChat login creates a new user for a new openid.
|
||||
- WeChat login returns the same user for a known openid.
|
||||
- Phone binding writes phone to current WeChat user when no existing phone user exists.
|
||||
- Phone binding binds openid to an existing phone user and returns that user's tokens.
|
||||
- Phone binding transfers temporary WeChat user data to the existing phone user before returning phone-user tokens.
|
||||
- Temporary WeChat user no longer matches future openid login after a successful transfer.
|
||||
- Phone binding rejects a phone user already bound to a different openid.
|
||||
- Existing phone SMS login still works.
|
||||
- Token validation and refresh still work after WeChat login.
|
||||
- `/api/auth/wechat/login` is public, and `/api/auth/wechat/phone` rejects unauthenticated requests.
|
||||
- `UserInfoResponse` returns `wechatBound` without returning openid.
|
||||
|
||||
Mini program:
|
||||
|
||||
- Build `npm run build:mp-weixin`.
|
||||
- Login page supports both WeChat and phone SMS login.
|
||||
- WeChat login success routes through the current profile/onboarding rules.
|
||||
- Phone login success routes through the current profile/onboarding rules.
|
||||
- Denying phone authorization does not break logged-in state.
|
||||
- Successful phone authorization refreshes tokens and profile.
|
||||
- Personal center reflects nickname/avatar/phone state where available.
|
||||
- Restored sessions fetch current user info so phone binding visibility is correct after app restart.
|
||||
|
||||
## References
|
||||
|
||||
- WeChat Mini Program `code2Session`: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
|
||||
- WeChat Mini Program phone number API: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html
|
||||
- WeChat Mini Program avatar/nickname capability: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
author: 部署脚本修复会话
|
||||
created_at: 2026-06-02
|
||||
purpose: 修复 deploy.py 的 nginx 站点启用命令 30s 超时问题(防御性诊断 + 清理)
|
||||
---
|
||||
|
||||
# 修复 deploy.py nginx 站点启用超时 — 设计文档
|
||||
|
||||
## 问题陈述
|
||||
|
||||
执行 `python deploy.py` 部署时,nginx 站点启用步骤失败:
|
||||
|
||||
```
|
||||
[INFO] 启用站点配置...
|
||||
[ERROR] 启用站点失败: 命令执行超时 (30s):
|
||||
ssh ... root@101.200.208.45
|
||||
ln -snf /etc/nginx/sites-available/lifescript.happylifeos.com.conf
|
||||
/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf
|
||||
&& rm -f /etc/nginx/sites-enabled/default
|
||||
```
|
||||
|
||||
**根因分析**(用户已确认):之前能成功部署,本次突然失败。`ln -snf` 目标在远程服务器上已存在为**目录**或**断链**(非空 symlink 指向已被删除/修改的源文件),`ln` 在某些情况下会递归进入等待而 hang。`&&` 链式让 `rm -f default` 也永不执行。30s 总超时被耗尽。
|
||||
|
||||
## 方案选择
|
||||
|
||||
经过 brainstorming 对话,用户选择 **方案 A:防御性诊断 + 清理**(修改脚本使其对异常状态自愈,不依赖手动干预)。
|
||||
|
||||
**未选方案**:
|
||||
- 方案 B(仅手动清理):不解决根本问题,未来重现概率高
|
||||
- 方案 C(手动 + 脚本加固):用户已先用方案 A 的脚本修复,验证后再决定是否需要手动清理
|
||||
- 方案 D(仅增超时):绕过问题,不修复根因
|
||||
|
||||
## 设计
|
||||
|
||||
### 核心思路
|
||||
|
||||
将 `ln -snf ... && rm -f ...` 的**链式单命令**拆为**多步独立 SSH 调用**,每步:
|
||||
- 独立超时(30s 对单步足够)
|
||||
- 独立返回 `ok/err/stderr`
|
||||
- 失败立即停止并打印详细诊断
|
||||
- 前置诊断 + 主动清理 hang 源(目录/断链)
|
||||
|
||||
### 新增函数 `enable_nginx_site(remote_conf, domain)`
|
||||
|
||||
签名:
|
||||
```python
|
||||
def enable_nginx_site(remote_conf: str, domain: str) -> bool:
|
||||
"""幂等地启用 nginx 站点(sites-enabled symlink)。
|
||||
|
||||
流程:
|
||||
1. 诊断目标路径当前状态
|
||||
2. 清理异常状态(目录 / 文件 / 断链)
|
||||
3. 创建 symlink
|
||||
4. 移除 default 站点
|
||||
5. 验证结果
|
||||
|
||||
Returns: True 成功;False 失败(stderr 已打印)
|
||||
"""
|
||||
```
|
||||
|
||||
### 5 步骤流程
|
||||
|
||||
| 步骤 | SSH 命令 | 失败处理 | 失败时是否继续 |
|
||||
|---|---|---|---|
|
||||
| 1. 诊断 | `test -e <target> && (test -L <target> && echo "symlink" \|\| (test -d <target> && echo "dir" \|\| echo "file")) \|\| echo "missing"` | 仅诊断,不失败(stdout 捕获到 `status` 变量) | — |
|
||||
| 2. 清理 | `case "$status" in symlink) unlink <target> ;; dir) rm -rf <target> ;; file) rm -f <target> ;; missing) ;; esac`(`$status` 是步骤 1 输出,通过 Python f-string 注入 SSH 命令) | 失败时打印 stderr + 提示"权限可能不足" | **否**(清理失败则终止,不重建链) |
|
||||
| 3. 建链 | 两段式:先 `if [ -e <target> ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi`,再 `ln -snf {remote_conf} <target>` | 失败时区分:二次检查失败 → 提示"并发进程干扰";ln 自身失败 → 提示"scp_file 是否成功"或"权限不足" | — |
|
||||
| 4. 清默认 | `rm -f /etc/nginx/sites-enabled/default` | 失败时**仅 warning**(不阻塞):打印"default 站点可能仍存在,请在步骤 5 验证时确认" | — |
|
||||
| 5. 验证 | `readlink /etc/nginx/sites-enabled/{domain}.conf && (test ! -e /etc/nginx/sites-enabled/default && echo "default:OK" \|\| echo "default:STILL_EXISTS") && ls -la /etc/nginx/sites-enabled/` | 失败时打印实际值与期望值(含 default 状态) | — |
|
||||
|
||||
**步骤间值传递**(Python 端实现):
|
||||
```python
|
||||
# 步骤 1:执行诊断,stdout 捕获为 status
|
||||
ok, status, err = ssh_command(diagnose_cmd, capture=True)
|
||||
status = status.strip() # 期望值: "symlink" | "dir" | "file" | "missing"
|
||||
|
||||
# 步骤 2:用 f-string 注入 status 到 case 语句
|
||||
cleanup_cmd = f'case "{status}" in symlink) unlink {target} ;; ...) ;; esac'
|
||||
ok, _, err = ssh_command(cleanup_cmd, capture=True)
|
||||
if not ok:
|
||||
log_error("清理步骤失败")
|
||||
return False # 短路
|
||||
```
|
||||
|
||||
**关键决策**:
|
||||
- **步骤 2 用 `test -e/-L/-d/-f` 替代 `ls -ld` 解析**:shell 内置 test 比解析 `ls` 输出更可靠,跨 Unix 工具版本差异小
|
||||
- **步骤 2 失败则短路终止**:清理失败时建链毫无意义(脏状态仍在),立即返回 False 让用户介入
|
||||
- **有效 symlink 走 `unlink` 而非 `rm -f`**:与目录/文件区分对待,避免误删有效链接
|
||||
- **缺失(missing)跳过清理**:直接进建链步骤,是最常见的健康场景
|
||||
- **步骤 3 加 `test ! -e <target>` 二次防护**:极端并发场景(步骤 2 清理后、步骤 3 执行前有其他进程瞬间创建同名项)下,`ln -snf` 仍会 hang;前置 test 检查 + 失败时明确提示"并发进程干扰",避免静默 hang
|
||||
- **步骤 3 用两段式而非 `&& ... ||` 链**:避免 `ln` 自身失败(如权限不足)被误报为"二次检查失败",错误信息更精准
|
||||
- **步骤 4 失败仅 warning 不短路**:default 站点清理是非关键操作(即使存在也不阻塞 symlink 启用),失败时打印 warning 告知"default 站点可能仍存在",由用户在步骤 5 验证时确认
|
||||
- **步骤 5 验证同时检查 default**:除 `readlink` 验证 symlink 外,追加 `test ! -e /etc/nginx/sites-enabled/default` 确认 default 站点已清理;失败时同时打印 symlink 状态和 default 状态
|
||||
|
||||
**超时设置**:每步独立调用 `ssh_command(cmd, timeout=30)`,与现有 `ssh_command()` 默认值一致。**不修改** `ssh_command()` / `run_ssh_args()` 底层,也不需要新增超时参数。30s 对单步 SSH 命令(不涉及大文件传输)足够;如未来出现网络慢问题,可在调用处单独覆盖(如 `ssh_command(cmd, timeout=60)`)。
|
||||
|
||||
### 集成位置
|
||||
|
||||
修改 `G:\IdeaProjects\emotion-museun\deploy.py`:
|
||||
|
||||
- **第 250-256 行**:替换 `ln -snf ... && rm -f ...` 块为 `enable_nginx_site(remote_conf, DOMAIN)` 调用
|
||||
- **第 232-266 行**(`deploy_nginx` 函数)保持其他逻辑不变(scp_file 上传 conf、nginx -t、reload)
|
||||
- **新增函数** `enable_nginx_site` 放在 `deploy_nginx` 之前
|
||||
- **实施方式**:5 步都是直接调用现有 `ssh_command()`(不修改底层),只是用 Python `if/elif/else` 在函数内串起来
|
||||
|
||||
### 变量安全校验
|
||||
|
||||
`enable_nginx_site(remote_conf, domain)` 函数入口处对 `domain` 和 `remote_conf` 都做基本校验,防止变量污染导致误操作:
|
||||
|
||||
```python
|
||||
import re
|
||||
_SAFE_DOMAIN = re.compile(r'^[a-zA-Z0-9.-]+$') # domain 字符集
|
||||
_SAFE_REMOTE_CONF = re.compile(r'^[a-zA-Z0-9./_-]+$') # 路径字符集(允许下划线)
|
||||
|
||||
if not domain or not _SAFE_DOMAIN.match(domain):
|
||||
log_error(f"非法 domain 名称: {domain!r}")
|
||||
return False
|
||||
# domain 额外结构校验:禁止首尾连字符/点、禁止连续点(非法 TLD)
|
||||
if domain.startswith('-') or domain.endswith('-') \
|
||||
or domain.startswith('.') or domain.endswith('.') \
|
||||
or '..' in domain:
|
||||
log_error(f"非法 domain 结构: {domain!r} (首尾连字符/点 或 连续点非法)")
|
||||
return False
|
||||
|
||||
if not remote_conf or not _SAFE_REMOTE_CONF.match(remote_conf):
|
||||
log_error(f"非法 remote_conf 路径: {remote_conf!r}")
|
||||
return False
|
||||
# 路径额外结构校验:禁止连续斜杠(路径注入)、禁止 .. 路径遍历
|
||||
if '//' in remote_conf or '..' in remote_conf:
|
||||
log_error(f"remote_conf 包含可疑路径片段: {remote_conf!r}")
|
||||
return False
|
||||
```
|
||||
|
||||
校验规则:
|
||||
- `domain` 非空,仅含字母、数字、点、连字符;**首尾不能是 `-` 或 `.`;不能含 `..` 连续点**
|
||||
- `remote_conf` 非空,仅含字母、数字、`/`、点、连字符、下划线;**不能含 `//` 连续斜杠或 `..` 路径遍历**
|
||||
- 不含 `;`、`&`、`|`、`$`、反引号、空格等 shell 特殊字符
|
||||
|
||||
虽然 `remote_conf` 当前来源固定(f-string 拼接),但**未来重构时**(如改为从配置文件读取)这个校验能防止意外引入危险字符。
|
||||
|
||||
### 错误信息改进
|
||||
|
||||
`enable_nginx_site` 内部用统一的 `log_error` 输出结构化信息,**使用描述性名称而非"步骤 N"**:
|
||||
|
||||
```
|
||||
[ERROR] 启用站点失败 - 建链步骤: Permission denied
|
||||
[ERROR] 上下文: 目标 = /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf
|
||||
[ERROR] 提示: 确认 scp_file 是否成功上传了 sites-available 下的 conf 文件
|
||||
```
|
||||
|
||||
各步骤使用的描述性名称:
|
||||
- 诊断步骤
|
||||
- 清理步骤
|
||||
- 建链步骤
|
||||
- 清默认步骤
|
||||
- 验证步骤
|
||||
|
||||
替代当前的"命令执行超时 (30s)",便于快速定位卡在哪一步。
|
||||
|
||||
## 范围
|
||||
|
||||
### 包含
|
||||
|
||||
- 修改 `deploy.py` 的 `deploy_nginx()` 函数调用方式
|
||||
- 新增 `enable_nginx_site()` 函数(约 50-70 行)
|
||||
- 错误信息结构化改进
|
||||
|
||||
### 不包含(明确排除)
|
||||
|
||||
- 不修改其他 deploy.py 子脚本(server/web/web-admin/life-script)
|
||||
- 不改 `ssh_command()` / `run_ssh_args()` 底层
|
||||
- 不增加 `--dry-run` 模式
|
||||
- 不改 nginx 配置文件本身
|
||||
- 不改默认 30s 超时(单步足够,问题在链式 hang,不在网络慢)
|
||||
|
||||
## 验证计划
|
||||
|
||||
### 单元级(脚本内)
|
||||
|
||||
部署后通过以下方式验证:
|
||||
|
||||
1. **干净服务器场景**:远程 `/etc/nginx/sites-enabled/{domain}.conf` 不存在 → 期望成功
|
||||
2. **断链场景**:手动 `ln -s /nonexistent /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 清理 + 步骤 3 重建
|
||||
3. **目录场景**:手动 `mkdir /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 `rm -rf` 清理 + 步骤 3 成功
|
||||
4. **文件场景**:手动 `touch /etc/nginx/sites-enabled/{domain}.conf` → 期望步骤 2 `rm -f` 清理 + 步骤 3 成功
|
||||
5. **正常 symlink 场景**:手动重建有效 symlink → 期望步骤 2 unlink + 步骤 3 重建
|
||||
6. **权限不足场景**:以非 root 用户运行 deploy.py(或临时 `chmod 000 /etc/nginx/sites-enabled/{domain}.conf` 父目录) → 期望步骤 2 正确报错 + 短路终止,不进入步骤 3
|
||||
7. **并发干扰场景**(极难手动复现,可选):在步骤 2 清理和步骤 3 之间用 `watch` 命令持续创建同名目录 → 期望步骤 3 二次防护触发,提示"并发进程干扰"
|
||||
|
||||
### 集成级
|
||||
|
||||
跑 `python deploy.py nginx` 完整流程,期望:
|
||||
- 不再出现"启用站点失败"
|
||||
- `/etc/nginx/sites-enabled/{domain}.conf` 是有效 symlink
|
||||
- `/etc/nginx/sites-enabled/default` 不存在
|
||||
- `nginx -t` 通过
|
||||
- `systemctl reload nginx` 成功
|
||||
|
||||
### 验证前置
|
||||
|
||||
修复后**必须**执行 `python deploy.py nginx`(或 `python deploy.py all`)进行端到端验证,**不能仅靠代码 review**。这是用户在前次 PATH 修复中明确要求的规则(CLAUDE.md "验证规则(强制)")。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
### 风险
|
||||
|
||||
1. **远程 rm -rf 误删**:仅针对 `/etc/nginx/sites-enabled/{domain}.conf` 单个路径(已通过 `{domain}` 限定 + 变量安全校验),不会影响其他站点
|
||||
2. **步骤 2 误判**:如果 `test -e/-L/-d/-f` 在远程 shell 版本异常或文件系统不支持(如 NFS 边缘情况)时输出不符合预期 → 步骤 3 二次防护 `test ! -e` 仍能检测到残留,整体仍自愈
|
||||
3. **SSH 连接问题掩盖**:如果 SSH 本身不通,新代码会立即在步骤 1 报告 stderr,不会比原代码更差
|
||||
4. **并发进程干扰**:步骤 2 清理和步骤 3 之间有其他进程创建同名目录 → 步骤 3 二次防护触发,提示"并发进程干扰"而非静默 hang
|
||||
5. **变量污染**:通过变量安全校验(domain + remote_conf)防御,即使未来重构时从配置读取也不会引入 shell 注入
|
||||
|
||||
### 回滚
|
||||
|
||||
- 改动局限在 `deploy.py` 单文件、新增 1 个函数
|
||||
- 如新代码有问题,`git checkout deploy.py` 即可恢复
|
||||
|
||||
## 后续可选增强(不在本次范围)
|
||||
|
||||
- `enable_nginx_site` 通用化到 `disable_nginx_site()`,支持 `deploy.py nginx disable <domain>`
|
||||
- 把 `enable_nginx_site` 提取到独立模块(`deploy_nginx_helpers.py`),便于其他 deploy.py 复用
|
||||
- 增加 `--dry-run` 模式,仅打印将执行命令不实际执行
|
||||
- 步骤级重试机制(如步骤 3 失败时自动重试 1 次)
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `G:\IdeaProjects\emotion-museun\deploy.py` — 主修改目标(第 232-266 行 `deploy_nginx`,新增 `enable_nginx_site`)
|
||||
- `G:\IdeaProjects\emotion-museun\conf\emotion-museum.conf` — nginx 配置文件(不修改)
|
||||
- `G:\IdeaProjects\emotion-museun\docs\superpowers\specs\2026-06-02-deploy-nginx-site-enable-fix-design.md` — 本设计文档
|
||||
@@ -0,0 +1,63 @@
|
||||
# 登录后路由跳转修复设计文档
|
||||
|
||||
## 问题描述
|
||||
|
||||
当前小程序登录成功后,系统会根据用户是否已有资料(`hasProfile`)进行分支路由:
|
||||
- 有资料 → 爽文生成页面 (`/pages/main/index?tab=script`)
|
||||
- 无资料 → 编辑资料页面 (`/pages/onboarding/index`)
|
||||
|
||||
这导致新用户或资料不完整的用户登录后被迫进入编辑资料页面,而非核心的爽文生成页面。产品要求登录后统一进入爽文生成页,不再强制填写资料。
|
||||
|
||||
## 方案概述
|
||||
|
||||
采用**方案 A**:直接移除登录后的资料检查,统一路由到爽文生成页。
|
||||
|
||||
编辑资料页面(`onboarding/index.vue`)继续保留,用户可从"我的" tab 中的编辑资料入口主动进入。
|
||||
|
||||
## 具体改动
|
||||
|
||||
### 1. `mini-program/src/pages/login/index.vue`
|
||||
|
||||
修改 `routeAfterLogin` 函数,移除 `profileReady` 参数和条件判断,登录成功后一律跳转至爽文生成页:
|
||||
|
||||
```javascript
|
||||
const routeAfterLogin = () => {
|
||||
uni.redirectTo({ url: '/pages/main/index?tab=script' })
|
||||
}
|
||||
```
|
||||
|
||||
调用处同步更新:
|
||||
- `handleWechatLogin` 中:`routeAfterLogin()`
|
||||
- `handleLogin` 中:`routeAfterLogin()`
|
||||
|
||||
### 2. `mini-program/src/pages/splash/index.vue`
|
||||
|
||||
修改 `resolveInitialRoute` 函数中已登录用户的跳转逻辑:
|
||||
|
||||
```javascript
|
||||
if (session.status === store.SESSION_STATUS.AUTHENTICATED) {
|
||||
routeOnce('/pages/main/index?tab=script', { reason: session.reason })
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
不再根据 `session.hasProfile` 进行分支。
|
||||
|
||||
### 3. 保留内容
|
||||
|
||||
- `stores/app.js` 中的 `hasProfile` computed 属性**不做修改**,它仍可在其他场景(如判断是否需要引导补充资料)中使用。
|
||||
- `onboarding/index.vue` 页面**不做修改**,保留为独立的编辑资料页面。
|
||||
|
||||
## 数据流影响
|
||||
|
||||
- 登录流程:`login/splash` → `main/index?tab=script`
|
||||
- 资料编辑入口:用户主动从"我的" tab 进入 `profile/index` 或 `onboarding/index`
|
||||
- 无后端接口变更
|
||||
|
||||
## 测试验证
|
||||
|
||||
1. 新用户手机号登录成功后,直接进入爽文生成页面(底部 tab 高亮"爽文生成")
|
||||
2. 新用户微信登录成功后,同样直接进入爽文生成页面
|
||||
3. 已登录用户冷启动(splash 页恢复会话),直接进入爽文生成页面
|
||||
4. 从"我的" tab 点击编辑资料,仍可正常进入资料编辑页面
|
||||
5. 资料编辑页面保存后,仍可正常返回爽文生成页面
|
||||
@@ -0,0 +1,110 @@
|
||||
# 个人中心页面动态数据修复设计文档
|
||||
|
||||
## 问题描述
|
||||
|
||||
小程序"我的"(个人中心)页面(`MineView.vue`)当前显示的是写死的固定信息,而非当前登录用户的实际数据:
|
||||
|
||||
- 头像区域显示纯 CSS 绘制的通用人形图标,未使用用户头像
|
||||
- 昵称 fallback 为固定值 `'张义明'`
|
||||
- 身份标签 fallback 为固定值 `'ENFJ · 狮子座 · 星民'`
|
||||
- 统计数据 `觉醒深度 Lv.4` 和 `星历契合 98%` 为硬编码
|
||||
|
||||
## 方案概述
|
||||
|
||||
采用**方案 A**:从现有 store 数据动态生成头像和统计,替换所有写死内容。仅修改 `MineView.vue`,无后端改动。
|
||||
|
||||
## 具体改动
|
||||
|
||||
### 1. `mini-program/src/pages/main/MineView.vue`
|
||||
|
||||
#### 头像区域
|
||||
|
||||
用 `<image>` 组件替换 CSS 人形图标(`.person-head` 和 `.person-body`)。
|
||||
|
||||
新增 `avatarUrl` computed:
|
||||
```javascript
|
||||
const avatarUrl = computed(() => {
|
||||
const seed = profile.value.nickname || 'user'
|
||||
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(seed)}&backgroundColor=b982ff`
|
||||
})
|
||||
```
|
||||
|
||||
模板中:
|
||||
```html
|
||||
<image class="avatar-img" :src="avatarUrl" mode="aspectFill" />
|
||||
```
|
||||
|
||||
保留原有 `.avatar-circle` 的圆形边框和发光效果。
|
||||
|
||||
#### 昵称与身份标签
|
||||
|
||||
- `displayName` 的 fallback 从 `'张义明'` 改为 `'未设置昵称'`
|
||||
- `metaLine`:当 `mbti`、`zodiac`、`profession` 均为空时显示 `'点击设置个人档案'`;否则按 `${mbti} · ${zodiac} · ${identity}` 拼接
|
||||
|
||||
#### 统计数据
|
||||
|
||||
**觉醒深度**:基于 `store.events.length` 映射等级:
|
||||
| 事件数量 | 等级 |
|
||||
|---------|------|
|
||||
| 0 | Lv.1 |
|
||||
| 1-2 | Lv.2 |
|
||||
| 3-5 | Lv.3 |
|
||||
| 6-9 | Lv.4 |
|
||||
| 10+ | Lv.5 |
|
||||
|
||||
```javascript
|
||||
const awakenLevel = computed(() => {
|
||||
const count = store.events?.length || 0
|
||||
if (count >= 10) return 'Lv.5'
|
||||
if (count >= 6) return 'Lv.4'
|
||||
if (count >= 3) return 'Lv.3'
|
||||
if (count >= 1) return 'Lv.2'
|
||||
return 'Lv.1'
|
||||
})
|
||||
```
|
||||
|
||||
**星历契合**:基于资料完整度计算百分比。检查字段:`nickname`、`gender`、`zodiac`、`mbti`、`profession`、`hobbies`(数组有长度计 1 分)、以及人生经历是否填写(`childhood.text`、`joy.text`、`low.text`、`future.ideal` 任一非空计 1 分)。满分 7 分。
|
||||
|
||||
```javascript
|
||||
const harmonyPercent = computed(() => {
|
||||
const hasLifeStory = profile.value.childhood?.text ||
|
||||
profile.value.joy?.text ||
|
||||
profile.value.low?.text ||
|
||||
profile.value.future?.ideal
|
||||
const fields = [
|
||||
profile.value.nickname,
|
||||
profile.value.gender,
|
||||
profile.value.zodiac,
|
||||
profile.value.mbti,
|
||||
profile.value.profession,
|
||||
profile.value.hobbies?.length,
|
||||
hasLifeStory
|
||||
]
|
||||
const filled = fields.filter(Boolean).length
|
||||
return Math.round((filled / 7) * 100) + '%'
|
||||
})
|
||||
```
|
||||
|
||||
模板中将硬编码的 `Lv.4` 和 `98%` 分别替换为 `{{ awakenLevel }}` 和 `{{ harmonyPercent }}`。
|
||||
|
||||
## 数据依赖
|
||||
|
||||
`MineView.vue` 通过 `useAppStore()` 访问 store。`main/index.vue` 在 `onMounted` 中已经调用了:
|
||||
- `store.fetchUserProfile()`
|
||||
- `store.fetchEvents()`
|
||||
|
||||
因此 `MineView.vue` 无需新增数据加载逻辑,可直接读取 `store.userProfile` 和 `store.events`。
|
||||
|
||||
## 未修改内容
|
||||
|
||||
- `stores/app.js` 不做修改
|
||||
- `services/userProfile.js` 不做修改
|
||||
- `onboarding/index.vue` 不做修改
|
||||
- 页面样式(`.profile-center`、`.stats-grid` 等)保持原样
|
||||
|
||||
## 测试验证
|
||||
|
||||
1. 有完整资料的用户进入"我的"页面,显示 Dicebear 生成头像、真实昵称、MBTI/星座/职业、根据事件数计算的觉醒深度、根据资料完整度计算的星历契合百分比
|
||||
2. 新用户(无资料)进入"我的"页面,显示默认头像、`未设置昵称`、`点击设置个人档案`、觉醒深度 Lv.1、星历契合 0%
|
||||
3. 点击"个人档案设置"仍能正常跳转到 onboarding 编辑资料页
|
||||
4. 编辑资料保存后返回"我的"页面,数据已更新
|
||||
@@ -0,0 +1,210 @@
|
||||
# 小程序用户信息去写死 + 编辑资料保存生效 — 设计文档
|
||||
|
||||
## 背景
|
||||
|
||||
小程序"人生轨迹"页和"编辑资料"页存在大量硬编码的用户信息(昵称、星座、MBTI、职业、爱好、示例事件等)。即使用户已登录并设置了真实资料,页面仍展示写死的数据。同时,编辑资料页中的 `city`/`industry`/`company`/`personalityTags`/`birthday` 字段因后端 API 不支持而无法真正保存,导致"保存成功但回显丢失"。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 人生轨迹页(`RecordView.vue`)必须展示当前登录用户的真实资料,去除一切写死数据。
|
||||
2. 编辑资料页(`onboarding/index.vue`)必须能成功保存、更新、回显用户信息。
|
||||
3. 后端扩展 `t_user_profile` 表和相关 DTO,支持前端已有的 `city`/`industry`/`company`/`personalityTags`/`birthday` 字段。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 个人中心页(`MineView.vue`)的修复不在本次任务范围内(已有独立任务)。
|
||||
- 不修改 UI 视觉风格,只改数据绑定和默认值策略。
|
||||
|
||||
## 架构与数据流
|
||||
|
||||
```
|
||||
编辑资料页 (onboarding/index.vue)
|
||||
↓ form 数据
|
||||
前端 userProfileService.transformToBackendFormat
|
||||
↓ 新增 city/industry/company/personalityTags/birthday
|
||||
后端 UserProfileController → UserProfileService
|
||||
↓
|
||||
数据库 t_user_profile (新增5个字段)
|
||||
↓
|
||||
后端返回 UserProfileResponse (含新增字段)
|
||||
↓
|
||||
前端 userProfileService.transformToFrontendFormat
|
||||
↓ store.userProfile
|
||||
人生轨迹页 (RecordView.vue) / 编辑资料页回显
|
||||
```
|
||||
|
||||
## 后端改动
|
||||
|
||||
### 1. 数据库迁移
|
||||
|
||||
文件:`sql/emotion_museum_ddl.sql`(追加)
|
||||
|
||||
```sql
|
||||
-- 用户档案扩展字段:城市、行业、公司、性格标签、生日
|
||||
alter table emotion_museum.t_user_profile
|
||||
add city varchar(100) null comment '所在城市',
|
||||
add industry varchar(100) null comment '行业',
|
||||
add company varchar(200) null comment '公司',
|
||||
add personality_tags json null comment '性格标签列表',
|
||||
add birthday date null comment '生日';
|
||||
```
|
||||
|
||||
### 2. 实体类扩展
|
||||
|
||||
文件:`server/src/main/java/com/emotion/entity/UserProfile.java`
|
||||
|
||||
新增字段(与数据库列名通过 `@TableField` 映射):
|
||||
|
||||
| 字段名 | 类型 | 数据库列 | 说明 |
|
||||
|--------|------|----------|------|
|
||||
| city | String | city | 所在城市 |
|
||||
| industry | String | industry | 行业 |
|
||||
| company | String | company | 公司 |
|
||||
| personalityTags | String | personality_tags | 性格标签 JSON 字符串 |
|
||||
| birthday | LocalDate | birthday | 生日 |
|
||||
|
||||
### 3. DTO 扩展
|
||||
|
||||
以下三个文件新增相同 5 个字段:
|
||||
|
||||
- `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 层
|
||||
|
||||
文件:`server/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java`
|
||||
|
||||
无需改动。`BeanUtils.copyProperties` 和 `BeanUtil.copyProperties(..., CopyOptions.create().setIgnoreNullValue(true))` 会自动映射同名字段。
|
||||
|
||||
## 前端改动
|
||||
|
||||
### 1. 人生轨迹页(`mini-program/src/pages/main/RecordView.vue`)
|
||||
|
||||
**移除示例事件:**
|
||||
- 删除 `sampleEvents` 数组(4 条写死的人生事件数据)。
|
||||
- `events` computed 逻辑改为:
|
||||
```js
|
||||
const events = computed(() => store.events || [])
|
||||
```
|
||||
- 空事件时保留现有空状态 UI("还没有人生轨迹")。
|
||||
|
||||
**移除硬编码默认值:**
|
||||
| 原代码 | 修改后 |
|
||||
|--------|--------|
|
||||
| `profile.nickname \|\| 'Zoey'` | `profile.nickname \|\| ''` |
|
||||
| `profile.zodiac \|\| '巨蟹座'` | `profile.zodiac \|\| ''` |
|
||||
| `profile.mbti \|\| 'ENTJ'` | `profile.mbti \|\| ''` |
|
||||
| `profile.profession \|\| '产品经理'` | `profile.profession \|\| ''` |
|
||||
| `heroTags` 默认 `['阅读', '旅行', '音乐', '创作']` | `[]` |
|
||||
| `avatar` 计算中的默认 `'Zoey'` | 保留 `'User'` 作为 Dicebear seed 的兜底(这是头像生成种子,非展示名称) |
|
||||
|
||||
**空值 UI 处理:**
|
||||
- 昵称空时,profile-info 区域不显示名称,但保留"正在成为更清晰的自己"作为固定标语(非用户数据)。
|
||||
- 星座/MBTI/职业空时,对应 meta-item 的 `meta-value` 显示 `"未设置"`。
|
||||
- 爱好空时,`hobby-text` 显示 `"未设置"`。
|
||||
|
||||
### 2. 编辑资料页(`mini-program/src/pages/onboarding/index.vue`)
|
||||
|
||||
**`syncFromStore()` 去除硬编码默认值:**
|
||||
|
||||
原逻辑在 `source` 为空时回退到大量硬编码值,改为全部回退到空值/空数组:
|
||||
|
||||
| 字段 | 原默认值 | 新默认值 |
|
||||
|------|----------|----------|
|
||||
| nickname | `''` | `''`(不变) |
|
||||
| gender | `'女'` | `''` |
|
||||
| zodiac | `'巨蟹座'` | `''` |
|
||||
| mbti | `'ENTJ'` | `''` |
|
||||
| profession | `'产品经理'` | `''` |
|
||||
| city | `'上海市'` | `''` |
|
||||
| industry | `'互联网'` | `''` |
|
||||
| company | `''` | `''`(不变) |
|
||||
| personalityTags | `['理性', '乐观', ...]` | `[]` |
|
||||
| hobbies | `['阅读', '旅行', '音乐']` | `[]` |
|
||||
| future.ideal | 长文本默认 | `''` |
|
||||
| birthday | `'1998-06-18'` | `''` |
|
||||
|
||||
**空值显示优化:**
|
||||
- `birthdayDisplay` computed:空值时返回 `"请选择生日"`。
|
||||
- `avatarUrl`:空昵称时保留默认 Dicebear 头像(seed 用 `"User"`),仅作为头像生成参数,不展示给用户。
|
||||
|
||||
**保存逻辑:**
|
||||
- `saveProfile` 逻辑不变,继续调用 `store.updateRegistration()` 和 `store.saveUserProfile()`。
|
||||
- 由于后端已扩展字段,所有字段均可正确保存。
|
||||
|
||||
### 3. 前端 Service 扩展(`mini-program/src/services/userProfile.js`)
|
||||
|
||||
**`transformToBackendFormat` 增加:**
|
||||
```js
|
||||
return {
|
||||
// ... 原有字段 ...
|
||||
city: frontendData.city || null,
|
||||
industry: frontendData.industry || null,
|
||||
company: frontendData.company || null,
|
||||
personalityTags: Array.isArray(frontendData.personalityTags)
|
||||
? JSON.stringify(frontendData.personalityTags)
|
||||
: frontendData.personalityTags,
|
||||
birthday: frontendData.birthday || null
|
||||
}
|
||||
```
|
||||
|
||||
**`transformToFrontendFormat` 增加:**
|
||||
```js
|
||||
return {
|
||||
// ... 原有字段 ...
|
||||
city: backendData.city || '',
|
||||
industry: backendData.industry || '',
|
||||
company: backendData.company || '',
|
||||
personalityTags: backendData.personalityTags
|
||||
? (typeof backendData.personalityTags === 'string'
|
||||
? JSON.parse(backendData.personalityTags)
|
||||
: backendData.personalityTags)
|
||||
: [],
|
||||
birthday: backendData.birthday || ''
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Store 扩展(`mini-program/src/stores/app.js`)
|
||||
|
||||
`registrationData` 初始结构增加:
|
||||
```js
|
||||
registrationData: {
|
||||
// ... 原有字段 ...
|
||||
city: '',
|
||||
industry: '',
|
||||
company: '',
|
||||
personalityTags: [],
|
||||
birthday: ''
|
||||
}
|
||||
```
|
||||
|
||||
## 文件变更清单
|
||||
|
||||
| 文件 | 改动类型 | 说明 |
|
||||
|------|----------|------|
|
||||
| `sql/emotion_museum_ddl.sql` | 追加 | 5 个 ALTER TABLE 语句 |
|
||||
| `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 |
|
||||
| `mini-program/src/pages/onboarding/index.vue` | 修改 | 去除硬编码默认值,优化空值展示 |
|
||||
|
||||
## 错误处理
|
||||
|
||||
- **保存失败**:`saveProfile` 已有 `uni.showToast({ title: res.error || '保存失败' })`,无需额外改动。
|
||||
- **后端字段新增后老数据兼容**:新增字段均为 `NULL`,老数据读取时前端 `transformToFrontendFormat` 会转为空字符串/空数组,不影响现有逻辑。
|
||||
- **数据库列已存在**:ALTER TABLE 添加已存在列会报错。本次为全新列名,安全。若生产环境已存在(极小概率),需人工确认。
|
||||
|
||||
## 测试验证点
|
||||
|
||||
1. 新用户首次编辑资料,所有字段留空,保存成功,回显为空。
|
||||
2. 填写城市、行业、公司、性格标签、生日后保存,重新进入编辑页,数据正确回显。
|
||||
3. 人生轨迹页未设置资料时,用户信息和爱好显示"未设置"或留空,不展示任何写死数据。
|
||||
4. 人生轨迹页无事件时,显示"还没有人生轨迹"空状态,不展示 sampleEvents。
|
||||
5. 已有用户(老数据)登录后,页面正常展示,无报错。
|
||||
@@ -0,0 +1,538 @@
|
||||
---
|
||||
author: 微信登录修复会话
|
||||
created_at: 2026-06-03
|
||||
purpose: 修复微信小程序登录 text/plain 错误(Spring RestTemplate 默认 User-Agent 触发 WAF 拦截 + 凭据硬编码安全问题)
|
||||
---
|
||||
|
||||
# 修复微信小程序登录 text/plain 错误 — 设计文档
|
||||
|
||||
## 问题陈述
|
||||
|
||||
微信小程序用户在小程序里点击「微信授权登录」时,后端 `/api/auth/wechat/login` 接口返回 500 错误:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "系统运行异常: Could not extract response: no suitable HttpMessageConverter found for response type [class com.emotion.dto.wechat.WechatCodeSessionResponse] and content type [text/plain]",
|
||||
"timestamp": 1780414444577
|
||||
}
|
||||
```
|
||||
|
||||
**业务影响**:
|
||||
- 真实用户(IP 106.39.148.160)2026-06-02 23:34:04 尝试登录失败
|
||||
- 21:22、22:11、23:06 还出现 3 次 `WARN 业务异常: 微信小程序登录未配置`(早期因 env var 未设被 `validateConfigured()` 拦截)
|
||||
- 微信登录完全不可用
|
||||
|
||||
## 根因分析
|
||||
|
||||
### 证据链
|
||||
|
||||
| # | 证据 | 结论 |
|
||||
|---|---|---|
|
||||
| 1 | 错误堆栈:`WechatMiniProgramServiceImpl.java:33` 调用 `restTemplate.getForObject(url, WechatCodeSessionResponse.class)` | ✓ 错误在微信 code2Session 调用 |
|
||||
| 2 | `application.yml:78-79` 微信配置**硬编码测试凭据**(`app-secret: 4f087bd363a4147ef543d634f9f6950d`)作为默认值 | ⚠️ **安全风险**(生产未设环境变量时会用测试 secret) |
|
||||
| 3 | `application-prod.yml` **不覆盖**微信配置 | ⚠️ 生产用 application.yml 默认值(测试 secret) |
|
||||
| 4 | `RestTemplateConfig.java:10-12` 仅 `new RestTemplate()`,13 行 | ⚠️ **无 User-Agent / Accept 头** |
|
||||
| 5 | Spring 默认 `RestTemplate` → `SimpleClientHttpRequestFactory` → `HttpURLConnection`,默认 `User-Agent: Java/17.0.x` | ❌ **触发了 WAF / CloudFlare / 边缘代理拦截** |
|
||||
| 6 | SSH+curl 测试(带 `User-Agent: curl/7.61.1`)→ `api.weixin.qq.com` 返回 `Content-Type: application/json; encoding=utf-8` | ✓ 微信 API 本身正常 |
|
||||
| 7 | 日志中 23:34:04 之前的多次 `Wechat mini program login is not configured` | 23:34 env var 设上了,进入了真实 API 调用 |
|
||||
| 8 | 已有 `docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md`(265 行完整设计) | 设计已存在,但未覆盖此 bug |
|
||||
|
||||
### 根因结论
|
||||
|
||||
**主要根因(用户已确认)**:生产环境的 WAF / CloudFlare / 边缘代理拦截了 Java 默认 `User-Agent: Java/17.0.x`,返回 `text/plain` 错误页面(这是 WAF 常见行为)。SSH+curl 用 `User-Agent: curl/7.61.1` 所以能正常通过。
|
||||
|
||||
**次要根因(必须一并修复)**:
|
||||
|
||||
1. **app-id 和 app-secret 硬编码在源码**(`wxaf2eaba72d28f0e4` / `4f087bd363a4147ef543d634f9f6950d`)—— 安全问题,必须移除默认值,强制从环境变量读取
|
||||
2. **错误日志不打印响应体** —— 看不到 text/plain 实际内容,下次出问题还是黑盒
|
||||
3. **无超时设置** —— 微信 API hang 住时整个请求线程被锁
|
||||
4. **RestTemplate 无日志拦截器** —— 出问题时无任何请求/响应可查
|
||||
5. **缺全局异常处理** —— `WechatApiException` 无 handler,会落到 RuntimeException handler 暴露内部消息
|
||||
6. **日志明文打印 openid / session_key** —— 违反项目规则第 37 条"敏感信息不得在日志中输出"
|
||||
|
||||
### 已排除假设
|
||||
|
||||
- ❌ 微信 API endpoint URL 配错:SSH+curl 直连 `https://api.weixin.qq.com/sns/jscode2session` 返回正常 JSON
|
||||
- ❌ 部署代码 ≠ 本地代码:日志中 `WechatMiniProgramServiceImpl.java:33` 与本地代码行号完全一致
|
||||
- ❌ 网络层完全不通:SSH+curl TLS 握手成功
|
||||
- ❌ `getPhoneNumber` / `getStableAccessToken` 也有 bug:当前 `WechatMiniProgramServiceImpl` 只实现了 `code2Session`(49 行),其他方法在 2026-05-31 设计文档中规划但尚未实施
|
||||
|
||||
## 目标
|
||||
|
||||
- **G1** 修复微信小程序登录 `text/plain` 错误,使用户能成功通过微信授权登录
|
||||
- **G2** 在 RestTemplate 中显式设置 `User-Agent`,绕过 WAF 对 `Java/*` 默认 UA 的拦截
|
||||
- **G3** 错误日志打印原始响应体(raw body)+ 脱敏(`openid` / `session_key` / `unionid` / `access_token` / `refresh_token`),下次类似问题能直接看到内容但不泄露敏感信息
|
||||
- **G4** 移除 `application.yml` 中微信 `app-id` 和 `app-secret` 的硬编码默认值,强制从环境变量读取
|
||||
- **G5** RestTemplate 统一配置:超时、User-Agent、Accept 头、StringHttpMessageConverter、LoggingInterceptor、ErrorHandler
|
||||
- **G6** `WechatApiException` 在 `GlobalExceptionHandler` 中有专门处理,返回脱敏的友好提示给前端
|
||||
- **G7** 不破坏其他 7 个 RestTemplate 使用点(用 `BufferingClientHttpRequestFactory` 让 body 可被多次读取)
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不改动现有 7 个 RestTemplate 使用点的业务代码(仅共享统一 bean)
|
||||
- 不做 RestTemplate 连接池(沿用 SimpleClientHttpRequestFactory,外面包一层 BufferingClientHttpRequestFactory)
|
||||
- 不重试 / 不熔断(业务层不需要)
|
||||
- 不做 phone number binding / merge logic(已有 plan 覆盖,且当前代码尚未实施)
|
||||
- 不改 mini-program 前端(纯后端 bug)
|
||||
|
||||
## 方案选择
|
||||
|
||||
经过 brainstorming 对话(澄清问题 → SSH+curl 探针 → 查 logs\server\emotion-single.log),用户选择 **方案 A:防御性增强 + 错误捕获(整合根因)**。
|
||||
|
||||
**未选方案**:
|
||||
- 方案 B(仅加 message converter):不解决根因(WAF 仍会拦截)
|
||||
- 方案 C(先用探针定位再设计):已通过日志 + SSH 探针定位根因,无需再加探针
|
||||
|
||||
## 设计
|
||||
|
||||
### 修改清单
|
||||
|
||||
| # | 文件 | 操作 | 行数估算 | 目的 |
|
||||
|---|---|---|---|---|
|
||||
| 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/status(INFO)+ 脱敏 body(DEBUG) |
|
||||
| 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。
|
||||
|
||||
### 详细设计
|
||||
|
||||
#### 1. RestTemplateConfig 重写(解决 P0-1 / P0-2 流消费问题)
|
||||
|
||||
**关键决策**:用 `BufferingClientHttpRequestFactory` 包装 `SimpleClientHttpRequestFactory`,让响应体被缓存到字节数组,可以被多次读取(Interceptor + ErrorHandler + Converter 都能读)。
|
||||
|
||||
```java
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.WechatApiLoggingInterceptor;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.BufferingClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
/**
|
||||
* 统一 RestTemplate Bean
|
||||
*
|
||||
* 设计要点:
|
||||
* 1. 用 BufferingClientHttpRequestFactory 包装 SimpleClientHttpRequestFactory,
|
||||
* 响应体被缓存到字节数组,可被 Interceptor / ErrorHandler / Converter 多次读取
|
||||
* (避免破坏其他 7 个 RestTemplate 使用点:DifyProviderAdapter、
|
||||
* CozeProviderAdapter、HttpTtsEngineClient、AsrServiceImpl、
|
||||
* ApiEndpointServiceImpl、AiChatServiceImpl、ApiTestProxyController)
|
||||
* 2. 显式设置 User-Agent 为 Mozilla/5.0,绕过 WAF / CloudFlare 对 Java/* 默认 UA 的拦截
|
||||
* 3. 显式设置连接 / 读取超时,避免线程被 hang 住
|
||||
* 4. 加 StringHttpMessageConverter(UTF_8) 支持 text/plain 响应
|
||||
* 5. 加 WechatApiLoggingInterceptor 记录请求 / 响应(脱敏)
|
||||
* 6. 加 WechatResponseErrorHandler 4xx/5xx 不抛异常
|
||||
*/
|
||||
@Bean
|
||||
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||
SimpleClientHttpRequestFactory simpleFactory = new SimpleClientHttpRequestFactory();
|
||||
simpleFactory.setConnectTimeout((int) Duration.ofSeconds(5).toMillis());
|
||||
simpleFactory.setReadTimeout((int) Duration.ofSeconds(10).toMillis());
|
||||
BufferingClientHttpRequestFactory bufferingFactory =
|
||||
new BufferingClientHttpRequestFactory(simpleFactory);
|
||||
|
||||
return builder
|
||||
.requestFactory(() -> bufferingFactory)
|
||||
.defaultHeader("User-Agent", "Mozilla/5.0 (compatible; EmotionMuseum/1.0)")
|
||||
.defaultHeader("Accept", "application/json, text/plain, */*")
|
||||
.additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8))
|
||||
.additionalInterceptors(new WechatApiLoggingInterceptor())
|
||||
.errorHandler(new WechatResponseErrorHandler())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. WechatApiLoggingInterceptor 新增(解决 P0-3 敏感信息泄露)
|
||||
|
||||
```java
|
||||
package com.emotion.interceptor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 微信 API HTTP 请求 / 响应日志拦截器
|
||||
*
|
||||
* 设计要点:
|
||||
* 1. 配合 BufferingClientHttpRequestFactory 使用,body 可被多次读取
|
||||
* 2. URL / method / status / content-type 用 INFO 级别(生产可见)
|
||||
* 3. body 用 DEBUG 级别(生产默认不输出,避免日志膨胀)
|
||||
* 4. body 中敏感字段(session_key / openid / unionid / access_token / refresh_token)脱敏
|
||||
*/
|
||||
@Slf4j
|
||||
public class WechatApiLoggingInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
private static final int MAX_BODY_LOG = 2048;
|
||||
private static final Pattern SENSITIVE_FIELDS = Pattern.compile(
|
||||
"\"(session_key|openid|unionid|access_token|refresh_token|secret|appsecret)\"\\s*:\\s*\"[^\"]*\"");
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(
|
||||
HttpRequest request, byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
log.info("[WeChatAPI] ---> {} {}", request.getMethod(), request.getURI());
|
||||
|
||||
ClientHttpResponse response = execution.execute(request, body);
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
|
||||
log.info("[WeChatAPI] <--- {} {} ({}ms) content-type={}",
|
||||
response.getStatusCode(), request.getURI(), duration,
|
||||
response.getHeaders().getContentType());
|
||||
|
||||
// body 读取(已由 BufferingClientHttpRequestFactory 缓存,可重复读)
|
||||
try {
|
||||
String bodyStr = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||
if (bodyStr.length() > MAX_BODY_LOG) {
|
||||
bodyStr = bodyStr.substring(0, MAX_BODY_LOG) + "...(truncated)";
|
||||
}
|
||||
log.debug("[WeChatAPI] body={}", desensitize(bodyStr));
|
||||
} catch (IOException e) {
|
||||
log.warn("[WeChatAPI] failed to read response body for logging", e);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏:将敏感字段值替换为 "***"
|
||||
*/
|
||||
private String desensitize(String body) {
|
||||
if (body == null || body.isEmpty()) {
|
||||
return body;
|
||||
}
|
||||
return SENSITIVE_FIELDS.matcher(body).replaceAll("\"$1\":\"***\"");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. WechatResponseErrorHandler 新增(解决 P0-2 显式 hasError)
|
||||
|
||||
```java
|
||||
package com.emotion.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.DefaultResponseErrorHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 微信 API 错误处理器
|
||||
*
|
||||
* 设计要点:
|
||||
* 1. 显式重写 hasError() 委托父类判断(4xx/5xx → true),让 handleError() 在错误时被调用
|
||||
* 2. handleError() 不抛 RestClientException,由业务层(WechatMiniProgramServiceImpl)
|
||||
* 根据 status code 判断如何处理
|
||||
* 3. 不在此处读 body(避免流消费,由 LoggingInterceptor 统一处理日志)
|
||||
*/
|
||||
@Slf4j
|
||||
public class WechatResponseErrorHandler extends DefaultResponseErrorHandler {
|
||||
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
// 委托父类 DefaultResponseErrorHandler.hasError():4xx/5xx 返回 true,其他返回 false
|
||||
return super.hasError(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
// 不抛异常;body 已在 LoggingInterceptor 中读取并脱敏记录
|
||||
// 业务层会通过 response.getStatusCode() 判断
|
||||
log.warn("[WeChatAPI] HTTP error response, status={}", response.getStatusCode());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. WechatApiException 新增(解决 P1-3 错误码风格)
|
||||
|
||||
```java
|
||||
package com.emotion.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 微信 API 业务异常
|
||||
*
|
||||
* code 字段使用 HTTP 风格状态码:
|
||||
* - 400: 微信 API 业务错误(errcode != 0)或 4xx 响应
|
||||
* - 500: 内部错误(JSON 解析失败)
|
||||
* - 502: 上游服务错误(5xx 响应 / network / non-JSON content-type)
|
||||
*/
|
||||
@Getter
|
||||
public class WechatApiException extends RuntimeException {
|
||||
|
||||
private final Integer code; // HTTP 风格状态码
|
||||
private final String rawBody; // 原始响应体(仅服务端排查用,绝不返回给前端)
|
||||
|
||||
public WechatApiException(Integer code, String message, String rawBody) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.rawBody = rawBody;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. WechatMiniProgramServiceImpl.code2Session 重写
|
||||
|
||||
```java
|
||||
@Override
|
||||
public WechatCodeSessionResponse code2Session(String code) {
|
||||
validateConfigured();
|
||||
String url = buildCode2SessionUrl(code);
|
||||
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
// 第一步:拿原始 String 响应(不直接反序列化为 DTO)
|
||||
response = restTemplate.exchange(url, HttpMethod.GET, null, String.class);
|
||||
} catch (RestClientException e) {
|
||||
// 网络 / DNS / SSL 错误 → 502
|
||||
log.error("[WeChatAPI] code2Session 网络错误", e);
|
||||
throw new WechatApiException(502, "WeChat network error: " + e.getMessage(), null);
|
||||
}
|
||||
|
||||
String rawBody = response.getBody();
|
||||
HttpStatusCode status = response.getStatusCode();
|
||||
MediaType contentType = response.getHeaders().getContentType();
|
||||
|
||||
// 第二步:HTTP 状态码检查
|
||||
if (status == null || !status.is2xxSuccessful()) {
|
||||
log.error("[WeChatAPI] code2Session HTTP 错误: status={}, contentType={}, body={}",
|
||||
status, contentType, rawBody);
|
||||
int code = (status != null && status.is4xxClientError()) ? 400 : 502;
|
||||
throw new WechatApiException(code, "WeChat HTTP " + status, rawBody);
|
||||
}
|
||||
|
||||
// 第三步:content-type 防御
|
||||
// 注意:Spring MediaType 没有 includesCompatibleWith 方法(编译错误),用 isCompatibleWith 替代
|
||||
if (contentType == null || !MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
|
||||
log.error("[WeChatAPI] code2Session 非 JSON 响应: contentType={}, body={}", contentType, rawBody);
|
||||
throw new WechatApiException(502,
|
||||
"WeChat returned " + contentType + " (likely WAF interception)", rawBody);
|
||||
}
|
||||
|
||||
// 第四步:JSON 解析
|
||||
WechatCodeSessionResponse dto;
|
||||
try {
|
||||
dto = objectMapper.readValue(rawBody, WechatCodeSessionResponse.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("[WeChatAPI] code2Session JSON 解析失败. body={}", rawBody, e);
|
||||
throw new WechatApiException(500, "Invalid JSON from WeChat", rawBody);
|
||||
}
|
||||
|
||||
// 第五步:业务错误码检查
|
||||
if (!dto.isSuccess()) {
|
||||
log.warn("[WeChatAPI] code2Session 业务错误: errcode={}, errmsg={}, body={}",
|
||||
dto.getErrcode(), dto.getErrmsg(), rawBody);
|
||||
throw new WechatApiException(400,
|
||||
dto.getErrmsg() != null ? dto.getErrmsg() : "WeChat business error",
|
||||
rawBody);
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. GlobalExceptionHandler 新增 WechatApiException handler(解决 P0-4)
|
||||
|
||||
在 `GlobalExceptionHandler.java` 中新增:
|
||||
|
||||
```java
|
||||
// 复用 Interceptor 的脱敏正则(避免重复定义)
|
||||
private static final java.util.regex.Pattern SENSITIVE_FIELDS_FOR_LOG = java.util.regex.Pattern.compile(
|
||||
"\"(session_key|openid|unionid|access_token|refresh_token|secret|appsecret|code)\"\\s*:\\s*\"[^\"]*\"");
|
||||
|
||||
private String desensitizeForLog(String body) {
|
||||
if (body == null || body.isEmpty()) {
|
||||
return body;
|
||||
}
|
||||
return SENSITIVE_FIELDS_FOR_LOG.matcher(body).replaceAll("\"$1\":\"***\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理微信 API 异常(脱敏后返回友好提示)
|
||||
*
|
||||
* 注意:不使用 @ResponseStatus,保持与现有 BusinessException handler 一致
|
||||
* (HTTP 层返回 200,业务错误通过 Result.code 表达)
|
||||
*/
|
||||
@ExceptionHandler(WechatApiException.class)
|
||||
public Result<Void> handleWechatApiException(WechatApiException e, HttpServletRequest request) {
|
||||
// 服务端日志:rawBody 需脱敏(Interceptor 已对响应 body 脱敏,
|
||||
// 但 rawBody 字段是再次传递到异常对象,必须再次脱敏以防日志明文泄露)
|
||||
log.error("[WeChatAPI] 异常: {} {} - code={}, msg={}, rawBody={}",
|
||||
request.getMethod(), request.getRequestURI(),
|
||||
e.getCode(), e.getMessage(), desensitizeForLog(e.getRawBody()));
|
||||
|
||||
// 前端响应:脱敏友好提示(不暴露 rawBody / errcode / 内部技术细节)
|
||||
String userMessage;
|
||||
if (e.getCode() != null && e.getCode() == 400) {
|
||||
userMessage = "微信授权失败,请重试";
|
||||
} else if (e.getCode() != null && e.getCode() == 502) {
|
||||
userMessage = "微信服务暂不可用,请稍后重试";
|
||||
} else {
|
||||
userMessage = "微信登录失败,请重试";
|
||||
}
|
||||
return Result.error(userMessage);
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. application.yml 修改(解决 P1-5 一致性)
|
||||
|
||||
```yaml
|
||||
wechat:
|
||||
mini-program:
|
||||
# 防御性:删除硬编码默认值,强制从环境变量读取
|
||||
# 生产必须设置 WECHAT_MINI_PROGRAM_APP_ID 和 WECHAT_MINI_PROGRAM_APP_SECRET
|
||||
app-id: ${WECHAT_MINI_PROGRAM_APP_ID:}
|
||||
app-secret: ${WECHAT_MINI_PROGRAM_APP_SECRET:}
|
||||
base-url: https://api.weixin.qq.com
|
||||
```
|
||||
|
||||
#### 8. WechatCodeSessionResponse 保持不动(解决 P2-6)
|
||||
|
||||
**决策**:rawBody 不放在 DTO,只在 `WechatApiException` 对象和服务端日志中。理由:
|
||||
- DTO 是纯数据传输对象,承载 rawBody 会污染数据模型
|
||||
- DTO 已经通过 `@JsonProperty` 等注解映射字段,加 `@JsonIgnore` 字段是反模式
|
||||
- rawBody 已经在异常对象中保留了,足够排查
|
||||
|
||||
### 包结构(解决 P1-2)
|
||||
|
||||
| 类 | 包 |
|
||||
|---|---|
|
||||
| `RestTemplateConfig` | `com.emotion.config`(保持) |
|
||||
| `WechatResponseErrorHandler` | `com.emotion.config`(与 RestTemplateConfig 同包) |
|
||||
| `WechatApiLoggingInterceptor` | `com.emotion.interceptor`(与 JwtAuthInterceptor 同包) |
|
||||
| `WechatApiException` | `com.emotion.exception`(与 BusinessException 同包) |
|
||||
| `WechatMiniProgramServiceImpl` | `com.emotion.service.impl`(保持) |
|
||||
| `WechatCodeSessionResponse` | `com.emotion.dto.wechat`(保持) |
|
||||
|
||||
不新增 `com.emotion.http` 包(与项目现有结构不一致)。
|
||||
|
||||
### 错误处理层次(解决 P0-4)
|
||||
|
||||
```
|
||||
WechatMiniProgramServiceImpl.code2Session()
|
||||
├─ RestClientException (网络/SSL/DNS) → WechatApiException(502)
|
||||
├─ HTTP 非 2xx
|
||||
│ ├─ 4xx → WechatApiException(400, rawBody)
|
||||
│ └─ 5xx → WechatApiException(502, rawBody)
|
||||
├─ Content-Type 非 application/json → WechatApiException(502, rawBody)
|
||||
├─ JSON 解析失败 → WechatApiException(500, rawBody)
|
||||
└─ errcode != 0 → WechatApiException(400, rawBody)
|
||||
|
||||
GlobalExceptionHandler.handleWechatApiException
|
||||
├─ 服务端日志: 完整 rawBody(脱敏)+ code + message
|
||||
└─ 前端响应: 脱敏友好提示("微信服务暂不可用..."等)
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
### 单元测试(WechatMiniProgramServiceImplTest)
|
||||
|
||||
| 场景 | 模拟返回 | 期望 |
|
||||
|---|---|---|
|
||||
| 1. 成功 | HTTP 200 + `application/json` + `{"openid":"oX","session_key":"sK"}` | 返回 DTO |
|
||||
| 2. 微信业务错误(40013) | HTTP 200 + JSON `{"errcode":40013,"errmsg":"invalid appid"}` | 抛 WechatApiException(400, rawBody) |
|
||||
| 3. text/plain(WAF) | HTTP 200 + `text/plain` + `"blocked by waf"` | 抛 WechatApiException(502, rawBody) |
|
||||
| 4. 4xx | HTTP 401 + body | 抛 WechatApiException(400, rawBody) |
|
||||
| 5. 5xx | HTTP 500 + body | 抛 WechatApiException(502, rawBody) |
|
||||
| 6. 非法 JSON | HTTP 200 + `application/json` + `not json` | 抛 WechatApiException(500, rawBody) |
|
||||
| 7. 网络异常 | RestClientException (IOException) | 抛 WechatApiException(502) |
|
||||
| 8. 未配置 | appId 为空 | 抛 BusinessException("微信小程序登录未配置") |
|
||||
| 9. **LoggingInterceptor 脱敏** | 响应含 `session_key` | 日志中 `session_key` 字段值变为 `***` |
|
||||
| 10. **其他 7 个使用点不受影响** | 模拟 DifyProviderAdapter 等调用 | 正常返回(验证 BufferingClientHttpRequestFactory 没破坏行为) |
|
||||
|
||||
### 集成验证
|
||||
|
||||
| 步骤 | 命令 / 操作 | 期望 |
|
||||
|---|---|---|
|
||||
| 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`) |
|
||||
| 5. 端到端(H5 模式) | 启动 mini-program H5 (端口 5173) → 微信授权登录 | 登录成功,跳转 onboarding 或主页 |
|
||||
| 6. 凭据验证 | 部署后从服务器内部 `curl` 微信 API | 返回正常 JSON errcode 或 openid |
|
||||
| 7. 错误响应验证 | 临时配置错误 appSecret 触发微信 40163 | 前端收到 "微信授权失败,请重试";后端日志有完整 rawBody |
|
||||
|
||||
### 回滚方案(解决 P2-5)
|
||||
|
||||
| 步骤 | 操作 |
|
||||
|---|---|
|
||||
| 1. 保留旧版本 | 部署脚本 `python deploy.py backend` 自动备份当前 JAR 到 `/opt/emotion-museum/backup/emotion-single-{timestamp}.jar` |
|
||||
| 2. 健康检查 | 部署后 30s 内 `curl https://lifescript.happylifeos.com/api/health` 返回 200 |
|
||||
| 3. 失败回滚 | 如果健康检查失败,执行 `python deploy.py backend --rollback`(脚本会读取最近一次备份 JAR 替换并重启) |
|
||||
| 4. 数据库兼容 | 本次修改不涉及 schema,无需数据迁移 |
|
||||
|
||||
### 防御性测试
|
||||
|
||||
- 在测试环境人为让微信 API 返回 `text/plain`,确认不会 500(而是 WechatApiException 被 GlobalExceptionHandler 捕获,返回 500 + 友好提示)
|
||||
- 单元测试覆盖 10 个场景(见上表)
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
|---|---|---|
|
||||
| 改 `RestTemplateConfig` 影响其他 7 处使用点 | 中 | 用 `BufferingClientHttpRequestFactory` 包装确保 body 可多次读;`additionalInterceptors` / `additionalMessageConverters` 是追加(不覆盖);3 天观察期;单元测试场景 10 覆盖 |
|
||||
| 默认 `app-id` / `app-secret` 删除后本地开发挂 | 低 | local 启动报错时本地 dev 配 env var;CI 配 secret;IDEA Run Configuration 加 env var |
|
||||
| 生产 `WECHAT_MINI_PROGRAM_*` 未设导致启动失败 | 低 | 启动日志 warn 但不阻断(保持现状);调用时才抛 BusinessException |
|
||||
| LoggingInterceptor 性能开销 | 低 | body 仅 DEBUG 级别(生产默认不输出);`MAX_BODY_LOG=2048` 截断 |
|
||||
| 错误日志泄露敏感信息 | 中 | Interceptor 中脱敏(session_key/openid/unionid/access_token/refresh_token → `***`);rawBody 不返回前端 |
|
||||
| 微信 API 真实业务错误码(如 40013 invalid appid)被脱敏后用户无法知道原因 | 低 | 后端日志记录完整 errcode + errmsg;运维可查;前端用户只需重试 |
|
||||
|
||||
### 影响的 7 个其他 RestTemplate 使用点(解决 P1-8)
|
||||
|
||||
| 类 | 调用方式 | 是否会受影响 |
|
||||
|---|---|---|
|
||||
| `DifyProviderAdapter` | `restTemplate.execute(url, POST, ..., body)` 流式 POST | ❌ BufferingClientHttpRequestFactory 兼容 |
|
||||
| `CozeProviderAdapter` | 同上 | ❌ 兼容 |
|
||||
| `HttpTtsEngineClient` | `restTemplate.postForEntity(url, body, Map.class)` | ❌ 兼容 |
|
||||
| `AsrServiceImpl` | `restTemplate.postForEntity(url, body, Map.class)` | ❌ 兼容 |
|
||||
| `ApiEndpointServiceImpl` | `restTemplate.getForObject(url, String.class)` | ❌ 兼容 |
|
||||
| `AiChatServiceImpl` | `restTemplate.exchange(...)` ResponseEntity + 部分 `HttpClient` | ❌ 兼容(RestTemplate 部分) |
|
||||
| `ApiTestProxyController` | `restTemplate.exchange(...)` + String.class | ❌ 兼容 |
|
||||
|
||||
**结论**:因为 `BufferingClientHttpRequestFactory` 把 body 缓存为字节数组,所有 RestTemplate 消费者(包括反序列化器)都能正常读 body。其他 7 个使用点的行为完全不变(仅多一份脱敏后的 body DEBUG 日志)。
|
||||
|
||||
## 范围外(Out of Scope)
|
||||
|
||||
- 不做 RestTemplate 连接池(沿用 SimpleClientHttpRequestFactory)
|
||||
- 不重试 / 不熔断(业务层不需要)
|
||||
- 不做 phone number binding / merge logic(已有 2026-05-31 plan 覆盖,且当前代码尚未实施)
|
||||
- 不改 mini-program 前端(纯后端 bug)
|
||||
- 不改其他 7 个 RestTemplate 使用点的业务代码
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 已有设计:`docs/superpowers/specs/2026-05-31-mini-program-wechat-auth-design.md`
|
||||
- 已有 plan:`docs/superpowers/plans/2026-05-31-mini-program-wechat-auth.md`
|
||||
- 微信 API:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
|
||||
- Spring RestTemplateBuilder:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/boot/web/client/RestTemplateBuilder.html
|
||||
- BufferingClientHttpRequestFactory:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/client/BufferingClientHttpRequestFactory.html
|
||||
- WAF / CloudFlare 对 Java UA 的拦截(业界已知问题,常见解决方案:显式设置 User-Agent)
|
||||
- 项目安全规则:`G:\IdeaProjects\emotion-museun\.cursor\rules\rules.mdc` 第 37 条「敏感信息不得在日志中输出」
|
||||
@@ -0,0 +1,192 @@
|
||||
# 小程序人生轨迹详情页操作区调整设计
|
||||
|
||||
日期:2026-06-09
|
||||
状态:待用户审核
|
||||
|
||||
## 背景
|
||||
|
||||
人生轨迹详情页当前由顶部事件信息卡、事件描述卡、AI 分析卡、标签卡和底部操作栏组成。截图中的详情页左上角存在编辑入口,右上角存在更多功能入口,底部操作栏同时承载编辑、收藏、聊天和分享。新的需求是减少顶部干扰,把事件标题和正文内容整合成一个更完整的阅读模块,并让正文使用现有 Markdown 组件渲染。
|
||||
|
||||
目标页面位于 `mini-program/src/pages/life-event/detail.vue`。现有 Markdown 组件位于 `mini-program/src/components/Markdown.vue`,已支持标题、分隔线、引用、列表、段落和粗体文本。
|
||||
|
||||
## 目标
|
||||
|
||||
1. 安全移除详情页左上角编辑 icon 和右上角功能 icon。
|
||||
2. 将事件标题区域和事件内容区域合并成一张统一内容卡。
|
||||
3. 标题区域背景加深,与正文区域形成清晰但克制的层次区分。
|
||||
4. 事件正文必须通过现有 `Markdown` 组件渲染。
|
||||
5. 调整操作按钮位置:
|
||||
- 收藏按钮在内容区域左上角。
|
||||
- 分享按钮与收藏按钮同一水平线。
|
||||
- 分析按钮弱化为浅色胶囊,凹到内容区域右上角。
|
||||
- 编辑按钮迁移到内容区域左下角。
|
||||
- 编辑按钮与收起按钮同一水平线。
|
||||
- 分析按钮与收起按钮同一垂直线。
|
||||
- 聊天按钮文案改为“聊聊”,居中显示。
|
||||
6. 保持当前小程序的宇宙深色、紫色玻璃、发光按钮和卡片边框风格。
|
||||
7. 保持现有功能完整,不改变接口和数据模型。
|
||||
|
||||
## 非目标
|
||||
|
||||
1. 不重写 `Markdown.vue`。
|
||||
2. 不调整人生轨迹列表页、记录页或 AI 分析数据结构。
|
||||
3. 不新增后端 API。
|
||||
4. 不引入新的 UI 库或图标库。
|
||||
5. 不改动当前收藏、分享、编辑、聊天的业务方法语义。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
采用“单卡合并,局部重排”方案。把当前 `hero-card` 和 `description-card` 合并为新的 `event-content-card`,在同一张卡中形成上下两个区域:
|
||||
|
||||
- `event-hero-section`:展示年份、日期、标签、标题、地点和插画。该区域背景使用更深的蓝紫色叠层和轻微内发光,视觉上像卡片的标题头部。
|
||||
- `event-body-section`:展示事件描述标题、正文 Markdown、收藏/分享/分析/编辑/收起等操作。正文区域背景更轻,保持玻璃感和可读性。
|
||||
|
||||
该方案改动集中,既能满足布局需求,也能最大程度复用现有方法与样式。
|
||||
|
||||
## 页面结构
|
||||
|
||||
调整前:
|
||||
|
||||
```text
|
||||
floating-nav
|
||||
hero-card
|
||||
description-card
|
||||
analysis-panel
|
||||
tags-card
|
||||
bottom-actions
|
||||
```
|
||||
|
||||
调整后:
|
||||
|
||||
```text
|
||||
event-content-card
|
||||
event-hero-section
|
||||
event-body-section
|
||||
event-body-toolbar
|
||||
favorite action
|
||||
analysis action
|
||||
share action
|
||||
section-title-row
|
||||
markdown description
|
||||
event-body-footer
|
||||
edit action
|
||||
collapse action
|
||||
analysis-panel
|
||||
tags-card
|
||||
bottom-chat-action
|
||||
```
|
||||
|
||||
`floating-nav` 整行不再渲染,避免左上角与右上角继续出现悬浮 icon。本次不新增替代顶部功能入口,页面返回依赖小程序或 H5 容器已有的导航能力。
|
||||
|
||||
## 交互设计
|
||||
|
||||
### 顶部图标
|
||||
|
||||
移除当前悬浮导航整行。对应的 `openMore` 和 `goBack` 可以保留为未使用方法,也可以在实施时移除无引用代码;推荐优先移除模板绑定,避免扩大行为改动。
|
||||
|
||||
### 标题与正文合并
|
||||
|
||||
事件标题区继续展示:
|
||||
|
||||
- 年份
|
||||
- 日期或日期范围
|
||||
- 主标签
|
||||
- 事件标题
|
||||
- 城市/地点
|
||||
- 当前插画占位
|
||||
|
||||
正文区展示:
|
||||
|
||||
- “事件描述”标题
|
||||
- 事件内容的 Markdown 渲染结果
|
||||
- 折叠/展开控制
|
||||
|
||||
折叠状态沿用 `isDescriptionCollapsed`。折叠时对 Markdown 容器外层做高度或行数限制,展开时完整展示。由于 Markdown 内部包含多种 block,推荐用外层 `max-height` 加 `overflow: hidden`,而不是直接给内部文本加 `-webkit-line-clamp`。
|
||||
|
||||
### 按钮位置
|
||||
|
||||
正文区域顶部工具行:
|
||||
|
||||
- 左侧:收藏按钮,复用当前 `toggleFavorite`。
|
||||
- 右侧:分享按钮,复用当前 `shareCurrent`。
|
||||
- 右上凹位:分析按钮,使用更浅的紫色半透明胶囊,文案为“分析”。点击后把当前 `scroll-view` 定位到下方 AI 分析区。
|
||||
|
||||
正文区域底部工具行:
|
||||
|
||||
- 左下角:编辑按钮,复用当前 `editEvent`。
|
||||
- 右侧:收起/展开按钮,复用 `isDescriptionCollapsed` 切换。
|
||||
|
||||
底部主聊天按钮:
|
||||
|
||||
- 保留底部固定操作区或压缩为一个居中主按钮。
|
||||
- 文案改为“聊聊”。
|
||||
- 点击继续调用 `chatEvent`。
|
||||
- 视觉延续当前紫色渐变发光按钮。
|
||||
|
||||
## 数据流
|
||||
|
||||
不改变数据来源:
|
||||
|
||||
- `displayEvent` 继续从 store 或缓存读取。
|
||||
- `eventDescription` 继续从 `content` 或 `description` 派生。
|
||||
- `markdownAnalysis` 和 `hasMarkdownAnalysis` 继续服务 AI 分析区。
|
||||
- `isFavorite` 继续使用当前 store 和本地缓存逻辑。
|
||||
|
||||
事件描述渲染方式从:
|
||||
|
||||
```vue
|
||||
<text class="description-text">{{ eventDescription }}</text>
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```vue
|
||||
<view class="description-markdown">
|
||||
<Markdown :content="eventDescription" />
|
||||
</view>
|
||||
```
|
||||
|
||||
折叠样式施加在 `description-markdown` 外层。
|
||||
|
||||
## 样式原则
|
||||
|
||||
1. 使用现有深色宇宙背景,不改变页面整体色板。
|
||||
2. 卡片半径、边框、内发光和文字颜色沿用当前 `glass-card`、`analysis-panel` 的风格。
|
||||
3. 标题区背景加深,但避免变成纯黑块;推荐使用深蓝紫渐变加低透明紫色光晕。
|
||||
4. 正文区保持阅读优先,文字对比度不低于当前 `Markdown.vue` 的默认表现。
|
||||
5. 所有操作按钮保持至少 64rpx 可点击高度。
|
||||
6. 不使用新图片资源;继续使用当前 CSS 绘制的封面占位。
|
||||
|
||||
## 风险与处理
|
||||
|
||||
### Markdown 折叠
|
||||
|
||||
Markdown 是 block 结构,不能简单给单个 text 行数截断。实施时用外层容器限制高度,避免破坏 Markdown 组件。
|
||||
|
||||
### 操作入口减少
|
||||
|
||||
移除更多按钮后,原 action sheet 的功能会分散到内容区按钮。收藏、分享、编辑都有显式入口,功能不丢失。
|
||||
|
||||
### 安全区与底部遮挡
|
||||
|
||||
底部“聊聊”按钮仍需使用 `safeAreaBottom`。`scroll-view` 底部 padding 要同步调整,避免最后一张卡被遮挡。
|
||||
|
||||
### 文件编码
|
||||
|
||||
当前终端读取部分中文文件时出现 mojibake 风险。实施时只写明确中文文案“聊聊”“收藏”“分享”“编辑”“分析”“收起/展开”,并通过浏览器实际渲染验证。
|
||||
|
||||
## 验证计划
|
||||
|
||||
1. 运行 `mini-program` H5 开发服务。
|
||||
2. 用浏览器打开人生轨迹详情页。
|
||||
3. 验证顶部编辑 icon 和更多 icon 已移除。
|
||||
4. 验证标题区与正文区位于同一张卡内,标题区背景更深。
|
||||
5. 验证事件描述通过 Markdown 组件渲染,标题、列表、粗体等格式正常。
|
||||
6. 验证收藏、分享、编辑、分析、收起/展开、聊聊按钮位置符合需求。
|
||||
7. 验证点击收藏、分享、编辑、聊聊不会报错。
|
||||
8. 验证浏览器 Console 无错误。
|
||||
9. 验证窄屏下文本不重叠、不被底部按钮遮挡。
|
||||
|
||||
## 待确认事项
|
||||
|
||||
用户已确认“标题区域和内容区域合并”指顶部事件信息卡与事件描述卡合并。实施时按该解释执行。
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
author: claude
|
||||
created_at: 2026-06-15
|
||||
purpose: 将人生轨迹页面的个人信息展示区域合并到我的页面,统一信息展示
|
||||
---
|
||||
|
||||
# 个人信息组件合并设计
|
||||
|
||||
## 背景
|
||||
|
||||
当前小程序中,个人信息分散在两个页面展示:
|
||||
- **RecordView(人生轨迹页面)**:顶部个人信息卡片,展示头像、昵称、星座、MBTI、职业、爱好
|
||||
- **MineView(我的页面)**:展示头像、昵称、MBTI·星座·职业(一行)、觉醒深度、星历契合、菜单
|
||||
|
||||
两个页面存在大量重复信息,且 MineView 展示的个人字段较少。需要将 RecordView 的个人信息卡片完整合并到 MineView,取并集展示所有字段,去除重复。
|
||||
|
||||
## 设计目标
|
||||
|
||||
1. RecordView 完全移除个人信息展示区域
|
||||
2. MineView 保留现有宇宙星空风格,新增「个人档案」信息区块
|
||||
3. 所有可用个人信息字段取并集,不重复展示
|
||||
4. 不新增后端接口,全部从现有 store 数据读取
|
||||
|
||||
## 变更范围
|
||||
|
||||
### RecordView.vue — 移除个人信息区域
|
||||
|
||||
**模板变更**:
|
||||
- 移除 `profile-card` 整个区域(包含头像、昵称、副标题、编辑按钮、meta-grid、爱好行、分隔线)
|
||||
|
||||
**脚本变更**:
|
||||
- 移除 `profile` computed(`store.userProfile || store.registrationData`)
|
||||
- 移除 `avatar` computed(dicebear 头像 URL)
|
||||
- 移除 `heroTags` computed(爱好数组)
|
||||
- 移除 `editProfile` 方法
|
||||
|
||||
**样式变更**:
|
||||
- 移除所有 `profile-card`、`profile-top`、`avatar-wrap`、`avatar`、`avatar-edit`、`edit-pen`、`tiny-pen`、`profile-info`、`name-row`、`profile-name`、`star`、`profile-subtitle`、`edit-profile`、`profile-divider`、`meta-grid`、`meta-item`、`meta-icon`、`meta-label`、`meta-value`、`person-icon`、`job-icon`、`heart`、`hobby-row`、`hobby-text` 相关样式
|
||||
|
||||
**保留不变**:
|
||||
- `section-head`("人生轨迹"标题 + "导入社交数据"按钮)
|
||||
- `filters`(筛选器)
|
||||
- `timeline`(事件时间线)
|
||||
- 空状态卡片
|
||||
- `create-fab`(创建 FAB 按钮)
|
||||
|
||||
### MineView.vue — 新增「个人档案」区块
|
||||
|
||||
**位置**:在 `stats-grid`(觉醒深度 | 星历契合)下方、`menu-list` 上方。
|
||||
|
||||
**布局结构**:
|
||||
|
||||
```
|
||||
✦ 个人档案 ← 区块标题
|
||||
|
||||
┌────────┬────────┬────────┐
|
||||
│ ♋ │ ◉ │ ◻ │
|
||||
│ 星座 │ MBTI │ 职业 │ ← 第一行:来自 RecordView
|
||||
│ 巨蟹座 │ INTJ │ 设计师 │
|
||||
├────────┼────────┼────────┤
|
||||
│ 📍 │ 🏢 │ 🏗 │
|
||||
│ 城市 │ 行业 │ 公司 │ ← 第二行:新增字段
|
||||
│ 上海 │ 互联网 │ xx公司 │
|
||||
├────────┼────────┼────────┤
|
||||
│ 🎂 │ ♂ │ ⏳ │
|
||||
│ 生日 │ 性别 │ 年龄 │ ← 第三行:新增字段
|
||||
│ 6.15 │ 男 │ 28 │
|
||||
└────────┴────────┴────────┘
|
||||
|
||||
♡ 爱好 摄影 · 阅读 · 音乐 ← 第四行:爱好
|
||||
```
|
||||
|
||||
**字段列表(并集)**:
|
||||
|
||||
| 字段 | 数据来源 | 图标 | 空值显示 |
|
||||
|------|---------|------|---------|
|
||||
| 星座 | `profile.zodiac` | ♋ | 未设置 |
|
||||
| MBTI | `profile.mbti` | ◉ | 未设置 |
|
||||
| 职业 | `profile.profession` | ◻ | 未设置 |
|
||||
| 城市 | `profile.city` | 📍 | 未设置 |
|
||||
| 行业 | `profile.industry` | 🏢 | 未设置 |
|
||||
| 公司 | `profile.company` | 🏗 | 未设置 |
|
||||
| 生日 | `profile.birthday` | 🎂 | 未设置 |
|
||||
| 性别 | `profile.gender` | ♂ | 未设置 |
|
||||
| 年龄 | 由 birthday 计算 | ⏳ | 未设置 |
|
||||
| 爱好 | `profile.hobbies` | ♡ | 未设置 |
|
||||
|
||||
**编辑入口**:整个区块可点击,跳转到 `/pages/onboarding/index?edit=1`。
|
||||
|
||||
### 样式规范
|
||||
|
||||
**区块标题**:
|
||||
- 字号 40rpx,字重 800,白色
|
||||
- 前缀 ✦ 金色星号(`#ffd184`),与 RecordView 的 `section-title` 风格一致
|
||||
- 与上方 `stats-grid` 间距 42rpx
|
||||
|
||||
**字段网格**:
|
||||
- CSS Grid 三栏布局:`grid-template-columns: repeat(3, 1fr)`
|
||||
- 每栏结构:图标(上)→ 标签(中,20rpx,`rgba(219, 204, 247, 0.54)`)→ 值(下,23rpx,白色,加粗 600)
|
||||
- 栏间竖线分隔:`border-right: 1rpx solid rgba(180, 139, 255, 0.22)`,最后一栏无右边框
|
||||
- 行间距 24rpx
|
||||
|
||||
**爱好行**:
|
||||
- ♡ 图标 + "爱好" 标签 + 值,水平排列
|
||||
- 与网格间距 20rpx,上方有细分隔线(`1rpx solid rgba(180, 139, 255, 0.22)`)
|
||||
- 爱好值用 ` · ` 连接,最多显示 4 个
|
||||
|
||||
**整体容器**:
|
||||
- 不加卡片背景,透明融入 MineView 星空背景
|
||||
- 与 `stats-grid`、`menu-list` 的间距保持一致(42rpx)
|
||||
|
||||
### 数据逻辑
|
||||
|
||||
**年龄计算**:
|
||||
```js
|
||||
const age = computed(() => {
|
||||
const birthday = profile.value.birthday
|
||||
if (!birthday) return null
|
||||
const birthYear = Number(String(birthday).slice(0, 4))
|
||||
if (!birthYear) return null
|
||||
return new Date().getFullYear() - birthYear
|
||||
})
|
||||
```
|
||||
|
||||
**性别映射**:
|
||||
```js
|
||||
const genderText = computed(() => {
|
||||
const map = { male: '男', female: '女', other: '其他' }
|
||||
return map[profile.value.gender] || null
|
||||
})
|
||||
```
|
||||
|
||||
**数据来源**:全部从 `store.userProfile || store.registrationData` 读取,不新增后端接口。
|
||||
|
||||
### MineView 现有信息处理
|
||||
|
||||
**保留不变**:
|
||||
- 头像(居中展示,保留现有宇宙光晕效果)
|
||||
- 昵称(大字号居中)
|
||||
- 签名行(`metaLine`:MBTI · 星座 · 职业 的组合文本)
|
||||
- 统计数据(觉醒深度、星历契合)
|
||||
- 菜单列表(个人档案设置、多账号切换、与开发者对话)
|
||||
- 退出登录按钮
|
||||
|
||||
**签名行处理**:`metaLine` 保留,作为昵称下方的补充签名文本,与下方「个人档案」区块的三栏网格不冲突。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 不修改后端接口
|
||||
- 不修改 `stores/app.js`
|
||||
- 不修改 `profile/index.vue`(该页面只是 MineView 的包装)
|
||||
- 不新增个人信息字段到 store(只展示已有字段)
|
||||
+67
@@ -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 无报错。
|
||||
+81
@@ -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` 数组
|
||||
|
||||
删除第 178–249 行的 `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 规则文档,与脚本行为对齐
|
||||
|
||||
## 方案选择
|
||||
|
||||
采用 **方案 A:dev-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` | 修改:更新本地服务管理规则 |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user