Compare commits

228 Commits

Author SHA1 Message Date
peanut daefcc89d6 fix(short-novel): 同步 SSE 事件顶层 session_id 修复澄清卡片累积丢失
上游 SSE 事件中 session_id 与 type 同级(不在 payload 内),原 forwardSse 仅在 status 事件从 payload 取 session_id,
导致首次 stream 的 sessionId 为 null,后续 clarification_card 因 sessionId 为空不进入全局累积器,
详情页首次进入时少显示第一张澄清卡片。

改为在每个事件处理前从顶层 session_id 同步 currentSessionId,确保所有事件链路都能拿到正确的 sessionId。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 22:55:21 +08:00
peanut 504ff8191a docs: 小程序 H5 测试登录凭证写入 CLAUDE.md 和 AGENTS.md
- 测试手机号固定使用 19928748688
- 测试验证码固定使用 123456(后端短信通道在测试模式下固定返回)
- 写入登录流程说明,便于每次做小程序功能验收直接参考

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 21:54:35 +08:00
peanut 44fe10950d feat(mini-program): ScriptDetailView 新增小说消息操作按钮组
- 新增 collapsedMessageIds ref + 5 个函数(isMessageCollapsed、getMessageDisplayContent、toggleMessageCollapse、copyMessageContent、playMessageTts)
- 为 kind === 'novel' 的消息添加折叠/展开、复制、播放按钮
- 添加 .collapse-toggle-row、.message-actions、.action-btn 样式
- H5 验收通过:复制按钮触发 Toast、播放按钮切换 TTS 状态(▶播放 ↔ Ⅱ暂停)、折叠按钮切换文案

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 21:53:24 +08:00
peanut 77ab1aee85 feat(mini-program): ScriptView 恢复小说消息操作按钮组
- 为 kind === 'novel' 的消息添加折叠/展开、复制、播放按钮
- 复用已有函数:getMessageDisplayContent、toggleMessageCollapse、copyMessageContent、playMessageTts
- 折叠按钮条件改用 ASSISTANT_MESSAGE_PREVIEW_LENGTH 常量(替代硬编码 120)
- 添加 .collapse-toggle-row、.message-actions、.action-btn 样式
- 修复 openScriptChat:payload 只有 id 时从 store 获取完整 script 数据,避免 conversationId 缺失导致 loadMessages 不调用
- H5 验收通过:复制/播放/折叠/展开功能正常

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 21:45:13 +08:00
peanut 318596c67e docs: 添加小说消息操作按钮恢复实施计划 2026-07-28 19:37:13 +08:00
peanut 760c1445fe docs: 添加小说消息操作按钮恢复设计文档 2026-07-28 19:32:28 +08:00
peanut 9808e2eb93 docs: 详情页澄清选项卡显示不全修复实施计划 2026-07-26 23:10:25 +08:00
peanut 4e79e2150b docs: 详情页澄清选项卡显示不全修复设计 2026-07-26 22:34:52 +08:00
peanut 502586b64a fix(mini-program): 修复 ScriptLibraryView 章节数展示为 0 的问题
getChapterCount 优先从 plotJson.stages.outline.beats.length 获取真实章节数(新剧本),
fallback 根据 length 字段估算:short→1章, medium→2章, long→4章。
2026-07-26 18:21:58 +08:00
peanut 321246c96c docs: 章节数展示修复设计文档 2026-07-26 18:18:14 +08:00
peanut 872cf63c7c fix:调整用户心愿消息到聊天流首位
问题:DB 中 messageOrder 顺序导致用户心愿(order=8)显示在澄清卡片(order=2,4)之后,
但逻辑上用户心愿应该是第一条消息。

修复:
- 在 rebuildResultMessages 完成后,检测 user 类型的 chat 消息位置
- 如果不在首位(index > 0),移动到 resultMessages 数组开头
- ScriptView.vue 和 ScriptDetailView.vue 保持一致

显示顺序现在为:
1. 用户心愿(第一条)
2. 澄清卡片 1 + 答案
3. 澄清卡片 2 + 答案
4. 大纲
5. 正文
2026-07-26 17:15:22 +08:00
peanut e373cb344f fix:过滤未配对澄清答案(避免显示孤立 answer 如「love」)
问题:DB 中部分 clarification_answer 没有对应 question(如 messageOrder=1 的「love」),
原逻辑将其渲染为独立 user 文本消息,导致用户看到无法理解的孤立答案气泡。

修复:
- 未配对的 clarification_answer 不再展示为 user 文本消息,直接 return 跳过
- 原因:没有 question 上下文的 answer 无法正确渲染,只显示答案会造成用户困惑
- ScriptView.vue 和 ScriptDetailView.vue 保持一致

数据影响:
- 「十年后的来信」剧本 messageOrder=1 的「love」answer 将被过滤
- 正常配对的 answer(如「family」→「家人」「sibling」→「兄弟姐妹」)仍正常显示
2026-07-26 17:11:52 +08:00
peanut ed67739a47 fix:澄清问答配对逻辑修复(按 messageOrder 升序匹配)
问题:DB 中 clarification_answer 可能先于 question 存入(messageOrder 更小),
导致原「找最后一张 card」的匹配逻辑把 answer 错误渲染成独立 user 文本消息
(如截图中的「love」紫色气泡)。

修复:
- rebuildResultMessages 改用 messageOrder 升序配对策略:
  1. 先分别提取所有 clarification_question 和 clarification_answer,按 messageOrder 排序
  2. 为每个 question 配对它之后第一个未分配的 answer
  3. 遍历时 question 替换为含答案的 card 消息,已配对 answer 跳过,未配对 answer 作为独立 user 文本

影响:
- ScriptView.vue(chat-bubble 流程视图)
- ScriptDetailView.vue(详情页只读视图)

数据基础:后端 answer 仍只存 value(语义化),前端渲染时通过 options 映射 label。
2026-07-26 17:07:22 +08:00
peanut 7d19fa5898 fix:澄清卡片显示完整选项和中文答案
问题:详情页 chat-bubble 卡片视图显示「已回答:family」等英文 value,
而不是中文 label(如「家人」),且不展示所有可选选项。

修复:
- 新增 getAnswerLabel(msg) 函数,通过 card.options 查找 value 对应的中文 label
- rebuildResultMessages 增加 selectedValue 字段,与 answer 同时存原始 value
- 卡片已回答视图新增 card-options-list,渲染所有选项,已选加 .selected 样式并标 ✓
- submitClarification(实时模式)同步存 selectedValue,保持渲染逻辑一致

影响:
- ScriptView.vue(chat-bubble 流程视图)
- ScriptDetailView.vue(详情页只读视图)

数据基础未动,后端澄清问答 answers 仍存原始 value(语义化)。
全局约束:rpx 单位、代码注释中文、禁止 mock 数据。
2026-07-26 16:49:44 +08:00
peanut 50fb2404fd fix:修复 ScriptView 详情面板重复展示及列表预览行数据路径
- ScriptView.openScriptChat 移除重复 push wishTheme/novelContent,改为从 messages 真实数据按 type 映射为 chat-bubble(与 ScriptDetailView.rebuildResultMessages 一致)
- ScriptLibraryView.getOutlinePreview 增加多路径探测(plotJson.stages.outline.beats / fullPayload.outline.beats / plotJson.outline.beats),兼容老剧本数据
2026-07-26 16:30:33 +08:00
peanut d220b01ba4 feat:ScriptLibraryView 新增大纲预览行
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 15:39:49 +08:00
peanut 95a3864250 feat:ScriptDetailView 改造为 chat-bubble 流程视图复刻生成时展示 2026-07-26 15:31:01 +08:00
peanut d2a051a938 feat:novel_done 读取累积 stages 并传入 saveNovelResult 2026-07-26 15:16:45 +08:00
peanut 8c2fe249e7 feat:saveNovelResult 创建 clarification/outline 中间步骤 message 2026-07-26 15:12:50 +08:00
peanut 87d668db1c feat:followup 累积用户澄清回答到全局缓存 2026-07-26 15:06:49 +08:00
peanut fcf4bcd9e4 feat:clarification_card 与 outline_created 事件累积到全局缓存
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 15:01:26 +08:00
peanut 7de91dbbb4 feat:新增 SESSION_STAGES_ACCUMULATOR 跨流中间步骤累积器
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 14:51:19 +08:00
peanut fc0f5b88c5 fix:修复小说保存与列表刷新
- ShortNovelServiceImpl:累积 novel_delta 事件,novel_done 未携带 full_text 时用累积内容兜底
- ScriptLibraryView:onMounted 主动拉取最新剧本,解决生成后列表不刷新问题
2026-07-26 14:45:51 +08:00
peanut 88e3ec32ef docs:小说列表/详情页展示改造实施计划 2026-07-26 14:43:29 +08:00
peanut 6f4d9ba0d8 docs:小说列表页/详情页展示改造设计文档
修复详情页重复展示问题,列表页增加大纲预览,后端保存澄清问答消息。
详情页复刻生成时 ScriptView 的 chat-bubble 展示结构。
2026-07-26 14:37:57 +08:00
peanut b5af8b4ce8 feat: ScriptDetailView 增强展示用户心愿和大纲元数据 2026-07-26 11:54:12 +08:00
peanut 0f0ae08b7e feat: 前端 store 增加 sessionMeta 状态 + ScriptView 增强 originalQuery 兜底和可靠刷新 2026-07-26 11:53:20 +08:00
peanut c0df76566d fix: 列表查询移除隐性写操作 2026-07-26 11:50:53 +08:00
peanut 06605bfd0d fix: 修复小说生成保存链路(事务+originalQuery兜底+中间步骤累积) 2026-07-26 11:49:31 +08:00
peanut 4931005102 docs: 心愿实现小说保存与复现完整性修复实施计划 2026-07-26 11:37:26 +08:00
peanut 644b8b9d2d docs: 心愿实现小说保存与复现完整性修复设计 2026-07-26 11:34:26 +08:00
peanut 1cf75a2b40 fix: 修复微信开发者工具 60 秒超时错误处理问题
- abort() 只清理资源,不再触发 onError(避免正常流程误报错误)
- 真正的错误(超时、网络错误)通过 Promise.race 的 catch 正确传递给用户
- 修复微信开发者工具强制终止 fetch 时 onError 不被调用的问题
- 使用 errorHandled 标志防止 onError 重复调用
2026-07-23 22:21:46 +08:00
peanut 6eadbce6f2 fix: 修复微信开发者工具 60 秒超时问题
- 将 AbortController 方案改为 Promise.race + setTimeout 外部超时控制
- 解决微信开发者工具底层 fetch 不兼容 AbortController signal 的问题
- 保持 300 秒超时时间不变
- 简化 abort 接口,统一使用 cleanup 函数
2026-07-23 22:12:42 +08:00
peanut bd706e4a5b fix: 修复 H5 模式 stream 请求 60 秒超时问题
- 在 H5 环境使用 fetch + ReadableStream + AbortController
- 支持自定义 300 秒超时
- 小程序环境保持原有 uni.request 逻辑不变
- 添加 CRLF 兼容性、主动中止与超时区分、资源清理
2026-07-22 22:57:29 +08:00
peanut 81786495a6 docs: H5 模式 stream 超时修复实施计划 2026-07-22 22:32:55 +08:00
peanut c64bb4285a docs: H5 模式 stream 超时修复设计文档 2026-07-22 22:31:52 +08:00
peanut e0d9c4a84b fix:修复 ClarificationCard 选项无法切换的 bug(增强 toggleOption 容错性) 2026-07-22 21:50:38 +08:00
peanut 235b08e01d docs:ClarificationCard 选项切换修复设计 2026-07-22 21:49:21 +08:00
peanut 6bf726d6a0 小程序对话页面优化 2026-07-22 21:36:11 +08:00
peanut ef23f3f51b docs: 记录 SSE 流式传输修复的测试结果 2026-07-21 23:54:51 +08:00
peanut bc2f9538f0 docs: 记录 SSE 流式传输修复的部署验证 2026-07-21 23:50:08 +08:00
peanut 82b138280b fix: 移除 console.log 中的敏感信息(firstQuery 文本、payload 完整内容) 2026-07-21 23:47:17 +08:00
peanut 748fc18c84 feat: 前端添加 SSE 事件诊断日志,确认 originalQuery 传递 2026-07-21 23:43:48 +08:00
peanut adeaa7a506 fix: 修复 nginx SSE 配置(添加 HTTP/1.1,移除冗余配置) 2026-07-21 23:40:38 +08:00
peanut feaf8922c5 docs: 记录 nginx SSE 配置优化 2026-07-21 23:34:23 +08:00
peanut 406f9b77b4 feat: 优化 SSE 转发逻辑,使用严格行读取和显式 flush 2026-07-21 23:28:00 +08:00
peanut 2b6dc72744 docs:创建 SSE 流式传输与历史列表保存修复实施计划 2026-07-21 23:24:14 +08:00
peanut 0c9ab9b974 docs:补充前端代码完整示例,移除占位符 2026-07-21 23:19:40 +08:00
peanut 461f993ab1 docs:SSE 流式传输与历史列表保存修复设计 2026-07-21 23:18:18 +08:00
peanut 3d1cc24fc4 fix: SSE 流式转发从 RestTemplate 改用 OkHttp,支持真正的逐行流式读取,解决缓冲导致的一下子全出来问题 2026-07-21 22:48:52 +08:00
peanut b24e176579 feat: 标签顺序改为全部/短篇/中篇/长篇/收藏夹;增加SSE事件诊断日志定位novel_done未触发问题 2026-07-21 21:56:22 +08:00
peanut 3813f91b8d feat: 历史列表分类改为固定5标签(全部/中篇/短篇/长篇/收藏夹),移除状态筛选,后端保存length改为short 2026-07-21 21:15:31 +08:00
peanut 9b496bb42a fix: 撤回 typewriter 改动,novel 改用直接累加 delta 保证流式输出
之前用 useTypewriterStream.writer.push + 同步读 visibleText 有时序问题:
writer 的 visibleText 由内部 setInterval 异步推进,push 后立即同步读得到空串,
气泡一直空白。

撤回 typewriter 改动,恢复 novel_delta 直接 lastNovel.content += delta,
SSE 本身就是流式推送,每次到达即显示。

保留 confirmOutline / modifyOutline 的 pendingNextResponse = true 修复(loading)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 20:43:11 +08:00
peanut 6afec090e4 fix: 大纲确认/修改时显示 loading + novel 消息使用 typewriter 逐字流式输出
- confirmOutline / modifyOutline 中设置 pendingNextResponse.value = true
  解决大纲确认/修改后等待响应期间无 loading 的问题
- novel_start 时为 novel 消息创建独立 typewriter writer 实例(novelWriters Map)
- novel_delta 时通过 writer.push() 推送增量,逐字渲染 visibleText
- novel_done 时 finish writer 并同步最终文本,清理 writer 实例

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 20:33:52 +08:00
peanut 7b53821cec fix: 澄清卡片 allow_custom 时允许纯自定义输入提交
- canSubmit 逻辑重构:纯文本卡片只需 customText 非空
- 有选项卡片:满足最小选择数,或(allow_custom 且 customText 非空)即可提交
- 修复其他,请简单描述输入自定义内容后提交按钮灰显的问题

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 20:25:24 +08:00
peanut 9a215a89e9 fix: 新生成短篇剧本刷新到历史列表 + 分类标签从实际数据动态生成
- novel_done 事件处理中调用 store.fetchScripts() 刷新剧本列表
- typeTabs 从硬编码改为 computed,根据实际 length 值动态生成
- activeType 默认值从 'long' 改为 'all'(显示全部)
- 过滤逻辑改为精确匹配 length 字段
- 移除无效的 '风格' 硬编码分类标签

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 08:39:14 +08:00
peanut 1f5ad3abe2 fix: 大纲修改意见输入框改 textarea,增高并完整显示 placeholder
- input 改为 textarea + auto-height,支持多行输入
- min-height 64rpx 保证 placeholder 完整显示(原来被压扁截断)
- max-height 120rpx 限制约 2 行,超出滚动显示最新底部内容
- line-height 1.5 比例协调,改动幅度小

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 23:52:46 +08:00
peanut 7c3bd5c0e3 fix: 心愿实现页面用 pendingNextResponse 标志修复第二轮 loading 显示
- 新增 pendingNextResponse ref 标志等待下一个 assistant 响应
- showChatLoading 改用该标志判断,替代不可靠的最后消息 role 判断
- submitClarification 移除重复 addResultMessage,消除重复用户气泡
- 规避 mp-weixin 对 v-for 中 computed 数组的错误编译导致的页面空白
- runGeneration/clarification/outline/novel_start/novel_done 正确设置标志

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 23:37:21 +08:00
peanut 4b3fa1552c fix: 第二轮 loading 不显示 — 用 filteredResultMessages 替代函数调用,避免 mp-weixin 编译丢失 2026-07-20 23:08:42 +08:00
peanut f8d6e881df fix: 恢复 addResultMessage + 渲染层过滤,避免重复气泡同时支持后续 loading 2026-07-20 22:57:24 +08:00
peanut 4afc632cac fix: 移除澄清卡片提交后的重复用户气泡(label 已在卡片内显示) 2026-07-20 21:55:35 +08:00
peanut f1ad9a6b47 test: H5 验证澄清卡片 label 显示 + AI 问题显示 2026-07-20 21:36:13 +08:00
peanut 180d21b66c fix: 已回答状态显示 AI 的问题(question) 2026-07-20 21:28:03 +08:00
peanut 41c2112c5b fix: 澄清卡片已回答用 label 显示,后端 payload 仍用 value 2026-07-20 21:25:09 +08:00
peanut 412d3f3a9a docs: 澄清卡片已回答显示修复实施计划 2026-07-20 21:17:34 +08:00
peanut 8693b3febc docs: 澄清卡片已回答显示修复设计(label 替代 value + 显示 AI 问题) 2026-07-20 21:15:36 +08:00
peanut d560f63d70 fix: chat 视图 loading 在等待首个 assistant 响应期间持续显示 2026-07-20 00:19:19 +08:00
peanut 979fcb5897 docs: 心愿实现 chat 视图 loading 标识修复实施计划 2026-07-20 00:17:18 +08:00
peanut 95ce23f172 docs: 心愿实现 chat 视图 loading 标识修复设计 2026-07-20 00:16:05 +08:00
peanut 4884099b9b fix: 收藏 toggle 用原生 SQL 绕过逻辑删除,彻底解决重新收藏冲突 2026-07-19 23:59:54 +08:00
peanut c0245d37fb fix: 收藏 toggle 支持重新收藏(修复逻辑删除与 unique key 冲突) 2026-07-19 23:56:41 +08:00
peanut 3666c3d530 fix: 收藏 toggle 增加实体存在性和权限校验(IDOR 修复) 2026-07-19 23:42:53 +08:00
peanut 33b24450db feat: 人生事件和剧本收藏从 localStorage 切换为真实 API
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 23:24:04 +08:00
peanut dd11cfae73 feat: 人生事件和剧本收藏从 localStorage 切换为真实 API
- 后端:新增 t_event_favorite 表(UNIQUE KEY + MyBatis-Plus Entity)
- 后端:LifeEventController 新增 /favorite/toggle、/favorite/page、/favorite/check 三个真实端点
- 后端:EventFavoriteService 完整 CRUD,含 toggle/分页/批量查询/状态检查
- 后端:删除旧的 favorite-placeholder mock 端点
- 前端:life-event/detail.vue 从 localStorage 假收藏切换为真实 API + localStorage 缓存防闪烁
- 前端:ScriptLibraryView.vue 从 localFavorites 切换为 checkFavoriteScript 真实 API
- 前端:stores/app.js 新增 toggleEventFavorite / checkEventFavorite 方法
- 修复:审核发现的 UNIQUE 约束缺失、未使用 watch 导入、Controller 参数注解遗漏、Controller 委托 Service 构造 Response

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 23:23:40 +08:00
peanut a3bfad099a docs: 收藏功能真实 API 化实施计划 2026-07-19 22:25:39 +08:00
peanut bebb7f1667 docs: 收藏功能真实 API 化设计文档 v2(沿用独立收藏表架构) 2026-07-19 22:16:15 +08:00
peanut ce9c9a8dc5 chore: 撤回旧收藏设计文档,改用独立收藏表方案 2026-07-19 22:13:17 +08:00
peanut 10d3c72082 docs: 收藏功能真实 API 化设计文档 2026-07-19 22:07:34 +08:00
peanut 05ea918ee1 feat(script-favorite): update toggleFavorite to use backend API
- Import toggleFavoriteScript from epicScript service
- Change toggleFavorite from sync local-only to async with API call
- Implement optimistic update pattern: update local cache first, then call backend
- Rollback on API failure with error toast
- Preserves localFavorites for offline/fast-render scenarios
2026-07-19 21:51:20 +08:00
peanut 50f1bdba38 feat(script-favorite): add favorite API wrappers to epicScript.js
- toggleFavoriteScript: POST /epicScript/favorite/toggle
- getFavoriteScriptPage: GET /epicScript/favorite/page
- checkFavoriteScript: GET /epicScript/favorite/check
- Added to default export object
2026-07-19 21:48:00 +08:00
peanut 74fa304cd9 feat(script-favorite): add 3 favorite endpoints to EpicScriptController
- POST /epicScript/favorite/toggle: toggle favorite for a script
- GET /epicScript/favorite/page: paginated favorite list
- GET /epicScript/favorite/check: check if script is favorited
- Add ScriptFavoriteToggleRequest, ScriptFavoriteResponse, ScriptFavoriteService imports
- Inject ScriptFavoriteService via @Autowired
2026-07-19 21:43:46 +08:00
peanut 9d352f40a0 feat(script-favorite): add ScriptFavoriteServiceImpl
- toggleFavorite: upsert via logical-delete check + save, @Transactional
- getFavoritePage: paginate favorites then fetch EpicScript details
- getFavoritedScriptIds: batch IN query for checking multiple scripts
- isFavorited: count > 0 check for single script
- getFavoriteCount: total count for current user
- Uses UserContextHolder for userId, consistent with project pattern
2026-07-19 21:41:54 +08:00
peanut 674188f471 feat(script-favorite): add ScriptFavoriteService interface
- toggleFavorite: toggle favorite status for a script
- getFavoritePage: paginated list of user's favorite scripts
- getFavoritedScriptIds: batch check which scripts are favorited
- isFavorited: check single script favorite status
- getFavoriteCount: count user's total favorites
2026-07-19 21:41:28 +08:00
peanut 62ca1730b9 feat(script-favorite): add ScriptFavoriteResponse DTO
- Fields: scriptId, isFavorited, favoriteTime
- Used by toggle/check endpoints to return favorite status
2026-07-19 21:41:14 +08:00
peanut 0d27d1a072 feat(script-favorite): add ScriptFavoriteToggleRequest DTO
- Single field: scriptId with @NotBlank validation
- Uses javax.validation.constraints.NotBlank matching project convention
2026-07-19 21:41:02 +08:00
peanut d74d8b22c6 feat(script-favorite): add ScriptFavoriteMapper
- Extends BaseMapper<ScriptFavorite>
- Standard MyBatis-Plus mapper pattern matching project convention
2026-07-19 21:40:06 +08:00
peanut 489131a04b feat(script-favorite): add ScriptFavorite entity
- Extends BaseEntity for id, createTime, updateTime, isDeleted
- Maps to t_script_favorite table via MyBatis-Plus @TableName
- Fields: userId, scriptId
2026-07-19 21:39:47 +08:00
peanut 43ac037f5b feat(script-favorite): add t_script_favorite migration SQL
- Create t_script_favorite table with user_id, script_id
- Add unique key uk_user_script (user_id, script_id)
- Add indexes idx_user_id and idx_script_id
- Support logical delete via is_deleted flag
2026-07-19 21:39:27 +08:00
peanut b20f30f91b feat(script-favorite): add database migration and backend service
- Add t_script_favorite table migration SQL
- Add ScriptFavorite entity, mapper, service interface and impl
- Add toggle/page/check endpoints in EpicScriptController
- Add toggleFavorite/getFavoritePage/checkFavorite in epicScript.js
- Update ScriptLibraryView with backend-backed toggleFavorite
2026-07-19 21:38:53 +08:00
peanut 7e52bb9ffb fix: .chat-send-btn 设 height:60rpx 与输入框等高 2026-07-19 21:04:50 +08:00
peanut 8a2001af3d docs: Chat 视图发送按钮等高修复设计文档 2026-07-19 21:04:04 +08:00
peanut 35b4f97527 fix: .chat-scroll-content padding-bottom 改回 40rpx 避免与父级 padding 叠加 2026-07-19 20:49:16 +08:00
peanut f5b26c2fc7 refactor: .chat-page 改为 height: 100% 让父级 padding 生效 2026-07-19 20:49:05 +08:00
peanut cb965fadff docs: Chat 视图底部遮挡架构层修复实施计划 2026-07-19 20:46:10 +08:00
peanut cafa8717ab docs: Chat 视图底部遮挡架构层修复设计文档 2026-07-19 20:45:09 +08:00
peanut 283914690a fix: .chat-page 增加 safe-area-inset-bottom + .chat-scroll-content padding-bottom 增至 120rpx 2026-07-19 20:21:41 +08:00
peanut 04ffa2dceb docs: Chat 视图底部内边距修复实施计划 2026-07-19 20:17:26 +08:00
peanut 0a9ea60795 docs: Chat 视图底部内边距修复设计文档 2026-07-19 19:57:57 +08:00
peanut 118d20914d fix: 修复 openScriptChat 设置 viewState='result' 导致页面空白的问题 2026-07-19 19:30:48 +08:00
peanut b194b343ad fix: 修复 mp-weixin 模板箭头函数和 TextDecoder 不兼容
- 微信小程序 wxml 模板不支持内联箭头函数
  @submit 改为方法引用,submitClarification 自己找未提交卡片
- TextDecoder 在 mp-weixin 不可用
  decodeChunk 改用纯 JS UTF-8 解码实现
2026-07-19 19:02:52 +08:00
peanut 7a0e729ab4 fix: chat-page 用 height: 100vh 替代 min-height: 100vh 修复真机滚动
根因:min-height: 100vh 允许容器超出视口增长,破坏 flex 布局,
导致 scroll-view 内部滚动层高度=内容高度(都不滚动)。

参考项目内已工作的 .result-page(height: 100% + min-height: 0)。
H5 实际验证:内容 1779px 滚动到底部后提交按钮 visibleInViewport: true
2026-07-19 18:50:57 +08:00
peanut f07817aaff fix: chat 视图 scroll-view 添加 height: 100% + min-height: 0
参考项目内已工作的 .result-view 模式:
- flex: 1 仅在 flex 布局父容器中生效
- 但 UniApp <scroll-view> 是原生组件,必须显式 height: 100%
- 同时 min-height: 0 确保 flex 子项可收缩

之前只设 flex: 1,真机上原生 scroll-view 无法计算可滚动区域。
2026-07-19 18:32:09 +08:00
peanut 30ecf9f4a3 fix: chat 视图 scroll-view 删除固定 height,改用 flex: 1 自适应真机高度
真机上状态栏/tab 栏/安全区域高度各不相同,
硬编码 calc(100vh - 220rpx) 导致 scroll-view 超出可视区域无法滚动。
2026-07-19 18:07:00 +08:00
peanut f6ef0222b3 fix: 修复 chat 视图无法滚动 + SSE 事件重复消费
- scrollResultToLatest 守卫条件兼容 chat 视图
- shortNovel.js 添加 chunkProcessed 标志位避免 H5 双重消费
2026-07-19 16:04:54 +08:00
peanut e109e59f1a feat(mini-program): sendChat 按 generationPhase 切换接口,消息进统一对话流 2026-07-19 15:24:49 +08:00
peanut e41266e6f4 feat(mini-program): 统一 chat 视图,删除 clarifying/outlining/result 独立视图 2026-07-19 15:21:43 +08:00
peanut b4fca0abfa refactor(mini-program): runGeneration+交互函数改为操作对话消息 2026-07-19 15:15:49 +08:00
peanut 84b6a77632 refactor(mini-program): handleShortNovelEvent 改为对话流追加消息 2026-07-19 15:10:42 +08:00
peanut c0adb48949 refactor(mini-program): 扩展 addResultMessage 消息模型 + 新增 generationPhase 2026-07-19 15:05:27 +08:00
peanut 25d8a9f8f2 docs:ScriptView 统一对话页改造实现计划
6 个任务:扩展消息模型、handleShortNovelEvent 改对话流、
runGeneration+交互函数改造、统一 chat 视图模板、
sendChat 接口切换、端到端浏览器验证
2026-07-19 14:55:27 +08:00
peanut b2df7d4de0 docs:ScriptView 统一对话页改造设计
将澄清/大纲/小说生成从独立视图改为统一对话流消息。
viewState 简化为 home+chat,扩展 resultMessages 消息模型
支持 kind: text/card/outline/novel,novel_done 后切换旧接口
做后续对话修改。
2026-07-19 14:45:45 +08:00
peanut 30e2836742 fix(mini-program): 修复心愿实现页接口报错 + .env 指向生产服务器
根因:.env.development 指向 localhost:19089,但本地未启动后端,
所有 API 请求 ERR_CONNECTION_REFUSED。

切换至 https://lifescript.happylifeos.com/api(生产服务器),
符合 CLAUDE.md 服务器端验收规则。

此修复与 generationDisplayText + 继续会话 一起完成完整心愿实现流程验证。
2026-07-19 14:17:11 +08:00
peanut c1293c28ac fix(mini-program): 修复心愿实现页用户消息不显示 + 支持继续会话
1. runGeneration 补赋值 generationDisplayText,修复用户输入心愿不显示
2. 识别外部服务 DAILY_GENERATION_IN_PROGRESS 错误码,保存 session_id
3. 失败态新增'继续创作'按钮,调用 followup action=retry 恢复中断会话
4. 新增 resumeSession 方法,重置状态后恢复流式生成
2026-07-19 13:58:16 +08:00
peanut d9a061a6bc fix(deploy): 使用 shlex.quote 防止 curl URL 命令注入 2026-07-19 13:27:03 +08:00
peanut e313086d9b fix(deploy): Windows 平台 curl 验证使用 NUL 替代 /dev/null
Windows cmd.exe 不支持 /dev/null(那是 Unix 概念),
导致 curl 写入失败返回 exit code 23,验证误报为'访问异常'。
改为根据 sys.platform 动态选择 null 设备。
2026-07-19 13:25:01 +08:00
peanut 1b566d574c fix(mini-program): ScriptView 模板闭合标签和 setupRecorder H5 兼容性 2026-07-19 12:56:38 +08:00
peanut 0190941df7 fix(mini-program): 短信验证码文案从 888888 改为 123456 2026-07-19 12:39:54 +08:00
peanut 34f443939e feat(mini-program): ScriptView 多阶段状态机改造 2026-07-19 12:39:50 +08:00
peanut b3a2f3a3d3 feat(mini-program): 澄清卡片组件 2026-07-19 12:36:11 +08:00
peanut ab13ac258b feat(mini-program): 短篇小说 SSE 流式服务 2026-07-19 12:36:06 +08:00
peanut 00f17d2f47 test(server): ShortNovelController 集成测试 2026-07-19 12:22:25 +08:00
peanut c06b04869b feat(server): 短篇小说 SSE 代理 Controller 2026-07-19 12:21:09 +08:00
peanut 02c58767bd fix(server): SSE 代理错误消息不再暴露原始异常信息给客户端 2026-07-19 12:18:54 +08:00
peanut 7018f5780a feat(server): 短篇小说服务 SSE 代理实现 2026-07-19 12:15:50 +08:00
peanut 031d824dd0 refactor(server): 抽取 saveNovelResult 方法供短篇小说服务复用 2026-07-19 12:08:54 +08:00
peanut 8f96239b56 style(server): 统一 ShortNovel 配置类和 DTO 的字段注释风格 2026-07-19 12:07:26 +08:00
peanut 21ce56c6da feat(server): 短篇小说外部服务配置类和请求 DTO 2026-07-19 12:02:17 +08:00
peanut ce244f9ad5 docs:短篇小说服务接口迁移实现计划
- 后端 SSE 代理层(Controller + Service + Config + DTO)
- 数据持久化 saveNovelResult 复用方法
- 前端 shortNovel.js SSE 流式服务
- ClarificationCard 组件 + ScriptView 状态机改造
- 短信验证码文案修复 + 端到端验收
2026-07-19 11:57:53 +08:00
peanut 99a3cb883e docs:短篇小说服务接口迁移设计方案
- 后端 SSE 代理 + 前端多轮交互状态机改造
- 外部 short-novel-service 接口替代内部 AI Runtime
- 数据持久化改为小说完成后一次性保存
- 短信验证码修复(888888→123456)
2026-07-19 11:52:41 +08:00
peanut 2c778fabbb refactor: ScriptView 删除 read 模式 + 兼容旧模式,统一对话流入口 2026-07-01 08:28:47 +08:00
peanut 6aeb5319e3 refactor: ScriptView 删除 read 模式,剧本入口直接进对话流 2026-07-01 07:59:48 +08:00
peanut 3bb6716e0f docs: ScriptView 简化为纯对话流模式设计文档 2026-06-30 22:45:29 +08:00
peanut 86543df75d fix: currentScriptContent 章节字段全空时用 plotJson.fullContent 作为 fallback 2026-06-30 07:45:20 +08:00
peanut 1073123360 fix: createMessage 补 userId 字段 + 使用 senderType/contentType 2026-06-29 23:52:37 +08:00
peanut 3829a3ef92 fix: createMessage 字段名改为 senderType/contentType 匹配后端 DTO 2026-06-29 23:36:26 +08:00
peanut f45fee7cce feat: chat 模式传 messageType/versionLabel/新 emits 给 MessageCard 2026-06-29 22:53:36 +08:00
peanut a713e8823c fix: loadMessages 对历史剧本 plotJson 自动初始化首条 AI 消息 2026-06-29 22:50:15 +08:00
peanut 4a7914eefe fix: displayMessages 去掉根版本过滤(chat 模式显示首条 AI 剧本) 2026-06-29 22:47:37 +08:00
peanut 88ff280b78 feat: MessageCard 支持按 messageType 条件渲染 script/chat 专属按钮组 2026-06-29 22:45:36 +08:00
peanut 912fc42860 fix: 剧本查询时自动为历史剧本补建 conversation(兼容前端临时 ID) 2026-06-29 22:40:59 +08:00
peanut 7fb90299d1 docs: ScriptView 对话模式按钮完整修复设计文档 2026-06-29 22:34:27 +08:00
peanut 6ae721eb89 fix: 回滚 ScriptView 临时 conversationId 正则保护(误伤历史剧本) 2026-06-28 23:33:29 +08:00
peanut 808a590b8d fix: nginx 根路径 301 重定向到 /emotion-museum/(修复首页 404) 2026-06-28 23:21:03 +08:00
peanut 19337b9371 fix: ScriptView loadMessages 对临时 conversationId 正则短路保护 2026-06-28 22:42:24 +08:00
peanut e3eb1afbb0 fix: listByConversation 在 conversation 不存在时返回空列表而非抛异常 2026-06-28 22:40:13 +08:00
peanut caaeb94477 feat: 增强 download-server-log.py 支持 latest/tail/errors/grep 子命令 2026-06-28 22:23:56 +08:00
peanut bd3ccbd757 test: 新增 EpicScriptDialogueService 未登录场景测试 2026-06-28 18:49:10 +08:00
peanut bf6f2797ad feat: ScriptView 改造为对话中心查看/修改模式
- 新增 scriptChat.js 和 message.js 服务导入
- 新增 viewMode、scriptId、conversationId、currentVersionMessageId、
  selectedVersionMessageId、messages、versions、chatInput 等状态
- 新增阅读模式:版本栏 + 剧本分章节展示 + 操作按钮
- 新增聊天模式:消息列表 + 聊天输入栏 + 对话修改功能
- 集成 createScriptWithDialogue、streamScriptChat、listMessagesByConversation、
  listMessageVersions、switchVersion、deleteVersion 等后端 API
- 替换 runGeneration 使用 createScriptWithDialogue 创建剧本和对话
- 替换 openScriptChat 使用后端消息加载,不再依赖 localStorage
- 移除 localStorage 作为主要消息存储(persistResultMessages、
  hydrateResultMessages、readStoredMessages、readScriptChatHistory 改为空操作)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:41:44 +08:00
peanut 6109202a26 chore: request.js 导出 API_BASE_URL 和 getAuthHeader 2026-06-28 18:26:30 +08:00
peanut 3b9a8222de feat: 新增 ScriptChatController 统一流式接口 2026-06-28 18:16:07 +08:00
peanut 8537c27015 feat: MessageController 新增 listByConversation、versions、deleteVersion 2026-06-28 18:09:07 +08:00
peanut a66a78a0f3 feat: EpicScriptController 新增 createWithDialogue 和 switchVersion 2026-06-28 18:09:06 +08:00
peanut 57f1e311b6 feat: 实现 EpicScriptService.switchVersion
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:00:27 +08:00
peanut 8618b8b80f feat: EpicScriptService 新增 switchVersion 方法声明
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:00:22 +08:00
peanut b11a3463b2 feat: 实现剧本消息版本管理服务
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:42:11 +08:00
peanut e252dc5fd2 feat: 新增 ScriptMessageService 接口
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:42:05 +08:00
peanut 0520f30d84 feat: 新增 ScriptChatService 接口 2026-06-28 17:36:02 +08:00
peanut 752fd30686 fix: 修复剧本对话创建服务实现问题
- 添加系统欢迎语消息(messageOrder=1, sender=system)
- 调整用户消息顺序为 messageOrder=2
- 添加 AI 助手开场白消息(messageOrder=3, sender=assistant, type=script)
- 从 AI 生成结果中解析标题并更新到剧本实体
- 修复 messageCount 为 3 以匹配实际消息数量
- 删除不再需要的 buildTitle 方法
- 新增 buildSystemWelcome 方法构建系统欢迎语

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:24:47 +08:00
peanut 56369fdeb2 feat: 新增 EpicScriptDialogueService 接口
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:15:47 +08:00
peanut 88d586bab1 feat: MessageResponse 新增 scriptId、parentMessageId、versionNumber、metadata、status、timestamp 2026-06-28 17:08:31 +08:00
peanut 0149bac281 feat: EpicScriptResponse 新增 conversationId 和 currentVersionMessageId 2026-06-28 17:08:26 +08:00
peanut 42d1350aff feat: 新增 SwitchVersionRequest 2026-06-28 17:08:22 +08:00
peanut 43622be5b0 feat: 新增 ScriptChatStreamRequest 2026-06-28 17:08:18 +08:00
peanut 74183ccb30 feat: 新增 EpicScriptCreateWithDialogueResponse 2026-06-28 17:08:13 +08:00
peanut 06b2ee58e2 feat: 新增 EpicScriptCreateWithDialogueRequest 2026-06-28 17:08:09 +08:00
peanut 83c379d5d5 feat: Message 实体新增 scriptId 和 versionNumber 字段 2026-06-28 17:01:52 +08:00
peanut 15fce9eadf feat: Conversation 实体新增 scriptId 和 currentMessageId 字段 2026-06-28 16:56:58 +08:00
peanut 0ba2960c1b feat: EpicScript 实体新增 conversationId 和 currentVersionMessageId 字段 2026-06-28 16:56:57 +08:00
peanut 0e2cc42a41 chore: 新增剧本对话关联字段迁移脚本 2026-06-28 16:50:31 +08:00
peanut 6bc00863fa docs: 更新本地服务管理规则
- 强制使用 dev-services.py 控制本地服务
- 明确前端/H5 固定端口 5178-5181
- 新增禁止无意义重启规则

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:30:11 +08:00
peanut 433ba4c498 docs:小程序标签库扩展功能实现计划 2026-06-24 21:26:53 +08:00
peanut 1cdee47f3a docs:小程序编辑资料页性格标签与兴趣爱好库扩展设计 2026-06-24 21:20:41 +08:00
peanut 3cd7de1f55 fix:ScriptView 语音按钮改为麦克风图标 2026-06-23 22:56:15 +08:00
peanut 1c45689f03 fix:ScriptView 底部输入框区域增加左右边距 2026-06-23 22:54:52 +08:00
peanut 99a06fcc28 fix:ScriptView 结果页顶部操作栏固定不随滚动 2026-06-23 22:54:11 +08:00
peanut 4cb6d6de9b docs:ScriptView 对话结果页 UI 调整实现计划 2026-06-23 22:51:55 +08:00
peanut 6b6af757d0 docs:ScriptView 对话结果页 UI 调整设计文档 2026-06-23 22:49:15 +08:00
peanut ae1463e400 fix:剧本列表工具函数去除默认值兜底 2026-06-23 22:19:30 +08:00
peanut 70434c1325 fix:剧本列表删除 fallbackScripts 假数据 2026-06-23 22:18:32 +08:00
peanut 04b184a3c1 fix:编辑资料页无昵称时显示未设置昵称 2026-06-23 22:17:45 +08:00
peanut e7531f86cf docs:去除编辑资料页与剧本列表 mock 数据实现计划 2026-06-23 22:16:27 +08:00
peanut a00ce0bf95 docs:去除编辑资料页与剧本列表 mock 数据设计文档 2026-06-23 22:14:31 +08:00
peanut 0eaf44ed93 fix:编辑资料页昵称未填时增加 Toast 提示 2026-06-23 21:47:32 +08:00
peanut eb50358e3d docs:小程序编辑资料页昵称必填提示优化实现计划 2026-06-23 21:34:53 +08:00
peanut 41ed8257f6 docs:小程序编辑资料页昵称必填提示优化设计文档 2026-06-23 21:29:54 +08:00
662 changed files with 30615 additions and 3859 deletions
-81
View File
@@ -1,81 +0,0 @@
---
type: "always_apply"
---
# 项目开发规则
## 基础设置
1. 保持对话语言为中文
2. 不允许在未经允许的情况下删除代码和文件,不允许破坏已经正常的业务代码
3. 执行终端命令时要关注执行情况,避免无效等待
## 代码规范
4. 生成代码时必须添加类级和函数级注释
5. 使用import导包,禁止使用全限定名称引用类
6. 禁止使用枚举类型作为entity、request、response、dto对象的字段类型,禁止以枚举类作为方法的入参,确保接口的通用性和可扩展性
7. 新增数据的id使用已存在的雪花算法生成器生成
## 架构规范
8. 所有开发必须遵循当前项目规范
9. Controller层禁止添加业务逻辑
10. 使用全局异常处理,禁止使用try-catch
11. 前端接口访问尽可能走网关调用
## 接口设计规范
12. Controller层接口定义要完整:
- 入参使用request封装传递到service层
- service层的方法命名与controller层定义的接口的方法名称保持一致
- 出参使用response封装由service层传递到controller层
- 禁止在controller层做entity/domain对象与request/response的转换
- 使用项目已有的Result做接口返回
13. 接口和方法参数不允许超过两个,超过时使用request或DTO对象封装
14. Controller层路由禁止添加/api前缀
15. Controller层接口的Mapping注解value属性值不允许重复且不允许为空,必须明确指定value属性值且使用驼峰结构命名,避免使用下划线分隔符。所有接口注解必须显式指定value属性,如@GetMapping(value = "/page")、@PostMapping(value = "/create")等,禁止使用@GetMapping()或@PostMapping等空注解形式
16. 用户相关接口禁止直接传递用户id,需要后端根据token获取当前登录用户信息
17. 禁止使用/{param}格式的路径参数,避免网关路由冲突和分发错误
18. 路径参数统一使用@RequestParam而非@PathVariable,确保网关分发准确性
19. 接口路径命名应具有明确的语义,避免使用通用词汇如/get、/list等
20. 批量操作接口应使用专门的Request对象封装参数,而非直接传递List
21. 接口路径应避免层级过深,建议不超过3级路径结构
22. 更新操作应直接使用封装了ID和其他数据的Request对象,而不是单独传递ID参数。Service层方法应保持参数简洁,业务逻辑所需数据应全部包含在Request对象中
23. Controller层接口应保持简洁,避免为特定字段创建独立的更新方法(如updateStatus等),应只保留一个通用的update方法,具体的业务逻辑在Service层实现
24. Service层在执行更新操作时,应对每个字段进行空值检查,只更新非空字段,避免空字段覆盖原有值,确保数据完整性
25. Controller层应避免创建多个特定条件的查询接口,只保留一个分页查询方法,通过在请求对象中包含所有可查询字段来支持不同的查询需求
26. 分页查询接口应支持模糊查询和精确查询,通过在Service层根据字段类型和业务需求判断查询方式
## 环境配置
27. 为不同环境(local、dev、prod)创建单独配置文件,部署时通过参数选择
28. 启动服务时禁止擅自修改端口号,使用配置文件中的端口设置
## 数据库规范
29. 所有数据表必须包含创建时间(create_time)和更新时间(update_time)字段
30. 删除操作优先使用逻辑删除,添加deleted字段标识
31. 数据库字段命名使用下划线分隔,Java实体类使用驼峰命名
32. 优先使用LambdaQueryWrapper构造条件查询,避免硬编码字段名
33. 使用Lambda表达式引用实体类属性,提高代码可维护性和类型安全
34. 复杂查询条件应使用LambdaQueryWrapper的链式调用,保持代码清晰
35. 避免在查询条件中使用字符串字段名,防止字段名变更导致的运行时错误
## 安全规范
36. 所有外部输入必须进行参数校验
37. 敏感信息不得在日志中输出
38. 数据库操作必须使用参数化查询,防止SQL注入
## 性能规范
39. 避免N+1查询问题,合理使用批量查询
40. 大数据量查询必须分页处理
41. 缓存策略要考虑数据一致性问题
## 日志规范
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
-184
View File
@@ -1,184 +0,0 @@
---
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Goal
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
## Operating Constraints
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
## Execution Steps
### 1. Initialize Analysis Context
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths:
- SPEC = FEATURE_DIR/spec.md
- PLAN = FEATURE_DIR/plan.md
- TASKS = FEATURE_DIR/tasks.md
Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command).
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
### 2. Load Artifacts (Progressive Disclosure)
Load only the minimal necessary context from each artifact:
**From spec.md:**
- Overview/Context
- Functional Requirements
- Non-Functional Requirements
- User Stories
- Edge Cases (if present)
**From plan.md:**
- Architecture/stack choices
- Data Model references
- Phases
- Technical constraints
**From tasks.md:**
- Task IDs
- Descriptions
- Phase grouping
- Parallel markers [P]
- Referenced file paths
**From constitution:**
- Load `.specify/memory/constitution.md` for principle validation
### 3. Build Semantic Models
Create internal representations (do not include raw artifacts in output):
- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`)
- **User story/action inventory**: Discrete user actions with acceptance criteria
- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases)
- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements
### 4. Detection Passes (Token-Efficient Analysis)
Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary.
#### A. Duplication Detection
- Identify near-duplicate requirements
- Mark lower-quality phrasing for consolidation
#### B. Ambiguity Detection
- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria
- Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.)
#### C. Underspecification
- Requirements with verbs but missing object or measurable outcome
- User stories missing acceptance criteria alignment
- Tasks referencing files or components not defined in spec/plan
#### D. Constitution Alignment
- Any requirement or plan element conflicting with a MUST principle
- Missing mandated sections or quality gates from constitution
#### E. Coverage Gaps
- Requirements with zero associated tasks
- Tasks with no mapped requirement/story
- Non-functional requirements not reflected in tasks (e.g., performance, security)
#### F. Inconsistency
- Terminology drift (same concept named differently across files)
- Data entities referenced in plan but absent in spec (or vice versa)
- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note)
- Conflicting requirements (e.g., one requires Next.js while other specifies Vue)
### 5. Severity Assignment
Use this heuristic to prioritize findings:
- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality
- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion
- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case
- **LOW**: Style/wording improvements, minor redundancy not affecting execution order
### 6. Produce Compact Analysis Report
Output a Markdown report (no file writes) with the following structure:
## Specification Analysis Report
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------|
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
(Add one row per finding; generate stable IDs prefixed by category initial.)
**Coverage Summary Table:**
| Requirement Key | Has Task? | Task IDs | Notes |
|-----------------|-----------|----------|-------|
**Constitution Alignment Issues:** (if any)
**Unmapped Tasks:** (if any)
**Metrics:**
- Total Requirements
- Total Tasks
- Coverage % (requirements with >=1 task)
- Ambiguity Count
- Duplication Count
- Critical Issues Count
### 7. Provide Next Actions
At end of report, output a concise Next Actions block:
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
### 8. Offer Remediation
Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.)
## Operating Principles
### Context Efficiency
- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation
- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis
- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow
- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts
### Analysis Guidelines
- **NEVER modify files** (this is read-only analysis)
- **NEVER hallucinate missing sections** (if absent, report them accurately)
- **Prioritize constitution violations** (these are always CRITICAL)
- **Use examples over exhaustive rules** (cite specific instances, not generic patterns)
- **Report zero issues gracefully** (emit success report with coverage statistics)
## Context
$ARGUMENTS
-294
View File
@@ -1,294 +0,0 @@
---
description: Generate a custom checklist for the current feature based on user requirements.
---
## Checklist Purpose: "Unit Tests for English"
**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain.
**NOT for verification/testing**:
- ❌ NOT "Verify the button clicks correctly"
- ❌ NOT "Test error handling works"
- ❌ NOT "Confirm the API returns 200"
- ❌ NOT checking if code/implementation matches the spec
**FOR requirements quality validation**:
- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness)
- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity)
- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency)
- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage)
- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases)
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Execution Steps
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list.
- All file paths must be absolute.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
- Only ask about information that materially changes checklist content
- Be skipped individually if already unambiguous in `$ARGUMENTS`
- Prefer precision over breadth
Generation algorithm:
1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts").
2. Cluster signals into candidate focus areas (max 4) ranked by relevance.
3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit.
4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria.
5. Formulate questions chosen from these archetypes:
- Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?")
- Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?")
- Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?")
- Audience framing (e.g., "Will this be used by the author only or peers during PR review?")
- Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?")
- Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?")
Question formatting rules:
- If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters
- Limit to AE options maximum; omit table if a free-form answer is clearer
- Never ask the user to restate what they already said
- Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope."
Defaults when interaction impossible:
- Depth: Standard
- Audience: Reviewer (PR) if code-related; Author otherwise
- Focus: Top 2 relevance clusters
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted followups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
- Derive checklist theme (e.g., security, review, deploy, ux)
- Consolidate explicit must-have items mentioned by user
- Map focus selections to category scaffolding
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
4. **Load feature context**: Read from FEATURE_DIR:
- spec.md: Feature requirements and scope
- plan.md (if exists): Technical details, dependencies
- tasks.md (if exists): Implementation tasks
**Context Loading Strategy**:
- Load only necessary portions relevant to active focus areas (avoid full-file dumping)
- Prefer summarizing long sections into concise scenario/requirement bullets
- Use progressive disclosure: add follow-on retrieval only if gaps detected
- If source docs are large, generate interim summary items instead of embedding raw text
5. **Generate checklist** - Create "Unit Tests for Requirements":
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
- Generate unique checklist filename:
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
- Format: `[domain].md`
- If file exists, append to existing file
- Number items sequentially starting from CHK001
- Each `/speckit.checklist` run creates a NEW file (never overwrites existing checklists)
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
- **Completeness**: Are all necessary requirements present?
- **Clarity**: Are requirements unambiguous and specific?
- **Consistency**: Do requirements align with each other?
- **Measurability**: Can requirements be objectively verified?
- **Coverage**: Are all scenarios/edge cases addressed?
**Category Structure** - Group items by requirement quality dimensions:
- **Requirement Completeness** (Are all necessary requirements documented?)
- **Requirement Clarity** (Are requirements specific and unambiguous?)
- **Requirement Consistency** (Do requirements align without conflicts?)
- **Acceptance Criteria Quality** (Are success criteria measurable?)
- **Scenario Coverage** (Are all flows/cases addressed?)
- **Edge Case Coverage** (Are boundary conditions defined?)
- **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?)
- **Dependencies & Assumptions** (Are they documented and validated?)
- **Ambiguities & Conflicts** (What needs clarification?)
**HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**:
**WRONG** (Testing implementation):
- "Verify landing page displays 3 episode cards"
- "Test hover states work on desktop"
- "Confirm logo click navigates home"
**CORRECT** (Testing requirements quality):
- "Are the exact number and layout of featured episodes specified?" [Completeness]
- "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity]
- "Are hover state requirements consistent across all interactive elements?" [Consistency]
- "Are keyboard navigation requirements defined for all interactive UI?" [Coverage]
- "Is the fallback behavior specified when logo image fails to load?" [Edge Cases]
- "Are loading states defined for asynchronous episode data?" [Completeness]
- "Does the spec define visual hierarchy for competing UI elements?" [Clarity]
**ITEM STRUCTURE**:
Each item should follow this pattern:
- Question format asking about requirement quality
- Focus on what's WRITTEN (or not written) in the spec/plan
- Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.]
- Reference spec section `[Spec §X.Y]` when checking existing requirements
- Use `[Gap]` marker when checking for missing requirements
**EXAMPLES BY QUALITY DIMENSION**:
Completeness:
- "Are error handling requirements defined for all API failure modes? [Gap]"
- "Are accessibility requirements specified for all interactive elements? [Completeness]"
- "Are mobile breakpoint requirements defined for responsive layouts? [Gap]"
Clarity:
- "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]"
- "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]"
- "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]"
Consistency:
- "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]"
- "Are card component requirements consistent between landing and detail pages? [Consistency]"
Coverage:
- "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]"
- "Are concurrent user interaction scenarios addressed? [Coverage, Gap]"
- "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]"
Measurability:
- "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]"
- "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]"
**Scenario Classification & Coverage** (Requirements Quality Focus):
- Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios
- For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?"
- If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]"
- Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]"
**Traceability Requirements**:
- MINIMUM: ≥80% of items MUST include at least one traceability reference
- Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]`
- If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]"
**Surface & Resolve Issues** (Requirements Quality Problems):
Ask questions about the requirements themselves:
- Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]"
- Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]"
- Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]"
- Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]"
- Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]"
**Content Consolidation**:
- Soft cap: If raw candidate items > 40, prioritize by risk/impact
- Merge near-duplicates checking the same requirement aspect
- If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]"
**🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test:
- ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior
- ❌ References to code execution, user actions, system behavior
- ❌ "Displays correctly", "works properly", "functions as expected"
- ❌ "Click", "navigate", "render", "load", "execute"
- ❌ Test cases, test plans, QA procedures
- ❌ Implementation details (frameworks, APIs, algorithms)
**✅ REQUIRED PATTERNS** - These test requirements quality:
- ✅ "Are [requirement type] defined/specified/documented for [scenario]?"
- ✅ "Is [vague term] quantified/clarified with specific criteria?"
- ✅ "Are requirements consistent between [section A] and [section B]?"
- ✅ "Can [requirement] be objectively measured/verified?"
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
- ✅ "Does the spec define [missing aspect]?"
6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
7. **Report**: Output full path to created checklist, item count, and remind user that each run creates a new file. Summarize:
- Focus areas selected
- Depth level
- Actor/timing
- Any explicit user-specified must-have items incorporated
**Important**: Each `/speckit.checklist` command invocation creates a checklist file using short, descriptive names unless file already exists. This allows:
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
- Simple, memorable filenames that indicate checklist purpose
- Easy identification and navigation in the `checklists/` folder
To avoid clutter, use descriptive types and clean up obsolete checklists when done.
## Example Checklist Types & Sample Items
**UX Requirements Quality:** `ux.md`
Sample items (testing the requirements, NOT the implementation):
- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]"
- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]"
- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]"
- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]"
- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]"
- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]"
**API Requirements Quality:** `api.md`
Sample items:
- "Are error response formats specified for all failure scenarios? [Completeness]"
- "Are rate limiting requirements quantified with specific thresholds? [Clarity]"
- "Are authentication requirements consistent across all endpoints? [Consistency]"
- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]"
- "Is versioning strategy documented in requirements? [Gap]"
**Performance Requirements Quality:** `performance.md`
Sample items:
- "Are performance requirements quantified with specific metrics? [Clarity]"
- "Are performance targets defined for all critical user journeys? [Coverage]"
- "Are performance requirements under different load conditions specified? [Completeness]"
- "Can performance requirements be objectively measured? [Measurability]"
- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]"
**Security Requirements Quality:** `security.md`
Sample items:
- "Are authentication requirements specified for all protected resources? [Coverage]"
- "Are data protection requirements defined for sensitive information? [Completeness]"
- "Is the threat model documented and requirements aligned to it? [Traceability]"
- "Are security requirements consistent with compliance obligations? [Consistency]"
- "Are security failure/breach response requirements defined? [Gap, Exception Flow]"
## Anti-Examples: What NOT To Do
**❌ WRONG - These test implementation, not requirements:**
```markdown
- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001]
- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003]
- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010]
- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005]
```
**✅ CORRECT - These test requirements quality:**
```markdown
- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001]
- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003]
- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010]
- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005]
- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap]
- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001]
```
**Key Differences:**
- Wrong: Tests if the system works correctly
- Correct: Tests if the requirements are written correctly
- Wrong: Verification of behavior
- Correct: Validation of requirement quality
- Wrong: "Does it do X?"
- Correct: "Is X clearly specified?"
-181
View File
@@ -1,181 +0,0 @@
---
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
Execution steps:
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
- `FEATURE_DIR`
- `FEATURE_SPEC`
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
Functional Scope & Behavior:
- Core user goals & success criteria
- Explicit out-of-scope declarations
- User roles / personas differentiation
Domain & Data Model:
- Entities, attributes, relationships
- Identity & uniqueness rules
- Lifecycle/state transitions
- Data volume / scale assumptions
Interaction & UX Flow:
- Critical user journeys / sequences
- Error/empty/loading states
- Accessibility or localization notes
Non-Functional Quality Attributes:
- Performance (latency, throughput targets)
- Scalability (horizontal/vertical, limits)
- Reliability & availability (uptime, recovery expectations)
- Observability (logging, metrics, tracing signals)
- Security & privacy (authN/Z, data protection, threat assumptions)
- Compliance / regulatory constraints (if any)
Integration & External Dependencies:
- External services/APIs and failure modes
- Data import/export formats
- Protocol/versioning assumptions
Edge Cases & Failure Handling:
- Negative scenarios
- Rate limiting / throttling
- Conflict resolution (e.g., concurrent edits)
Constraints & Tradeoffs:
- Technical constraints (language, storage, hosting)
- Explicit tradeoffs or rejected alternatives
Terminology & Consistency:
- Canonical glossary terms
- Avoided synonyms / deprecated terms
Completion Signals:
- Acceptance criteria testability
- Measurable Definition of Done style indicators
Misc / Placeholders:
- TODO markers / unresolved decisions
- Ambiguous adjectives ("robust", "intuitive") lacking quantification
For each category with Partial or Missing status, add a candidate question opportunity unless:
- Clarification would not materially change implementation or validation strategy
- Information is better deferred to planning phase (note internally)
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
- Maximum of 10 total questions across the whole session.
- Each question must be answerable with EITHER:
- A short multiplechoice selection (25 distinct, mutually exclusive options), OR
- A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words").
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
4. Sequential questioning loop (interactive):
- Present EXACTLY ONE question at a time.
- For multiplechoice questions:
- **Analyze all options** and determine the **most suitable option** based on:
- Best practices for the project type
- Common patterns in similar implementations
- Risk reduction (security, performance, maintainability)
- Alignment with any explicit project goals or constraints visible in the spec
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
- Format as: `**Recommended:** Option [X] - <reasoning>`
- Then render all options as a Markdown table:
| Option | Description |
|--------|-------------|
| A | <Option A description> |
| B | <Option B description> |
| C | <Option C description> (add D/E as needed up to 5) |
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
- For shortanswer style (no meaningful discrete options):
- Provide your **suggested answer** based on best practices and context.
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
- After the user answers:
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
- Stop asking further questions when:
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
- User signals completion ("done", "good", "no more"), OR
- You reach 5 asked questions.
- Never reveal future queued questions in advance.
- If no valid questions exist at start, immediately report no critical ambiguities.
5. Integration after EACH accepted answer (incremental update approach):
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
- For the first integrated answer in this session:
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
- Then immediately apply the clarification to the most appropriate section(s):
- Functional ambiguity → Update or add a bullet in Functional Requirements.
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
- Keep each inserted clarification minimal and testable (avoid narrative drift).
6. Validation (performed after EACH write plus final pass):
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
- Total asked (accepted) questions ≤ 5.
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
- No contradictory earlier statement remains (scan for now-invalid alternative choices removed).
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
- Terminology consistency: same canonical term used across all updated sections.
7. Write the updated spec back to `FEATURE_SPEC`.
8. Report completion (after questioning loop ends or early termination):
- Number of questions asked & answered.
- Path to updated spec.
- Sections touched (list names).
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
- Suggested next command.
Behavior rules:
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
- Respect user early termination signals ("stop", "done", "proceed").
- If no questions asked due to full coverage, output a compact coverage summary (all categories Clear) then suggest advancing.
- If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale.
Context for prioritization: $ARGUMENTS
-84
View File
@@ -1,84 +0,0 @@
---
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
handoffs:
- label: Build Specification
agent: speckit.specify
prompt: Implement the feature specification based on the updated constitution. I want to build...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
Follow this execution flow:
1. Load the existing constitution at `.specify/memory/constitution.md`.
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
2. Collect/derive values for placeholders:
- If user input (conversation) supplies a value, use it.
- Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded).
- For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous.
- `CONSTITUTION_VERSION` must increment according to semantic versioning rules:
- MAJOR: Backward incompatible governance/principle removals or redefinitions.
- MINOR: New principle/section added or materially expanded guidance.
- PATCH: Clarifications, wording, typo fixes, non-semantic refinements.
- If version bump type ambiguous, propose reasoning before finalizing.
3. Draft the updated constitution content:
- Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left).
- Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance.
- Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing nonnegotiable rules, explicit rationale if not obvious.
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
4. Consistency propagation checklist (convert prior checklist into active validations):
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
- Version change: old → new
- List of modified principles (old title → new title if renamed)
- Added sections
- Removed sections
- Templates requiring updates (✅ updated / ⚠ pending) with file paths
- Follow-up TODOs if any placeholders intentionally deferred.
6. Validation before final output:
- No remaining unexplained bracket tokens.
- Version line matches report.
- Dates ISO format YYYY-MM-DD.
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite).
8. Output a final summary to the user with:
- New version and bump rationale.
- Any files flagged for manual follow-up.
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
Formatting & Style Requirements:
- Use Markdown headings exactly as in the template (do not demote/promote levels).
- Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks.
- Keep a single blank line between sections.
- Avoid trailing whitespace.
If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps.
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file.
-135
View File
@@ -1,135 +0,0 @@
---
description: Execute the implementation plan by processing and executing all tasks defined in tasks.md
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
- Scan all checklist files in the checklists/ directory
- For each checklist, count:
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
- Completed items: Lines matching `- [X]` or `- [x]`
- Incomplete items: Lines matching `- [ ]`
- Create a status table:
```text
| Checklist | Total | Completed | Incomplete | Status |
|-----------|-------|-----------|------------|--------|
| ux.md | 12 | 12 | 0 | ✓ PASS |
| test.md | 8 | 5 | 3 | ✗ FAIL |
| security.md | 6 | 6 | 0 | ✓ PASS |
```
- Calculate overall status:
- **PASS**: All checklists have 0 incomplete items
- **FAIL**: One or more checklists have incomplete items
- **If any checklist is incomplete**:
- Display the table with incomplete item counts
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
- Wait for user response before continuing
- If user says "no" or "wait" or "stop", halt execution
- If user says "yes" or "proceed" or "continue", proceed to step 3
- **If all checklists are complete**:
- Display the table showing all checklists passed
- Automatically proceed to step 3
3. Load and analyze the implementation context:
- **REQUIRED**: Read tasks.md for the complete task list and execution plan
- **REQUIRED**: Read plan.md for tech stack, architecture, and file structure
- **IF EXISTS**: Read data-model.md for entities and relationships
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
- **IF EXISTS**: Read research.md for technical decisions and constraints
- **IF EXISTS**: Read quickstart.md for integration scenarios
4. **Project Setup Verification**:
- **REQUIRED**: Create/verify ignore files based on actual project setup:
**Detection & Creation Logic**:
- Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so):
```sh
git rev-parse --git-dir 2>/dev/null
```
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc* exists → create/verify .eslintignore
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc* exists → create/verify .prettierignore
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
- Check if terraform files (*.tf) exist → create/verify .terraformignore
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
**If ignore file missing**: Create with full pattern set for detected technology
**Common Patterns by Technology** (from plan.md tech stack):
- **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*`
- **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/`
- **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/`
- **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/`
- **Go**: `*.exe`, `*.test`, `vendor/`, `*.out`
- **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/`
- **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env`
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `Makefile`, `config.log`, `.idea/`, `*.log`, `.env*`
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
**Tool-Specific Patterns**:
- **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/`
- **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js`
- **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
- **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl`
- **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt`
5. Parse tasks.md structure and extract:
- **Task phases**: Setup, Tests, Core, Integration, Polish
- **Task dependencies**: Sequential vs parallel execution rules
- **Task details**: ID, description, file paths, parallel markers [P]
- **Execution flow**: Order and dependency requirements
6. Execute implementation following the task plan:
- **Phase-by-phase execution**: Complete each phase before moving to the next
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
- **File-based coordination**: Tasks affecting the same files must run sequentially
- **Validation checkpoints**: Verify each phase completion before proceeding
7. Implementation execution rules:
- **Setup first**: Initialize project structure, dependencies, configuration
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
- **Core development**: Implement models, services, CLI commands, endpoints
- **Integration work**: Database connections, middleware, logging, external services
- **Polish and validation**: Unit tests, performance optimization, documentation
8. Progress tracking and error handling:
- Report progress after each completed task
- Halt execution if any non-parallel task fails
- For parallel tasks [P], continue with successful tasks, report failed ones
- Provide clear error messages with context for debugging
- Suggest next steps if implementation cannot proceed
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
9. Completion validation:
- Verify all required tasks are completed
- Check that implemented features match the original specification
- Validate that tests pass and coverage meets requirements
- Confirm the implementation follows the technical plan
- Report final status with summary of completed work
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
-89
View File
@@ -1,89 +0,0 @@
---
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
handoffs:
- label: Create Tasks
agent: speckit.tasks
prompt: Break the plan into tasks
send: true
- label: Create Checklist
agent: speckit.checklist
prompt: Create a checklist for the following domain...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load context**: Read FEATURE_SPEC and `.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied).
3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
- Fill Constitution Check section from constitution
- Evaluate gates (ERROR if violations unjustified)
- Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
- Phase 1: Generate data-model.md, contracts/, quickstart.md
- Phase 1: Update agent context by running the agent script
- Re-evaluate Constitution Check post-design
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
## Phases
### Phase 0: Outline & Research
1. **Extract unknowns from Technical Context** above:
- For each NEEDS CLARIFICATION → research task
- For each dependency → best practices task
- For each integration → patterns task
2. **Generate and dispatch research agents**:
```text
For each unknown in Technical Context:
Task: "Research {unknown} for {feature context}"
For each technology choice:
Task: "Find best practices for {tech} in {domain}"
```
3. **Consolidate findings** in `research.md` using format:
- Decision: [what was chosen]
- Rationale: [why chosen]
- Alternatives considered: [what else evaluated]
**Output**: research.md with all NEEDS CLARIFICATION resolved
### Phase 1: Design & Contracts
**Prerequisites:** `research.md` complete
1. **Extract entities from feature spec** → `data-model.md`:
- Entity name, fields, relationships
- Validation rules from requirements
- State transitions if applicable
2. **Generate API contracts** from functional requirements:
- For each user action → endpoint
- Use standard REST/GraphQL patterns
- Output OpenAPI/GraphQL schema to `/contracts/`
3. **Agent context update**:
- Run `.specify/scripts/bash/update-agent-context.sh cursor-agent`
- These scripts detect which AI agent is in use
- Update the appropriate agent-specific context file
- Add only new technology from current plan
- Preserve manual additions between markers
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
## Key rules
- Use absolute paths
- ERROR on gate failures or unresolved clarifications
-258
View File
@@ -1,258 +0,0 @@
---
description: Create or update the feature specification from a natural language feature description.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
- label: Clarify Spec Requirements
agent: speckit.clarify
prompt: Clarify specification requirements
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
Given that feature description, do this:
1. **Generate a concise short name** (2-4 words) for the branch:
- Analyze the feature description and extract the most meaningful keywords
- Create a 2-4 word short name that captures the essence of the feature
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
- Keep it concise but descriptive enough to understand the feature at a glance
- Examples:
- "I want to add user authentication" → "user-auth"
- "Implement OAuth2 integration for the API" → "oauth2-api-integration"
- "Create a dashboard for analytics" → "analytics-dashboard"
- "Fix payment processing timeout bug" → "fix-payment-timeout"
2. **Check for existing branches before creating new one**:
a. First, fetch all remote branches to ensure we have the latest information:
```bash
git fetch --all --prune
```
b. Find the highest feature number across all sources for the short-name:
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
c. Determine the next available number:
- Extract all numbers from all three sources
- Find the highest number N
- Use N+1 for the new branch number
d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name:
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
- Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"`
- PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
**IMPORTANT**:
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
- Only match branches/directories with the exact short-name pattern
- If no existing branches/directories found with this short-name, start with number 1
- You must only ever run this script once per feature
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
3. Load `.specify/templates/spec-template.md` to understand required sections.
4. Follow this execution flow:
1. Parse user description from Input
If empty: ERROR "No feature description provided"
2. Extract key concepts from description
Identify: actors, actions, data, constraints
3. For unclear aspects:
- Make informed guesses based on context and industry standards
- Only mark with [NEEDS CLARIFICATION: specific question] if:
- The choice significantly impacts feature scope or user experience
- Multiple reasonable interpretations exist with different implications
- No reasonable default exists
- **LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total**
- Prioritize clarifications by impact: scope > security/privacy > user experience > technical details
4. Fill User Scenarios & Testing section
If no clear user flow: ERROR "Cannot determine user scenarios"
5. Generate Functional Requirements
Each requirement must be testable
Use reasonable defaults for unspecified details (document assumptions in Assumptions section)
6. Define Success Criteria
Create measurable, technology-agnostic outcomes
Include both quantitative metrics (time, performance, volume) and qualitative measures (user satisfaction, task completion)
Each criterion must be verifiable without implementation details
7. Identify Key Entities (if data involved)
8. Return: SUCCESS (spec ready for planning)
5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items:
```markdown
# Specification Quality Checklist: [FEATURE NAME]
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: [DATE]
**Feature**: [Link to spec.md]
## Content Quality
- [ ] No implementation details (languages, frameworks, APIs)
- [ ] Focused on user value and business needs
- [ ] Written for non-technical stakeholders
- [ ] All mandatory sections completed
## Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable
- [ ] Success criteria are technology-agnostic (no implementation details)
- [ ] All acceptance scenarios are defined
- [ ] Edge cases are identified
- [ ] Scope is clearly bounded
- [ ] Dependencies and assumptions identified
## Feature Readiness
- [ ] All functional requirements have clear acceptance criteria
- [ ] User scenarios cover primary flows
- [ ] Feature meets measurable outcomes defined in Success Criteria
- [ ] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
```
b. **Run Validation Check**: Review the spec against each checklist item:
- For each item, determine if it passes or fails
- Document specific issues found (quote relevant spec sections)
c. **Handle Validation Results**:
- **If all items pass**: Mark checklist complete and proceed to step 6
- **If items fail (excluding [NEEDS CLARIFICATION])**:
1. List the failing items and specific issues
2. Update the spec to address each issue
3. Re-run validation until all items pass (max 3 iterations)
4. If still failing after 3 iterations, document remaining issues in checklist notes and warn user
- **If [NEEDS CLARIFICATION] markers remain**:
1. Extract all [NEEDS CLARIFICATION: ...] markers from the spec
2. **LIMIT CHECK**: If more than 3 markers exist, keep only the 3 most critical (by scope/security/UX impact) and make informed guesses for the rest
3. For each clarification needed (max 3), present options to user in this format:
```markdown
## Question [N]: [Topic]
**Context**: [Quote relevant spec section]
**What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
**Suggested Answers**:
| Option | Answer | Implications |
|--------|--------|--------------|
| A | [First suggested answer] | [What this means for the feature] |
| B | [Second suggested answer] | [What this means for the feature] |
| C | [Third suggested answer] | [What this means for the feature] |
| Custom | Provide your own answer | [Explain how to provide custom input] |
**Your choice**: _[Wait for user response]_
```
4. **CRITICAL - Table Formatting**: Ensure markdown tables are properly formatted:
- Use consistent spacing with pipes aligned
- Each cell should have spaces around content: `| Content |` not `|Content|`
- Header separator must have at least 3 dashes: `|--------|`
- Test that the table renders correctly in markdown preview
5. Number questions sequentially (Q1, Q2, Q3 - max 3 total)
6. Present all questions together before waiting for responses
7. Wait for user to respond with their choices for all questions (e.g., "Q1: A, Q2: Custom - [details], Q3: B")
8. Update the spec by replacing each [NEEDS CLARIFICATION] marker with the user's selected or provided answer
9. Re-run validation after all clarifications are resolved
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
## General Guidelines
## Quick Guidelines
- Focus on **WHAT** users need and **WHY**.
- Avoid HOW to implement (no tech stack, APIs, code structure).
- Written for business stakeholders, not developers.
- DO NOT create any checklists that are embedded in the spec. That will be a separate command.
### Section Requirements
- **Mandatory sections**: Must be completed for every feature
- **Optional sections**: Include only when relevant to the feature
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
### For AI Generation
When creating this spec from a user prompt:
1. **Make informed guesses**: Use context, industry standards, and common patterns to fill gaps
2. **Document assumptions**: Record reasonable defaults in the Assumptions section
3. **Limit clarifications**: Maximum 3 [NEEDS CLARIFICATION] markers - use only for critical decisions that:
- Significantly impact feature scope or user experience
- Have multiple reasonable interpretations with different implications
- Lack any reasonable default
4. **Prioritize clarifications**: scope > security/privacy > user experience > technical details
5. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
6. **Common areas needing clarification** (only if no reasonable default exists):
- Feature scope and boundaries (include/exclude specific use cases)
- User types and permissions (if multiple conflicting interpretations possible)
- Security/compliance requirements (when legally/financially significant)
**Examples of reasonable defaults** (don't ask about these):
- Data retention: Industry-standard practices for the domain
- Performance targets: Standard web/mobile app expectations unless specified
- Error handling: User-friendly messages with appropriate fallbacks
- Authentication method: Standard session-based or OAuth2 for web apps
- Integration patterns: RESTful APIs unless specified otherwise
### Success Criteria Guidelines
Success criteria must be:
1. **Measurable**: Include specific metrics (time, percentage, count, rate)
2. **Technology-agnostic**: No mention of frameworks, languages, databases, or tools
3. **User-focused**: Describe outcomes from user/business perspective, not system internals
4. **Verifiable**: Can be tested/validated without knowing implementation details
**Good examples**:
- "Users can complete checkout in under 3 minutes"
- "System supports 10,000 concurrent users"
- "95% of searches return results in under 1 second"
- "Task completion rate improves by 40%"
**Bad examples** (implementation-focused):
- "API response time is under 200ms" (too technical, use "Users see results instantly")
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
- "React components render efficiently" (framework-specific)
- "Redis cache hit rate above 80%" (technology-specific)
-137
View File
@@ -1,137 +0,0 @@
---
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
handoffs:
- label: Analyze For Consistency
agent: speckit.analyze
prompt: Run a project analysis for consistency
send: true
- label: Implement Project
agent: speckit.implement
prompt: Start the implementation in phases
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load design documents**: Read from FEATURE_DIR:
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
- **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios)
- Note: Not all projects have all documents. Generate tasks based on what's available.
3. **Execute task generation workflow**:
- Load plan.md and extract tech stack, libraries, project structure
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
- If data-model.md exists: Extract entities and map to user stories
- If contracts/ exists: Map endpoints to user stories
- If research.md exists: Extract decisions for setup tasks
- Generate tasks organized by user story (see Task Generation Rules below)
- Generate dependency graph showing user story completion order
- Create parallel execution examples per user story
- Validate task completeness (each user story has all needed tasks, independently testable)
4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
- Correct feature name from plan.md
- Phase 1: Setup tasks (project initialization)
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)
- Phase 3+: One phase per user story (in priority order from spec.md)
- Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks
- Final Phase: Polish & cross-cutting concerns
- All tasks must follow the strict checklist format (see Task Generation Rules below)
- Clear file paths for each task
- Dependencies section showing story completion order
- Parallel execution examples per story
- Implementation strategy section (MVP first, incremental delivery)
5. **Report**: Output path to generated tasks.md and summary:
- Total task count
- Task count per user story
- Parallel opportunities identified
- Independent test criteria for each story
- Suggested MVP scope (typically just User Story 1)
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
Context for task generation: $ARGUMENTS
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
## Task Generation Rules
**CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
**Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature specification or if user requests TDD approach.
### Checklist Format (REQUIRED)
Every task MUST strictly follow this format:
```text
- [ ] [TaskID] [P?] [Story?] Description with file path
```
**Format Components**:
1. **Checkbox**: ALWAYS start with `- [ ]` (markdown checkbox)
2. **Task ID**: Sequential number (T001, T002, T003...) in execution order
3. **[P] marker**: Include ONLY if task is parallelizable (different files, no dependencies on incomplete tasks)
4. **[Story] label**: REQUIRED for user story phase tasks only
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
- Setup phase: NO story label
- Foundational phase: NO story label
- User Story phases: MUST have story label
- Polish phase: NO story label
5. **Description**: Clear action with exact file path
**Examples**:
- ✅ CORRECT: `- [ ] T001 Create project structure per implementation plan`
- ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py`
- ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py`
- ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py`
- ❌ WRONG: `- [ ] Create User model` (missing ID and Story label)
- ❌ WRONG: `T001 [US1] Create model` (missing checkbox)
- ❌ WRONG: `- [ ] [US1] Create User model` (missing Task ID)
- ❌ WRONG: `- [ ] T001 [US1] Create model` (missing file path)
### Task Organization
1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION:
- Each user story (P1, P2, P3...) gets its own phase
- Map all related components to their story:
- Models needed for that story
- Services needed for that story
- Endpoints/UI needed for that story
- If tests requested: Tests specific to that story
- Mark story dependencies (most stories should be independent)
2. **From Contracts**:
- Map each contract/endpoint → to the user story it serves
- If tests requested: Each contract → contract test task [P] before implementation in that story's phase
3. **From Data Model**:
- Map each entity to the user story(ies) that need it
- If entity serves multiple stories: Put in earliest story or Setup phase
- Relationships → service layer tasks in appropriate story phase
4. **From Setup/Infrastructure**:
- Shared infrastructure → Setup phase (Phase 1)
- Foundational/blocking tasks → Foundational phase (Phase 2)
- Story-specific setup → within that story's phase
### Phase Structure
- **Phase 1**: Setup (project initialization)
- **Phase 2**: Foundational (blocking prerequisites - MUST complete before user stories)
- **Phase 3+**: User Stories in priority order (P1, P2, P3...)
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
- Each phase should be a complete, independently testable increment
- **Final Phase**: Polish & Cross-Cutting Concerns
-30
View File
@@ -1,30 +0,0 @@
---
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
tools: ['github/github-mcp-server/issue_write']
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
1. From the executed script, extract the path to **tasks**.
1. Get the Git remote by running:
```bash
git config --get remote.origin.url
```
> [!CAUTION]
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
> [!CAUTION]
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
-80
View File
@@ -1,80 +0,0 @@
---
alwaysApply: true
---
# 项目开发规则
## 基础设置
1. 保持对话语言为中文
2. 不允许在未经允许的情况下删除代码和文件,不允许破坏已经正常的业务代码
3. 执行终端命令时要关注执行情况,避免无效等待
## 代码规范
4. 生成代码时必须添加类级和函数级注释
5. 使用import导包,禁止使用全限定名称引用类
6. 禁止使用枚举类型作为entity、request、response、dto对象的字段类型,禁止以枚举类作为方法的入参,确保接口的通用性和可扩展性
7. 新增数据的id使用已存在的雪花算法生成器生成
## 架构规范
8. 所有开发必须遵循当前项目规范
9. Controller层禁止添加业务逻辑
10. 使用全局异常处理,禁止使用try-catch
11. 前端接口访问尽可能走网关调用
## 接口设计规范
12. Controller层接口定义要完整:
- 入参使用request封装传递到service层
- service层的方法命名与controller层定义的接口的方法名称保持一致
- 出参使用response封装由service层传递到controller层
- 禁止在controller层做entity/domain对象与request/response的转换
- 使用项目已有的Result做接口返回
13. 接口和方法参数不允许超过两个,超过时使用request或DTO对象封装
14. Controller层路由禁止添加/api前缀
15. Controller层接口的Mapping注解value属性值不允许重复且不允许为空,必须明确指定value属性值且使用驼峰结构命名,避免使用下划线分隔符。所有接口注解必须显式指定value属性,如@GetMapping(value = "/page")、@PostMapping(value = "/create")等,禁止使用@GetMapping()或@PostMapping等空注解形式
16. 用户相关接口禁止直接传递用户id,需要后端根据token获取当前登录用户信息
17. 禁止使用/{param}格式的路径参数,避免网关路由冲突和分发错误
18. 路径参数统一使用@RequestParam而非@PathVariable,确保网关分发准确性
19. 接口路径命名应具有明确的语义,避免使用通用词汇如/get、/list等
20. 批量操作接口应使用专门的Request对象封装参数,而非直接传递List
21. 接口路径应避免层级过深,建议不超过3级路径结构
22. 更新操作应直接使用封装了ID和其他数据的Request对象,而不是单独传递ID参数。Service层方法应保持参数简洁,业务逻辑所需数据应全部包含在Request对象中
23. Controller层接口应保持简洁,避免为特定字段创建独立的更新方法(如updateStatus等),应只保留一个通用的update方法,具体的业务逻辑在Service层实现
24. Service层在执行更新操作时,应对每个字段进行空值检查,只更新非空字段,避免空字段覆盖原有值,确保数据完整性
25. Controller层应避免创建多个特定条件的查询接口,只保留一个分页查询方法,通过在请求对象中包含所有可查询字段来支持不同的查询需求
26. 分页查询接口应支持模糊查询和精确查询,通过在Service层根据字段类型和业务需求判断查询方式
## 环境配置
27. 为不同环境(local、dev、prod)创建单独配置文件,部署时通过参数选择
28. 启动服务时禁止擅自修改端口号,使用配置文件中的端口设置
## 数据库规范
29. 所有数据表必须包含创建时间(create_time)和更新时间(update_time)字段
30. 删除操作优先使用逻辑删除,添加deleted字段标识
31. 数据库字段命名使用下划线分隔,Java实体类使用驼峰命名
32. 优先使用LambdaQueryWrapper构造条件查询,避免硬编码字段名
33. 使用Lambda表达式引用实体类属性,提高代码可维护性和类型安全
34. 复杂查询条件应使用LambdaQueryWrapper的链式调用,保持代码清晰
35. 避免在查询条件中使用字符串字段名,防止字段名变更导致的运行时错误
## 安全规范
36. 所有外部输入必须进行参数校验
37. 敏感信息不得在日志中输出
38. 数据库操作必须使用参数化查询,防止SQL注入
## 性能规范
39. 避免N+1查询问题,合理使用批量查询
40. 大数据量查询必须分页处理
41. 缓存策略要考虑数据一致性问题
## 日志规范
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
+5 -1
View File
@@ -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
-10
View File
@@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
File diff suppressed because one or more lines are too long
-29
View File
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/common/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.projectlombok:lombok:1.18.30" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-annotation:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-core:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-extension:3.5.4" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:6.1.1" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:6.1.1" level="project" />
</component>
</module>
-82
View File
@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="emotion-museum-backend" />
<module name="emotion-single" />
<module name="backend-single" />
</profile>
<profile name="Annotation profile for emotion-museum-server" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" />
</processorPath>
<module name="emotion-museum-server" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="admin" target="17" />
<module name="admin-api" target="17" />
<module name="admin-server" target="17" />
<module name="ai" target="17" />
<module name="ai-api" target="17" />
<module name="ai-server" target="17" />
<module name="auth" target="17" />
<module name="auth-api" target="17" />
<module name="auth-server" target="17" />
<module name="backend" target="17" />
<module name="common" target="17" />
<module name="customer" target="17" />
<module name="emotion-ai" target="17" />
<module name="emotion-auth" target="1.5" />
<module name="emotion-common" target="17" />
<module name="emotion-explore" target="17" />
<module name="emotion-gateway" target="17" />
<module name="emotion-growth" target="17" />
<module name="emotion-record" target="17" />
<module name="emotion-reward" target="17" />
<module name="emotion-stats" target="17" />
<module name="emotion-user" target="17" />
<module name="explore" target="17" />
<module name="explore-api" target="17" />
<module name="explore-server" target="17" />
<module name="gateway" target="17" />
<module name="growth" target="17" />
<module name="growth-api" target="17" />
<module name="growth-server" target="17" />
<module name="record" target="17" />
<module name="record-api" target="17" />
<module name="record-server" target="17" />
<module name="reward" target="17" />
<module name="reward-api" target="17" />
<module name="reward-server" target="17" />
<module name="server" target="1.5" />
<module name="service" target="17" />
<module name="stats" target="17" />
<module name="stats-api" target="17" />
<module name="stats-server" target="17" />
<module name="user" target="17" />
<module name="user-api" target="17" />
<module name="user-server" target="17" />
<module name="websocket" target="17" />
<module name="websocket-api" target="17" />
<module name="websocket-server" target="17" />
</bytecodeTargetLevel>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="backend" options="" />
<module name="common" options="" />
<module name="customer" options="" />
<module name="emotion-museum-backend" options="-parameters" />
<module name="emotion-museum-server" options="-parameters" />
</option>
</component>
</project>
-61
View File
@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="localhost" uuid="5d3e10c1-409f-42d2-98e4-fb21f0c9102b">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="emotion_museum" uuid="01470282-aaf1-4f16-a5c2-d558bf1bc142">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend/emotion-websocket/src/main/resources/application-prod.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://101.200.208.45:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="@localhost" uuid="c0166ff4-a42d-4e97-b828-e64e5885093a">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend-single/src/main/resources/application-local.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="emotion_museum@101.200.208.45" uuid="8f6bf0e5-3002-455c-9ab9-a1176155030c">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend-single/src/main/resources/application-prod.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://101.200.208.45:3306/emotion_museum?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
-105
View File
@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend-single/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend-single/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-websocket/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/resources" charset="UTF-8" />
</component>
</project>
-65
View File
@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.huaweicloud.com/repository/maven/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-ai" />
<option name="name" value="Spring AI Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-milestones" />
<option name="name" value="Spring Milestones" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-snapshots" />
<option name="name" value="Spring Snapshots" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyunmaven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-37f43507:191123ac8a3:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/server/pom.xml" />
<option value="$PROJECT_DIR$/backend/pom.xml" />
<option value="$PROJECT_DIR$/backend-single/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/EmotionMuseum.iml" filepath="$PROJECT_DIR$/.idea/EmotionMuseum.iml" />
</modules>
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/sql/emotion_museum.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/sql/emotion_museum_ddl.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/sql/emotion_museum_init.sql" dialect="MySQL" />
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+71 -17
View File
@@ -20,7 +20,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
```
.
├── backend-single/ # Spring Boot 单体后端服务
├── server/ # Spring Boot 单体后端服务
├── web/ # 用户前端 (Vue3 + TS + Vite)
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
@@ -34,11 +34,11 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
## 常用命令
### 后端 (backend-single)
### 后端 (server)
```bash
# 进入后端目录
cd backend-single
cd server
# 编译打包
mvn clean package -DskipTests
@@ -47,7 +47,7 @@ mvn clean package -DskipTests
mvn spring-boot:run
# 或直接运行 JAR
java -jar target/backend-single-1.0.0.jar
java -jar target/server-1.0.0.jar
# 运行测试
mvn test
@@ -144,6 +144,31 @@ npm run dev:mp-weixin
**开发工作流说明**:详见 [小程序开发工作流程设计](docs/superpowers/specs/2026-04-07-mini-program-dev-workflow-design.md)
#### 小程序 H5 测试登录凭证(强制默认)
所有小程序功能的 H5 验收必须使用以下测试账号,**禁止在测试中使用真实手机号或重复触发验证码接口**:
- **手机号**`19928748688`(固定测试账号,11 位中国大陆手机号格式)
- **验证码**`123456`(固定 6 位数字,生产环境短信通道返回此固定值便于测试)
**登录流程**(参考 `mini-program/src/pages/login/index.vue` + `mini-program/src/services/auth.js`):
1. 浏览器访问 `http://localhost:5180`,进入登录页
2. 在手机号输入框填写 `19928748688`
3. 点击「获取验证码」按钮,调用 `GET /api/auth/sms-code?phone=19928748688`,后端返回 `data.code: "123456"`
4. 在验证码输入框填写 `123456`
5. 点击「登录」按钮,调用 `POST /api/auth/login`,后端返回 JWT
6. Token 写入 `uni.setStorageSync('access_token', ...)`(下划线键名)
**为什么固定这两个值**
- 后端短信通道在测试模式下总是返回 `123456`,避免真实短信下发和验证码频繁失效
- `19928748688` 是项目的固定测试账号,已绑定测试数据,避免污染真实用户数据
#### 小程序页面固定端口
小程序 H5 固定端口 **5180**(参考 `前端端口管理规则`
### 一键部署
```bash
@@ -216,24 +241,53 @@ bash deploy-all.sh admin
---
## 验证规则(强制
## 服务器端验收强制规则(最高优先级,不可违反
**修改任何前后端代码后,必须使用浏览器进行验证**
**所有前端功能验收(小程序、用户端 web、管理后台 admin)必须使用服务器上部署的后端服务,禁止使用本地后端服务验收。**
### 验证步骤
### 强制流程
1. **使用浏览器工具打开对应页面**
- 使用 `agent-browser` skill 或 MCP 工具
- 前台页面:通常是 `http://localhost:5173` 或项目指定端口
- 管理端页面:通常是 `http://localhost:5174` 或项目指定路径
修改任意后端代码后,必须按以下顺序操作:
2. **验证标准**(全部满足才算通过)
- ✅ 前端修改已生效
- ✅ 页面没有报错
- ✅ 可以正常访问
- ✅ 浏览器 Console 中没有任何错误
1. **编译验证**:本地 `mvn clean install -DskipTests -am` 必须通过,零报错。
2. **部署到服务器**:执行 `python deploy.py backend`(或其他对应模块名),确保部署成功、无报错。
3. **服务器验收**:在服务器上已部署的应用进行功能验收。
3. **只有验证通过才算任务完成**
### 各前端验收方式
| 前端 | 验收方式 |
|---|---|
| 小程序 | 本地 `npm run dev:h5` 启动 H5`.env.development` 已默认指向 `lifescript.happylifeos.com`),浏览器访问本地 H5 URL,后端请求走服务器 |
| 用户端 web | 直接访问服务器 URL(`https://lifescript.happylifeos.com/`)验收 |
| 管理后台 admin | 直接访问服务器 URL`https://lifescript.happylifeos.com/emotion-museum-admin/`)验收 |
### 禁止行为
- ❌ 禁止使用 `localhost` / `127.0.0.1` 后端服务做功能验收
- ❌ 禁止绕过 `deploy.py` 手动部署
- ❌ 禁止在后端编译有报错的情况下部署
- ❌ 禁止部署后不验收就认为任务完成
### 允许的唯一例外
- ✅ 单元测试 / 集成测试(测试代码可连本地或测试数据库)
- ✅ 纯前端样式/UI 调整(不涉及后端接口,可本地启动 H5 自测)
- ✅ 后端开发阶段的结构验证(仅 mvn 编译,不部署不验收)
### deploy.py 模块名速查
- `backend` — 部署后端到服务器
- `frontend` — 部署用户端 web
- `admin` — 部署管理后台
- `all` — 部署所有服务
- `verify` — 验证部署状态
### 验收通过标准
- 浏览器 Console 无任何错误
- 服务器接口响应正常(Network 面板查看)
- 业务功能按预期工作
- 只有验收通过才算任务完成
---
+227 -39
View File
@@ -20,7 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
```
.
├── backend-single/ # Spring Boot 单体后端服务
├── server/ # Spring Boot 单体后端服务
├── web/ # 用户前端 (Vue3 + TS + Vite)
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
@@ -34,20 +34,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 常用命令
### 后端 (backend-single)
### 后端 (server)
```bash
# 进入后端目录
cd backend-single
cd server
# 编译打包
mvn clean package -DskipTests
# 本地运行
mvn spring-boot:run
# 本地运行(必须使用 dev-services.py
python dev-services.py start server
# 或直接运行 JAR
java -jar target/backend-single-1.0.0.jar
java -jar target/server-1.0.0.jar
# 运行测试
mvn test
@@ -65,8 +65,8 @@ cd web
# 安装依赖
npm install
# 启动开发服务器 (端口 5173)
npm run dev
# 启动开发服务器(必须使用 dev-services.py,固定端口 5178
python dev-services.py start web
# 类型检查
npm run type-check
@@ -93,8 +93,8 @@ cd web-admin
# 安装依赖
npm install
# 启动开发服务器 (端口 5174)
npm run dev
# 启动开发服务器(必须使用 dev-services.py,固定端口 5179
python dev-services.py start web-admin
# 类型检查
npm run type-check
@@ -130,10 +130,10 @@ npm run build:h5
**H5 开发模式**(日常开发,推荐):
```bash
cd mini-program
# 或使用启动脚本
start-h5-dev.bat
# 或使用 dev-services.py 启动(固定端口 5180
python dev-services.py start mini-program
```
访问 `http://localhost:5173`,登录态保留,支持热更新。
访问 `http://localhost:5180`,登录态保留,支持热更新。
**微信小程序开发模式**(仅用于小程序特性验证):
```bash
@@ -144,6 +144,31 @@ npm run dev:mp-weixin
**开发工作流说明**:详见 [小程序开发工作流程设计](docs/superpowers/specs/2026-04-07-mini-program-dev-workflow-design.md)
#### 小程序 H5 测试登录凭证(强制默认)
所有小程序功能的 H5 验收必须使用以下测试账号,**禁止在测试中使用真实手机号或重复触发验证码接口**:
- **手机号**`19928748688`(固定测试账号,11 位中国大陆手机号格式)
- **验证码**`123456`(固定 6 位数字,生产环境短信通道返回此固定值便于测试)
**登录流程**(参考 `mini-program/src/pages/login/index.vue` + `mini-program/src/services/auth.js`):
1. 浏览器访问 `http://localhost:5180`,进入登录页
2. 在手机号输入框填写 `19928748688`
3. 点击「获取验证码」按钮,调用 `GET /api/auth/sms-code?phone=19928748688`,后端返回 `data.code: "123456"`
4. 在验证码输入框填写 `123456`
5. 点击「登录」按钮,调用 `POST /api/auth/login`,后端返回 JWT
6. Token 写入 `uni.setStorageSync('access_token', ...)`(下划线键名)
**为什么固定这两个值**
- 后端短信通道在测试模式下总是返回 `123456`,避免真实短信下发和验证码频繁失效
- `19928748688` 是项目的固定测试账号,已绑定测试数据,避免污染真实用户数据
#### 小程序页面固定端口
小程序 H5 固定端口 **5180**(参考 `前端端口管理规则`
### 一键部署
```bash
@@ -216,48 +241,161 @@ 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 面板查看)
- 业务功能按预期工作
- 只有验收通过才算任务完成
---
## 热加载规则(强制
## 自动验证验收规则(最高优先级,不可违反
**修改前后端代码后,优先利用热加载,避免不必要的重启**
**每次代码变更任务完成前,必须自动通过浏览器进行验证验收,无需用户额外要求。**
### 判断逻辑
### 自动触发条件
以下任一种情况都视为"任务完成前",必须自动执行验证:
- 修改了后端 Java 代码,完成部署后,必须通过 H5/浏览器调用真实接口验证
- 修改了 mini-program 前端代码,热加载生效后,必须在浏览器中操作验证
- 修改了任意代码并声称"完成",必须走一遍受影响功能的完整体验流程
### 验证方式(按受影响端选择)
| 变更范围 | 验证方式 |
|---|---|
| 后端 Java 代码 | 部署后,通过本地 H5(连服务器 API)或 curl 验证接口 |
| mini-program(小程序) | H5 模式浏览器验证(`http://localhost:5180` |
| web 用户端 | 浏览器访问 `http://localhost:5178` 或服务器 URL |
| web-admin 管理后台 | 浏览器访问 `http://localhost:5179` 或服务器 URL |
### 禁止行为
- ❌ 禁止代码改完就直接回复"完成",不执行验证
- ❌ 禁止仅凭编译通过就认为任务结束
- ❌ 禁止等用户提醒后才去验证
- ❌ 禁止跳过浏览器 Console / Network 面板检查
### 验收通过标准
- 浏览器 Console 无**新增**错误(既有的已知兼容性警告除外)
- Network 面板中受影响的 API 返回正常(200 或业务预期状态码)
- 受影响的 UI 交互逻辑按预期工作(点击、切换、展示)
---
## 部署强制规则(最高优先级,不可违反)
**本地开发完成、验证通过、验收没有问题后,必须执行 `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 +476,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 层**:参数校验失败时返回友好错误提示
- **前端**:接口调用前做空值检查,避免发送无效请求
+1 -1
View File
@@ -70,7 +70,7 @@
│ ├── emotion-reward/ # 成就奖励服务
│ ├── emotion-stats/ # 统计分析服务
│ └── emotion-common/ # 公共模块
├── backend-single/ # SpringBoot单体后端服务
├── server/ # SpringBoot单体后端服务
├── web/ # 旧版Web前端
├── web-new/ # 新版Web前端(Vue3+TS)
└── 开心APP网页代码v1.1/ # 早期网页版本
+33 -113
View File
@@ -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;
}
+7 -4
View File
@@ -23,6 +23,7 @@ Purpose: 统一部署所有服务到生产服务器,替代原有的多个 shel
import io
import os
import re
import shlex
import sys
import time
import subprocess
@@ -395,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
@@ -410,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
)
@@ -524,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:
+4 -4
View File
@@ -74,16 +74,16 @@ deploy_nginx() {
}
# ============================================================================
# 后端 - 调用 backend-single/deploy.sh
# 后端 - 调用 server/deploy.sh
# ============================================================================
deploy_backend() {
log_section "部署后端服务"
if [ ! -f "backend-single/deploy.sh" ]; then
log_error "后端部署脚本不存在: backend-single/deploy.sh"
if [ ! -f "server/deploy.sh" ]; then
log_error "后端部署脚本不存在: server/deploy.sh"
return 1
fi
log_info "执行后端部署..."
cd backend-single
cd server
if bash deploy.sh remote; then
cd ..
log_info "✅ 后端部署完成"
+112 -8
View File
@@ -67,6 +67,25 @@ DEFAULT_PORTS = {
"spring-boot-gradle": 8080,
}
# 前端/H5 服务固定端口映射(按项目目录名匹配)
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
# 必须重启才能生效的文件模式
RESTART_REQUIRED_PATTERNS = [
re.compile(r'vite\.config\.(ts|js|mjs|cjs)$'),
re.compile(r'tsconfig\.json$'),
re.compile(r'\.env(\.[^/]*)?$'),
re.compile(r'package\.json$'),
re.compile(r'application.*\.ya?ml$'),
re.compile(r'pom\.xml$'),
re.compile(r'build\.gradle(\.kts)?$'),
]
# ANSI 颜色
COLORS = {
"INFO": "\033[37m",
@@ -328,36 +347,103 @@ def _update_service_port(svc: Service, port: int):
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
has_port_arg = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
if part == "--port":
has_port_arg = True
if index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
has_port_arg = True
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
# 前端框架如果没有 --port 参数,追加 --port
if not has_port_arg and svc.project_type in {"vite", "uniapp", "taro", "next"}:
updated.extend(["--port", str(port)])
svc.start_cmd = updated
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
# 按目录名匹配固定前端端口
dir_name_to_service = {}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
dir_name_to_service[dir_name] = svc
# 先为固定前端服务分配端口
for dir_name, svc in dir_name_to_service.items():
fixed_port = FIXED_FRONTEND_PORTS[dir_name]
if svc.port != fixed_port:
log(f"{svc.name} 使用固定前端端口 {fixed_port}", "INFO")
_update_service_port(svc, fixed_port)
# 检查端口冲突
used_ports = {}
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
if port in used_ports:
other = used_ports[port]
log(f"端口冲突: {svc.name} ({port}) 与 {other.name} ({port}) 冲突", "ERROR")
sys.exit(1)
used_ports[port] = svc
# 非固定前端服务按原有逻辑处理冲突
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
continue
port = svc.port
if port in used_ports and used_ports[port] is not svc:
while port in used_ports:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
def _should_restart_for_changes(service_dir: Path) -> tuple[bool, list[str]]:
"""
检查指定服务目录下是否有必须重启才能生效的变更。
返回: (是否需要重启, 已修改文件列表)
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--", str(service_dir)],
capture_output=True,
text=True,
cwd=SCRIPT_DIR,
check=True,
)
changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
except subprocess.CalledProcessError:
# 非 git 仓库或 git 命令失败,默认允许重启
return True, []
except FileNotFoundError:
return True, []
if not changed_files:
return False, []
for file_path in changed_files:
for pattern in RESTART_REQUIRED_PATTERNS:
if pattern.search(file_path):
return True, changed_files
return False, changed_files
def _extract_port_from_pom(pom_path: Path) -> Optional[int]:
"""从 pom.xml 提取 server.port"""
try:
@@ -1553,6 +1639,7 @@ def main():
parser.add_argument("--type", dest="svc_type", default=None, help="服务类型过滤: vite|python|spring-boot|node-backend")
parser.add_argument("--port", dest="port", type=int, default=None, help="端口号过滤")
parser.add_argument("--foreground", "-f", action="store_true", help="前台模式运行")
parser.add_argument("--force", action="store_true", help="强制重启,忽略热加载保护")
args = parser.parse_args()
@@ -1631,6 +1718,23 @@ def main():
return
if args.action == "restart":
if not args.force:
blocked = []
for svc in services:
should_restart, changed_files = _should_restart_for_changes(svc.project_dir)
if not should_restart:
blocked.append((svc.name, changed_files))
if blocked:
log("检测到以下服务无需重启(当前修改支持热加载):", "WARN")
for name, files in blocked:
if files:
log(f" - {name}: 修改文件 {', '.join(files)}", "WARN")
else:
log(f" - {name}: 未检测到代码变更", "WARN")
log("如需强制重启,请使用: python dev-services.py restart --force", "WARN")
sys.exit(0)
for svc in services:
restart_service(svc)
else:
@@ -0,0 +1,28 @@
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 SSE 流式传输修复的部署验证过程
---
# SSE 流式传输修复 - 部署验证
**部署时间:** 2026-07-21
**修改内容:**
1. 后端:使用 `readUtf8LineStrict()` + 显式 flush
2. nginx:添加 `proxy_buffering off` 等配置
3. 前端:添加诊断日志(已脱敏)
**部署步骤:**
1. 编译后端:`mvn clean install -DskipTests`
2. 上传 JAR 到服务器 ✅
3. 重启服务 ✅
4. 验证 Tomcat 启动 ✅
5. 验证 SSE 接口可访问 ✅
**后续验证(需要用户手动测试):**
- [ ] 后端日志显示事件逐条到达(时间间隔 > 100ms)
- [ ] 前端 Console 显示 novel_delta 事件逐条接收
- [ ] 小说内容逐字显示
- [ ] 历史列表正确保存新生成的小说
- [ ] 数据库 t_epic_script 表有新记录
@@ -0,0 +1,30 @@
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 nginx 配置优化,禁用 SSE 响应缓冲
---
# nginx 配置优化 - ShortNovel SSE
**文件:** `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf`
**变更内容:**
`location /api/shortNovel/` 块中使用以下配置:
- `proxy_http_version 1.1;` - 确保 SSE 长连接使用 HTTP/1.1
- `proxy_buffering off;` - 禁用代理缓冲
- `chunked_transfer_encoding on;` - 启用分块传输编码
- `proxy_set_header Connection "keep-alive";` - 保持上游连接
**原因:**
- nginx 默认启用 `proxy_buffering on`,会缓冲后端响应
- 对于 SSEServer-Sent Events)长连接,缓冲会导致事件无法立即到达前端
- 禁用缓冲后,每个事件都能立即转发给客户端,实现真正的流式传输
**验证:**
- nginx 语法检查通过:`nginx -t` → syntax is ok / test is successful
- nginx 重载成功:worker 进程已更新
- 使用 `curl -N https://lifescript.happylifeos.com/api/shortNovel/stream` 测试 SSE 流式传输
**备份文件:** `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf.backup.20260721_233252`
**管理方式:** 宝塔面板,通过 `/etc/init.d/nginx reload` 重载配置
@@ -570,13 +570,13 @@ deploy_ssl() {
deploy_backend() {
log_section "部署后端服务"
if [ ! -f "backend-single/deploy.sh" ]; then
if [ ! -f "server/deploy.sh" ]; then
log_error "后端部署脚本不存在"
return 1
fi
log_info "执行后端部署..."
cd backend-single
cd server
bash deploy.sh remote
local result=$?
cd ..
@@ -828,7 +828,7 @@ git commit -m "config: 更新管理后台部署脚本访问地址提示"
### Task 9: 检查并修改后端 CORS 配置
**Files:**
- Modify: `backend-single/src/main/resources/application.yml` (或相应配置文件)
- Modify: `server/src/main/resources/application.yml` (或相应配置文件)
- [ ] **Step 1: 读取当前后端配置文件**
@@ -841,7 +841,7 @@ git commit -m "config: 更新管理后台部署脚本访问地址提示"
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/resources/application.yml
git add server/src/main/resources/application.yml
git commit -m "config: 更新 CORS 配置允许新域名访问"
```
@@ -4,7 +4,7 @@
**Goal:** Add mini program behavior analytics collection, backend event storage and aggregation, and a web-admin behavior analytics dashboard.
**Architecture:** The mini program queues analytics events locally and flushes them to `POST /analytics/events/batch`. `backend-single` stores raw events in `t_analytics_event` and exposes admin-only aggregation endpoints under `/admin/analytics`. `web-admin` renders overview, trend, funnel, preference, and top-event stats using Element Plus and ECharts.
**Architecture:** The mini program queues analytics events locally and flushes them to `POST /analytics/events/batch`. `server` stores raw events in `t_analytics_event` and exposes admin-only aggregation endpoints under `/admin/analytics`. `web-admin` renders overview, trend, funnel, preference, and top-event stats using Element Plus and ECharts.
**Tech Stack:** Spring Boot 2.7, MyBatis Plus, MySQL JSON, Java 17, uni-app/Vue, Vue 3, Element Plus, ECharts.
@@ -13,25 +13,25 @@
## File Structure
- Create `sql/2026-05-17-analytics-event.sql`: migration for `t_analytics_event`.
- Create `backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java`: MyBatis Plus entity.
- Create `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`: mapper plus aggregation SQL.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`: mini program batch request.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`: one event payload.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`: admin query filters.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsBatchResponse.java`: accepted/rejected counts.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsOverviewResponse.java`: overview cards.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`: trend item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsFunnelItem.java`: funnel item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`: preference item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`: top event item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsUserItem.java`: user ranking item.
- Create `backend-single/src/main/java/com/emotion/service/AnalyticsService.java`: write and aggregate contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`: validation, persistence, aggregation.
- Create `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`: mini program write endpoint.
- Create `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`: admin read endpoints.
- Modify `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`: allow anonymous analytics batch endpoint while keeping admin analytics protected.
- Create `backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java`: service validation and aggregation tests.
- Create `backend-single/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`: endpoint tests.
- Create `server/src/main/java/com/emotion/entity/AnalyticsEvent.java`: MyBatis Plus entity.
- Create `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`: mapper plus aggregation SQL.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`: mini program batch request.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`: one event payload.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`: admin query filters.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsBatchResponse.java`: accepted/rejected counts.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsOverviewResponse.java`: overview cards.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`: trend item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsFunnelItem.java`: funnel item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`: preference item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`: top event item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsUserItem.java`: user ranking item.
- Create `server/src/main/java/com/emotion/service/AnalyticsService.java`: write and aggregate contract.
- Create `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`: validation, persistence, aggregation.
- Create `server/src/main/java/com/emotion/controller/AnalyticsController.java`: mini program write endpoint.
- Create `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`: admin read endpoints.
- Modify `server/src/main/java/com/emotion/config/WebMvcConfig.java`: allow anonymous analytics batch endpoint while keeping admin analytics protected.
- Create `server/src/test/java/com/emotion/service/AnalyticsServiceTest.java`: service validation and aggregation tests.
- Create `server/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`: endpoint tests.
- Create `mini-program/src/services/analytics.js`: client SDK.
- Modify `mini-program/src/App.vue`: initialize and flush analytics.
- Modify `mini-program/src/pages/main/index.vue`: track main tab/page activity if this page owns tab switching.
@@ -121,11 +121,11 @@ git commit -m "feat: add analytics event table"
### Task 2: Add Backend DTOs and Entity
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`
- Create: response DTO files under `backend-single/src/main/java/com/emotion/dto/response/analytics/`
- Create: `server/src/main/java/com/emotion/entity/AnalyticsEvent.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`
- Create: response DTO files under `server/src/main/java/com/emotion/dto/response/analytics/`
- [ ] **Step 1: Add the entity**
@@ -387,7 +387,7 @@ public class AnalyticsUserItem {
Run:
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -396,7 +396,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java backend-single/src/main/java/com/emotion/dto/request/analytics backend-single/src/main/java/com/emotion/dto/response/analytics
git add server/src/main/java/com/emotion/entity/AnalyticsEvent.java server/src/main/java/com/emotion/dto/request/analytics server/src/main/java/com/emotion/dto/response/analytics
git commit -m "feat: add analytics DTOs and entity"
```
@@ -405,10 +405,10 @@ git commit -m "feat: add analytics DTOs and entity"
### Task 3: Add Analytics Mapper and Service
**Files:**
- Create: `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Create: `backend-single/src/main/java/com/emotion/service/AnalyticsService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java`
- Create: `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Create: `server/src/main/java/com/emotion/service/AnalyticsService.java`
- Create: `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/AnalyticsServiceTest.java`
- [ ] **Step 1: Write service tests**
@@ -462,7 +462,7 @@ public class AnalyticsServiceTest {
Run:
```bash
cd backend-single
cd server
mvn -Dtest=AnalyticsServiceTest test
```
@@ -676,7 +676,7 @@ public class AnalyticsServiceImpl extends ServiceImpl<AnalyticsEventMapper, Anal
Run:
```bash
cd backend-single
cd server
mvn -Dtest=AnalyticsServiceTest test
```
@@ -685,7 +685,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 7: Commit**
```bash
git add backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java backend-single/src/main/java/com/emotion/service/AnalyticsService.java backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java
git add server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java server/src/main/java/com/emotion/service/AnalyticsService.java server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java server/src/test/java/com/emotion/service/AnalyticsServiceTest.java
git commit -m "feat: add analytics service"
```
@@ -694,10 +694,10 @@ git commit -m "feat: add analytics service"
### Task 4: Add Analytics Controllers and Auth Exclusion
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`
- Create: `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`
- Test: `backend-single/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`
- Create: `server/src/main/java/com/emotion/controller/AnalyticsController.java`
- Create: `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/config/WebMvcConfig.java`
- Test: `server/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`
- [ ] **Step 1: Add write controller**
@@ -794,7 +794,7 @@ Do not exclude `/admin/analytics/**`.
Run:
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -803,7 +803,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AnalyticsController.java backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java backend-single/src/main/java/com/emotion/config/WebMvcConfig.java
git add server/src/main/java/com/emotion/controller/AnalyticsController.java server/src/main/java/com/emotion/controller/AdminAnalyticsController.java server/src/main/java/com/emotion/config/WebMvcConfig.java
git commit -m "feat: expose analytics APIs"
```
@@ -1323,7 +1323,7 @@ git commit -m "feat: add analytics admin dashboard"
- [ ] **Step 1: Run backend tests**
```bash
cd backend-single
cd server
mvn test
```
@@ -4,7 +4,7 @@
**Goal:** Add CPU-friendly open-source text-to-speech generation for user scripts, with cached backend tasks and mini program playback.
**Architecture:** `backend-single` creates TTS tasks, validates source ownership, cleans script text, checks audio cache, and calls a private Python FastAPI TTS service. The Python service runs on `101.200.208.45` and writes audio files under `/data/uploads/emotion-museum/tts`. The mini program requests generation from `ScriptDetailView.vue`, polls status, and plays the returned audio URL with `uni.createInnerAudioContext()`.
**Architecture:** `server` creates TTS tasks, validates source ownership, cleans script text, checks audio cache, and calls a private Python FastAPI TTS service. The Python service runs on `101.200.208.45` and writes audio files under `/data/uploads/emotion-museum/tts`. The mini program requests generation from `ScriptDetailView.vue`, polls status, and plays the returned audio URL with `uni.createInnerAudioContext()`.
**Tech Stack:** Spring Boot 2.7, MyBatis Plus, Java 17, MySQL, Python FastAPI, MeloTTS or equivalent CPU-friendly Chinese TTS engine, systemd, uni-app/Vue.
@@ -13,22 +13,22 @@
## File Structure
- Create `sql/2026-05-17-tts-task.sql`: `t_tts_task` migration.
- Create `backend-single/src/main/java/com/emotion/entity/TtsTask.java`: task entity.
- Create `backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java`: task mapper.
- Create `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`: create request.
- Create `backend-single/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`: task response.
- Create `backend-single/src/main/java/com/emotion/service/TtsTaskService.java`: contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`: ownership, cache, async processing.
- Create `backend-single/src/main/java/com/emotion/service/TtsEngineClient.java`: internal engine client contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`: calls Python service.
- Create `backend-single/src/main/java/com/emotion/controller/TtsController.java`: user-facing TTS endpoints.
- Modify `backend-single/src/main/resources/application.yml`: TTS config defaults.
- Modify `backend-single/src/main/resources/application-prod.yml`: production paths and internal URL.
- Create `backend-single/src/test/java/com/emotion/service/TtsTaskServiceTest.java`: service tests.
- Create `backend-single/tts-service/requirements.txt`: FastAPI deps; MeloTTS is installed from the official repository because the official docs use editable install plus `python -m unidic download`.
- Create `backend-single/tts-service/app.py`: FastAPI service.
- Create `backend-single/tts-service/README.md`: server setup.
- Create `backend-single/tts-service/emotion-museum-tts.service`: systemd unit.
- Create `server/src/main/java/com/emotion/entity/TtsTask.java`: task entity.
- Create `server/src/main/java/com/emotion/mapper/TtsTaskMapper.java`: task mapper.
- Create `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`: create request.
- Create `server/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`: task response.
- Create `server/src/main/java/com/emotion/service/TtsTaskService.java`: contract.
- Create `server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`: ownership, cache, async processing.
- Create `server/src/main/java/com/emotion/service/TtsEngineClient.java`: internal engine client contract.
- Create `server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`: calls Python service.
- Create `server/src/main/java/com/emotion/controller/TtsController.java`: user-facing TTS endpoints.
- Modify `server/src/main/resources/application.yml`: TTS config defaults.
- Modify `server/src/main/resources/application-prod.yml`: production paths and internal URL.
- Create `server/src/test/java/com/emotion/service/TtsTaskServiceTest.java`: service tests.
- Create `server/tts-service/requirements.txt`: FastAPI deps; MeloTTS is installed from the official repository because the official docs use editable install plus `python -m unidic download`.
- Create `server/tts-service/app.py`: FastAPI service.
- Create `server/tts-service/README.md`: server setup.
- Create `server/tts-service/emotion-museum-tts.service`: systemd unit.
- Create `mini-program/src/services/tts.js`: TTS API client.
- Create `mini-program/src/components/ScriptAudioPlayer.vue`: player component.
- Modify `mini-program/src/pages/main/ScriptDetailView.vue`: add player.
@@ -93,10 +93,10 @@ git commit -m "feat: add tts task table"
### Task 2: Add Backend TTS Entity, DTOs, and Mapper
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/TtsTask.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`
- Create: `server/src/main/java/com/emotion/entity/TtsTask.java`
- Create: `server/src/main/java/com/emotion/mapper/TtsTaskMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`
- [ ] **Step 1: Add entity**
@@ -205,7 +205,7 @@ public class TtsTaskResponse {
- [ ] **Step 4: Compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -214,7 +214,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/TtsTask.java backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java backend-single/src/main/java/com/emotion/dto/request/tts backend-single/src/main/java/com/emotion/dto/response/tts
git add server/src/main/java/com/emotion/entity/TtsTask.java server/src/main/java/com/emotion/mapper/TtsTaskMapper.java server/src/main/java/com/emotion/dto/request/tts server/src/main/java/com/emotion/dto/response/tts
git commit -m "feat: add tts task model"
```
@@ -223,13 +223,13 @@ git commit -m "feat: add tts task model"
### Task 3: Add TTS Service and Engine Client
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/TtsTaskService.java`
- Create: `backend-single/src/main/java/com/emotion/service/TtsEngineClient.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `backend-single/src/main/resources/application-prod.yml`
- Test: `backend-single/src/test/java/com/emotion/service/TtsTaskServiceTest.java`
- Create: `server/src/main/java/com/emotion/service/TtsTaskService.java`
- Create: `server/src/main/java/com/emotion/service/TtsEngineClient.java`
- Create: `server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`
- Create: `server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`
- Modify: `server/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application-prod.yml`
- Test: `server/src/test/java/com/emotion/service/TtsTaskServiceTest.java`
- [ ] **Step 1: Add service contracts**
@@ -503,7 +503,7 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
- [ ] **Step 5: Run compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -512,7 +512,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 6: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/TtsTaskService.java backend-single/src/main/java/com/emotion/service/TtsEngineClient.java backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java backend-single/src/main/resources/application.yml backend-single/src/main/resources/application-prod.yml
git add server/src/main/java/com/emotion/service/TtsTaskService.java server/src/main/java/com/emotion/service/TtsEngineClient.java server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java server/src/main/resources/application.yml server/src/main/resources/application-prod.yml
git commit -m "feat: add tts backend service"
```
@@ -521,7 +521,7 @@ git commit -m "feat: add tts backend service"
### Task 4: Add TTS Controller
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/TtsController.java`
- Create: `server/src/main/java/com/emotion/controller/TtsController.java`
- [ ] **Step 1: Add controller**
@@ -569,7 +569,7 @@ public class TtsController {
- [ ] **Step 2: Run compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -578,7 +578,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/TtsController.java
git add server/src/main/java/com/emotion/controller/TtsController.java
git commit -m "feat: expose tts task APIs"
```
@@ -587,10 +587,10 @@ git commit -m "feat: expose tts task APIs"
### Task 5: Add Python TTS Service Skeleton
**Files:**
- Create: `backend-single/tts-service/requirements.txt`
- Create: `backend-single/tts-service/app.py`
- Create: `backend-single/tts-service/README.md`
- Create: `backend-single/tts-service/emotion-museum-tts.service`
- Create: `server/tts-service/requirements.txt`
- Create: `server/tts-service/app.py`
- Create: `server/tts-service/README.md`
- Create: `server/tts-service/emotion-museum-tts.service`
- [ ] **Step 1: Add requirements**
@@ -717,7 +717,7 @@ curl http://127.0.0.1:19110/health
- [ ] **Step 5: Run local syntax check**
```bash
python -m py_compile backend-single/tts-service/app.py
python -m py_compile server/tts-service/app.py
```
Expected: no output and exit code 0.
@@ -725,7 +725,7 @@ Expected: no output and exit code 0.
- [ ] **Step 6: Commit**
```bash
git add backend-single/tts-service
git add server/tts-service
git commit -m "feat: add private tts service scaffold"
```
@@ -960,7 +960,7 @@ git commit -m "feat: add tts control to script detail"
- [ ] **Step 1: Upload service directory**
```bash
scp -r backend-single/tts-service root@101.200.208.45:/data/programs/emotion-museum/tts-service
scp -r server/tts-service root@101.200.208.45:/data/programs/emotion-museum/tts-service
```
Expected: files are copied.
@@ -988,7 +988,7 @@ Expected:
- [ ] **Step 4: Install systemd unit after health passes**
```bash
scp backend-single/tts-service/emotion-museum-tts.service root@101.200.208.45:/etc/systemd/system/emotion-museum-tts.service
scp server/tts-service/emotion-museum-tts.service root@101.200.208.45:/etc/systemd/system/emotion-museum-tts.service
ssh root@101.200.208.45 "systemctl daemon-reload && systemctl enable emotion-museum-tts && systemctl restart emotion-museum-tts && systemctl status emotion-museum-tts --no-pager"
```
@@ -997,7 +997,7 @@ Expected: service status is active.
- [ ] **Step 5: Commit deployment doc tweaks if needed**
```bash
git add backend-single/tts-service/README.md
git add server/tts-service/README.md
git commit -m "docs: update tts deployment notes"
```
@@ -1013,7 +1013,7 @@ Only run this commit if README changed.
- [ ] **Step 1: Run backend tests**
```bash
cd backend-single
cd server
mvn test
```
@@ -44,21 +44,21 @@ This plan does not implement:
Backend:
- Create: `backend-single/src/main/java/com/emotion/entity/SocialContentItem.java`
- Create: `backend-single/src/main/java/com/emotion/entity/SocialProfileInsight.java`
- Create: `backend-single/src/main/java/com/emotion/entity/UserConsentLog.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/SocialContentItemMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/SocialProfileInsightMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/UserConsentLogMapper.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/social/*.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/social/*.java`
- Create: `backend-single/src/main/java/com/emotion/controller/SocialContentController.java`
- Create: `backend-single/src/main/java/com/emotion/controller/SocialInsightController.java`
- Create: `backend-single/src/main/java/com/emotion/service/SocialContentService.java`
- Create: `backend-single/src/main/java/com/emotion/service/SocialInsightService.java`
- Create: `backend-single/src/main/java/com/emotion/service/ScriptContextService.java`
- Create implementations under `backend-single/src/main/java/com/emotion/service/impl/`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- Create: `server/src/main/java/com/emotion/entity/SocialContentItem.java`
- Create: `server/src/main/java/com/emotion/entity/SocialProfileInsight.java`
- Create: `server/src/main/java/com/emotion/entity/UserConsentLog.java`
- Create: `server/src/main/java/com/emotion/mapper/SocialContentItemMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/SocialProfileInsightMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/UserConsentLogMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/social/*.java`
- Create: `server/src/main/java/com/emotion/dto/response/social/*.java`
- Create: `server/src/main/java/com/emotion/controller/SocialContentController.java`
- Create: `server/src/main/java/com/emotion/controller/SocialInsightController.java`
- Create: `server/src/main/java/com/emotion/service/SocialContentService.java`
- Create: `server/src/main/java/com/emotion/service/SocialInsightService.java`
- Create: `server/src/main/java/com/emotion/service/ScriptContextService.java`
- Create implementations under `server/src/main/java/com/emotion/service/impl/`
- Modify: `server/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- Modify SQL schema/migration file used by this repo.
Mini program:
@@ -212,7 +212,7 @@ public interface SocialContentItemMapper extends BaseMapper<SocialContentItem> {
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -221,8 +221,8 @@ Expected: compile succeeds.
## Task 3: Social Content API
**Files:**
- Create DTOs under `backend-single/src/main/java/com/emotion/dto/request/social/`
- Create DTOs under `backend-single/src/main/java/com/emotion/dto/response/social/`
- Create DTOs under `server/src/main/java/com/emotion/dto/request/social/`
- Create DTOs under `server/src/main/java/com/emotion/dto/response/social/`
- Create: `SocialContentController.java`
- Create: `SocialContentService.java`
- Create: `SocialContentServiceImpl.java`
@@ -317,7 +317,7 @@ Do not accept screenshots larger than the configured limit.
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -405,7 +405,7 @@ Rules:
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -414,8 +414,8 @@ Expected: compile succeeds.
## Task 5: Script Context Integration
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ScriptContextService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/ScriptContextServiceImpl.java`
- Create: `server/src/main/java/com/emotion/service/ScriptContextService.java`
- Create: `server/src/main/java/com/emotion/service/impl/ScriptContextServiceImpl.java`
- Modify: `EpicScriptServiceImpl.java`
- [ ] **Step 1: Implement context service**
@@ -468,7 +468,7 @@ When social insight context is non-empty, add analytics/event hook if backend an
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -658,7 +658,7 @@ Expected: build succeeds.
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -707,7 +707,7 @@ Check:
- [ ] **Step 5: Commit**
```powershell
git add backend-single mini-program sql docs/superpowers/specs/2026-05-19-social-data-import-script-profile-design.md docs/superpowers/plans/2026-05-19-social-data-import-script-profile-plan.md
git add server mini-program sql docs/superpowers/specs/2026-05-19-social-data-import-script-profile-design.md docs/superpowers/plans/2026-05-19-social-data-import-script-profile-plan.md
git commit -m "feat: add social data import design and profile plan"
```
@@ -13,10 +13,10 @@
### Task 1: ASR Service
**Files:**
- Create: `backend-single/asr-service/app.py`
- Create: `backend-single/asr-service/requirements.txt`
- Create: `backend-single/asr-service/emotion-museum-asr.service`
- Create: `backend-single/asr-service/README.md`
- Create: `server/asr-service/app.py`
- Create: `server/asr-service/requirements.txt`
- Create: `server/asr-service/emotion-museum-asr.service`
- Create: `server/asr-service/README.md`
- [x] Add a FastAPI service with `/health` and `/transcribe`.
- [x] Accept an uploaded audio file, save it under `/tmp/emotion-museum-asr`, run the ASR model, and return JSON with `success`, `text`, `language`, `durationMs`, `engine`, and `errorMessage`.
@@ -25,12 +25,12 @@
### Task 2: Java Backend Proxy
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/response/asr/AsrTranscribeResponse.java`
- Create: `backend-single/src/main/java/com/emotion/service/AsrService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/AsrServiceImpl.java`
- Create: `backend-single/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `backend-single/src/main/resources/application-prod.yml`
- Create: `server/src/main/java/com/emotion/dto/response/asr/AsrTranscribeResponse.java`
- Create: `server/src/main/java/com/emotion/service/AsrService.java`
- Create: `server/src/main/java/com/emotion/service/impl/AsrServiceImpl.java`
- Create: `server/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `server/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application-prod.yml`
- [x] Add `emotion.asr` config for `enabled`, `engine-url`, `max-file-size`, and `allowed-types`.
- [x] Validate upload presence, size, and suffix.
@@ -14,10 +14,10 @@
| 文件 | 职责 | 操作 |
|------|------|------|
| `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` | DTO:新增 endpointId 字段,更新 RESERVED_KEYS | 修改 |
| `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java` | Service 接口:新增 testEndpoint + invokeEndpointStream | 修改 |
| `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` | Service 实现:新增两个方法的完整实现 | 修改 |
| `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` | Controller:新增 /endpoint/test 和 /endpoint/stream | 修改 |
| `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` | DTO:新增 endpointId 字段,更新 RESERVED_KEYS | 修改 |
| `server/src/main/java/com/emotion/service/AiRuntimeService.java` | Service 接口:新增 testEndpoint + invokeEndpointStream | 修改 |
| `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` | Service 实现:新增两个方法的完整实现 | 修改 |
| `server/src/main/java/com/emotion/controller/AiRoutingController.java` | Controller:新增 /endpoint/test 和 /endpoint/stream | 修改 |
| `web-admin/src/types/aiconfig.ts` | TypeScript 类型:新增 AiEndpointRuntimeRequest | 修改 |
| `web-admin/src/api/aiconfig.ts` | API 层:新增 testEndpointRuntime、streamEndpointRuntime、提取 fetchSseStream | 修改 |
| `web-admin/src/views/aiconfig/AiRoutingList.vue` | Vue 组件:增加行内测试按钮 + endpoint 测试对话框 | 修改 |
@@ -27,7 +27,7 @@
### Task 1: DTO 增强 — AiRuntimeRequest 新增 endpointId
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- [ ] **Step 1: 新增 endpointId 字段和 RESERVED_KEYS**
@@ -82,13 +82,13 @@ public static AiRuntimeRequest fromPayload(JSONObject payload) {
- [ ] **Step 3: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESSendpointId 字段新增不破坏任何编译,因为这是新增字段)
- [ ] **Step 4: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java
git add server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java
git commit -m "feat: AiRuntimeRequest DTO 新增 endpointId 字段
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -99,7 +99,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 2: Service 接口层 — 新增 testEndpoint + invokeEndpointStream
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `server/src/main/java/com/emotion/service/AiRuntimeService.java`
- [ ] **Step 1: 新增两个接口方法**
@@ -128,13 +128,13 @@ public interface AiRuntimeService {
- [ ] **Step 2: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS(接口新增方法,但实现类尚未修改,此时不会编译失败 — 因为 ServiceImpl 编译是单独的任务)
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/AiRuntimeService.java
git add server/src/main/java/com/emotion/service/AiRuntimeService.java
git commit -m "feat: AiRuntimeService 接口新增 endpoint 测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -145,7 +145,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 3: Service 实现层 — testEndpoint + invokeEndpointStream 实现
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- [ ] **Step 1: 新增 testEndpoint 方法**
@@ -271,13 +271,13 @@ public void invokeEndpointStream(String endpointId, Map<String, Object> inputs,
- [ ] **Step 3: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS
- [ ] **Step 4: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git commit -m "feat: AiRuntimeServiceImpl 实现 endpoint 直接测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -288,7 +288,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 4: Controller 层 — 新增 /endpoint/test 和 /endpoint/stream
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [ ] **Step 1: 在 runtimeStream 方法之后新增两个 endpoint 方法**
@@ -323,13 +323,13 @@ public SseEmitter endpointStream(@RequestBody JSONObject payload) {
- [ ] **Step 2: 编译并验证完整后端**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
git commit -m "feat: AiRoutingController 新增 /endpoint/test 和 /endpoint/stream 接口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -13,10 +13,10 @@
### Task 1: Backend Test Sample Contract
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/response/ai/AiTestTemplateResponse.java`
- Modify: `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Create: `server/src/main/java/com/emotion/dto/response/ai/AiTestTemplateResponse.java`
- Modify: `server/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [x] Add a response DTO with `sceneCode`, `endpointId`, `endpointCode`, `providerType`, `inputs`, and `paramFields`.
- [x] Add service methods `buildEndpointTestTemplate(String endpointId)` and `buildSceneTestTemplate(String sceneCode)`.
@@ -26,9 +26,9 @@
### Task 2: Provider Runtime Compatibility
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/ai/ProviderHttpSupport.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/ProviderHttpSupport.java`
- Modify: `server/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- [x] Add `Accept: text/event-stream` for stream requests.
- [x] Make Coze workflow body match historical calls: `workflow_id`, `user_id`, `stream`, `parameters`, and optional `bot_id`.
@@ -22,9 +22,9 @@
### Task 2: User Runtime Request Shape
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- [x] Accept user calls as `sceneCode` plus `inputs` JSON.
- [x] Keep user identity server-side by reading `UserContextHolder`.
@@ -13,8 +13,8 @@
### Task 1: Backend Chat Bridge To Runtime
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java`
- Modify: `server/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java`
- [x] Route chat generation through `AiRuntimeService` with `sceneCode=chat`.
- [x] Push `start`, `delta`, `done`, and `error` events over the existing user WebSocket topic.
@@ -13,8 +13,8 @@
### Task 1: Backend Stream Text Normalization
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- [x] Add a private helper that parses JSON string wrappers and returns `output`, `answer`, `content`, `text`, `result`, `data.*`, or `outputs.*`.
- [x] Call the helper before returning every extracted delta string.
@@ -54,7 +54,7 @@
**Commands:**
- `cd web-admin && npm run build`
- `cd backend-single && mvn test`
- `cd server && mvn test`
- `git diff --check`
- [x] Build passes.
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**backend-single 下全部 39 个 Controller 及其关联 Request DTO 补全 OpenAPI 中文注解(@Tag/@Operation/@Parameter/@Schema),使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
**Goal:**server 下全部 39 个 Controller 及其关联 Request DTO 补全 OpenAPI 中文注解(@Tag/@Operation/@Parameter/@Schema),使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
**Architecture:** 在不改变现有代码结构和业务逻辑的前提下,为每个 Controller 类添加 `@Tag` 注解、为每个 public 方法添加 `@Operation` 注解、为 `@RequestParam`/`@PathVariable` 参数添加 `@Parameter` 注解、为被 Controller 直接引用的 Request DTO 字段添加 `@Schema` 注解。分 3 批实施,每批完成后编译验证。
@@ -25,12 +25,12 @@
### Task 1.1: AiChatController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiChatController.java`
- Modify: `backend-single/src/main/java/com/com/emotion/dto/request/AiChatRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/AiSummaryRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/GuestChatRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ConversationCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ChatStatsRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiChatController.java`
- Modify: `server/src/main/java/com/com/emotion/dto/request/AiChatRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/AiSummaryRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/GuestChatRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ConversationCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ChatStatsRequest.java`
- [ ] **Step 1: 为 AiChatController 添加 @Tag 和 @Operation 注解**
@@ -162,8 +162,8 @@ public class ChatStatsRequest extends BaseRequest {
### Task 1.2: AiRoutingController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- [ ] **Step 1: 为 AiRoutingController 添加注解**
@@ -215,10 +215,10 @@ public class AiRuntimeRequest {
### Task 1.3: CozeApiCallController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CozeApiCallController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CozeApiCallController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallUpdateRequest.java`
- [ ] **Step 1: 为 CozeApiCallController 添加 @Tag 注解**
@@ -339,10 +339,10 @@ public class CozeApiCallUpdateRequest {
### Task 1.4: CommunityPostController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CommunityPostController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CommunityPostController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -387,10 +387,10 @@ CommunityPostPageRequest 各字段加 @Schema(description = "页码")、@Schema(
### Task 1.5: CommentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CommentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CommentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -429,10 +429,10 @@ public class CommentController {
### Task 1.6: SocialContentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/SocialContentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentManualImportRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentLinkImportRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentApprovalRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/SocialContentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentManualImportRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentLinkImportRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentApprovalRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -476,9 +476,9 @@ public class SocialContentController {
### Task 1.7: SocialInsightController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/SocialInsightController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialInsightGenerateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialInsightUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/SocialInsightController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialInsightGenerateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialInsightUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -514,8 +514,8 @@ public class SocialInsightController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS,无编译错误。
@@ -523,8 +523,8 @@ Expected: BUILD SUCCESS,无编译错误。
- [ ] **Step 2: 提交 Batch 1 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiChatController.java
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiChatController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
# ... 添加 Batch 1 所有修改的文件
git commit -m "feat: Batch 1 — AI + 社区 Controller 中文注解补全"
```
@@ -536,10 +536,10 @@ git commit -m "feat: Batch 1 — AI + 社区 Controller 中文注解补全"
### Task 2.1: EmotionAnalysisController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionAnalysisController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -578,10 +578,10 @@ public class EmotionAnalysisController {
### Task 2.2: EmotionRecordController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionRecordController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionRecordController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -625,9 +625,9 @@ public class EmotionRecordController {
### Task 2.3: EmotionSummaryController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionSummaryController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionSummaryGenerateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionSummaryStatusRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionSummaryController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionSummaryGenerateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionSummaryStatusRequest.java`
- [ ] **Step 1: 补充缺失的 @Operation 注解**
@@ -646,10 +646,10 @@ public class EmotionRecordController {
### Task 2.4: DiaryPostController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DiaryPostController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DiaryPostController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -706,9 +706,9 @@ public class DiaryPostController {
### Task 2.5: DiaryCommentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DiaryCommentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryCommentCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryCommentPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DiaryCommentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryCommentCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryCommentPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -759,10 +759,10 @@ public class DiaryCommentController {
### Task 2.6: GrowthTopicController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/GrowthTopicController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/GrowthTopicController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -793,10 +793,10 @@ public class GrowthTopicController {
### Task 2.7: TopicInteractionController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/TopicInteractionController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/TopicInteractionController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -829,8 +829,8 @@ public class TopicInteractionController {
### Task 2.8: ConversationController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/ConversationController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ConversationPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/ConversationController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ConversationPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -872,11 +872,11 @@ public class ConversationController {
### Task 2.9: MessageController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/MessageController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessagePageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageSearchRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageRecentRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/MessageController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessagePageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageSearchRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageRecentRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -915,10 +915,10 @@ public class MessageController {
### Task 2.10: UserProfileController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java`
- [ ] **Step 1: 补充 @Operation 注解(该 Controller 已有 @Tag 但方法级注解缺失)**
@@ -951,12 +951,12 @@ public class MessageController {
### Task 2.11: AchievementController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AchievementController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementProgressUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementUnlockRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AchievementController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementProgressUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementUnlockRequest.java`
- [ ] **Step 1: 补充 @Parameter 注解(已有 @Tag 和 @Operation,但缺少 @RequestParam 的 @Parameter**
@@ -1042,8 +1042,8 @@ public class MessageController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS
@@ -1051,7 +1051,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 2: 提交 Batch 2 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java
git add server/src/main/java/com/emotion/controller/EmotionAnalysisController.java
# ... 添加 Batch 2 所有修改的文件
git commit -m "feat: Batch 2 — 情绪 + 日记 + 互动 Controller 中文注解补全"
```
@@ -1063,10 +1063,10 @@ git commit -m "feat: Batch 2 — 情绪 + 日记 + 互动 Controller 中文注
### Task 3.1: LifePathController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/LifePathController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/LifePathController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1113,10 +1113,10 @@ public class LifePathController {
### Task 3.2: RewardController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/RewardController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/RewardController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1149,11 +1149,11 @@ public class RewardController {
### Task 3.3: UserStatsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/UserStatsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsIncrementRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsUpdateValueRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/UserStatsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsIncrementRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsUpdateValueRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1194,11 +1194,11 @@ public class UserStatsController {
### Task 3.4: EpicScriptController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EpicScriptController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptInspirationRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EpicScriptController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptInspirationRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1247,10 +1247,10 @@ public class EpicScriptController {
### Task 3.5: DictionaryController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DictionaryController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DictionaryController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1266,10 +1266,10 @@ public class DictionaryController {
### Task 3.6: GuestUserController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/GuestUserController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/GuestUserController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1285,8 +1285,8 @@ public class GuestUserController {
### Task 3.7: TtsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/TtsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/TtsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1302,7 +1302,7 @@ public class TtsController {
### Task 3.8: AsrController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `server/src/main/java/com/emotion/controller/AsrController.java`
- [ ] **Step 1: Controller 注解**
@@ -1318,7 +1318,7 @@ public class AsrController {
### Task 3.9: HealthController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/HealthController.java`
- Modify: `server/src/main/java/com/emotion/controller/HealthController.java`
- [ ] **Step 1: Controller 注解**
@@ -1334,10 +1334,10 @@ public class HealthController {
### Task 3.10: LifeEventController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/LifeEventController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/LifeEventController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1353,8 +1353,8 @@ public class LifeEventController {
### Task 3.11: ChatWebSocketController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/ChatWebSocketController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/WebSocketRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/ChatWebSocketController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/WebSocketRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1370,8 +1370,8 @@ public class ChatWebSocketController {
### Task 3.12: AnalyticsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1387,13 +1387,13 @@ public class AnalyticsController {
### Task 3.13: AiConfigController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiConfigController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigEnableRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigDefaultRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigTestUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiConfigController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigEnableRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigDefaultRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigTestUpdateRequest.java`
- [ ] **Step 1: 补充缺失的 @Operation 注解(已有 @Tag 但方法级注解不全)**
@@ -1404,8 +1404,8 @@ public class AnalyticsController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS
@@ -1413,7 +1413,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 2: 提交 Batch 3 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/LifePathController.java
git add server/src/main/java/com/emotion/controller/LifePathController.java
# ... 添加 Batch 3 所有修改的文件
git commit -m "feat: Batch 3 — 其他 Controller 中文注解补全"
```
@@ -1559,8 +1559,8 @@ private String fieldName;
- [ ] **Step 1: 全量编译**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
- [ ] **Step 2: 启动服务验证 OpenAPI 同步**
@@ -13,10 +13,10 @@
### Task 1: Backend Analytics Dictionary
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/analytics/AnalyticsDictionary.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`
- Create: `server/src/main/java/com/emotion/service/analytics/AnalyticsDictionary.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`
- [x] Add event, event type, page, dimension, value, and API label maps.
- [x] Add DTO fields for Chinese labels while keeping original keys.
@@ -25,8 +25,8 @@
### Task 2: Backend Aggregation Enrichment
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- [x] Query `page_path` and `api_path` for top events.
- [x] Fill `eventLabel`, `eventTypeLabel`, `pageLabel`, `apiLabel` in top events.
@@ -58,7 +58,7 @@
### Task 5: Verification
**Commands:**
- `cd backend-single && mvn test`
- `cd server && mvn test`
- `cd web-admin && npm run build`
- `cd mini-program && npm run build:mp-weixin` or available project build command
- `git diff --check`
@@ -13,10 +13,10 @@
### Task 1: 数据库实体 + Mapper 层
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java`
- Create: `backend-single/src/main/java/com/emotion/entity/ApiParam.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java`
- Create: `server/src/main/java/com/emotion/entity/ApiEndpoint.java`
- Create: `server/src/main/java/com/emotion/entity/ApiParam.java`
- Create: `server/src/main/java/com/emotion/mapper/ApiEndpointMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/ApiParamMapper.java`
- [ ] **Step 1: 创建 ApiEndpoint 实体**
@@ -153,10 +153,10 @@ public interface ApiParamMapper extends BaseMapper<ApiParam> {
- [ ] **Step 4: 提交**
```bash
git add backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java
git add backend-single/src/main/java/com/emotion/entity/ApiParam.java
git add backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java
git add backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java
git add server/src/main/java/com/emotion/entity/ApiEndpoint.java
git add server/src/main/java/com/emotion/entity/ApiParam.java
git add server/src/main/java/com/emotion/mapper/ApiEndpointMapper.java
git add server/src/main/java/com/emotion/mapper/ApiParamMapper.java
git commit -m "feat: 添加接口端点实体和Mapper"
```
@@ -165,11 +165,11 @@ git commit -m "feat: 添加接口端点实体和Mapper"
### Task 2: DTO 层 — Request/Response
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java`
- Create: `server/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java`
- [ ] **Step 1: 创建 ApiEndpointListRequest(分页查询入参)**
@@ -309,11 +309,11 @@ public class ApiTestProxyRequest {
- [ ] **Step 6: 提交**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java
git add backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java
git add server/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java
git add server/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java
git add server/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java
git add server/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java
git add server/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java
git commit -m "feat: 添加接口管理DTO"
```
@@ -322,8 +322,8 @@ git commit -m "feat: 添加接口管理DTO"
### Task 3: Service 层 — 核心解析与同步逻辑
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java`
- Create: `server/src/main/java/com/emotion/service/ApiEndpointService.java`
- Create: `server/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java`
- [ ] **Step 1: 创建 Service 接口**
@@ -676,8 +676,8 @@ public class ApiEndpointServiceImpl implements ApiEndpointService {
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/service/ApiEndpointService.java
git add backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java
git add server/src/main/java/com/emotion/service/ApiEndpointService.java
git add server/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java
git commit -m "feat: 添加接口端点Service层"
```
@@ -686,7 +686,7 @@ git commit -m "feat: 添加接口端点Service层"
### Task 4: ApplicationRunner — 启动自动同步
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java`
- Create: `server/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java`
- [ ] **Step 1: 创建 SyncRunner**
@@ -729,7 +729,7 @@ public class ApiEndpointSyncRunner implements ApplicationRunner {
- [ ] **Step 2: 提交**
```bash
git add backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java
git add server/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java
git commit -m "feat: 添加启动自动同步接口数据"
```
@@ -738,7 +738,7 @@ git commit -m "feat: 添加启动自动同步接口数据"
### Task 5: Controller 层 — 分页查询 + 详情 + 手动同步
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java`
- Create: `server/src/main/java/com/emotion/controller/ApiEndpointController.java`
- [ ] **Step 1: 创建 Controller**
@@ -815,7 +815,7 @@ public class ApiEndpointController {
- [ ] **Step 2: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java
git add server/src/main/java/com/emotion/controller/ApiEndpointController.java
git commit -m "feat: 添加接口管理Controller"
```
@@ -824,8 +824,8 @@ git commit -m "feat: 添加接口管理Controller"
### Task 6: Controller 层 — 代理测试接口
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java`
- Create: `server/src/main/java/com/emotion/controller/ApiTestProxyController.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java`
- [ ] **Step 1: 创建 ApiTestProxyResponse**
@@ -990,8 +990,8 @@ public class ApiTestProxyController {
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java
git add server/src/main/java/com/emotion/controller/ApiTestProxyController.java
git add server/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java
git commit -m "feat: 添加接口代理测试Controller"
```
@@ -1002,8 +1002,8 @@ git commit -m "feat: 添加接口代理测试Controller"
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
预期:BUILD SUCCESS
@@ -15,28 +15,28 @@
### 后端改动文件(按批次)
**Batch 1 (P0):**
- `backend-single/src/main/java/com/emotion/controller/AdminAuthController.java` — 已有 @Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/AuthController.java` — 补全缺少的 @Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/AdminController.java` — 已有 @Tag/@Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/TokenController.java` — 已有 @Operation,检查完整度
- `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/controller/UserController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/controller/UserProfileController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/dto/request/AdminLoginRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminCreateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminPageRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserPageRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/controller/AdminAuthController.java` — 已有 @Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/AuthController.java` — 补全缺少的 @Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/AdminController.java` — 已有 @Tag/@Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/TokenController.java` — 已有 @Operation,检查完整度
- `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/controller/UserController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/controller/UserProfileController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/dto/request/AdminLoginRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminCreateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminPageRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/RegisterRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/LoginRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserCreateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserPageRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java` — 补全 @Schema
### 前端改动文件
@@ -48,8 +48,8 @@
## Task 1: 后端 Batch 1 - AdminAuth + Auth Controller 注解补全
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminAuthController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminAuthController.java`
- Modify: `server/src/main/java/com/emotion/controller/AuthController.java`
- [ ] **Step 1: 查看 AdminAuthController 已有注解**
@@ -85,7 +85,7 @@ AuthController 已有 @Tag。需要补全以下方法的 @Operation:
- [ ] **Step 3: 为 AuthController 缺少的方法补全注解**
修改 `backend-single/src/main/java/com/emotion/controller/AuthController.java`
修改 `server/src/main/java/com/emotion/controller/AuthController.java`
为以下方法添加 @Operation(在方法上方的空白行插入):
@@ -133,7 +133,7 @@ AdminAuthController 的 6 个方法全部有 @Operation,无需修改。
- [ ] **Step 5: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/AuthController.java
git add server/src/main/java/com/emotion/controller/AuthController.java
git commit -m "feat: 补全 AuthController 缺失的 @Operation@Parameter 注解"
```
@@ -142,16 +142,16 @@ git commit -m "feat: 补全 AuthController 缺失的 @Operation 和 @Parameter
## Task 2: 后端 Batch 1 - Admin + Token Controller 注解补全
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/TokenController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/UserController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminController.java`
- Modify: `server/src/main/java/com/emotion/controller/TokenController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/controller/UserController.java`
- Modify: `server/src/main/java/com/emotion/controller/UserProfileController.java`
- [ ] **Step 1: 读取 AdminController**
```bash
cat backend-single/src/main/java/com/emotion/controller/AdminController.java
cat server/src/main/java/com/emotion/controller/AdminController.java
```
检查每个方法的 @Operation 是否有 @Parameter,为路径参数和查询参数补全 @Parameter 注解。
@@ -159,7 +159,7 @@ cat backend-single/src/main/java/com/emotion/controller/AdminController.java
- [ ] **Step 2: 读取 TokenController**
```bash
cat backend-single/src/main/java/com/emotion/controller/TokenController.java
cat server/src/main/java/com/emotion/controller/TokenController.java
```
已有 3 个 @Operation,检查是否缺少 @Parameter,补全。
@@ -194,11 +194,11 @@ public Result<AnalyticsOverviewResponse> overview(@Validated AnalyticsQueryReque
- [ ] **Step 6: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/AdminController.java
git add backend-single/src/main/java/com/emotion/controller/TokenController.java
git add backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java
git add backend-single/src/main/java/com/emotion/controller/UserController.java
git add backend-single/src/main/java/com/emotion/controller/UserProfileController.java
git add server/src/main/java/com/emotion/controller/AdminController.java
git add server/src/main/java/com/emotion/controller/TokenController.java
git add server/src/main/java/com/emotion/controller/AdminAnalyticsController.java
git add server/src/main/java/com/emotion/controller/UserController.java
git add server/src/main/java/com/emotion/controller/UserProfileController.java
git commit -m "feat: 为 Admin/Token/AdminAnalytics/User/UserProfile Controller 补全 OpenAPI 注解"
```
@@ -257,21 +257,21 @@ import io.swagger.v3.oas.annotations.media.Schema;
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminLoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminCreateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminPageRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserPageRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java
git add server/src/main/java/com/emotion/dto/request/LoginRequest.java
git add server/src/main/java/com/emotion/dto/request/RegisterRequest.java
git add server/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminLoginRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminPageRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/UserCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/UserUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/UserPageRequest.java
git add server/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java
git commit -m "feat: 为 Batch 1 请求 DTO 补全 @Schema 中文描述注解"
```
@@ -665,7 +665,7 @@ git commit -m "feat: 测试面板增加请求头展示区域"
- [ ] **Step 1: 本地构建后端**
```bash
cd backend-single
cd server
mvn clean install -DskipTests
```
@@ -19,10 +19,10 @@
| `web-admin/src/components/JsonViewer.vue` | 创建 | 通用 JSON 格式化高亮 + 复制组件 |
| `web-admin/src/views/aiconfig/components/AiCallLogDetailDialog.vue` | 创建 | 调用日志详情弹窗 |
| `web-admin/src/views/aiconfig/AiRoutingList.vue` | 修改 | 调用日志 Tab 增加筛选栏、展开行、分页 |
| `backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java` | 创建 | 日志查询请求 DTO |
| `backend-single/src/main/java/com/emotion/service/AiCallLogService.java` | 修改 | 新增 `query` 方法签名 |
| `backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java` | 修改 | 实现 `query` 分页查询 |
| `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` | 修改 | 新增 POST /call-logs 接口 |
| `server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java` | 创建 | 日志查询请求 DTO |
| `server/src/main/java/com/emotion/service/AiCallLogService.java` | 修改 | 新增 `query` 方法签名 |
| `server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java` | 修改 | 实现 `query` 分页查询 |
| `server/src/main/java/com/emotion/controller/AiRoutingController.java` | 修改 | 新增 POST /call-logs 接口 |
---
@@ -428,18 +428,18 @@ git commit -m "feat: 新增 AI 调用日志详情弹窗组件"
## Task 5: 后端 AiCallLogQueryRequest DTO
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
- [ ] **Step 1: 确认 dto/request/ai 目录存在**
```bash
ls backend-single/src/main/java/com/emotion/dto/request/ai/
ls server/src/main/java/com/emotion/dto/request/ai/
```
如果不存在则创建:
```bash
mkdir -p backend-single/src/main/java/com/emotion/dto/request/ai
mkdir -p server/src/main/java/com/emotion/dto/request/ai
```
- [ ] **Step 2: 创建 AiCallLogQueryRequest.java**
@@ -498,7 +498,7 @@ public class AiCallLogQueryRequest {
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java
git add server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java
git commit -m "feat: 新增 AI 调用日志查询请求 DTO"
```
@@ -507,8 +507,8 @@ git commit -m "feat: 新增 AI 调用日志查询请求 DTO"
## Task 6: 后端 Service 接口和实现
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/AiCallLogService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/AiCallLogService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java`
- [ ] **Step 1: 在 AiCallLogService 接口中新增 query 方法**
@@ -562,8 +562,8 @@ public PageResult<AiCallLog> query(AiCallLogQueryRequest request) {
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/AiCallLogService.java
git add backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java
git add server/src/main/java/com/emotion/service/AiCallLogService.java
git add server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java
git commit -m "feat: AI 调用日志 Service 增加分页查询方法"
```
@@ -572,7 +572,7 @@ git commit -m "feat: AI 调用日志 Service 增加分页查询方法"
## Task 7: 后端 Controller 新增 POST 接口
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [ ] **Step 1: 在 AiRoutingController 中导入新类型**
@@ -599,7 +599,7 @@ public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLog
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
git commit -m "feat: Controller 新增 AI 调用日志分页查询接口"
```
@@ -949,7 +949,7 @@ git commit -m "feat: 调用日志 Tab 增加筛选栏、展开行、分页和详
- [ ] **Step 1: 编译后端**
```bash
cd backend-single
cd server
mvn clean install -DskipTests
```
@@ -970,7 +970,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 1: 启动后端**
```bash
cd backend-single
cd server
mvn spring-boot:run
```
@@ -16,10 +16,10 @@
| 操作 | 文件 | 说明 |
|------|------|------|
| Modify | `backend-single/src/main/java/com/emotion/entity/AiCallLog.java` | 实体新增 `userName` 字段 |
| Create | `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql` | 数据库迁移脚本 |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:68-73` | invokeStream 写入 userName |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:219-225` | invokeEndpointStream 写入 userName |
| Modify | `server/src/main/java/com/emotion/entity/AiCallLog.java` | 实体新增 `userName` 字段 |
| Create | `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql` | 数据库迁移脚本 |
| Modify | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:68-73` | invokeStream 写入 userName |
| Modify | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:219-225` | invokeEndpointStream 写入 userName |
| Modify | `web-admin/src/types/aiconfig.ts` | AiCallLog 接口新增 userName |
| Modify | `web-admin/src/views/aiconfig/AiRoutingList.vue` | 列表新增"调用用户"列 + userDisplay 函数 |
| Modify | `web-admin/src/views/aiconfig/components/AiCallLogDetailDialog.vue` | 详情弹窗"用户 ID"改为"调用用户" |
@@ -29,9 +29,9 @@
### Task 1: 后端实体 + 数据库迁移
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/entity/AiCallLog.java:29-31`
- Create: `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`
- Test: `backend-single/` 目录下执行编译验证
- Modify: `server/src/main/java/com/emotion/entity/AiCallLog.java:29-31`
- Create: `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`
- Test: `server/` 目录下执行编译验证
- [ ] **Step 1: 在 AiCallLog 实体中新增 userName 字段**
@@ -46,7 +46,7 @@ private String userName;
- [ ] **Step 2: 创建数据库迁移脚本**
创建 `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`:
创建 `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`:
```sql
-- 调用日志表新增 user_name 字段
@@ -56,7 +56,7 @@ ALTER TABLE t_ai_call_log ADD COLUMN user_name VARCHAR(100) DEFAULT NULL COMMENT
- [ ] **Step 3: 编译验证后端代码**
```bash
cd backend-single
cd server
mvn clean compile -DskipTests
```
@@ -65,8 +65,8 @@ mvn clean compile -DskipTests
- [ ] **Step 4: 提交**
```bash
git add backend-single/src/main/java/com/emotion/entity/AiCallLog.java
git add backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql
git add server/src/main/java/com/emotion/entity/AiCallLog.java
git add server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql
git commit -m "feat: 调用日志实体新增 userName 字段及数据库迁移脚本"
```
@@ -75,8 +75,8 @@ git commit -m "feat: 调用日志实体新增 userName 字段及数据库迁移
### Task 2: 后端日志保存逻辑写入 userName
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`
- [ ] **Step 1: 在 invokeStream 方法中写入 userName**
@@ -123,7 +123,7 @@ callLog.setUserName(request.getUserName());
- [ ] **Step 3: 更新单元测试,验证 userName 被正确写入**
修改 `backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`,在 `invokeStreamRecoversWhenOutputExists` 测试中:
修改 `server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`,在 `invokeStreamRecoversWhenOutputExists` 测试中:
在已有的 `request.setRequestId("client-request-1");` 之后添加 `request.setUserName("测试用户");`,然后在断言部分新增 userName 的验证:
@@ -138,7 +138,7 @@ assertEquals("测试用户", savedLog.getUserName());
- [ ] **Step 4: 运行测试验证**
```bash
cd backend-single
cd server
mvn test -Dtest=AiRuntimeServiceImplTest
```
@@ -147,13 +147,13 @@ mvn test -Dtest=AiRuntimeServiceImplTest
- [ ] **Step 5: 编译验证 + 提交**
```bash
cd backend-single
cd server
mvn clean compile -DskipTests
```
```bash
git add backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java
git add server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java
git commit -m "feat: 日志保存时写入 userName 字段"
```
@@ -16,28 +16,28 @@
| Action | File | Responsibility |
| --- | --- | --- |
| Create | `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java` | Binds WeChat mini program appid, secret, and API base URL from application config. |
| Create | `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql` | Prevents duplicate non-null WeChat third-party identities. |
| Modify | `backend-single/src/main/resources/application.yml` | Adds default WeChat config keys and public login endpoint allowlist. |
| Modify | `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java` | Excludes `/auth/wechat/login` from JWT interception. |
| Modify | `backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java` | Excludes `/api/auth/wechat/login` from the interceptor's internal public endpoint list. |
| Create | `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java` | Request body for WeChat openid login. |
| Create | `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java` | Request body for WeChat phone binding. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | Internal DTO for WeChat `code2Session` response. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java` | Internal DTO for WeChat access token response. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java` | Internal DTO for WeChat phone number response. |
| Create | `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java` | Interface for WeChat API calls. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | RestTemplate-backed WeChat client with Redis token caching. |
| Create | `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java` | Defines temporary WeChat user to phone user data transfer. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java` | Transfers user-owned rows and avoids singleton row conflicts. |
| Modify | `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java` | Adds non-sensitive `wechatBound` flag for frontend display logic. |
| Modify | `backend-single/src/main/java/com/emotion/service/UserService.java` | Adds third-party identity lookup helper. |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java` | Implements third-party identity lookup. |
| Modify | `backend-single/src/main/java/com/emotion/service/AuthService.java` | Adds WeChat login and phone binding operations. |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` | Implements openid login, phone identity merge/bind, and token issuing reuse. |
| Modify | `backend-single/src/main/java/com/emotion/controller/AuthController.java` | Adds `/auth/wechat/login` and `/auth/wechat/phone`. |
| Test | `backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java` | Verifies controller contract. |
| Test | `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java` | Verifies identity matching and binding behavior. |
| Create | `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java` | Binds WeChat mini program appid, secret, and API base URL from application config. |
| Create | `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql` | Prevents duplicate non-null WeChat third-party identities. |
| Modify | `server/src/main/resources/application.yml` | Adds default WeChat config keys and public login endpoint allowlist. |
| Modify | `server/src/main/java/com/emotion/config/WebMvcConfig.java` | Excludes `/auth/wechat/login` from JWT interception. |
| Modify | `server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java` | Excludes `/api/auth/wechat/login` from the interceptor's internal public endpoint list. |
| Create | `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java` | Request body for WeChat openid login. |
| Create | `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java` | Request body for WeChat phone binding. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | Internal DTO for WeChat `code2Session` response. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java` | Internal DTO for WeChat access token response. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java` | Internal DTO for WeChat phone number response. |
| Create | `server/src/main/java/com/emotion/service/WechatMiniProgramService.java` | Interface for WeChat API calls. |
| Create | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | RestTemplate-backed WeChat client with Redis token caching. |
| Create | `server/src/main/java/com/emotion/service/UserIdentityMergeService.java` | Defines temporary WeChat user to phone user data transfer. |
| Create | `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java` | Transfers user-owned rows and avoids singleton row conflicts. |
| Modify | `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java` | Adds non-sensitive `wechatBound` flag for frontend display logic. |
| Modify | `server/src/main/java/com/emotion/service/UserService.java` | Adds third-party identity lookup helper. |
| Modify | `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java` | Implements third-party identity lookup. |
| Modify | `server/src/main/java/com/emotion/service/AuthService.java` | Adds WeChat login and phone binding operations. |
| Modify | `server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` | Implements openid login, phone identity merge/bind, and token issuing reuse. |
| Modify | `server/src/main/java/com/emotion/controller/AuthController.java` | Adds `/auth/wechat/login` and `/auth/wechat/phone`. |
| Test | `server/src/test/java/com/emotion/controller/AuthControllerTest.java` | Verifies controller contract. |
| Test | `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java` | Verifies identity matching and binding behavior. |
| Modify | `mini-program/src/services/auth.js` | Adds `wechatLogin` and `bindWechatPhone`. |
| Modify | `mini-program/src/stores/app.js` | Adds store methods and tracks returned auth user info. |
| Modify | `mini-program/src/pages/login/index.vue` | Adds WeChat primary login while keeping phone/SMS form. |
@@ -48,20 +48,20 @@
### Task 1: Backend WeChat Configuration And DTOs
**Files:**
- Create: `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`
- Create: `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Create: `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`
- Create: `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`
- Create: `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`
- Modify: `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Modify: `server/src/main/resources/application.yml`
- Test: backend compile
- [ ] **Step 1: Create WeChat configuration properties**
Create `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`:
Create `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`:
```java
package com.emotion.config;
@@ -82,7 +82,7 @@ public class WechatMiniProgramProperties {
- [ ] **Step 2: Create public request DTOs**
Create `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`:
Create `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`:
```java
package com.emotion.dto.request;
@@ -102,7 +102,7 @@ public class WechatLoginRequest {
}
```
Create `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`:
Create `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`:
```java
package com.emotion.dto.request;
@@ -120,7 +120,7 @@ public class WechatPhoneBindRequest {
- [ ] **Step 3: Create internal WeChat API response DTOs**
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -147,7 +147,7 @@ public class WechatCodeSessionResponse {
}
```
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -173,7 +173,7 @@ public class WechatAccessTokenResponse {
}
```
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -213,7 +213,7 @@ public class WechatPhoneNumberResponse {
- [ ] **Step 4: Add WeChat config keys**
In `backend-single/src/main/resources/application.yml`, under `emotion:`, add:
In `server/src/main/resources/application.yml`, under `emotion:`, add:
```yaml
wechat:
@@ -239,7 +239,7 @@ HAVING COUNT(*) > 1;
Expected: no rows. If rows exist, resolve duplicates before adding the index.
Create `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`:
Create `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`:
```sql
ALTER TABLE t_user
@@ -250,7 +250,7 @@ MySQL permits multiple `NULL` values in a unique index, so users without WeChat
- [ ] **Step 6: Add non-sensitive WeChat binding flag**
In `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`, add:
In `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`, add:
```java
/**
@@ -264,7 +264,7 @@ private Boolean wechatBound;
Run:
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -273,15 +273,15 @@ Expected: command exits with status `0`.
- [ ] **Step 8: Commit**
```bash
git add backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
git add backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql
git add backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/main/resources/application.yml
git add server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
git add server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql
git add server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java
git add server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java
git add server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java
git add server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java
git add server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java
git add server/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add server/src/main/resources/application.yml
git commit -m "feat: add wechat mini program auth DTOs and config"
```
@@ -290,13 +290,13 @@ git commit -m "feat: add wechat mini program auth DTOs and config"
### Task 2: Backend WeChat API Client
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`
- Create: `server/src/main/java/com/emotion/service/WechatMiniProgramService.java`
- Create: `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`
- [ ] **Step 1: Create service interface**
Create `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java`:
Create `server/src/main/java/com/emotion/service/WechatMiniProgramService.java`:
```java
package com.emotion.service;
@@ -313,7 +313,7 @@ public interface WechatMiniProgramService {
- [ ] **Step 2: Create RestTemplate implementation**
Create `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`:
Create `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`:
```java
package com.emotion.service.impl;
@@ -434,7 +434,7 @@ public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
- [ ] **Step 3: Create focused unit tests for error behavior**
Create `backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`:
Create `server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`:
```java
package com.emotion.service;
@@ -470,7 +470,7 @@ class WechatMiniProgramServiceImplTest {
- [ ] **Step 4: Run test**
```bash
cd backend-single
cd server
mvn -q -Dtest=WechatMiniProgramServiceImplTest test
```
@@ -479,9 +479,9 @@ Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java
git add backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java
git add server/src/main/java/com/emotion/service/WechatMiniProgramService.java
git add server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java
git commit -m "feat: add wechat mini program API client"
```
@@ -490,13 +490,13 @@ git commit -m "feat: add wechat mini program API client"
### Task 3: Backend User Identity Merge Service
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`
- Create: `server/src/main/java/com/emotion/service/UserIdentityMergeService.java`
- Create: `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`
- [ ] **Step 1: Create merge service interface**
Create `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`:
Create `server/src/main/java/com/emotion/service/UserIdentityMergeService.java`:
```java
package com.emotion.service;
@@ -508,7 +508,7 @@ public interface UserIdentityMergeService {
- [ ] **Step 2: Create JdbcTemplate implementation**
Create `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`:
Create `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`:
```java
package com.emotion.service.impl;
@@ -592,7 +592,7 @@ public class UserIdentityMergeServiceImpl implements UserIdentityMergeService {
- [ ] **Step 3: Add merge service test**
Create `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`:
Create `server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`:
```java
package com.emotion.service;
@@ -626,7 +626,7 @@ class UserIdentityMergeServiceImplTest {
- [ ] **Step 4: Run merge service test**
```bash
cd backend-single
cd server
mvn -q -Dtest=UserIdentityMergeServiceImplTest test
```
@@ -635,9 +635,9 @@ Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java
git add server/src/main/java/com/emotion/service/UserIdentityMergeService.java
git add server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java
git add server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java
git commit -m "feat: add wechat user identity merge service"
```
@@ -646,22 +646,22 @@ git commit -m "feat: add wechat user identity merge service"
### Task 4: Backend User Lookup And Auth Service Methods
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/UserService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/service/AuthService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Test: `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`
- Modify: `server/src/main/java/com/emotion/service/UserService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/AuthService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Test: `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java`
- [ ] **Step 1: Add third-party lookup to UserService**
In `backend-single/src/main/java/com/emotion/service/UserService.java`, add:
In `server/src/main/java/com/emotion/service/UserService.java`, add:
```java
User getByThirdParty(String thirdPartyType, String thirdPartyId);
```
In `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java`, add:
In `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java`, add:
```java
@Override
@@ -679,7 +679,7 @@ public User getByThirdParty(String thirdPartyType, String thirdPartyId) {
- [ ] **Step 2: Add AuthService methods**
In `backend-single/src/main/java/com/emotion/service/AuthService.java`, import the two request DTOs and add:
In `server/src/main/java/com/emotion/service/AuthService.java`, import the two request DTOs and add:
```java
AuthResponse wechatLogin(WechatLoginRequest request);
@@ -858,7 +858,7 @@ public AuthResponse bindWechatPhone(String currentUserId, WechatPhoneBindRequest
- [ ] **Step 8: Compile backend**
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -866,7 +866,7 @@ Expected: command exits with status `0`.
- [ ] **Step 9: Add service tests for identity rules**
Create `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`:
Create `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java`:
```java
package com.emotion.service;
@@ -1002,7 +1002,7 @@ class AuthServiceWechatTest {
- [ ] **Step 10: Run auth tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,UserIdentityMergeServiceImplTest test
```
@@ -1011,12 +1011,12 @@ Expected: command exits with status `0`.
- [ ] **Step 11: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java
git add backend-single/src/main/java/com/emotion/service/AuthService.java
git add backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java
git add server/src/main/java/com/emotion/service/UserService.java
git add server/src/main/java/com/emotion/service/impl/UserServiceImpl.java
git add server/src/main/java/com/emotion/service/AuthService.java
git add server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java
git add server/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add server/src/test/java/com/emotion/service/AuthServiceWechatTest.java
git commit -m "feat: add wechat auth identity binding logic"
```
@@ -1025,10 +1025,10 @@ git commit -m "feat: add wechat auth identity binding logic"
### Task 5: Backend Auth Controller And Security Allowlist
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`
- Modify: `backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java`
- Test: `backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java`
- Modify: `server/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `server/src/main/java/com/emotion/config/WebMvcConfig.java`
- Modify: `server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java`
- Test: `server/src/test/java/com/emotion/controller/AuthControllerTest.java`
- [ ] **Step 1: Add controller endpoints**
@@ -1126,7 +1126,7 @@ public void testWechatPhoneBind() throws Exception {
- [ ] **Step 5: Run controller tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest test
```
@@ -1135,10 +1135,10 @@ Expected: command exits with status `0`.
- [ ] **Step 6: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AuthController.java
git add backend-single/src/main/java/com/emotion/config/WebMvcConfig.java
git add backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java
git add backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java
git add server/src/main/java/com/emotion/controller/AuthController.java
git add server/src/main/java/com/emotion/config/WebMvcConfig.java
git add server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java
git add server/src/test/java/com/emotion/controller/AuthControllerTest.java
git commit -m "feat: expose wechat auth endpoints"
```
@@ -1596,7 +1596,7 @@ git commit -m "feat: add wechat phone binding entry"
- [ ] **Step 1: Run backend auth tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,WechatMiniProgramServiceImplTest,UserIdentityMergeServiceImplTest test
```
@@ -1605,7 +1605,7 @@ Expected: command exits with status `0`.
- [ ] **Step 2: Compile backend**
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -1633,7 +1633,7 @@ Open `mini-program/unpackage/dist/build/mp-weixin` in WeChat DevTools and verify
- [ ] **Step 5: Add README note if config is not documented elsewhere**
If no deployment doc already lists WeChat env vars, add this section to `mini-program/README.md` or `backend-single/部署说明.md`:
If no deployment doc already lists WeChat env vars, add this section to `mini-program/README.md` or `server/部署说明.md`:
```markdown
### WeChat Mini Program Login
@@ -15,10 +15,10 @@
| 文件 | 改动 | 职责 |
|------|------|------|
| `sql/emotion_museum_ddl.sql` | 追加 | 数据库迁移:新增5列 |
| `backend-single/.../entity/UserProfile.java` | 修改 | ORM 实体新增字段 |
| `backend-single/.../request/UserProfileCreateRequest.java` | 修改 | 创建请求 DTO 新增字段 |
| `backend-single/.../request/UserProfileUpdateRequest.java` | 修改 | 更新请求 DTO 新增字段 |
| `backend-single/.../response/UserProfileResponse.java` | 修改 | 响应 DTO 新增字段 |
| `server/.../entity/UserProfile.java` | 修改 | ORM 实体新增字段 |
| `server/.../request/UserProfileCreateRequest.java` | 修改 | 创建请求 DTO 新增字段 |
| `server/.../request/UserProfileUpdateRequest.java` | 修改 | 更新请求 DTO 新增字段 |
| `server/.../response/UserProfileResponse.java` | 修改 | 响应 DTO 新增字段 |
| `mini-program/src/services/userProfile.js` | 修改 | 前后端数据格式双向转换 |
| `mini-program/src/stores/app.js` | 修改 | 全局状态 registrationData 扩展 |
| `mini-program/src/pages/main/RecordView.vue` | 修改 | 去除写死默认值和 sampleEvents |
@@ -57,7 +57,7 @@ git commit -m "db: add city, industry, company, personality_tags, birthday to t_
### Task 2: 后端实体扩展 — UserProfile.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/entity/UserProfile.java`
- Modify: `server/src/main/java/com/emotion/entity/UserProfile.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -100,7 +100,7 @@ git commit -m "db: add city, industry, company, personality_tags, birthday to t_
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/UserProfile.java
git add server/src/main/java/com/emotion/entity/UserProfile.java
git commit -m "feat: extend UserProfile entity with city/industry/company/personalityTags/birthday"
```
@@ -109,7 +109,7 @@ git commit -m "feat: extend UserProfile entity with city/industry/company/person
### Task 3: 后端创建请求 DTO 扩展 — UserProfileCreateRequest.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -145,7 +145,7 @@ git commit -m "feat: extend UserProfile entity with city/industry/company/person
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java
git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/personalityTags/birthday"
```
@@ -154,7 +154,7 @@ git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/
### Task 4: 后端更新请求 DTO 扩展 — UserProfileUpdateRequest.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -190,7 +190,7 @@ git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java
git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/personalityTags/birthday"
```
@@ -199,7 +199,7 @@ git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/
### Task 5: 后端响应 DTO 扩展 — UserProfileResponse.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- Modify: `server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -236,7 +236,7 @@ git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java
git add server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java
git commit -m "feat: extend UserProfileResponse with city/industry/company/personalityTags/birthday"
```
@@ -631,7 +631,7 @@ git commit -m "fix: remove hardcoded defaults from profile edit page"
- [ ] **Step 1: 后端编译验证**
```bash
cd backend-single && mvn compile -q
cd server && mvn compile -q
```
预期:编译成功,无错误。
@@ -20,13 +20,13 @@ purpose: 微信小程序登录修复 spec 的实施计划(TDD + 频繁 commit
| 状态 | 文件 | 责任 | 行数 |
|---|---|---|---|
| 重写 | `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java` | 统一 RestTemplate BeanUser-Agent + 超时 + 拦截器 + 错误处理) | 13 → 40 |
| 新增 | `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | 请求/响应日志(INFO+ 脱敏 bodyDEBUG | 60 |
| 新增 | `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | 4xx/5xx 不抛异常 | 30 |
| 新增 | `backend-single/src/main/java/com/emotion/exception/WechatApiException.java` | 业务异常类(HTTP 风格 code + rawBody | 30 |
| 重写 | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | `code2Session` 五步防御 | 49 → 90 |
| 修改 | `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | 新增 `handleWechatApiException` + 脱敏日志 | +25 |
| 修改 | `backend-single/src/main/resources/application.yml` | 移除 `app-id` / `app-secret` 硬编码默认值 | 2 行 |
| 重写 | `server/src/main/java/com/emotion/config/RestTemplateConfig.java` | 统一 RestTemplate BeanUser-Agent + 超时 + 拦截器 + 错误处理) | 13 → 40 |
| 新增 | `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | 请求/响应日志(INFO+ 脱敏 bodyDEBUG | 60 |
| 新增 | `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | 4xx/5xx 不抛异常 | 30 |
| 新增 | `server/src/main/java/com/emotion/exception/WechatApiException.java` | 业务异常类(HTTP 风格 code + rawBody | 30 |
| 重写 | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | `code2Session` 五步防御 | 49 → 90 |
| 修改 | `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | 新增 `handleWechatApiException` + 脱敏日志 | +25 |
| 修改 | `server/src/main/resources/application.yml` | 移除 `app-id` / `app-secret` 硬编码默认值 | 2 行 |
**总计**:2 重写 + 3 新增 + 2 修改,约 280 行 Java + 2 行 YAML
@@ -38,9 +38,9 @@ purpose: 微信小程序登录修复 spec 的实施计划(TDD + 频繁 commit
cd "G:\IdeaProjects\emotion-museun"
git status # 确认在 master 分支
git log --oneline -1 # 确认最新 commit 是 82c308b
ls backend-single/src/main/java/com/emotion/exception/ # 确认 GlobalExceptionHandler.java 存在
ls backend-single/src/main/java/com/emotion/config/ # 确认 RestTemplateConfig.java 存在
ls backend-single/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniProgramServiceImpl.java 存在
ls server/src/main/java/com/emotion/exception/ # 确认 GlobalExceptionHandler.java 存在
ls server/src/main/java/com/emotion/config/ # 确认 RestTemplateConfig.java 存在
ls server/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniProgramServiceImpl.java 存在
```
期望:全部存在,工作树只有 spec 文件修改(无业务代码污染)
@@ -50,7 +50,7 @@ ls backend-single/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniPr
## Task 1: 创建 WechatApiException 异常类
**Files:**
- Create: `backend-single/src/main/java/com/emotion/exception/WechatApiException.java`
- Create: `server/src/main/java/com/emotion/exception/WechatApiException.java`
- [ ] **Step 1.1: 写入 WechatApiException.java**
@@ -95,7 +95,7 @@ public class WechatApiException extends RuntimeException {
- [ ] **Step 1.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -105,7 +105,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/exception/WechatApiException.java
git add server/src/main/java/com/emotion/exception/WechatApiException.java
git commit -m "feat(wechat): 新增 WechatApiException 业务异常类
- 继承 RuntimeException
@@ -119,12 +119,12 @@ git commit -m "feat(wechat): 新增 WechatApiException 业务异常类
## Task 2: 创建 WechatApiLoggingInterceptor(含脱敏单元测试)
**Files:**
- Create: `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
- Create: `backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
- Create: `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
- Create: `server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
- [ ] **Step 2.1: 写失败测试(验证脱敏逻辑)**
创建 `backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
创建 `server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
```java
package com.emotion.interceptor;
@@ -197,7 +197,7 @@ class WechatApiLoggingInterceptorTest {
- [ ] **Step 2.2: 运行测试验证失败**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatApiLoggingInterceptorTest
```
@@ -205,7 +205,7 @@ mvn test -Dtest=WechatApiLoggingInterceptorTest
- [ ] **Step 2.3: 写最小实现让测试通过**
创建 `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
创建 `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
```java
package com.emotion.interceptor;
@@ -282,7 +282,7 @@ public class WechatApiLoggingInterceptor implements ClientHttpRequestInterceptor
- [ ] **Step 2.4: 运行测试验证通过**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatApiLoggingInterceptorTest
```
@@ -292,8 +292,8 @@ mvn test -Dtest=WechatApiLoggingInterceptorTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java
git add backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java
git add server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java
git add server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java
git commit -m "feat(wechat): 新增 WechatApiLoggingInterceptor 拦截器(含脱敏单元测试)
- ClientHttpRequestInterceptor 实现
@@ -309,7 +309,7 @@ git commit -m "feat(wechat): 新增 WechatApiLoggingInterceptor 拦截器(含
## Task 3: 创建 WechatResponseErrorHandler
**Files:**
- Create: `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java`
- Create: `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java`
- [ ] **Step 3.1: 写入 WechatResponseErrorHandler.java**
@@ -354,7 +354,7 @@ public class WechatResponseErrorHandler extends DefaultResponseErrorHandler {
- [ ] **Step 3.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -364,7 +364,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java
git add server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java
git commit -m "feat(wechat): 新增 WechatResponseErrorHandler
- 继承 DefaultResponseErrorHandler
@@ -378,12 +378,12 @@ git commit -m "feat(wechat): 新增 WechatResponseErrorHandler
## Task 4: 重写 RestTemplateConfig 集成新组件
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java`
- Modify: `server/src/main/java/com/emotion/config/RestTemplateConfig.java`
- [ ] **Step 4.1: 备份并读取原文件**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single\src\main\java\com\emotion\config"
cd "G:\IdeaProjects\emotion-museun\server\src\main\java\com\emotion\config"
copy RestTemplateConfig.java RestTemplateConfig.java.bak
```
@@ -467,8 +467,8 @@ public class RestTemplateConfig {
- [ ] **Step 4.3: mvn 编译验证(确保 7 个其他使用点不受影响)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
mvn clean install -pl :backend-single -am -DskipTests
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean install -pl :server -am -DskipTests
```
期望:`BUILD SUCCESS`,无编译错误(如果 7 个其他使用点有编译错误,说明 RestTemplate API 不兼容,需要回滚)
@@ -476,7 +476,7 @@ mvn clean install -pl :backend-single -am -DskipTests
- [ ] **Step 4.4: 删除备份文件**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single\src\main\java\com\emotion\config"
cd "G:\IdeaProjects\emotion-museun\server\src\main\java\com\emotion\config"
del RestTemplateConfig.java.bak
```
@@ -484,7 +484,7 @@ del RestTemplateConfig.java.bak
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java
git add server/src/main/java/com/emotion/config/RestTemplateConfig.java
git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
解决问题:
@@ -514,8 +514,8 @@ git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
## Task 5: 重写 WechatMiniProgramServiceImpl.code2SessionTDD 10 个场景)
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Create: `backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
- Modify: `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Create: `server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
- [ ] **Step 5.1: 读取当前 WechatMiniProgramServiceImpl.java**
@@ -523,7 +523,7 @@ git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
- [ ] **Step 5.2: 写失败测试(覆盖 spec 10 个场景)**
创建 `backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
创建 `server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
```java
package com.emotion.service.impl;
@@ -659,7 +659,7 @@ class WechatMiniProgramServiceImplTest {
- [ ] **Step 5.3: 运行测试验证失败(确认 service 现有方法未做防御)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatMiniProgramServiceImplTest
```
@@ -667,7 +667,7 @@ mvn test -Dtest=WechatMiniProgramServiceImplTest
- [ ] **Step 5.4: 重写 code2Session 方法(五步防御)**
修改 `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
修改 `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
在 class 顶部添加 imports
```java
@@ -748,7 +748,7 @@ public WechatCodeSessionResponse code2Session(String code) {
- [ ] **Step 5.5: 运行测试验证通过**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatMiniProgramServiceImplTest
```
@@ -758,8 +758,8 @@ mvn test -Dtest=WechatMiniProgramServiceImplTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java
git add server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java
git commit -m "feat(wechat): 重写 code2Session 五步防御
解决问题:
@@ -783,7 +783,7 @@ git commit -m "feat(wechat): 重写 code2Session 五步防御
## Task 6: 修改 application.yml 移除硬编码
**Files:**
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application.yml`
- [ ] **Step 6.1: 定位并修改微信配置**
@@ -820,7 +820,7 @@ $env:WECHAT_MINI_PROGRAM_APP_SECRET = "4f087bd363a4147ef543d634f9f6950d"
- [ ] **Step 6.3: mvn 编译验证(确保 YAML 可加载)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -830,7 +830,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/resources/application.yml
git add server/src/main/resources/application.yml
git commit -m "fix(wechat): 移除 app-id/app-secret 硬编码默认值
安全问题:
@@ -854,7 +854,7 @@ git commit -m "fix(wechat): 移除 app-id/app-secret 硬编码默认值
## Task 7: 修改 GlobalExceptionHandler 添加 WechatApiException handler
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java`
- Modify: `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java`
- [ ] **Step 7.1: 定位文件并添加新方法**
@@ -911,7 +911,7 @@ public Result<Void> handleWechatApiException(WechatApiException e, HttpServletRe
- [ ] **Step 7.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -919,7 +919,7 @@ mvn clean compile -DskipTests
- [ ] **Step 7.3: 写简单测试验证 handler 行为**
创建 `backend-single/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java`
创建 `server/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java`
```java
package com.emotion.exception;
@@ -976,7 +976,7 @@ class GlobalExceptionHandlerWechatTest {
- [ ] **Step 7.4: 运行测试**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=GlobalExceptionHandlerWechatTest
```
@@ -986,8 +986,8 @@ mvn test -Dtest=GlobalExceptionHandlerWechatTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java
git add backend-single/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java
git add server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java
git add server/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java
git commit -m "feat(wechat): GlobalExceptionHandler 新增 WechatApiException handler
解决问题:
@@ -1014,7 +1014,7 @@ git commit -m "feat(wechat): GlobalExceptionHandler 新增 WechatApiException ha
```bash
cd "G:\IdeaProjects\emotion-museun"
mvn clean install -pl :backend-single -am -DskipTests
mvn clean install -pl :server -am -DskipTests
```
期望:`BUILD SUCCESS`
@@ -1023,7 +1023,7 @@ mvn clean install -pl :backend-single -am -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
mvn test -pl :backend-single
mvn test -pl :server
```
期望:所有测试通过,无失败
@@ -0,0 +1,125 @@
# 小程序编辑资料页昵称必填提示优化实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在小程序编辑资料页面,当用户点击保存按钮但昵称未填写时,弹出 Toast 提示「请填写昵称」,避免静默无反馈。
**Architecture:** 在现有 `saveProfile` 方法内部调整校验顺序:先拦截重复提交,再校验昵称非空;校验失败时调用 `uni.showToast` 后返回。不引入新文件,不改动其他字段逻辑。
**Tech Stack:** Vue 3 + UniApp + TypeScript + Pinia
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 编辑资料页面,包含 `saveProfile` 保存方法 | 修改 |
---
### Task 1: 修改 `saveProfile` 校验逻辑
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue:310-328`
- [ ] **Step 1: 查看当前 `saveProfile` 方法**
确认当前代码如下:
```javascript
const saveProfile = async () => {
if (!form.nickname.trim() || saving.value) return
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
saving.value = false
if (!res.success) {
uni.showToast({ title: res.error || '保存失败', icon: 'none' })
return
}
syncFromStore()
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}, 350)
}
```
- [ ] **Step 2: 修改校验顺序并添加 Toast 提示**
将第一行拆分为两步:先判断 `saving.value`,再校验昵称;昵称为空时弹出 Toast 后返回。
修改后代码:
```javascript
const saveProfile = async () => {
if (saving.value) return
if (!form.nickname.trim()) {
uni.showToast({ title: '请填写昵称', icon: 'none' })
return
}
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
saving.value = false
if (!res.success) {
uni.showToast({ title: res.error || '保存失败', icon: 'none' })
return
}
syncFromStore()
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}, 350)
}
```
- [ ] **Step 3: 在 H5 模式下验证**
1. 确保 mini-program H5 开发服务器已启动:
```bash
cd mini-program
npm run dev:h5
```
访问 `http://localhost:5173`
2. 进入编辑资料页面(从「我的」→「个人档案」进入)。
3. 清空昵称输入框,点击「保存」。
4. 期望结果:
- 页面顶部弹出 Toast「请填写昵称」。
- 页面不跳转。
- 按钮文字保持「保存」,不显示「保存中」。
- 浏览器 Console 无报错。
5. 填写昵称后再次点击「保存」。
6. 期望结果:
- 弹出 Toast「已保存」。
- 编辑模式下返回上一页。
- 浏览器 Console 无报错。
- [ ] **Step 4: 提交代码**
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "fix:编辑资料页昵称未填时增加 Toast 提示"
```
---
## Self-Review
1. **Spec coverage:** 设计文档要求「昵称未填时点击保存给出 Toast 提示」,Task 1 Step 2 直接实现该逻辑。
2. **Placeholder scan:** 无 TBD、TODO 或模糊描述,代码和命令均已给出。
3. **Type consistency:** 仅使用现有 `form.nickname``saving.value``uni.showToast`,与源码保持一致。
@@ -0,0 +1,218 @@
# 去除编辑资料页与剧本列表 mock 数据实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将编辑资料页顶部默认昵称从「Zoey」改为「未设置昵称」,并彻底删除剧本列表中的 `fallbackScripts` 数组及各工具函数的兜底默认值。
**Architecture:** 直接修改两个 Vue 文件:在 `onboarding/index.vue` 替换默认文案;在 `ScriptLibraryView.vue` 删除 `fallbackScripts`、让 `scripts` 直接返回 `store.scripts`,并清理 `getTags``getWordCount``getDateText``getProgress``getChapterCount` 中的默认假值。
**Tech Stack:** Vue 3 + UniApp + TypeScript + Pinia
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 编辑资料页面顶部预览卡片 | 修改 |
| `mini-program/src/pages/main/ScriptLibraryView.vue` | 剧本列表页面 | 修改 |
---
### Task 1: 修改编辑资料页顶部默认昵称
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue:22`
- [ ] **Step 1: 查看当前代码**
确认第 22 行代码:
```vue
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
```
- [ ] **Step 2: 替换默认文案**
将第 22 行改为:
```vue
<text class="hero-name">{{ form.nickname || '未设置昵称' }}</text>
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "fix:编辑资料页无昵称时显示未设置昵称"
```
---
### Task 2: 删除剧本列表 fallbackScripts 数组
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue:178-254`
- [ ] **Step 1: 查看当前代码**
确认第 178249 行为 `fallbackScripts` 数组:
```javascript
const fallbackScripts = [
{
id: 'demo-1',
title: '逆袭人生:从低谷到巅峰',
// ...
},
// ... 共 6 条
]
```
确认第 251254 行为 `scripts` computed
```javascript
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
```
- [ ] **Step 2: 删除 fallbackScripts 并修改 scripts computed**
删除 `fallbackScripts` 数组及其后的空行,将 `scripts` computed 改为:
```javascript
const scripts = computed(() => store.scripts || [])
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "fix:剧本列表删除 fallbackScripts 假数据"
```
---
### Task 3: 清理剧本列表工具函数默认值
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue:297-317`
- [ ] **Step 1: 查看当前工具函数代码**
确认以下函数当前实现:
```javascript
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return [script.style || '逆袭成长', '都市', '事业', '热血']
}
const getChapterCount = (script) => script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (!count) return '3.1万字'
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}字`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || '2025.05.10'}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || '2025.05.08'}`
return `最近更新:${script.updatedAt || script.date || '今天 21:30'}`
}
const getProgress = (script) => Math.max(1, Math.min(99, Number(script.progress || 28)))
```
- [ ] **Step 2: 修改各函数去掉兜底假值**
将上述函数改为:
```javascript
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return []
}
const getChapterCount = (script) => script.chapterCount || script.chapters || 0
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}字`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || ''}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || ''}`
return `最近更新:${script.updatedAt || script.date || ''}`
}
const getProgress = (script) => Math.max(0, Math.min(99, Number(script.progress || 0)))
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "fix:剧本列表工具函数去除默认值兜底"
```
---
### Task 4: 编译与 H5 验证
**Files:**
- 无新增/修改文件
- [ ] **Step 1: 运行 H5 生产构建**
```bash
cd mini-program
npm run build:h5
```
期望输出包含 `DONE Build complete.`,无编译错误。
- [ ] **Step 2: 启动 H5 开发服务器**
确保 5173/5175 端口未被占用,然后:
```bash
cd mini-program
npm run dev:h5
```
- [ ] **Step 3: 验证编辑资料页**
1. 在浏览器中打开 `http://localhost:5173`(或实际分配的端口)。
2. 登录后进入编辑资料页面。
3. 清空昵称输入框,点击保存,确认顶部显示「未设置昵称」。
- [ ] **Step 4: 验证剧本列表空状态**
1. 从爽文生成页面点击左上角「历史」按钮进入剧本列表。
2. 如果当前账号没有剧本数据,确认页面显示「还没有人生剧本」空状态,不展示任何 demo 剧本。
3. 如果账号有真实剧本数据,确认真实数据正常显示。
- [ ] **Step 5: 检查浏览器 Console**
确认没有新增的 JavaScript 错误。
---
## Self-Review
1. **Spec coverage:**
- 编辑资料页默认昵称 → Task 1
- 删除 fallbackScripts → Task 2
- 清理工具函数默认值 → Task 3
- 编译与验证 → Task 4
2. **Placeholder scan:** 无 TBD、TODO、"implement later" 或模糊描述。
3. **Type consistency:** 所有函数签名、字段名与源码保持一致。
@@ -0,0 +1,285 @@
# ScriptView 对话结果页 UI 调整实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让 ScriptView.vue 结果页顶部的历史按钮和关闭按钮不随内容滚动,底部输入框增加左右边距,语音按钮改为麦克风图标。
**Architecture:** 将 `result-top-actions``scroll-view` 内部移到外部作为独立层,利用 flex 布局使其固定;调整 `.result-chat-bar` 内边距和语音按钮内容。
**Tech Stack:** Vue 3 + UniApp + CSS (rpx)
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/main/ScriptView.vue` | 剧本生成/对话结果页 | 修改 |
---
### Task 1: 将顶部操作栏移出滚动区域
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:135-158`
- [ ] **Step 1: 查看当前结构**
当前 `result-page` 结构(关键部分):
```vue
<view v-else class="result-page">
<scroll-view
class="result-view"
scroll-y
...
>
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
...
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
...
</view>
</scroll-view>
...
</view>
```
- [ ] **Step 2: 移动 result-top-actions 到 scroll-view 外部**
`result-top-actions` 块从 `result-scroll-content` 中移出,放到 `scroll-view` 之前:
```vue
<view v-else class="result-page">
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
<view class="history-lines">
<view></view>
<view></view>
<view></view>
</view>
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
<scroll-view
class="result-view"
scroll-y
:scroll-top="resultCommandScrollTop"
:scroll-into-view="resultScrollTarget"
:scroll-with-animation="true"
:enhanced="true"
:show-scrollbar="false"
@scroll="handleResultScroll"
>
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
...
</view>
</scroll-view>
...
</view>
```
- [ ] **Step 3: 调整 result-scroll-content 的 padding**
`.result-scroll-content``padding: 0 0 220rpx;`。由于顶部栏已移出,内容顶部不再需要额外留白,保持 `padding-bottom: 220rpx` 即可:
```css
.result-scroll-content {
min-height: 100%;
box-sizing: border-box;
padding: 0 0 220rpx;
}
```
- [ ] **Step 4: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 结果页顶部操作栏固定不随滚动"
```
---
### Task 2: 底部输入框增加左右边距
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:3021-3034`
- [ ] **Step 1: 查看当前样式**
```css
.result-chat-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
min-height: 104rpx;
display: flex;
align-items: flex-end;
gap: 12rpx;
padding: 14rpx 0 22rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(5, 2, 13, 0), rgba(5, 2, 13, 0.9) 30%, rgba(5, 2, 13, 0.96));
}
```
- [ ] **Step 2: 修改 padding 增加左右边距**
`padding: 14rpx 0 22rpx` 改为 `padding: 14rpx 24rpx 22rpx`
```css
.result-chat-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
min-height: 104rpx;
display: flex;
align-items: flex-end;
gap: 12rpx;
padding: 14rpx 24rpx 22rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(5, 2, 13, 0), rgba(5, 2, 13, 0.9) 30%, rgba(5, 2, 13, 0.96));
}
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 底部输入框区域增加左右边距"
```
---
### Task 3: 语音按钮改为麦克风图标
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:266-275`
- Modify: `mini-program/src/pages/main/ScriptView.vue:3036-3056`
- [ ] **Step 1: 修改语音按钮内容**
将:
```vue
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text>语音</text>
</view>
```
改为:
```vue
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text class="voice-icon">🎤</text>
</view>
```
- [ ] **Step 2: 增加语音图标样式**
`.chat-voice-btn` 样式后增加:
```css
.voice-icon {
font-size: 32rpx;
line-height: 1;
}
```
并修改按下状态的颜色继承:
```css
.chat-voice-btn.pressing,
.chat-voice-btn.recognizing {
color: #fff;
background: linear-gradient(145deg, #934dff, #4d1ccb);
box-shadow: 0 0 30rpx rgba(168, 85, 247, 0.38);
}
```
`.chat-voice-btn` 本身已有 `color: #e8ccff;`,图标会继承该颜色;按下时父元素颜色变为 `#fff`,图标也会变白。
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 语音按钮改为麦克风图标"
```
---
### Task 4: 编译与 H5 验证
**Files:**
- 无新增/修改文件
- [ ] **Step 1: 运行 H5 生产构建**
```bash
cd mini-program
npm run build:h5
```
期望输出包含 `DONE Build complete.`,无编译错误。
- [ ] **Step 2: 启动 H5 开发服务器**
确保端口未被占用,然后:
```bash
cd mini-program
npm run dev:h5
```
- [ ] **Step 3: 验证顶部按钮固定**
1. 登录后进入爽文生成页。
2. 从历史列表点击一个已有剧本,进入对话结果页。
3. 上下滚动剧本内容,确认左上角「历史」按钮和右上角「×」按钮始终保持在原位,不随内容滚动。
- [ ] **Step 4: 验证底部输入框样式**
1. 确认底部输入框区域左右与屏幕边缘有 24rpx 边距。
2. 确认语音按钮显示为麦克风图标,默认颜色 `#e8ccff`,按下时变为白色。
3. 确认发送按钮大小不变,可正常点击发送消息。
- [ ] **Step 5: 检查浏览器 Console**
确认没有新增的 JavaScript 错误。
---
## Self-Review
1. **Spec coverage:**
- 顶部按钮固定 → Task 1
- 底部边距 → Task 2
- 麦克风图标 → Task 3
- 编译与验证 → Task 4
2. **Placeholder scan:** 无 TBD、TODO、"implement later" 或模糊描述。
3. **Type consistency:** 所有 class 名、事件名与源码保持一致。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,679 @@
# 小程序添加标签按钮简化与自定义弹窗 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 简化编辑资料页"添加"按钮文字、收紧字数限制到 4 字、标签不换行省略号截断、用自定义深色太空主题弹窗组件替换 `uni.showModal`
**Architecture:** 新建 `TagDialog.vue` 组件支持 input / confirm 双模式,样式与小程序深色太空主题一致;编辑资料页和管理页分别引入该组件,重构 `addCustomTag``confirmDelete``uni.showModal` 切换到自定义弹窗;标签样式增加 `white-space: nowrap` 三件套防止换行。
**Tech Stack:** UniApp 3 + Vue 3 Composition API (`<script setup>`),样式用 rpx,深色太空主题(glass-card 毛玻璃 + 紫色渐变 + 金色强调)。
**Spec 文档:** `docs/superpowers/specs/2026-06-24-tag-add-button-simplify-design.md`
---
## 文件结构总览
### 新增文件
| 文件 | 职责 |
|---|---|
| `mini-program/src/components/TagDialog.vue` | 自定义弹窗组件,input / confirm 双模式,深色太空主题样式 |
### 修改文件
| 文件 | 修改内容 |
|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 按钮文字简化为" 添加"、引入 TagDialog、重构 addCustomTag、字数限制 8→4、toast 文案、`.tag-choice` 不换行 |
| `mini-program/src/pages/onboarding/tag-manage.vue` | 引入 TagDialog、重构 confirmDelete、`.tag-text` 不换行 |
---
## Task 1: 新建 TagDialog.vue 组件
**目标:** 创建自定义弹窗组件,支持 input(输入)和 confirm(确认)两种模式,样式与小程序深色太空主题一致。
**Files:**
- Create: `mini-program/src/components/TagDialog.vue`
### Step 1: 创建组件文件
创建 `mini-program/src/components/TagDialog.vue`,完整内容:
```vue
<template>
<view v-if="visible" class="tag-dialog-mask" @click="onMaskClick">
<view class="tag-dialog-card glass-card" @click.stop>
<view class="tag-dialog-title-row">
<text class="tag-dialog-gold">✦</text>
<text class="tag-dialog-title">{{ title }}</text>
</view>
<input
v-if="mode === 'input'"
class="tag-dialog-input"
:value="modelValue"
:placeholder="placeholder"
placeholder-class="tag-dialog-placeholder"
:maxlength="4"
@input="onInput"
@confirm="onInputConfirm"
/>
<text v-if="mode === 'confirm'" class="tag-dialog-content">{{ content }}</text>
<view class="tag-dialog-actions">
<view class="tag-dialog-btn tag-dialog-btn-cancel" @click="onCancel">
<text>取消</text>
</view>
<view class="tag-dialog-btn tag-dialog-btn-confirm" @click="onConfirm">
<text>确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
visible: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'input'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['confirm', 'cancel', 'update:modelValue', 'update:visible'])
const onInput = (event) => {
const value = event.detail.value
emit('update:modelValue', value)
}
const onInputConfirm = () => {
emit('confirm', props.modelValue)
}
const onConfirm = () => {
if (props.mode === 'input') {
emit('confirm', props.modelValue)
} else {
emit('confirm')
}
}
const onCancel = () => {
emit('cancel')
emit('update:visible', false)
}
const onMaskClick = () => {
emit('cancel')
emit('update:visible', false)
}
</script>
<style scoped>
.tag-dialog-mask {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(3, 2, 13, 0.72);
}
.tag-dialog-card {
width: 560rpx;
box-sizing: border-box;
padding: 36rpx 32rpx 28rpx;
border-radius: 22rpx;
border: 1rpx solid rgba(155, 110, 255, 0.14);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.1), transparent 34%),
rgba(10, 13, 43, 0.54);
box-shadow: inset 0 0 30rpx rgba(123, 60, 255, 0.05), 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.tag-dialog-title-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
}
.tag-dialog-gold {
color: #ffd58c;
font-size: 26rpx;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.45);
}
.tag-dialog-title {
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.tag-dialog-input {
width: 100%;
box-sizing: border-box;
height: 80rpx;
margin-bottom: 28rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid rgba(151, 111, 255, 0.22);
background: rgba(10, 12, 40, 0.66);
color: #fff;
font-size: 26rpx;
}
.tag-dialog-placeholder {
color: rgba(214, 204, 235, 0.42);
}
.tag-dialog-content {
display: block;
margin-bottom: 32rpx;
color: rgba(239, 232, 255, 0.86);
font-size: 26rpx;
line-height: 1.6;
}
.tag-dialog-actions {
display: flex;
gap: 20rpx;
}
.tag-dialog-btn {
flex: 1;
height: 72rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
}
.tag-dialog-btn-cancel {
color: rgba(239, 232, 255, 0.82);
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
}
.tag-dialog-btn-confirm {
color: #fff;
border: 1rpx solid rgba(206, 82, 255, 0.95);
background: linear-gradient(180deg, rgba(169, 61, 255, 0.62), rgba(107, 41, 206, 0.5));
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.52), inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
</style>
```
### Step 2: 提交
```bash
git add mini-program/src/components/TagDialog.vue
git commit -m "feat:新增 TagDialog 自定义弹窗组件,深色太空主题样式"
```
---
## Task 2: 编辑资料页改动(index.vue
**目标:** 简化添加按钮文字、引入 TagDialog、重构 addCustomTag 用自定义弹窗、字数限制收紧到 4、标签不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue`
### Step 1: 模板 — 简化按钮文字
修改 `mini-program/src/pages/onboarding/index.vue`
找到性格标签 panel 末尾占位(约第 146 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加标签</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加</text>
```
找到兴趣爱好 panel 末尾占位(约第 167 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加兴趣</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加</text>
```
### Step 2: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 185-186 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 3: script — 引入组件
找到 import 区(约第 189-193 行):
```javascript
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 4: script — 新增弹窗响应式状态
找到 `const saving = ref(false)` 这一行(约第 198 行附近,在 `const isEdit = ref(false)` 之后),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input')
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null)
```
### Step 5: script — 重构 addCustomTag 并新增 handleAddTag / onDialogConfirm / onDialogCancel
找到现有的 `addCustomTag` 函数(约第 410-449 行,从 `/**` 注释到 `}` 结束):
```javascript
/**
* 新增自定义标签(性格标签 / 兴趣爱好通用)
* @param {string} type 'personality' 或 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
const title = isPersonality ? '添加性格标签' : '添加兴趣爱好'
uni.showModal({
title,
editable: true,
placeholderText: '请输入标签(最多8个字符)',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm) return
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 8) {
uni.showToast({ title: '标签最多8个字符', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
// 新标签插入到库数组最前面
library.unshift(value)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
})
}
```
完整替换为以下四个函数:
```javascript
/**
* 打开添加标签弹窗
* @param {string} type 'personality' 或 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
dialogMode.value = 'input'
dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
dialogPlaceholder.value = '请输入标签(最多4个字)'
dialogInput.value = ''
dialogAction.value = { kind: 'add', type }
dialogVisible.value = true
}
/**
* 处理添加标签的实际逻辑(字数限制 4 字)
*/
const handleAddTag = (type, rawValue) => {
const value = String(rawValue || '').trim()
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 4) {
uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
// 新标签插入到库数组最前面
library.unshift(value)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
}
dialogAction.value = null
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
```
### Step 6: 样式 — .tag-choice 不换行
找到 `.tag-choice` 样式(约第 1008-1011 行):
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
}
```
改为:
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 7: 提交
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "feat:编辑页添加按钮简化为添加,字数限制4字,标签不换行,接入自定义弹窗"
```
---
## Task 3: 管理页改动(tag-manage.vue
**目标:** 引入 TagDialog、重构 confirmDelete 用自定义弹窗、标签文字不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/tag-manage.vue`
### Step 1: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 44-46 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model:visible="dialogVisible"
mode="confirm"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 2: script — 引入组件
找到 import 区(约第 48-51 行):
```javascript
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 3: script — 新增弹窗响应式状态
找到 `const type = ref('personality')` 这一行(约第 56 行),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```
### Step 4: script — 重构 confirmDelete 并新增 onDialogConfirm / onDialogCancel
找到现有的 `confirmDelete` 函数(约第 127-139 行):
```javascript
/**
* 删除确认
*/
const confirmDelete = (tag) => {
uni.showModal({
title: '确认删除',
content: `确定删除「${tag}」吗?删除后不可恢复`,
success: (res) => {
if (!res.confirm) return
deleteTag(tag)
}
})
}
```
完整替换为以下三个函数:
```javascript
/**
* 打开删除确认弹窗
*/
const confirmDelete = (tag) => {
pendingTag.value = tag
dialogContent.value = `确定删除「${tag}」吗?删除后不可恢复`
dialogVisible.value = true
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = () => {
if (pendingTag.value) {
deleteTag(pendingTag.value)
pendingTag.value = null
}
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
pendingTag.value = null
dialogVisible.value = false
}
```
### Step 5: 样式 — .tag-text 不换行
找到 `.tag-text` 样式(约第 311-314 行):
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
}
```
改为:
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 6: 提交
```bash
git add mini-program/src/pages/onboarding/tag-manage.vue
git commit -m "feat:管理页删除确认接入自定义弹窗,标签文字不换行"
```
---
## Task 4: H5 浏览器验证
**目标:** 在 H5 模式下验证 spec 的 8 条验收标准。
### Step 1: 确认 H5 dev server 运行
```bash
python dev-services.py status
```
确认 `Emotion-museum-uniapp 前端` 状态为 `运行中`(端口 5175)。若未运行:
```bash
cd mini-program
npm run dev:h5
```
### Step 2: 逐条验证验收标准
在浏览器中打开 `http://localhost:5175`,登录后进入编辑资料页(`?edit=1`):
| # | 验收标准 | 验证方法 |
|---|---|---|
| 1 | 添加按钮只显示"+ 添加" | 观察性格标签、兴趣爱好 panel 末尾占位文字 |
| 2 | 点击"+ 添加"弹出深色太空主题弹窗 | 点击按钮,验证弹窗为毛玻璃卡片 + 紫色渐变按钮 + 金色 ✦,非白底蓝按钮 |
| 3 | 输入 5 字 toast 提示"内容不能超过4个字" | 输入 5 个字点确定,验证 toast 文案 |
| 4 | 输入 4 字以内新标签,确认后出现在最前 | 输入"测试"点确定,验证标签出现在列表第一位 |
| 5 | 标签过长时省略号截断不换行 | 输入 4 字标签,观察窄屏下是否单行省略号 |
| 6 | 管理页删除弹窗为深色主题 | 点"管理"进入管理页,点 × 或左滑,验证弹窗样式与文案 |
| 7 | 取消按钮和遮罩层点击都能关闭弹窗 | 分别测试取消按钮、点击遮罩层,验证弹窗关闭且不触发动作 |
| 8 | 弹窗 H5 下样式与深色主题一致 | 整体视觉检查 |
### Step 3: 验收通过后报告
所有 8 条通过后,确认工作区干净:
```bash
git status
```
---
## 执行顺序建议
1. Task 1(新建组件)→ Task 2(编辑页接入)→ Task 3(管理页接入)→ Task 4(验证)
2. Task 1 必须先完成,Task 2/3 依赖组件存在。
3. Task 2/3 完成后,H5 dev server 热加载自动生效,可直接进入 Task 4 验证。
@@ -0,0 +1,467 @@
---
author: claude
created_at: 2026-06-27
purpose: AI 消息卡片功能按钮统一实现计划
---
# AI 消息卡片功能按钮统一实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 统一小程序实现心愿页面中所有 AI 消息卡片的功能按钮,并修复 story-card 展开失效 bug
**Architecture:** 在现有单文件组件 `ScriptView.vue` 内修复 + 扩展,不提取独立组件,保持现有文件结构
**Tech Stack:** Vue 3, UniApp, 微信小程序
---
## 文件结构
**修改:**
- `mini-program/src/pages/main/ScriptView.vue` - 主页面组件(模板 + 脚本 + 样式)
---
### Task 1: 修复 story-card 展开失效 bug
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:192-210`
- [ ] **Step 1: 移除 resultHasScriptReplies 条件包裹**
打开 `mini-program/src/pages/main/ScriptView.vue`,找到第 192 行的 `<template v-if="!resultHasScriptReplies">`,删除这个条件包裹标签和对应的 `</template>` 闭合标签(第 210 行)。
修改前:
```vue
<template v-if="!resultHasScriptReplies">
<scroll-view
v-if="storyCollapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">
<text class="collapse-row-text">{{ storyCollapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</view>
</template>
```
修改后:
```vue
<scroll-view
v-if="storyCollapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">
<text class="collapse-row-text">{{ storyCollapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</view>
```
- [ ] **Step 2: 验证修复**
在微信开发者工具中编译运行,进入「实现心愿」页面:
1. 输入心愿并生成故事
2. 点击「继续生成」创建 script 类型消息
3. 点击 story-card 的展开/收起按钮,验证故事正文能正确展开和收起
4. 确认控制台无报错
- [ ] **Step 3: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复 story-card 展开失效 bug
移除 resultHasScriptReplies 条件包裹,让故事正文始终由 storyCollapsed 控制展开/收起"
```
---
### Task 2: 添加新方法支持消息卡片功能
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue` (script 部分)
- [ ] **Step 1: 添加 copyMessageContent 方法**
`toggleMessageCollapse` 方法之后(约第 450 行),添加复制消息内容的方法:
```javascript
const copyMessageContent = (message) => {
const content = message?.content || ''
if (!content.trim()) {
uni.showToast({ title: '暂无可复制内容', icon: 'none' })
return
}
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
analytics.track('script_message_copy_click', {
message_id: message?.id || '',
content_length: content.length
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 2: 添加 playMessageTts 方法**
`copyMessageContent` 方法之后,添加播放 TTS 的方法:
```javascript
const playMessageTts = (message) => {
const scriptId = currentResult.value?.id || ''
ttsPlayer.playSource(scriptId)
analytics.track('script_message_tts_click', {
message_id: message?.id || '',
script_id: scriptId
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 3: 修改 toggleMessageCollapse 方法添加埋点**
找到 `toggleMessageCollapse` 方法(约第 442-450 行),在方法开头添加埋点代码:
修改前:
```javascript
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value[message.id] = !isMessageCollapsed(message)
}
```
修改后:
```javascript
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value[message.id] = !isMessageCollapsed(message)
analytics.track('script_message_collapse_toggle', {
message_id: message?.id || '',
collapsed: collapsedMessageIds.value[message.id]
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 4: 验证方法添加**
在微信开发者工具中编译运行,确认控制台无语法错误。新方法暂时不会被调用,后续任务会在模板中使用。
- [ ] **Step 5: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 添加消息卡片功能方法
- copyMessageContent: 复制消息内容到剪贴板
- playMessageTts: 播放故事 TTS 音频
- toggleMessageCollapse: 添加展开/收起埋点"
```
---
### Task 3: 在 result-chat-list 中添加功能按钮组
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:245-270`
- [ ] **Step 1: 在 assistant 消息中添加功能按钮组**
找到 `result-chat-list` 部分(约第 245-270 行),在消息内容和展开/收起按钮之后、时间戳之前,添加功能按钮组。
修改前:
```vue
<view
v-for="(message, index) in resultMessages"
:key="message.id"
class="result-chat-list"
>
<view
v-if="isAssistantMessage(message)"
class="chat-bubble assistant"
>
<text class="message-content">{{ getMessageContent(message) }}</text>
<view class="message-toggle" @click="toggleMessageCollapse(message)">
<text>{{ isMessageCollapsed(message) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(message) }">
<view></view>
<view></view>
</view>
</view>
<text class="message-time">{{ formatMessageTime(message.createdAt) }}</text>
</view>
<!-- user message -->
</view>
```
修改后:
```vue
<view
v-for="(message, index) in resultMessages"
:key="message.id"
class="result-chat-list"
>
<view
v-if="isAssistantMessage(message)"
class="chat-bubble assistant"
>
<text class="message-content">{{ getMessageContent(message) }}</text>
<view class="message-toggle" @click="toggleMessageCollapse(message)">
<text>{{ isMessageCollapsed(message) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(message) }">
<view></view>
<view></view>
</view>
</view>
<view class="message-actions">
<view class="action-btn" @click="copyMessageContent(message)">
<text>复制</text>
</view>
<view class="action-btn" @click="changeDirection">
<text>换个方向</text>
</view>
<view class="action-btn" @click="notLikeMe">
<text>不像我</text>
</view>
<view class="action-btn" @click="continueInChat">
<text>继续生成</text>
</view>
<view class="action-btn primary" @click="playMessageTts(message)">
<text>▶ 播放</text>
</view>
</view>
<text class="message-time">{{ formatMessageTime(message.createdAt) }}</text>
</view>
<!-- user message -->
</view>
```
**说明:**
- 按钮组使用 `.message-actions` 容器,2 列网格布局
- 复制、换个方向、不像我、继续生成使用 `.action-btn` 样式
- 播放按钮使用 `.action-btn.primary` 样式(主色调突出)
- 所有按钮都调用 Task 2 中定义的方法或复用现有方法
- [ ] **Step 2: 验证按钮组显示**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面,生成故事
2. 点击「继续生成」创建 assistant 消息
3. 验证每条 assistant 消息下方显示 5 个功能按钮(复制、换个方向、不像我、继续生成、播放)
4. 验证按钮布局为 2 列网格
- [ ] **Step 3: 验证按钮功能**
逐个测试按钮功能:
1. **复制**:点击后应复制消息内容,显示「已复制」提示
2. **换个方向**:点击后应进入「换方向」修订确认流程
3. **不像我**:点击后应进入「不像我」修订确认流程
4. **继续生成**:点击后应收起故事并聚焦输入框
5. **播放**:点击后应播放故事 TTS 音频
确认控制台无报错,所有功能正常。
- [ ] **Step 4: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 在 result-chat-list 中添加功能按钮组
为每条 assistant 消息添加完整功能按钮:
- 复制:复制消息内容到剪贴板
- 换个方向:进入换方向修订确认
- 不像我:进入不像我修订确认
- 继续生成:收起故事并聚焦输入框
- 播放:播放故事 TTS 音频"
```
---
### Task 4: 添加消息按钮组样式
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue` (style 部分)
- [ ] **Step 1: 添加 .message-actions 样式**
`<style scoped>` 部分(约第 1500 行之后),找到 `.message-toggle` 样式附近,添加按钮组样式:
```css
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
margin-top: 24rpx;
}
.message-actions .action-btn {
padding: 16rpx 24rpx;
background: rgba(255, 255, 255, 0.08);
border: 2rpx solid rgba(255, 255, 255, 0.15);
border-radius: 16rpx;
color: #e0e0e0;
font-size: 24rpx;
text-align: center;
transition: all 0.2s;
}
.message-actions .action-btn:active {
background: rgba(255, 255, 255, 0.12);
transform: scale(0.98);
}
.message-actions .action-btn.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: transparent;
color: #ffffff;
font-weight: 500;
}
.message-actions .action-btn.primary:active {
opacity: 0.85;
}
```
- [ ] **Step 2: 验证样式效果**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面,生成故事
2. 点击「继续生成」创建 assistant 消息
3. 验证按钮样式:
- 按钮组为 2 列网格布局
- 普通按钮为半透明白色背景,圆角边框
- 播放按钮为主色调渐变(紫色)
- 点击时有缩放反馈效果
4. 验证按钮在不同屏幕尺寸下的响应式表现
- [ ] **Step 3: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: 添加消息按钮组样式
- .message-actions: 2 列网格布局,间距 16rpx
- .action-btn: 半透明白色背景,圆角边框,点击反馈
- .action-btn.primary: 主色调渐变(紫色),白色文字"
```
---
### Task 5: 最终验证和边界情况测试
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 测试 story-card 展开/收起**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面
2. 输入心愿并生成故事
3. 点击 story-card 的「展开全文」按钮,验证故事正文展开
4. 点击「收起全文」按钮,验证故事正文收起
5. 点击「继续生成」创建 script 类型消息
6. 再次点击 story-card 的展开/收起按钮,验证功能正常(之前会失效)
**预期结果:** 所有展开/收起操作正常工作
- [ ] **Step 2: 测试 assistant 消息功能按钮**
继续在上一步的基础上:
1. 验证每条 assistant 消息下方显示 5 个功能按钮
2. 点击「复制」按钮,验证复制成功提示
3. 点击「换个方向」按钮,验证进入修订确认流程
4. 点击「不像我」按钮,验证进入修订确认流程
5. 点击「继续生成」按钮,验证故事收起且输入框获得焦点
6. 点击「播放」按钮,验证 TTS 音频播放
**预期结果:** 所有按钮功能正常
- [ ] **Step 3: 测试边界情况**
测试以下边界场景:
**场景 1:空内容的 assistant 消息**
- 手动构造一条空内容的 assistant 消息(或等待 AI 返回空内容)
- 点击「复制」按钮
- **预期:** 显示「暂无可复制内容」提示
**场景 2pending 状态的 assistant 消息**
- 生成故事后,快速点击「继续生成」
- 观察正在加载的 assistant 消息(pending 状态)
- **预期:** pending 消息不显示功能按钮组
**场景 3user 消息**
- 查看用户发送的消息
- **预期:** user 消息不显示功能按钮组
**场景 4:无 currentResult 时的播放**
- 如果可能,测试在未生成故事时点击播放按钮
- **预期:** 按钮仍可点击,TTS 播放器内部处理空 id
- [ ] **Step 4: 验证埋点**
打开微信开发者工具的控制台,查看埋点日志:
1. 点击 assistant 消息的展开/收起按钮,验证 `script_message_collapse_toggle` 埋点
2. 点击「复制」按钮,验证 `script_message_copy_click` 埋点
3. 点击「播放」按钮,验证 `script_message_tts_click` 埋点
**预期结果:** 所有埋点正确触发,包含正确的参数
- [ ] **Step 5: 检查控制台错误**
在整个测试过程中,持续关注微信开发者工具的控制台:
- 确认无 Vue 警告
- 确认无 JavaScript 错误
- 确认无网络请求错误
**预期结果:** 控制台干净,无任何错误
- [ ] **Step 6: 最终提交(如有修改)**
如果在前面的测试中发现了需要修复的问题,修复后提交:
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复最终验证中发现的问题
[描述修复的具体问题]"
```
如果测试全部通过,无需额外提交。
- [ ] **Step 7: 创建 Pull Request(可选)**
如果项目使用 PR 流程,创建 PR 合并这些更改:
```bash
git push origin HEAD
```
然后在代码托管平台创建 PR,标题:
```
feat: 统一 AI 消息卡片功能按钮并修复 story-card 展开失效
```
PR 描述包含:
- 修复 story-card 展开失效 bug
- 为每条 assistant 消息添加完整功能按钮组
- 添加相应的样式和埋点
- 所有功能已通过测试
@@ -0,0 +1,462 @@
# backend-single 重命名为 server 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 `backend-single` 目录重命名为 `server`,同步更新 Maven artifactId、部署脚本、配置文件、项目文档和历史文档中的所有引用。
**Architecture:** 分 10 个任务按依赖顺序执行。先移动目录(Task 1),再更新构建配置(Task 2-3),然后更新根级脚本(Task 4-5),接着更新文档(Task 6-8),最后批量替换历史文档(Task 9)并全面验证(Task 10)。
**Tech Stack:** Git, Maven, Python (deploy scripts), Bash, sed (batch replace)
---
### Task 1: 目录重命名
**Files:**
- Move: `backend-single/``server/`
- [ ] **Step 1: 使用 git mv 重命名目录**
```bash
cd /g/IdeaProjects/emotion-museun
git mv backend-single server
```
Expected: 目录从 `backend-single/` 重命名为 `server/`Git 历史完整保留。
- [ ] **Step 2: 确认目录已移动**
```bash
ls -d server/
ls server/pom.xml
```
Expected: 两个命令都成功,无报错。
- [ ] **Step 3: 提交目录移动**
```bash
git commit -m "refactor: 重命名 backend-single 目录为 server"
```
---
### Task 2: 更新 Maven pom.xml
**Files:**
- Modify: `server/pom.xml:8,12`
- [ ] **Step 1: 修改 artifactId 和 name**
`server/pom.xml` 中,将第 8 行和第 12 行的 `backend-single` 替换为 `server`
```xml
<!-- 第 8 行 -->
<artifactId>server</artifactId>
<!-- 第 12 行 -->
<name>server</name>
```
- [ ] **Step 2: 验证 pom.xml 修改**
```bash
grep -n "backend-single" server/pom.xml
```
Expected: 无任何输出(无残留引用)。
- [ ] **Step 3: 提交 pom.xml 修改**
```bash
git add server/pom.xml
git commit -m "refactor: 更新 Maven artifactId 为 server"
```
---
### Task 3: 更新后端部署脚本 JAR 名称
**Files:**
- Modify: `server/deploy.py:22`
- Modify: `server/deploy.sh:10`
- [ ] **Step 1: 修改 server/deploy.py 中的 JAR_NAME**
`server/deploy.py` 第 22 行:
```python
JAR_NAME = "backend-single-1.0.0.jar"
```
改为:
```python
JAR_NAME = "server-1.0.0.jar"
```
- [ ] **Step 2: 修改 server/deploy.sh 中的 JAR_NAME**
`server/deploy.sh` 第 10 行:
```bash
JAR_NAME="backend-single-1.0.0.jar"
```
改为:
```bash
JAR_NAME="server-1.0.0.jar"
```
- [ ] **Step 3: 验证后端部署脚本无残留**
```bash
grep -n "backend-single" server/deploy.py server/deploy.sh
```
Expected: 无任何输出。
- [ ] **Step 4: 提交后端部署脚本修改**
```bash
git add server/deploy.py server/deploy.sh
git commit -m "refactor: 更新后端部署脚本 JAR 名称为 server-1.0.0.jar"
```
---
### Task 4: 更新根目录部署脚本
**Files:**
- Modify: `deploy.py:398,405,413`
- Modify: `deploy.sh:77,81,82,86`
- [ ] **Step 1: 修改根目录 deploy.py**
`deploy.py` 中 3 处 `backend-single` 替换为 `server`
第 398 行注释:
```python
# 后端 - 调用 server/deploy.sh remote
```
第 405 行路径拼接:
```python
deploy_script = PROJECT_DIR / "server" / "deploy.py"
```
第 413 行 cwd
```python
cwd=str(PROJECT_DIR / "server"),
```
- [ ] **Step 2: 修改根目录 deploy.sh**
`deploy.sh` 中 4 处 `backend-single` 替换为 `server`
第 77 行注释:
```bash
# 后端 - 调用 server/deploy.sh
```
第 81 行存在性检查:
```bash
if [ ! -f "server/deploy.sh" ]; then
```
第 82 行错误信息:
```bash
log_error "后端部署脚本不存在: server/deploy.sh"
```
第 86 行 cd 命令:
```bash
cd server
```
- [ ] **Step 3: 验证根部署脚本无残留**
```bash
grep -n "backend-single" deploy.py deploy.sh
```
Expected: 无任何输出。
- [ ] **Step 4: 提交根部署脚本修改**
```bash
git add deploy.py deploy.sh
git commit -m "refactor: 更新根部署脚本中的后端目录引用为 server"
```
---
### Task 5: 更新 .gitignore
**Files:**
- Modify: `.gitignore:386`
- [ ] **Step 1: 修改 .gitignore**
`.gitignore` 第 386 行:
```
backend-single/backend.err
```
改为:
```
server/backend.err
```
- [ ] **Step 2: 验证并提交**
```bash
grep -n "backend-single" .gitignore
git add .gitignore
git commit -m "refactor: 更新 .gitignore 中的后端日志路径"
```
Expected: grep 无输出,提交成功。
---
### Task 6: 更新核心项目文档
**Files:**
- Modify: `CLAUDE.md` (4 处)
- Modify: `AGENTS.md` (4 处)
- Modify: `README.md` (1 处)
- [ ] **Step 1: 修改 CLAUDE.md**
`CLAUDE.md` 中所有 `backend-single` 替换为 `server`
第 23 行项目结构树:
```
├── server/ # Spring Boot 单体后端服务
```
第 37 行标题:
```
### 后端 (server)
```
第 41 行命令:
```
cd server
```
第 50 行 JAR 名称:
```
java -jar target/server-1.0.0.jar
```
- [ ] **Step 2: 修改 AGENTS.md**
`AGENTS.md` 中所有 `backend-single` 替换为 `server`(与 CLAUDE.md 相同的 4 处位置和内容)。
- [ ] **Step 3: 修改 README.md**
`README.md` 第 73 行:
```
├── backend-single/ # SpringBoot单体后端服务
```
改为:
```
├── server/ # SpringBoot单体后端服务
```
- [ ] **Step 4: 验证并提交**
```bash
grep -n "backend-single" CLAUDE.md AGENTS.md README.md
git add CLAUDE.md AGENTS.md README.md
git commit -m "docs: 更新核心文档中的后端目录引用为 server"
```
Expected: grep 无输出,提交成功。
---
### Task 7: 更新部署相关文档
**Files:**
- Modify: `server/部署说明.md` (1 处)
- Modify: `快速部署参考.md` (4 处)
- Modify: `一键部署说明.md` (5 处)
- [ ] **Step 1: 修改 server/部署说明.md**
`server/部署说明.md` 第 14 行:
```
# 在 backend-single 目录下执行
```
改为:
```
# 在 server 目录下执行
```
- [ ] **Step 2: 修改 快速部署参考.md**
`快速部署参考.md` 中所有 `backend-single` 替换为 `server`4 处:第 71、83、134、136 行)。
- [ ] **Step 3: 修改 一键部署说明.md**
`一键部署说明.md` 中所有 `backend-single` 替换为 `server`5 处:第 78、152、156、301、304 行)。注意第 156 行 `backend-single-1.0.0.jar` 变为 `server-1.0.0.jar`
- [ ] **Step 4: 验证并提交**
```bash
grep -n "backend-single" "server/部署说明.md" "快速部署参考.md" "一键部署说明.md"
git add "server/部署说明.md" "快速部署参考.md" "一键部署说明.md"
git commit -m "docs: 更新部署文档中的后端目录引用为 server"
```
Expected: grep 无输出,提交成功。
---
### Task 8: 更新前端代码和其他文档
**Files:**
- Modify: `web/src/types/auth.ts:5`
- Modify: `web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md:284-287`
- [ ] **Step 1: 修改 web/src/types/auth.ts 注释**
将第 5 行:
```ts
// 登录请求(对齐 backend-single:手机号 + 短信验证码)
```
改为:
```ts
// 登录请求(对齐 server:手机号 + 短信验证码)
```
- [ ] **Step 2: 修改 web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md**
将文件中 4 处 `backend-single/src/main/java/` 替换为 `server/src/main/java/`(第 284-287 行)。
- [ ] **Step 3: 验证并提交**
```bash
grep -rn "backend-single" web/ web-admin/
git add "web/src/types/auth.ts" "web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md"
git commit -m "docs: 更新前端代码和文档中的后端目录引用为 server"
```
Expected: grep 无输出(在这两个目录中),提交成功。
---
### Task 9: 批量替换历史文档
**Files:**
- Modify: `docs/superpowers/` 下约 37 个 `.md` 文件(排除本次新建的设计文档)
- [ ] **Step 1: 使用 sed 批量替换**
在 Windows Git Bash 中执行:
```bash
cd /g/IdeaProjects/emotion-museun
find docs/superpowers -name "*.md" \
! -name "2026-06-27-backend-single-rename-to-server-design.md" \
-exec sed -i 's/backend-single/server/g' {} +
```
说明:
- `sed -i` 原地替换
- `! -name` 排除本次设计文档自身(避免修改自己的文件名引用)
- `s/backend-single/server/g` 全局替换
- [ ] **Step 2: 验证 docs/superpowers/ 中无残留**
```bash
grep -rn "backend-single" docs/superpowers/ \
--include="*.md" \
! --include="2026-06-27-backend-single-rename-to-server-design.md"
```
Expected: 无任何输出。
- [ ] **Step 3: 提交历史文档更新**
```bash
git add docs/superpowers/
git commit -m "docs: 批量更新历史文档中的 backend-single 引用为 server"
```
---
### Task 10: 全面验证
**Files:** 无新增修改
- [ ] **Step 1: 全项目 grep 验证无残留**
```bash
grep -rn "backend-single" --include="*.md" --include="*.py" --include="*.sh" \
--include="*.xml" --include="*.ts" --include="*.java" --include="*.yml" \
--include="*.yaml" --include="*.json" --include="*.properties" .
```
Expected: **无任何输出**。如果有残留,逐一修复。
**注意**:以下文件中出现 `backend-single` 是允许的(不需要修改):
- `docs/superpowers/specs/2026-06-27-backend-single-rename-to-server-design.md`(本次设计文档自身)
- `docs/superpowers/plans/2026-06-27-backend-single-rename-to-server.md`(本次计划文档自身)
- [ ] **Step 2: Maven 编译验证**
```bash
cd /g/IdeaProjects/emotion-museun/server
mvn clean install -DskipTests
```
Expected: BUILD SUCCESS,生成 `target/server-1.0.0.jar`
- [ ] **Step 3: 验证 JAR 文件名称**
```bash
ls -lh server/target/server-1.0.0.jar
```
Expected: 文件存在,名称正确。
- [ ] **Step 4: 验证 Git 历史保留**
```bash
git log --follow --oneline server/pom.xml | head -5
```
Expected: 能看到旧的重命名之前的提交历史。
- [ ] **Step 5: 验证 .gitignore 生效**
```bash
git status
```
Expected: 工作区干净(无意外未跟踪文件)。
- [ ] **Step 6: 最终提交(如有修复)**
如果 Step 1 中发现了残留并修复了,执行:
```bash
git add -A
git commit -m "fix: 修复遗漏的 backend-single 引用"
```
@@ -0,0 +1,606 @@
---
author: claude
created_at: 2026-06-27
purpose: 本地服务管理规则优化实现计划
---
# 本地服务管理规则优化实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 优化 dev-services.py,强制使用脚本管理本地服务,固定前端/H5 端口从 5178 开始累加,并在无必须重启的变更时禁止 restart
**Architecture:** 在 dev-services.py 中内置固定前端端口映射并强制使用,修改各前端项目 vite 配置和 env 文件同步端口,新增 restart 命令热加载保护,更新 CLAUDE.md 规则
**Tech Stack:** Python, Vite, UniApp, Spring Boot
---
## 文件结构
**修改:**
- `dev-services.py` - 添加固定端口映射、强制端口分配、热加载保护
- `web/vite.config.ts` - 端口改为 5178
- `web/.env.development` - 添加 `VITE_PORT=5178`
- `web-admin/vite.config.ts` - 端口改为 5179
- `web-admin/.env.development` - `VITE_APP_PORT` 改为 5179
- `mini-program/vite.config.js` - 端口改为 5180
- `mini-program/.env.development` - 添加 `VITE_PORT=5180`
- `life-script/vite.config.js` - 端口改为 5181
- `life-script/.env.development` - 添加 `VITE_PORT=5181`
- `CLAUDE.md` - 更新本地服务管理规则
---
### Task 1: 修改 dev-services.py 固定前端端口并保护端口分配
**Files:**
- Modify: `dev-services.py`
- [ ] **Step 1: 在常量区添加固定前端端口映射**
找到 `DEFAULT_PORTS` 定义(约第 58-68 行),在其后添加:
```python
# 前端/H5 服务固定端口映射(按项目目录名匹配)
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
```
- [ ] **Step 2: 修改 `_update_service_port()` 函数以支持 Vite 端口**
当前 `_update_service_port()` 已处理 `--port``--server.port` 参数,还需要处理 Vite 的 `server.port` 配置更新。函数本身不需要修改,因为端口值更新后,`assign_unique_ports()` 会通过 `_service_has_explicit_port()` 判断。
但需要注意:对于 Vite/UniApp/Taro 服务,如果 `start_cmd` 中没有 `--port` 参数(当前 UniApp 的 `dev:h5` 脚本就没有),需要在启动命令前注入 `VITE_PORT` 环境变量或在命令中追加 `--port` 参数。
修改 `_update_service_port()` 函数,使其在更新 `start_cmd` 时,如果命令中没有 `--port` 参数,则在命令末尾追加 `--port {port}`
修改前:
```python
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
svc.start_cmd = updated
```
修改后:
```python
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
has_port_arg = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port":
has_port_arg = True
if index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
has_port_arg = True
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
# Vite/UniApp/Taro/Next 服务如果没有 --port 参数,追加 --port
if not has_port_arg and svc.project_type in {"vite", "uniapp", "taro", "next"}:
updated.extend(["--port", str(port)])
svc.start_cmd = updated
```
- [ ] **Step 3: 重写 `assign_unique_ports()` 强制前端固定端口**
修改前:
```python
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
```
修改后:
```python
def assign_unique_ports(services: list[Service]) -> list[Service]:
# 按目录名匹配固定前端端口
dir_name_to_service = {}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
dir_name_to_service[dir_name] = svc
# 先为固定前端服务分配端口
for dir_name, svc in dir_name_to_service.items():
fixed_port = FIXED_FRONTEND_PORTS[dir_name]
if svc.port != fixed_port:
log(f"{svc.name} 使用固定前端端口 {fixed_port}", "INFO")
_update_service_port(svc, fixed_port)
# 检查固定端口冲突
used_ports = {}
for svc in services:
port = svc.port
if port in used_ports:
other = used_ports[port]
log(f"端口冲突: {svc.name} ({port}) 与 {other.name} ({port}) 冲突", "ERROR")
sys.exit(1)
used_ports[port] = svc
# 非固定前端服务按原有逻辑处理冲突
reserved = {svc.port for svc in services}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
continue # 固定前端服务已处理
port = svc.port
if port in used_ports and used_ports[port] is not svc:
while port in used_ports:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
return services
```
- [ ] **Step 4: 验证 dev-services.py 语法**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:成功列出所有发现的服务,且前端服务端口为 5178-5181
- [ ] **Step 5: 提交**
```bash
git add dev-services.py
git commit -m "feat: dev-services.py 强制前端服务使用固定端口
- 新增 FIXED_FRONTEND_PORTS 映射
- 前端/H5 端口从 5178 开始固定分配
- 端口冲突时不再自动递增前端服务端口,改为报错"
```
---
### Task 2: 为 dev-services.py 添加 restart 热加载保护
**Files:**
- Modify: `dev-services.py`
- [ ] **Step 1: 添加"必须重启"文件模式常量**
`RESTART_REQUIRED_PATTERNS` 常量区(与 `FIXED_FRONTEND_PORTS` 一起)添加:
```python
# 必须重启才能生效的文件模式
RESTART_REQUIRED_PATTERNS = [
re.compile(r'vite\.config\.(ts|js|mjs|cjs)$'),
re.compile(r'tsconfig\.json$'),
re.compile(r'\.env(\.[^/]*)?$'),
re.compile(r'package\.json$'),
re.compile(r'application.*\.ya?ml$'),
re.compile(r'pom\.xml$'),
re.compile(r'build\.gradle(\.kts)?$'),
]
```
注意:需要确保文件顶部已导入 `re``sys`
- [ ] **Step 2: 添加 `_should_restart_for_changes()` 函数**
`assign_unique_ports()` 函数附近添加:
```python
def _should_restart_for_changes(service_dir: Path) -> tuple[bool, list[str]]:
"""
检查指定服务目录下是否有必须重启才能生效的变更。
返回: (是否需要重启, 已修改文件列表)
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--", str(service_dir)],
capture_output=True,
text=True,
cwd=SCRIPT_DIR,
check=True,
)
changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
except subprocess.CalledProcessError:
# 非 git 仓库或 git 命令失败,默认允许重启
return True, []
except FileNotFoundError:
return True, []
if not changed_files:
return False, []
for file_path in changed_files:
for pattern in RESTART_REQUIRED_PATTERNS:
if pattern.search(file_path):
return True, changed_files
return False, changed_files
```
- [ ] **Step 3: 修改 restart 命令解析支持 --force**
找到 `main()` 函数中的 `restart` 子命令解析部分(约文件末尾),添加 `--force` 参数。
当前 restart 解析可能类似:
```python
restart_parser = subparsers.add_parser("restart", help="重启所有或指定服务")
restart_parser.add_argument("service", nargs="?", help="指定服务名")
```
修改后:
```python
restart_parser = subparsers.add_parser("restart", help="重启所有或指定服务")
restart_parser.add_argument("service", nargs="?", help="指定服务名")
restart_parser.add_argument(
"--force",
action="store_true",
help="强制重启,忽略热加载保护"
)
```
- [ ] **Step 4: 在 restart 执行前加入热加载保护**
找到 `cmd_restart()` 函数,在其开头添加检测逻辑。
假设 `cmd_restart()` 函数签名当前为:
```python
def cmd_restart(args):
services = discover_services()
services = _filter_services(services, args.service)
```
修改后:
```python
def cmd_restart(args):
services = discover_services()
services = _filter_services(services, args.service)
if not args.force:
blocked = []
for svc in services:
should_restart, changed_files = _should_restart_for_changes(svc.project_dir)
if not should_restart:
if changed_files:
blocked.append((svc.name, changed_files))
else:
blocked.append((svc.name, []))
if blocked:
log("检测到以下服务无需重启(当前修改支持热加载):", "WARN")
for name, files in blocked:
if files:
log(f" - {name}: 修改文件 {', '.join(files)}", "WARN")
else:
log(f" - {name}: 未检测到代码变更", "WARN")
log("如需强制重启,请使用: python dev-services.py restart --force", "WARN")
sys.exit(0)
# 原有 restart 逻辑继续...
```
注意:实际代码中 `cmd_restart()` 的函数名和参数可能不同,请根据实际代码调整。
- [ ] **Step 5: 验证热加载保护**
1. 修改一个 `.vue` 文件(例如 `mini-program/src/pages/main/ScriptView.vue`
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:提示无需重启,并建议使用 `--force`
3. 运行:
```bash
python dev-services.py restart mini-program --force
```
预期:强制重启
- [ ] **Step 6: 提交**
```bash
git add dev-services.py
git commit -m "feat: dev-services.py restart 命令新增热加载保护
- 无必须重启的变更时禁止 restart
- 支持 --force 参数强制重启
- 避免无意义重启前端热加载服务"
```
---
### Task 3: 同步各前端项目端口配置
**Files:**
- Modify: `web/vite.config.ts`, `web/.env.development`
- Modify: `web-admin/vite.config.ts`, `web-admin/.env.development`
- Modify: `mini-program/vite.config.js`, `mini-program/.env.development`
- Modify: `life-script/vite.config.js`, `life-script/.env.development`
- [ ] **Step 1: 修改 web/ 端口为 5178**
修改 `web/vite.config.ts` 中的 `server.port`
```typescript
server: {
port: 5178,
// ... 其他配置
}
```
修改/创建 `web/.env.development`,添加:
```
VITE_PORT=5178
```
- [ ] **Step 2: 修改 web-admin/ 端口为 5179**
修改 `web-admin/vite.config.ts` 中的 `server.port`
```typescript
server: {
port: 5179,
// ... 其他配置
}
```
修改 `web-admin/.env.development`,将 `VITE_APP_PORT` 改为:
```
VITE_APP_PORT=5179
```
- [ ] **Step 3: 修改 mini-program/ 端口为 5180**
修改 `mini-program/vite.config.js` 中的 `server.port`
```javascript
server: {
port: 5180,
// ... 其他配置
}
```
修改/创建 `mini-program/.env.development`,添加:
```
VITE_PORT=5180
```
- [ ] **Step 4: 修改 life-script/ 端口为 5181**
修改 `life-script/vite.config.js` 中的 `server.port`
```javascript
server: {
port: 5181,
// ... 其他配置
}
```
修改/创建 `life-script/.env.development`,添加:
```
VITE_PORT=5181
```
- [ ] **Step 5: 验证端口配置**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:
- web 服务端口为 5178
- web-admin 服务端口为 5179
- mini-program 服务端口为 5180
- life-script 服务端口为 5181
- [ ] **Step 6: 提交**
```bash
git add web/ web-admin/ mini-program/ life-script/
git commit -m "chore: 统一前端/H5 服务固定端口
- web: 5178
- web-admin: 5179
- mini-program: 5180
- life-script: 5181"
```
---
### Task 4: 更新 CLAUDE.md 规则文档
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: 添加/更新本地服务管理规则章节**
`CLAUDE.md` 中找到现有的"本地服务管理规则"或"热加载规则"章节,合并更新为统一的"本地服务管理规则(强制)":
```markdown
## 本地服务管理规则(强制)
**启动、重启、停止本地前后端服务时,必须使用项目根目录下的 `dev-services.py` 脚本。**
### 强制要求
- ✅ 必须使用:`python dev-services.py [start|stop|restart|status|discover] [服务名]`
- ❌ 禁止直接使用 `npm run dev` / `pnpm run dev` 启动前端
- ❌ 禁止直接使用 `npm run dev:h5` 启动小程序 H5
- ❌ 禁止直接使用 `mvn spring-boot:run` 启动后端
- ❌ 禁止无意义重启支持热加载的服务
### 前端/H5 固定端口
| 服务 | 目录 | 固定端口 |
|---|---|---|
| web | `web/` | 5178 |
| web-admin | `web-admin/` | 5179 |
| mini-program | `mini-program/` | 5180 |
| life-script | `life-script/` | 5181 |
### 热加载规则
1. **不需要重启的场景**(热加载自动生效)
- 前端:修改 `.vue``.tsx``.ts``.js``.css``.scss` 等源码文件
- 样式、模板、静态资源等修改
2. **需要重启的场景**
- 前端:修改 `vite.config.ts``tsconfig.json``.env``package.json`、别名配置等
- 后端:按现有后端规则处理(本地不启动后端)
3. **禁止无意义重启**
- 执行 `python dev-services.py restart` 时,脚本会自动检测是否有必须重启才能生效的变更
- 如果只有支持热加载的源码文件变更,脚本会拒绝重启并提示:
> 当前修改只涉及支持热加载的源码文件,无需重启。如需强制重启,请使用 `python dev-services.py restart --force`
- 确需强制重启时,使用 `--force` 参数
```
- [ ] **Step 2: 删除冲突的旧规则**
检查 CLAUDE.md 中是否有与上述新规则冲突的旧规则(如旧的前端端口说明、热加载规则),删除或更新它们,确保文档内部一致。
- [ ] **Step 3: 提交**
```bash
git add CLAUDE.md
git commit -m "docs: 更新本地服务管理规则
- 强制使用 dev-services.py
- 明确前端/H5 固定端口
- 新增禁止无意义重启规则"
```
---
### Task 5: 最终验证
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 验证 dev-services.py 发现服务端口正确**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:
- web 端口 5178
- web-admin 端口 5179
- mini-program 端口 5180
- life-script 端口 5181
- 后端服务保持原有端口
- [ ] **Step 2: 验证热加载保护**
1. 修改 `mini-program/src/pages/main/ScriptView.vue` 中任意一处(例如加一个空行)
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:提示无需重启
3. 运行:
```bash
python dev-services.py restart mini-program --force
```
预期:强制重启
- [ ] **Step 3: 验证必须重启的场景**
1. 修改 `mini-program/vite.config.js` 中任意一处
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:正常允许重启
- [ ] **Step 4: 验证端口冲突检测**
1. 手动启动一个占用 5178 端口的进程(例如另一个 node 服务)
2. 运行:
```bash
python dev-services.py start web
```
预期:报错,提示端口 5178 被占用
3. 终止占用进程
- [ ] **Step 5: 检查 CLAUDE.md 一致性**
快速浏览 `CLAUDE.md`,确认:
- 没有重复的本地服务管理规则
- 前端端口说明与新的固定端口表一致
- 热加载规则与 dev-services.py 行为一致
- [ ] **Step 6: 提交验证修复(如有)**
如果在验证中修复了问题:
```bash
git add dev-services.py CLAUDE.md
git commit -m "fix: 修复本地服务管理规则验证中的问题
[描述具体修复]"
```
如果全部通过,无需额外提交。
---
## 自审检查
- **Spec 覆盖:**
- ✅ dev-services.py 固定前端端口 → Task 1
- ✅ 热加载保护 → Task 2
- ✅ 前端项目端口配置同步 → Task 3
- ✅ CLAUDE.md 规则更新 → Task 4
- ✅ 验证 → Task 5
- **无占位符:** 计划中没有 TBD/TODO
- **类型一致:** `FIXED_FRONTEND_PORTS``RESTART_REQUIRED_PATTERNS``_should_restart_for_changes` 命名和用法一致
@@ -0,0 +1,658 @@
---
author: claude
created_at: 2026-06-27
purpose: MessageCard 组件统一提取实现计划
---
# MessageCard 组件统一实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 提取 MessageCard.vue 组件,统一 story-card 和 result-chat-list 中 AI 消息卡片的样式,并按 100 字阈值自动切换短消息/完整卡片模式
**Architecture:** 新建 `MessageCard.vue` 组件封装长消息卡片结构,ScriptView.vue 中 story-card 和 result-chat-list 都复用该组件,短消息直接渲染简洁气泡
**Tech Stack:** Vue 3, UniApp, 微信小程序
---
## 文件结构
**创建:**
- `mini-program/src/components/MessageCard.vue` - 通用 AI 消息卡片组件
**修改:**
- `mini-program/src/pages/main/ScriptView.vue` - 替换 story-card 和 result-chat-list 中的 assistant 消息渲染,并迁移相关样式
---
### Task 1: 创建 MessageCard.vue 组件(短消息模式 + 基础结构)
**Files:**
- Create: `mini-program/src/components/MessageCard.vue`
- Modify: 无
- [ ] **Step 1: 创建组件文件并写入基础模板**
`mini-program/src/components/MessageCard.vue` 写入以下内容:
```vue
<template>
<view v-if="isShortMessage" class="chat-bubble system">
<text>{{ content }}</text>
</view>
<view v-else class="story-card" :class="{ collapsed }">
<view class="story-head">
<view class="story-title-wrap">
<text class="story-title">{{ title || '我的人生剧本' }}</text>
<view v-if="tags?.length" class="tag-row">
<text v-for="tag in tags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
<view class="story-head-actions">
<button class="collapse-icon" @click="$emit('toggle-collapse')">
<text class="collapse-icon-text">{{ collapsed ? '展开' : '收起' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</button>
<button class="copy-card-btn" @click="$emit('copy')">复制</button>
</view>
</view>
<scroll-view
v-if="collapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
<view class="collapse-row" @click="$emit('toggle-collapse')">
<text class="collapse-row-text">{{ collapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</view>
<view class="result-actions">
<button class="action-btn" @click="$emit('change-direction')">换个方向</button>
<button class="action-btn" @click="$emit('not-like-me')">不像我</button>
<button class="action-btn" @click="$emit('continue')">继续生成</button>
<button class="action-btn primary" @click="$emit('play-tts')">
<text class="action-icon">{{ ttsIcon || '▶' }}</text>
<text>{{ ttsText || '播放' }}</text>
</button>
</view>
</view>
</template>
<script setup>
const props = defineProps({
content: { type: String, required: true },
title: { type: String, default: '' },
tags: { type: Array, default: () => [] },
collapsed: { type: Boolean, default: false },
contentLength: { type: Number, required: true },
isShortMessage: { type: Boolean, required: true },
ttsIcon: { type: String, default: '▶' },
ttsText: { type: String, default: '播放' }
})
defineEmits([
'toggle-collapse',
'copy',
'change-direction',
'not-like-me',
'continue',
'play-tts'
])
</script>
<style scoped>
.chat-bubble {
max-width: 86%;
display: flex;
flex-direction: column;
gap: 8rpx;
padding: 24rpx 28rpx;
border-radius: 36rpx;
font-size: 34rpx;
line-height: 52rpx;
}
.chat-bubble.system {
align-self: flex-start;
background: rgba(16, 8, 34, 0.28);
border: 1rpx solid rgba(192, 132, 252, 0.3);
color: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.story-card {
border-radius: 52rpx;
padding: 34rpx;
background: rgba(12, 5, 28, 0.34);
border: 1rpx solid rgba(192, 132, 252, 0.46);
box-shadow: 0 0 48rpx rgba(125, 55, 205, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.story-card.collapsed {
box-shadow: 0 0 42rpx rgba(125, 55, 205, 0.14);
}
.story-head {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.story-head-actions {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 12rpx;
}
.story-title-wrap {
flex: 1;
min-width: 0;
}
.story-title {
display: block;
font-size: 52rpx;
font-weight: 700;
line-height: 1.35;
}
.collapse-icon {
width: 120rpx;
min-width: 120rpx;
height: 58rpx;
min-height: 58rpx;
line-height: 1;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: rgba(255, 244, 255, 0.95);
background:
linear-gradient(135deg, rgba(134, 72, 255, 0.44), rgba(39, 15, 76, 0.42)),
rgba(255, 255, 255, 0.05);
border: 1rpx solid rgba(216, 180, 254, 0.46);
box-shadow:
0 14rpx 30rpx rgba(61, 18, 113, 0.22),
inset 0 1rpx 0 rgba(255, 255, 255, 0.26),
inset 0 -10rpx 18rpx rgba(33, 9, 73, 0.16);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
}
.collapse-icon::after {
border: 0;
}
.collapse-icon:active,
.copy-card-btn:active,
.collapse-row:active {
transform: scale(0.98);
opacity: 0.92;
}
.copy-card-btn {
width: 120rpx;
min-width: 120rpx;
height: 50rpx;
min-height: 50rpx;
line-height: 1;
padding: 0;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 244, 255, 0.9);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
background:
radial-gradient(circle at 50% -30%, rgba(255, 255, 255, 0.18), transparent 50%),
rgba(88, 28, 135, 0.26);
border: 1rpx solid rgba(216, 180, 254, 0.32);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 10rpx 22rpx rgba(61, 18, 113, 0.12);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.copy-card-btn::after {
border: 0;
}
.collapse-icon-text,
.collapse-row-text {
line-height: 1;
white-space: nowrap;
}
.collapse-chevron {
position: relative;
width: 20rpx;
height: 16rpx;
flex-shrink: 0;
transition: transform 0.18s ease;
}
.collapse-chevron.down {
transform: rotate(180deg);
}
.collapse-chevron view {
position: absolute;
top: 7rpx;
width: 12rpx;
height: 4rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #fff3b0, #ffd86b);
box-shadow: 0 0 12rpx rgba(255, 216, 107, 0.5);
}
.collapse-chevron view:first-child {
left: 0;
transform: rotate(-38deg);
transform-origin: right center;
}
.collapse-chevron view:last-child {
right: 0;
transform: rotate(38deg);
transform-origin: left center;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.tag {
padding: 6rpx 16rpx;
border-radius: 999rpx;
color: #d18aff;
font-size: 24rpx;
background: rgba(168, 85, 247, 0.22);
}
.story-body {
display: block;
margin-top: 28rpx;
font-size: 32rpx;
font-weight: 400;
line-height: 1.78;
color: rgba(255, 255, 255, 0.92);
white-space: pre-wrap;
}
.story-body-scroll {
max-height: 420rpx;
height: auto;
margin-top: 28rpx;
padding-right: 8rpx;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.story-body-scroll .story-body {
margin-top: 0;
}
.collapse-row {
position: relative;
height: 62rpx;
margin-top: 22rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
border-radius: 999rpx;
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 800;
overflow: hidden;
background:
radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.2), transparent 46%),
linear-gradient(135deg, rgba(98, 40, 174, 0.3), rgba(24, 8, 58, 0.24));
border: 1rpx solid rgba(192, 132, 252, 0.36);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 12rpx 28rpx rgba(61, 18, 113, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.collapse-row::before {
content: '';
position: absolute;
left: 34rpx;
right: 34rpx;
top: 0;
height: 1rpx;
background: linear-gradient(90deg, transparent, rgba(255, 236, 180, 0.6), transparent);
}
.result-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 26rpx;
}
.action-btn {
height: 72rpx;
min-height: 72rpx;
padding: 0 8rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e8ccff;
font-size: 26rpx;
font-weight: 700;
line-height: 1.15;
text-align: center;
white-space: normal;
box-sizing: border-box;
background: rgba(88, 28, 135, 0.18);
border: 1rpx solid rgba(192, 132, 252, 0.35);
}
.action-btn.primary {
color: #fff;
background: linear-gradient(145deg, #8c44f2, #5f1db8);
}
.action-icon {
margin-right: 8rpx;
font-size: 24rpx;
font-weight: 900;
line-height: 1;
}
.action-btn::after {
border: 0;
}
</style>
```
- [ ] **Step 2: 验证组件文件创建成功**
确认文件路径:`mini-program/src/components/MessageCard.vue` 存在。
```bash
ls "G:/IdeaProjects/emotion-museun/mini-program/src/components/MessageCard.vue"
```
- [ ] **Step 3: 提交组件创建**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/components/MessageCard.vue
git commit -m "feat: 创建 MessageCard 组件
封装长消息/故事卡片结构,支持短消息简洁气泡模式"
```
---
### Task 2: 在 ScriptView.vue 中注册并使用 MessageCard 组件
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- [ ] **Step 1: 导入组件**
`ScriptView.vue``<script setup>` 顶部,导入 MessageCard 组件:
修改前:
```javascript
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
```
修改后:
```javascript
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import MessageCard from '../../components/MessageCard.vue'
```
- [ ] **Step 2: 替换 story-card 模板**
找到第 172-219 行的 `story-card` 模板,替换为 MessageCard 组件。
修改前:
```vue
<view class="story-card" :class="{ collapsed: storyCollapsed, dialogMode: resultHasScriptReplies }">
...(原有 story-card 全部内容)...
</view>
```
修改后:
```vue
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
```
- [ ] **Step 3: 替换 result-chat-list 中的 assistant 消息**
找到第 221-254 行的 `result-chat-list` 模板,替换为使用 MessageCard。
修改前:
```vue
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
class="chat-bubble"
:class="{ user: message.role === 'user', system: message.role === 'assistant', pending: message.pending }"
>
<text selectable user-select>{{ getMessageDisplayContent(message) }}</text>
<view v-if="message.pending" class="thinking-dots">
<view></view>
<view></view>
<view></view>
</view>
<view
v-if="isAssistantMessage(message)"
class="message-toggle"
@click.stop="toggleMessageCollapse(message)"
>
<text>{{ isMessageCollapsed(message) ? '展开' : '收起' }}</text>
...
</view>
<view v-if="isAssistantMessage(message)" class="message-actions">
...
</view>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
```
修改后:
```vue
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
>
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
</view>
```
- [ ] **Step 4: 提交使用组件**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: ScriptView 复用 MessageCard 组件
- story-card 使用 MessageCard
- result-chat-list 中的 assistant 消息按内容长度自动切换短消息/完整卡片模式"
```
---
### Task 3: 迁移并清理样式
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- [ ] **Step 1: 从 ScriptView.vue 中删除已迁移到 MessageCard 的样式**
删除以下 CSS 类(从 `<style scoped>` 中移除):
- `.story-card``.story-card.collapsed`
- `.story-head``.story-head-actions``.story-title-wrap`
- `.story-title``.tag-row``.tag`
- `.collapse-icon``.copy-card-btn``.collapse-row`
- `.collapse-chevron``.story-body``.story-body-scroll`
- `.result-actions``.action-btn`
- `.message-actions`(上一轮临时添加的,不再使用)
- `.message-toggle`(不再使用)
保留 `ScriptView.vue` 中仍然需要的样式,如:
- `.chat-bubble.user`
- `.bubble-time`
- `.result-chat-list`
- `.thinking-dots`
- 页面整体布局相关样式
- [ ] **Step 2: 验证样式清理后无遗漏**
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run build:h5 2>&1 | tail -20
```
预期输出:`DONE Build complete.`
- [ ] **Step 3: 提交样式清理**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor: 迁移 story-card 相关样式到 MessageCard
删除 ScriptView.vue 中已迁移到 MessageCard 的样式类"
```
---
### Task 4: 最终验证
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 构建验证**
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run build:h5 2>&1 | tail -20
```
预期输出:`DONE Build complete.`
- [ ] **Step 2: 功能验证(在 H5 浏览器中)**
启动 H5 开发服务器并验证:
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run dev:h5
```
打开浏览器访问显示的端口(默认 `http://localhost:5173`),执行以下验证:
1. **第一次生成故事**:确认 story-card 样式、标题、标签、展开/收起、复制、功能按钮组均正常
2. **重新生成(生成 script 类型长消息)**
- 内容 ≥ 100 字:显示与 story-card 完全一致的卡片样式
- 内容 < 100 字:显示简洁气泡,无任何功能按钮
3. **普通聊天 assistant 消息**
- 内容 ≥ 100 字:显示完整卡片
- 内容 < 100 字:显示简洁气泡
4. **用户消息**:保持原有 `.chat-bubble.user` 样式不变
5. **控制台**:确认无 Vue 警告和 JavaScript 错误
- [ ] **Step 3: 提交验证结果(如有修复)**
如果在验证中修复了问题,提交:
```bash
git add mini-program/src/components/MessageCard.vue mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复 MessageCard 组件验证中发现的问题
[描述具体修复内容]"
```
如果验证全部通过,无需额外提交。
---
## 自审检查
- **Spec 覆盖:**
- ✅ 创建 MessageCard.vue 组件 → Task 1
- ✅ ScriptView.vue 复用组件 → Task 2
- ✅ 100 字阈值判断 → Task 2 (`:is-short-message="message.content.length < 100"`)
- ✅ 样式迁移和清理 → Task 3
- ✅ 验证 → Task 4
- **无占位符:** 计划中没有 TBD/TODO
- **类型一致:** props 名称 `contentLength` 与模板中使用一致,事件名称在组件和父组件中一致
File diff suppressed because it is too large Load Diff
@@ -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,163 @@
# 心愿实现 chat 视图 Loading 标识修复实施计划
> **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 视图在等待后端 SSE 响应期间 loading 立即消失导致页面空白的问题。
**Architecture:** 新增 computed `showChatLoading`,基于 `generationPhase === 'generating'` + `generationStatus !== 'failed'` + `resultMessages 最后一条消息 role === 'user'` 三个条件判断是否显示 loading。替换原 `v-if="generating && generationStatus !== 'failed'"`
**Tech Stack:** UniApp Vue 3 `<script setup>` + mp-weixin 编译
---
### Task 1: 新增 computed showChatLoading 并替换 v-if
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- 新增 computed `showChatLoading`(在 `generationCopy` 附近,约 line 942
- 替换 line 177 的 `v-if` 条件
- [ ] **Step 1: 新增 computed showChatLoading**
打开 `mini-program/src/pages/main/ScriptView.vue`,在 line 942`generationCopy` computed 之后)新增:
```js
const showChatLoading = computed(() => {
if (generationPhase.value !== 'generating') return false
if (generationStatus.value === 'failed') return false
const lastMsg = resultMessages.value[resultMessages.value.length - 1]
// 最后一条消息是 user(刚发送的请求),说明还没收到 assistant 响应
return lastMsg && lastMsg.role === 'user'
})
```
- [ ] **Step 2: 替换 line 177 的 v-if 条件**
定位到 line 177
```html
<view v-if="generating && generationStatus !== 'failed'" class="chat-loading">
```
改为:
```html
<view v-if="showChatLoading" class="chat-loading">
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: chat 视图 loading 在等待首个 assistant 响应期间持续显示"
```
---
### Task 2: 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
```
- [ ] **Step 2: 浏览器打开页面并进入 chat 视图**
1. 用浏览器打开 `http://localhost:5180`
2. 确认已登录(如未登录,使用手机号验证码 123456 登录)
3. 点击底部导航「爽文生成」
4. 在输入框输入内容(或点击灵感推荐),点击「发送」
- [ ] **Step 3: 验证 loading 立即显示**
发送后**立即**观察 chat 视图:
- ✅ 用户消息气泡出现后,下方立刻显示 loading 动画(loading-orbit + loading-copy 文本,如 "正在把你的心愿写成故事")
- ✅ loading 不是空白,也不是瞬间消失
- ✅ 浏览器 Network 面板能看到 SSE 请求正在进行
- [ ] **Step 4: 验证 loading 在收到首个 assistant 响应后隐藏**
等待 SSE 响应(通常 3-10 秒):
- ✅ 收到 `clarification_card` 后,澄清卡片出现,loading 自动隐藏
- ✅ 或直接收到 `outline_created` 后,大纲出现,loading 自动隐藏
- ✅ 或直接收到 `novel_start` 后,小说内容开始流式出现,loading 自动隐藏
- [ ] **Step 5: 验证澄清回答后 loading 重新显示**
如果出现澄清卡片:
1. 选择一个选项,点击「提交」
2. ✅ 用户回答作为消息追加后,loading 立即重新显示
3. ✅ 等待下一个 assistant 响应(outline_created / novel_start
4. ✅ 响应到达后,loading 自动隐藏
- [ ] **Step 6: 检查浏览器 Console 无报错**
打开浏览器 DevTools → Console 面板,确认:
- ✅ 无新增红色 ERROR`uni.getRecorderManager not supported` 是 H5 环境预期内的 warning,可忽略)
- [ ] **Step 7: 验证失败状态不显示 loading**
如果生成失败(网络异常或后端错误):
- ✅ loading 不显示
- ✅ 显示「继续创作」按钮(resume 按钮)
---
### Task 3: 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 "showChatLoading" mini-program/unpackage/dist/build/mp-weixin/pages/main/ScriptView.wxml
```
Expected: 编译产物中 `.chat-loading``v-if` 使用了 `showChatLoading`(或编译后的等效表达式),不再使用原来的 `generating && generationStatus !== 'failed'`
- [ ] **Step 3: 提交(如有微调)**
如有需要,执行:
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: mp-weixin 编译产物验证通过"
```
---
### Task 4: 通知用户在微信开发者工具中验证
- [ ] **Step 1: 提示用户操作**
告知用户:
> mp-weixin 编译产物已更新(`mini-program/unpackage/dist/build/mp-weixin/`)。请在**微信开发者工具**中:
> 1. 打开 `unpackage/dist/build/mp-weixin` 目录
> 2. 点击「编译」按钮重新编译
> 3. 进入心愿实现页面,发送一条消息
> 4. 观察等待 SSE 响应期间是否有 loading 动画持续显示(loading-orbit 动画 + "正在把你的心愿写成故事" 等文案)
> 5. 等待澄清卡片 / 大纲 / 小说内容出现后,确认 loading 自动隐藏
> 6. 如有澄清问题流程,回答后确认 loading 重新显示直到下一个响应到达
>
> 如有问题,截图反馈。
@@ -0,0 +1,183 @@
# 澄清卡片已回答显示修复实施计划
> **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:** 修复澄清卡片提交后"已回答:xxx"显示英文 value 而非中文 label,以及已回答状态不显示 AI 问题的问题。
**Architecture:** 在 `ScriptView.vue``submitClarification` 函数中,通过 value 查找 `card.options` 对应的 label,用 label 显示(`card.answer``addResultMessage`),后端 payload 仍用 value。在 `card-answered` 模板中显示 AI 的问题(`msg.card?.question`)。
**Tech Stack:** UniApp Vue 3 `<script setup>` + mp-weixin 编译
---
### Task 1: 修改 submitClarification 用 label 显示
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- 修改 `submitClarification` 函数(约 line 1746-1764
- [ ] **Step 1: 修改 submitClarification**
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到 `submitClarification` 函数(约 line 1746-1764)。
替换为:
```js
const submitClarification = (answer) => {
if (answeringClarification.value) return
// 找最后一张未提交的卡片消息(mp-weixin 模板不支持内联箭头函数传 msg,改为自己查找)
const card = [...resultMessages.value].reverse().find(m => m.kind === 'card' && !m.submitted)
// 通过 value 查找选项的 label,用于前端显示;后端 payload 仍用 value(语义化)
const option = card?.card?.options?.find(opt => opt.value === answer)
const label = option?.label || answer
if (card) {
card.submitted = true
card.answer = label // 显示用 label(中文)
}
addResultMessage({ role: 'user', kind: 'text', content: label })
answeringClarification.value = true
currentStreamTask.value = followupStream({
sessionId: novelSessionId.value,
action: 'answer_clarification',
payload: { answer }, // 后端仍收 value
onEvent: handleShortNovelEvent,
onError: (errMsg) => markGenerationFailed(errMsg)
})
setTimeout(() => { answeringClarification.value = false }, 200)
}
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 澄清卡片已回答用 label 显示,后端 payload 仍用 value"
```
---
### Task 2: 修改 card-answered 模板显示 AI 的问题
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- 修改 `card-answered` 模板(约 line 131-133
- 新增 `.card-question-text` 样式(在 `.card-answered` 样式附近)
- [ ] **Step 1: 修改 card-answered 模板**
定位到约 line 131-133
```html
<view v-else class="card-answered">
<text>已回答:{{ msg.answer }}</text>
</view>
```
替换为:
```html
<view v-else class="card-answered">
<text v-if="msg.card?.question" class="card-question-text">{{ msg.card.question }}</text>
<text>已回答:{{ msg.answer }}</text>
</view>
```
- [ ] **Step 2: 新增 .card-question-text 样式**
`.card-answered` 样式块附近(用 Grep 找 `.card-answered {` 位置),新增样式:
```css
.card-question-text {
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
margin-bottom: 16rpx;
line-height: 1.5;
display: block;
}
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 已回答状态显示 AI 的问题(question"
```
---
### Task 3: H5 浏览器验证
**Files:**
- Test: `http://localhost:5180` (H5 开发服务器)
- [ ] **Step 1: 确认 H5 开发服务器运行中**
```bash
python dev-services.py status
```
如未运行:`python dev-services.py start mini-program`
- [ ] **Step 2: 浏览器打开页面并进入 chat 视图**
1. 打开 `http://localhost:5180`
2. 登录(如未登录,用 123456 验证码)
3. 点击「爽文生成」→ 输入内容 → 点击「发送」
- [ ] **Step 3: 验证澄清卡片提交后的显示**
等待澄清卡片出现:
1. 选择一个选项(如"工作/学习上的事"),点击「提交」
2. 确认:
- ✅ 澄清卡片位置显示 AI 的问题(如"你想重写今天的什么经历?")
- ✅ "已回答:"后面显示中文 label(如"工作/学习上的事"),而非英文 value(如"competition"
- ✅ 下方用户气泡显示中文 label(如"工作/学习上的事"
- [ ] **Step 4: 检查浏览器 Console 无报错**
打开 DevTools → Console 面板,确认:
- ✅ 无新增红色 ERROR(预先存在的 favicon 404、`uni.getRecorderManager not supported` 可忽略)
---
### 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 输出。
- [ ] **Step 2: 提交(如有微调)**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: mp-weixin 编译产物验证通过"
```
---
### Task 5: 通知用户在微信开发者工具中验证
- [ ] **Step 1: 提示用户操作**
告知用户:
> mp-weixin 编译产物已更新。请在**微信开发者工具**中:
> 1. 打开 `unpackage/dist/build/mp-weixin` 目录
> 2. 点击「编译」重新编译
> 3. 进入心愿实现页面,发送一条消息
> 4. 等待澄清卡片出现,选择一个选项,点击「提交」
> 5. 确认:
> - 已回答状态显示 AI 的问题(如"你想重写今天的什么经历?")
> - "已回答:"后面显示中文 label(如"工作/学习上的事"),而非英文 value
> - 用户气泡显示中文 label
>
> 如有问题,截图反馈。
@@ -0,0 +1,939 @@
---
author: AI Assistant
created_at: 2026-07-19
purpose: 小程序收藏功能真实 API 化实施计划
---
# 收藏功能真实 API 化实施计划
> **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:** 前端收藏功能从 localStorage 切换到真实 API。剧本收藏沿用现有后端 API,人生事件收藏新建独立表和 API。
**Architecture:** 剧本收藏后端已完整(`t_script_favorite` 表 + `ScriptFavoriteService`),前端 `ScriptLibraryView.vue` 未对接。人生事件收藏仿照剧本,新建 `t_event_favorite` 表 + `EventFavoriteService` + Controller,删除假接口 `/lifeEvent/favorite-placeholder`。前端所有 localStorage 收藏逻辑切换到 API。
**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus 3.5.3.1 + UniApp Vue 3 + Pinia + Element Plus + rpx
---
### Task 1: 后端人生事件收藏功能(Entity + Mapper + Service + Controller
**Files:**
- Create: `server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql`
- Create: `server/src/main/java/com/emotion/entity/EventFavorite.java`
- Create: `server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java`
- Create: `server/src/main/java/com/emotion/service/EventFavoriteService.java`
- Create: `server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/controller/LifeEventController.java`
- [ ] **Step 1: 创建数据库迁移 SQL**
```sql
-- server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql
CREATE TABLE t_event_favorite (
id VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL COMMENT '用户ID',
event_id VARCHAR(64) NOT NULL COMMENT '事件ID',
create_by VARCHAR(64) COMMENT '创建人',
create_time DATETIME COMMENT '创建时间',
update_by VARCHAR(64) COMMENT '更新人',
update_time DATETIME COMMENT '更新时间',
is_deleted TINYINT(1) DEFAULT 0 COMMENT '是否删除:0-否 1-是',
remarks VARCHAR(500) COMMENT '备注',
INDEX idx_user_event (user_id, event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人生事件收藏关联表';
```
- [ ] **Step 2: 创建 EventFavorite 实体类**
```java
// server/src/main/java/com/emotion/entity/EventFavorite.java
package com.emotion.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.emotion.common.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 人生事件收藏关联实体
*
* @author AI Assistant
* @date 2026-07-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_event_favorite")
public class EventFavorite extends BaseEntity {
@TableField("user_id")
private String userId;
@TableField("event_id")
private String eventId;
}
```
- [ ] **Step 3: 创建 EventFavoriteMapper**
```java
// server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java
package com.emotion.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.emotion.entity.EventFavorite;
import org.apache.ibatis.annotations.Mapper;
/**
* 人生事件收藏 Mapper
*
* @author AI Assistant
* @date 2026-07-19
*/
@Mapper
public interface EventFavoriteMapper extends BaseMapper<EventFavorite> {
}
```
- [ ] **Step 4: 创建 EventFavoriteToggleRequest DTO**
```java
// server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java
package com.emotion.dto.request;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 人生事件收藏切换请求
*
* @author AI Assistant
* @date 2026-07-19
*/
@Data
public class EventFavoriteToggleRequest {
@NotBlank(message = "事件ID不能为空")
private String eventId;
}
```
- [ ] **Step 5: 创建 EventFavoriteResponse DTO**
```java
// server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java
package com.emotion.dto.response;
import lombok.Data;
/**
* 人生事件收藏状态响应
*
* @author AI Assistant
* @date 2026-07-19
*/
@Data
public class EventFavoriteResponse {
private String eventId;
private Boolean isFavorited;
private String favoriteTime;
}
```
- [ ] **Step 6: 创建 EventFavoriteService 接口**
```java
// server/src/main/java/com/emotion/service/EventFavoriteService.java
package com.emotion.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.emotion.common.PageResult;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.dto.response.LifeEventResponse;
import com.emotion.entity.EventFavorite;
import java.util.Set;
/**
* 人生事件收藏服务接口
*
* @author AI Assistant
* @date 2026-07-19
*/
public interface EventFavoriteService extends IService<EventFavorite> {
EventFavoriteResponse toggleFavorite(String eventId);
PageResult<LifeEventResponse> getFavoritePage(long current, long size);
Set<String> getFavoritedEventIds(Set<String> eventIds);
boolean isFavorited(String eventId);
long getFavoriteCount();
}
```
- [ ] **Step 7: 创建 EventFavoriteServiceImpl 实现**
```java
// server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java
package com.emotion.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.emotion.common.PageResult;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.dto.response.LifeEventResponse;
import com.emotion.entity.EventFavorite;
import com.emotion.mapper.EventFavoriteMapper;
import com.emotion.service.EventFavoriteService;
import com.emotion.service.LifeEventService;
import com.emotion.util.UserContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 人生事件收藏服务实现
*
* @author AI Assistant
* @date 2026-07-19
*/
@Slf4j
@Service
public class EventFavoriteServiceImpl extends ServiceImpl<EventFavoriteMapper, EventFavorite>
implements EventFavoriteService {
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private LifeEventService lifeEventService;
@Override
@Transactional(rollbackFor = Exception.class)
public EventFavoriteResponse toggleFavorite(String eventId) {
String userId = UserContextHolder.getCurrentUserId();
EventFavoriteResponse r = new EventFavoriteResponse();
r.setEventId(eventId);
if (!StringUtils.hasText(userId)) {
r.setIsFavorited(false);
return r;
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getEventId, eventId)
.eq(EventFavorite::getIsDeleted, 0);
EventFavorite existing = this.getOne(w);
if (existing != null) {
this.removeById(existing.getId());
r.setIsFavorited(false);
} else {
EventFavorite fav = new EventFavorite();
fav.setUserId(userId);
fav.setEventId(eventId);
this.save(fav);
r.setIsFavorited(true);
if (fav.getCreateTime() != null) {
r.setFavoriteTime(fav.getCreateTime().format(FMT));
}
}
return r;
}
@Override
public PageResult<LifeEventResponse> getFavoritePage(long current, long size) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId)) {
return new PageResult<>();
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getIsDeleted, 0)
.orderByDesc(EventFavorite::getCreateTime);
Page<EventFavorite> page = new Page<>(current, size);
Page<EventFavorite> result = this.page(page, w);
List<LifeEventResponse> records = result.getRecords().stream()
.map(EventFavorite::getEventId)
.map(lifeEventService::getEventById)
.filter(Objects::nonNull)
.collect(Collectors.toList());
PageResult<LifeEventResponse> pr = new PageResult<>();
pr.setCurrent(result.getCurrent());
pr.setSize(result.getSize());
pr.setTotal(result.getTotal());
pr.setPages(result.getPages());
pr.setRecords(records);
return pr;
}
@Override
public Set<String> getFavoritedEventIds(Set<String> eventIds) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId) || eventIds == null || eventIds.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.in(EventFavorite::getEventId, eventIds)
.eq(EventFavorite::getIsDeleted, 0);
return this.list(w).stream()
.map(EventFavorite::getEventId)
.collect(Collectors.toSet());
}
@Override
public boolean isFavorited(String eventId) {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId) || !StringUtils.hasText(eventId)) {
return false;
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getEventId, eventId)
.eq(EventFavorite::getIsDeleted, 0);
return this.count(w) > 0;
}
@Override
public long getFavoriteCount() {
String userId = UserContextHolder.getCurrentUserId();
if (!StringUtils.hasText(userId)) {
return 0L;
}
LambdaQueryWrapper<EventFavorite> w = new LambdaQueryWrapper<>();
w.eq(EventFavorite::getUserId, userId)
.eq(EventFavorite::getIsDeleted, 0);
return this.count(w);
}
}
```
- [ ] **Step 8: 修改 LifeEventController 添加收藏 API**
`server/src/main/java/com/emotion/controller/LifeEventController.java` 中添加:
1. 在类顶部添加注入:
```java
@Autowired
private EventFavoriteService eventFavoriteService;
```
2. 添加 import
```java
import com.emotion.dto.request.EventFavoriteToggleRequest;
import com.emotion.dto.response.EventFavoriteResponse;
import com.emotion.service.EventFavoriteService;
```
3. 在类末尾(`readTags` 方法之前)添加收藏 API
```java
// ==================== 人生事件收藏 ====================
@Operation(summary = "切换收藏", description = "收藏或取消收藏指定事件。")
@PostMapping(value = "/favorite/toggle")
public Result<EventFavoriteResponse> toggleFavorite(
@Valid @RequestBody EventFavoriteToggleRequest request) {
return Result.success(eventFavoriteService.toggleFavorite(request.getEventId()));
}
@Operation(summary = "分页查询收藏", description = "分页查询当前用户收藏的事件列表。")
@GetMapping(value = "/favorite/page")
public Result<PageResult<LifeEventResponse>> getFavoritePage(
@RequestParam(defaultValue = "1") long current,
@RequestParam(defaultValue = "10") long size) {
return Result.success(eventFavoriteService.getFavoritePage(current, size));
}
@Operation(summary = "检查收藏状态", description = "检查指定事件是否已被当前用户收藏。")
@GetMapping(value = "/favorite/check")
public Result<EventFavoriteResponse> checkFavorite(@RequestParam String eventId) {
boolean favorited = eventFavoriteService.isFavorited(eventId);
EventFavoriteResponse r = new EventFavoriteResponse();
r.setEventId(eventId);
r.setIsFavorited(favorited);
return Result.success(r);
}
```
4. **删除** `/lifeEvent/favorite-placeholder` 假接口(第 153-163 行):
```java
// 删除以下方法
@Operation(summary = "收藏占位接口", description = "返回收藏功能的占位响应。")
@PostMapping(value = "/favorite-placeholder")
public Result<Map<String, Object>> favoritePlaceholder(@RequestBody Map<String, Object> request) {
// ... 整段删除
}
```
- [ ] **Step 9: 提交**
```bash
git add server/src/main/resources/db/migration/V20260719000002__create_event_favorite.sql
git add server/src/main/java/com/emotion/entity/EventFavorite.java
git add server/src/main/java/com/emotion/mapper/EventFavoriteMapper.java
git add server/src/main/java/com/emotion/dto/request/EventFavoriteToggleRequest.java
git add server/src/main/java/com/emotion/dto/response/EventFavoriteResponse.java
git add server/src/main/java/com/emotion/service/EventFavoriteService.java
git add server/src/main/java/com/emotion/service/impl/EventFavoriteServiceImpl.java
git add server/src/main/java/com/emotion/controller/LifeEventController.java
git commit -m "feat: 新增人生事件收藏功能(独立收藏表 + API)"
```
---
### Task 2: 后端编译 + 部署到服务器
**Files:**
- Build: `server/target/`
- Deploy: 服务器
- [ ] **Step 1: 本地编译验证**
```bash
cd server
mvn clean install -DskipTests
```
Expected: BUILD SUCCESS,无编译错误。
- [ ] **Step 2: 部署到服务器**
```bash
cd ..
python deploy.py backend
```
Expected: 部署成功,服务器自动重启。
- [ ] **Step 3: 验证部署**
```bash
python deploy.py verify
```
Expected: 后端服务运行正常。
- [ ] **Step 4: 提交**
```bash
git commit -m "chore: 后端人生事件收藏功能已部署" --allow-empty
```
---
### Task 3: 前端 ScriptLibraryView.vue 切换到剧本收藏 API
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue`
- [ ] **Step 1: 删除 localFavorites ref,改用内存状态**
删除第 160 行:
```javascript
// 删除这一行
const localFavorites = ref(uni.getStorageSync('script_favorites') || {})
```
替换为:
```javascript
const favoriteStatus = ref({})
```
- [ ] **Step 2: 修改 isFavorite 计算**
修改第 245-247 行:
```javascript
// 之前:
const isFavorite = (script) => {
return Boolean(script.isFavorite || script.favorite || localFavorites.value[String(script.id)])
}
// 之后:
const isFavorite = (script) => {
return Boolean(favoriteStatus.value[String(script.id)])
}
```
- [ ] **Step 3: 修改 toggleFavorite,删除 localStorage 写**
修改第 320-348 行:
```javascript
// 之前:
const toggleFavorite = async (script) => {
const wasFav = isFavorite(script)
// 乐观更新:先改本地缓存
const next = { ...localFavorites.value }
if (wasFav) {
delete next[String(script.id)]
} else {
next[String(script.id)] = true
}
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
closeScriptMenu()
try {
const result = await toggleFavoriteScript(script.id)
uni.showToast({ title: result.isFavorited ? '已收藏' : '已取消收藏', icon: 'success' })
} catch (error) {
// 回滚乐观更新
const restored = { ...localFavorites.value }
if (wasFav) {
restored[String(script.id)] = true
} else {
delete restored[String(script.id)]
}
localFavorites.value = restored
uni.setStorageSync('script_favorites', restored)
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
}
}
// 之后:
const toggleFavorite = async (script) => {
const wasFav = isFavorite(script)
// 乐观更新:先改内存状态
const next = { ...favoriteStatus.value }
if (wasFav) {
delete next[String(script.id)]
} else {
next[String(script.id)] = true
}
favoriteStatus.value = next
closeScriptMenu()
try {
const result = await toggleFavoriteScript(script.id)
uni.showToast({ title: result.isFavorited ? '已收藏' : '已取消收藏', icon: 'success' })
} catch (error) {
// 回滚乐观更新
const restored = { ...favoriteStatus.value }
if (wasFav) {
restored[String(script.id)] = true
} else {
delete restored[String(script.id)]
}
favoriteStatus.value = restored
uni.showToast({ title: error?.message || '操作失败', icon: 'none' })
}
}
```
- [ ] **Step 4: 修改 confirmDeleteScript,删除 localStorage 清理**
修改第 386-389 行:
```javascript
// 之前:
const next = { ...localFavorites.value }
delete next[id]
localFavorites.value = next
uni.setStorageSync('script_favorites', next)
// 之后:
const next = { ...favoriteStatus.value }
delete next[id]
favoriteStatus.value = next
```
- [ ] **Step 5: 列表加载后批量检查收藏状态**
`visibleScripts` computed 后添加 watch 或 onMounted 钩子,批量调用 `checkFavoriteScript`
在文件顶部添加 import
```javascript
import { checkFavoriteScript } from '../../services/epicScript.js'
```
`visibleScripts` computed 后添加:
```javascript
// 列表加载后批量检查收藏状态
const loadFavoriteStatus = async () => {
const ids = scripts.value.map(s => String(s.id)).filter(id => !id.startsWith('demo-'))
if (!ids.length) return
const status = {}
await Promise.all(ids.map(async (id) => {
try {
status[id] = await checkFavoriteScript(id)
} catch {
status[id] = false
}
}))
favoriteStatus.value = status
}
// 监听 scripts 变化时重新加载收藏状态
watch(scripts, () => {
loadFavoriteStatus()
}, { immediate: true })
```
确保 import watch
```javascript
import { computed, ref, watch } from 'vue'
```
- [ ] **Step 6: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "refactor: 剧本收藏从 localStorage 切换到真实 API"
```
---
### Task 4: 前端 life-event 切换到人生事件收藏 API
**Files:**
- Modify: `mini-program/src/services/lifeEvent.js`
- Modify: `mini-program/src/pages/life-event/detail.vue`
- Modify: `mini-program/src/stores/app.js`
- [ ] **Step 1: 修改 lifeEvent.js 新增收藏 API 封装**
`mini-program/src/services/lifeEvent.js` 中:
1. **删除**旧的 `favoriteEvent` 方法(第 67-69 行):
```javascript
// 删除:
export const favoriteEvent = async ({ id, favorite }) => {
return post('/lifeEvent/favorite-placeholder', { id, favorite })
}
```
2. **新增**真实的收藏 API
```javascript
/**
* 切换人生事件收藏状态(收藏/取消收藏)
* @param {string} eventId 事件ID
* @returns {Promise<{success: boolean, isFavorited: boolean}>}
*/
export const toggleFavoriteEvent = async (eventId) => {
const res = await post('/lifeEvent/favorite/toggle', { eventId })
return {
success: true,
isFavorited: res.data?.isFavorited ?? false,
favoriteTime: res.data?.favoriteTime ?? null
}
}
/**
* 检查人生事件是否已收藏
* @param {string} eventId 事件ID
* @returns {Promise<boolean>}
*/
export const checkFavoriteEvent = async (eventId) => {
const res = await get('/lifeEvent/favorite/check', { eventId })
return res.data?.isFavorited ?? false
}
```
3. 在 default export 中更新方法列表:
```javascript
export default {
getEventList,
getEventPage,
getEventById,
createEvent,
updateEvent,
deleteEvent,
assistEventWriting,
chatAboutEvent,
shareEvent,
toggleFavoriteEvent,
checkFavoriteEvent,
transformToFrontendFormat,
transformListToFrontend
}
```
- [ ] **Step 2: 修改 stores/app.js 的 favoriteEvent action**
`mini-program/src/stores/app.js` 中:
1. **删除**旧的 `favoriteEvent` action(第 235-242 行):
```javascript
// 删除:
const favoriteEvent = async ({ id, favorite }) => {
try {
const res = await lifeEventService.favoriteEvent({ id, favorite })
return { success: true, data: res.data }
} catch (error) {
return { success: false, error: error.message }
}
}
```
2. **新增**新的 action
```javascript
const toggleEventFavorite = async (eventId) => {
try {
const result = await lifeEventService.toggleFavoriteEvent(eventId)
return { success: true, data: result }
} catch (error) {
return { success: false, error: error.message }
}
}
const checkEventFavorite = async (eventId) => {
try {
const isFavorited = await lifeEventService.checkFavoriteEvent(eventId)
return { success: true, data: isFavorited }
} catch (error) {
return { success: false, error: error.message }
}
}
```
3. 在 return 对象中更新导出:
```javascript
return {
// ... 其他方法
toggleEventFavorite,
checkEventFavorite,
// 删除 favoriteEvent
}
```
- [ ] **Step 3: 修改 detail.vue 初始化时检查收藏状态**
`mini-program/src/pages/life-event/detail.vue` 中:
1. 修改 `onMounted`(第 137-149 行):
```javascript
// 之前:
onMounted(async () => {
const info = uni.getWindowInfo()
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
const pages = getCurrentPages()
eventId.value = pages[pages.length - 1]?.options?.id || ''
cachedEvent.value = uni.getStorageSync('current_life_event') || null
if (!store.events?.length) await store.fetchEvents()
isFavorite.value = Boolean(uni.getStorageSync(`event_favorite_${eventId.value}`))
analytics.trackPageView(pagePath, {
life_event_id: eventId.value,
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
})
})
// 之后:
onMounted(async () => {
const info = uni.getWindowInfo()
safeAreaBottom.value = info.safeAreaInsets?.bottom || 0
const pages = getCurrentPages()
eventId.value = pages[pages.length - 1]?.options?.id || ''
cachedEvent.value = uni.getStorageSync('current_life_event') || null
if (!store.events?.length) await store.fetchEvents()
// 从 API 检查收藏状态
const result = await store.checkEventFavorite(eventId.value)
if (result.success) {
isFavorite.value = result.data
}
analytics.trackPageView(pagePath, {
life_event_id: eventId.value,
tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
})
})
```
- [ ] **Step 4: 修改 detail.vue 的 toggleFavorite,删除 localStorage 写**
修改第 306-322 行:
```javascript
// 之前:
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 toggleFavorite = async () => {
if (!eventId.value) return
const result = await store.toggleEventFavorite(eventId.value)
if (!result.success) {
uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
return
}
isFavorite.value = result.data?.isFavorited ?? false
analytics.track('life_event_favorite', {
life_event_id: eventId.value,
favorite: isFavorite.value
}, { eventType: 'life_event', pagePath })
uni.showToast({ title: isFavorite.value ? '已收藏' : '已取消收藏', icon: 'success' })
}
```
- [ ] **Step 5: 提交**
```bash
git add mini-program/src/services/lifeEvent.js
git add mini-program/src/stores/app.js
git add mini-program/src/pages/life-event/detail.vue
git commit -m "refactor: 人生事件收藏从 localStorage 切换到真实 API"
```
---
### Task 5: H5 浏览器验证
**Files:**
- Test: `http://localhost:5180` (H5 开发服务器)
- [ ] **Step 1: 确认 H5 开发服务器运行中**
```bash
python dev-services.py status
```
Expected: `mini-program` 服务在端口 5180 上运行。如未运行,执行:
```bash
python dev-services.py start mini-program
```
- [ ] **Step 2: 浏览器打开页面并登录**
1. 打开 `http://localhost:5180`
2. 如未登录,使用手机号验证码 123456 登录
- [ ] **Step 3: 验证剧本收藏**
1. 点击底部导航「爽文生成」
2. 点击「剧本库」
3. 点击任意剧本的收藏图标(心形)
4. 确认:
- ✅ Toast 提示「已收藏」
- ✅ 心形图标变为实心
- ✅ Network 面板中有 `POST /epicScript/favorite/toggle` 请求,返回 `isFavorited: true`
5. 刷新页面
6. 确认收藏状态仍保持(心形图标仍为实心)
7. 再次点击收藏图标取消收藏
8. 确认:
- ✅ Toast 提示「已取消收藏」
- ✅ 心形图标变为空心
- ✅ Network 面板中有 `POST /epicScript/favorite/toggle` 请求,返回 `isFavorited: false`
- [ ] **Step 4: 验证人生事件收藏**
1. 点击底部导航「人生轨迹」
2. 点击任意事件进入详情页
3. 点击收藏按钮
4. 确认:
- ✅ Toast 提示「已收藏」
- ✅ 收藏按钮状态变化
- ✅ Network 面板中有 `POST /lifeEvent/favorite/toggle` 请求,返回 `isFavorited: true`
5. 返回列表页,再次进入同一事件详情
6. 确认收藏状态仍保持
7. 再次点击收藏按钮取消收藏
8. 确认:
- ✅ Toast 提示「已取消收藏」
- ✅ 收藏按钮状态恢复
- ✅ Network 面板中有 `POST /lifeEvent/favorite/toggle` 请求,返回 `isFavorited: false`
- [ ] **Step 5: 检查浏览器 Console 无报错**
打开浏览器 DevTools → Console 面板,确认:
- ✅ 无红色 ERROR
- ✅ 无 localStorage 相关的 `script_favorites``event_favorite_` 读写
- [ ] **Step 6: 提交(如有微调)**
```bash
git add mini-program/src/
git commit -m "test: H5 浏览器验证收藏功能通过"
```
---
### Task 6: 通知用户验证
- [ ] **Step 1: 提示用户操作**
告知用户:
> 收藏功能已完成真实 API 化改造:
>
> **剧本收藏**
> - 前端从 localStorage 切换到现有后端 API`POST /epicScript/favorite/toggle`
> - 收藏状态从服务器读取,不再依赖本地缓存
>
> **人生事件收藏**
> - 后端新建独立的 `t_event_favorite` 表 + API
> - 前端从 localStorage 切换到真实 API`POST /lifeEvent/favorite/toggle`
> - 删除了假接口 `/lifeEvent/favorite-placeholder`
>
> **验证方式**
> 1. 本地 H5`http://localhost:5180`)已验证通过
> 2. 后端已部署到服务器
> 3. 请在**微信开发者工具**中:
> - 打开 `unpackage/dist/dev/mp-weixin` 目录
> - 点击「编译」重新编译
> - 测试剧本收藏和人生事件收藏功能
> - 确认收藏状态刷新后仍保持
>
> 如有问题,截图反馈。
@@ -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 消息并调 followupStreamnovel_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
@@ -0,0 +1,639 @@
# SSE 流式传输与历史列表保存修复实施计划
> **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:** 修复小说生成页面的 SSE 流式传输(实现逐字输出)和历史列表保存问题
**Architecture:** 通过三个层面的修改实现真正的流式传输:后端 OkHttp 使用严格行读取 + 显式 flush、nginx 禁用代理缓冲、前端添加诊断日志确认 originalQuery 传递
**Tech Stack:** Spring Boot 2.7.18, OkHttp 4.12.0, nginx, Vue 3 (UniApp)
---
## 文件结构映射
**修改的文件:**
1. `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java:140-192` - 后端 SSE 转发逻辑
2. `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf` - nginx 配置(服务器)
3. `mini-program/src/pages/main/ScriptView.vue:1650-1843` - 前端事件处理
**不创建新文件** - 所有修改都是在现有文件中添加日志和优化逻辑
---
## Task 1: 后端 OkHttp 读取优化
**Files:**
- Modify: `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java:140-192`
- [ ] **Step 1: 在 forwardSse 方法开头添加开始日志**
打开 `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java`,在第 140 行(`BufferedSource source = responseBody.source();` 之后)添加:
```java
log.info("[ShortNovel SSE] 开始读取上游响应: path={}, userId={}", path, currentUserId);
```
- [ ] **Step 2: 将 readUtf8Line() 改为 readUtf8LineStrict()**
在第 145 行,将:
```java
String line = source.readUtf8Line();
```
改为:
```java
String line = source.readUtf8LineStrict();
```
- [ ] **Step 3: 在读取行后添加调试日志**
在第 146 行(`if (line == null) break;` 之后)添加:
```java
log.debug("[ShortNovel SSE] 读取到行: length={}, timestamp={}",
line.length(), System.currentTimeMillis());
```
- [ ] **Step 4: 在事件解析后添加时间戳日志**
在第 160 行(`String type = event.getString("type");` 之后)添加:
```java
long eventTimestamp = System.currentTimeMillis();
log.info("[ShortNovel SSE] 处理事件: type={}, timestamp={}", type, eventTimestamp);
```
- [ ] **Step 5: 在 novel_done 事件处理中添加详细日志**
在第 165 行(`if ("novel_done".equals(type)) {` 之后)添加:
```java
JSONObject payload = event.getJSONObject("payload");
log.info("[ShortNovel SSE] novel_done 事件: originalQuery={}, payload={}",
originalQuery, payload != null);
```
- [ ] **Step 6: 在保存前添加 originalQuery 空字符串检查**
将第 165 行的条件:
```java
if (payload != null && originalQuery != null) {
```
改为:
```java
if (payload != null && originalQuery != null && !originalQuery.trim().isEmpty()) {
```
- [ ] **Step 7: 在保存小说前添加日志**
在第 172 行(`Map<String, String> saveResult = epicScriptDialogueServiceImpl.saveNovelResult(` 之前)添加:
```java
log.info("[ShortNovel SSE] 开始保存小说: userId={}, queryLength={}, textLength={}",
currentUserId, originalQuery.length(), fullText.length());
```
- [ ] **Step 8: 在保存成功后添加日志**
在第 176 行(`Map<String, String> saveResult = ...` 之后)添加:
```java
log.info("[ShortNovel SSE] 小说保存成功: scriptId={}", saveResult.get("scriptId"));
```
- [ ] **Step 9: 在保存失败时添加警告日志**
在第 183 行(`} else {` 之后)添加:
```java
log.warn("[ShortNovel SSE] novel_done 事件缺少 full_text");
```
- [ ] **Step 10: 在跳过保存时添加警告日志**
在第 185 行(`} else {` 之后)添加:
```java
log.warn("[ShortNovel SSE] novel_done 事件跳过保存: originalQuery={}, payload={}",
originalQuery, payload);
```
- [ ] **Step 11: 在 emitter.send() 后添加 flush 触发**
在第 187 行(`emitter.send(SseEmitter.event().name(type).data(event.toJSONString()));` 之后)添加:
```java
emitter.send(SseEmitter.event().comment("")); // 触发 flush
```
- [ ] **Step 12: 在循环结束后添加完成日志**
在第 192 行(`}` 之前)添加:
```java
log.info("[ShortNovel SSE] 完成读取上游响应");
```
- [ ] **Step 13: 编译后端验证**
```bash
cd server
mvn clean install -DskipTests
```
预期输出:`BUILD SUCCESS`
- [ ] **Step 14: 提交后端修改**
```bash
git add server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java
git commit -m "feat: 优化 SSE 转发逻辑,使用严格行读取和显式 flush"
```
---
## Task 2: nginx 配置优化
**Files:**
- Modify: `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf`(服务器)
- [ ] **Step 1: SSH 登录服务器**
```bash
ssh root@101.200.208.45
```
- [ ] **Step 2: 备份当前 nginx 配置**
```bash
sudo cp /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf.backup.$(date +%Y%m%d_%H%M%S)
```
- [ ] **Step 3: 编辑 nginx 配置文件**
```bash
sudo nano /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf
```
找到 `location /api/shortNovel/ {` 块(应该在第 50-65 行左右),将其修改为:
```nginx
location /api/shortNovel/ {
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;
# 禁用代理缓冲,确保 SSE 事件立即转发
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_set_header Connection '';
# 超时配置
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
```
**关键新增行:**
- `proxy_buffering off;` - 禁用响应缓冲
- `proxy_cache off;` - 禁用缓存
- `chunked_transfer_encoding on;` - 启用分块传输
- `proxy_set_header Connection '';` - 清除 Connection 头
- [ ] **Step 4: 验证 nginx 配置语法**
```bash
sudo nginx -t
```
预期输出:
```
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
- [ ] **Step 5: 重新加载 nginx 配置**
```bash
sudo systemctl reload nginx
```
- [ ] **Step 6: 验证 nginx 服务状态**
```bash
sudo systemctl status nginx
```
预期输出:`active (running)`
- [ ] **Step 7: 提交 nginx 配置变更(到 git**
```bash
# 在本地创建 nginx 配置文档记录变更
cat > docs/nginx-config-changes/2026-07-21-shortnovel-sse-optimization.md << 'EOF'
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 nginx 配置优化,禁用 SSE 响应缓冲
---
# nginx 配置优化 - ShortNovel SSE
**文件:** `/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf`
**变更内容:**
`location /api/shortNovel/` 块中添加了以下配置:
- `proxy_buffering off;` - 禁用代理缓冲
- `proxy_cache off;` - 禁用缓存
- `chunked_transfer_encoding on;` - 启用分块传输编码
- `proxy_set_header Connection '';` - 清除 Connection 头
**原因:**
- nginx 默认启用 `proxy_buffering on`,会缓冲后端响应
- 对于 SSEServer-Sent Events)长连接,缓冲会导致事件无法立即到达前端
- 禁用缓冲后,每个事件都能立即转发给客户端,实现真正的流式传输
**验证:**
- 使用 `curl -N https://lifescript.happylifeos.com/api/shortNovel/stream` 测试
- 应该能够逐条接收 SSE 事件,而不是等待所有事件完成后一次性接收
EOF
git add docs/nginx-config-changes/2026-07-21-shortnovel-sse-optimization.md
git commit -m "docs: 记录 nginx SSE 配置优化"
```
---
## Task 3: 前端事件处理优化
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:1650-1843`
- [ ] **Step 1: 在 handleShortNovelEvent 函数开头添加日志**
打开 `mini-program/src/pages/main/ScriptView.vue`,在第 1691 行(`const handleShortNovelEvent = (event) => {` 之后)修改为:
```javascript
const handleShortNovelEvent = (event) => {
const { type, session_id, payload = {} } = event
const timestamp = Date.now()
console.log('[ScriptView] 收到事件:', { type, session_id, timestamp, payload })
if (session_id) novelSessionId.value = session_id
```
- [ ] **Step 2: 在 novel_delta case 中添加日志**
在第 1715 行(`case 'novel_delta': {` 之后)修改为:
```javascript
case 'novel_delta': {
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
if (lastNovel) {
const delta = payload.delta || ''
console.log('[ScriptView] novel_delta:', {
deltaLength: delta.length,
currentLength: lastNovel.content.length,
timestamp
})
lastNovel.content += delta
}
keepResultAtBottom()
break
}
```
- [ ] **Step 3: 在 novel_done case 中添加日志**
在第 1724 行(`case 'novel_done': {` 之后)修改为:
```javascript
case 'novel_done': {
console.log('[ScriptView] novel_done:', {
scriptId: payload.scriptId,
hasFullText: !!payload.full_text,
timestamp
})
// 标记最后一条 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'
pendingNextResponse.value = false
persistResultMessages()
store.fetchScripts()
break
}
```
- [ ] **Step 4: 在 startNovelGeneration 函数中添加日志确认 firstQuery 设置**
在第 1650 行(`firstQuery.value = text` 之后)添加:
```javascript
firstQuery.value = text // 记住首次心愿,用于后续 followup 保存剧本
console.log('[ScriptView] 设置 firstQuery:', { text, length: text.length })
```
- [ ] **Step 5: 在 submitClarification 函数中添加日志**
在第 1768 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] submitClarification:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 6: 在 confirmOutline 函数中添加日志**
在第 1786 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] confirmOutline:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 7: 在 modifyOutline 函数中添加日志**
在第 1802 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] modifyOutline:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 8: 在 resumeSession 函数中添加日志**
在第 1826 行(`currentStreamTask.value = followupStream({` 之前)添加:
```javascript
console.log('[ScriptView] resumeSession:', {
sessionId: novelSessionId.value,
originalQuery: firstQuery.value,
originalQueryLength: firstQuery.value?.length
})
```
- [ ] **Step 9: 构建小程序验证**
```bash
cd mini-program
npm run build:mp-weixin
```
预期输出:`Build complete.`
- [ ] **Step 10: 提交前端修改**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 前端添加 SSE 事件诊断日志,确认 originalQuery 传递"
```
---
## Task 4: 部署与验证
**Files:**
- 无文件修改,纯部署和验证任务
- [ ] **Step 1: 上传后端 JAR 到服务器**
```bash
scp server/target/server-1.0.0.jar root@101.200.208.45:/data/programs/emotion-museum/emotion-single-1.0.0.jar
```
预期输出:`100%` 传输完成
- [ ] **Step 2: 重启后端服务**
```bash
ssh root@101.200.208.45 "cd /data/programs/emotion-museum && ./deploy-server.sh test"
```
预期输出:`服务启动成功!`
- [ ] **Step 3: 验证后端日志 - 检查事件逐条到达**
```bash
ssh root@101.200.208.45 "tail -f /data/logs/emotion-museum/emotion-single.log | grep 'ShortNovel SSE'"
```
然后在浏览器中触发一次小说生成,观察日志应该显示:
- `[ShortNovel SSE] 开始读取上游响应`
- 多个 `[ShortNovel SSE] 处理事件: type=novel_delta, timestamp=...`(时间戳应该逐条递增,间隔 > 100ms)
- `[ShortNovel SSE] novel_done 事件: originalQuery=..., payload=...`
- `[ShortNovel SSE] 开始保存小说: ...`
- `[ShortNovel SSE] 小说保存成功: scriptId=...`
- `[ShortNovel SSE] 完成读取上游响应`
- [ ] **Step 4: 验证前端日志 - 检查事件逐字显示**
在微信开发者工具或浏览器中打开小程序,进入小说生成页面:
1. 输入心愿文本并生成
2. 打开 Console 面板
3. 观察日志应该显示:
- `[ScriptView] 设置 firstQuery: {text: "...", length: ...}`
- `[ScriptView] 收到事件: {type: "novel_start", ...}`
- 多个 `[ScriptView] novel_delta: {deltaLength: 1-5, currentLength: ..., timestamp: ...}`(应该逐条出现)
- `[ScriptView] novel_done: {scriptId: "...", hasFullText: true, ...}`
4. **关键验证点:** 小说内容应该逐字显示,而不是一次性出现
- [ ] **Step 5: 验证历史列表保存**
1. 完成一次完整的小说生成流程
2. 返回主页,进入"历史"页面
3. 验证新生成的小说是否出现在列表中
4. 点击该小说,确认内容完整
- [ ] **Step 6: 验证数据库记录**
```bash
ssh root@101.200.208.45 "mysql -u root -pEmotionMuseum2025*# emotion_museum -e \"SELECT id, user_id, prompt, LEFT(content, 50) as content_preview, create_time FROM t_epic_script ORDER BY create_time DESC LIMIT 5;\""
```
预期输出:应该看到最新的记录,`create_time` 应该是刚才生成的时间
- [ ] **Step 7: 如果流式输出仍然不工作,检查上游服务**
如果日志显示所有 `novel_delta` 事件的时间戳都在同一毫秒,说明上游服务没有真正流式发送:
```bash
ssh root@101.200.208.45 "tail -100 /data/logs/emotion-museum/emotion-single.log | grep 'novel_delta' | awk '{print \$1, \$2}' | uniq -c"
```
如果输出显示所有事件都在同一秒,需要联系上游服务提供者确认是否支持真正的流式传输。
- [ ] **Step 8: 提交部署验证文档**
```bash
cat > docs/deployment-verification/2026-07-21-sse-streaming-fix.md << 'EOF'
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 SSE 流式传输修复的部署验证过程
---
# SSE 流式传输修复 - 部署验证
**部署时间:** 2026-07-21
**修改内容:**
1. 后端:使用 `readUtf8LineStrict()` + 显式 flush
2. nginx:添加 `proxy_buffering off` 等配置
3. 前端:添加诊断日志
**验证结果:**
- [ ] 后端日志显示事件逐条到达(时间间隔 > 100ms)
- [ ] 前端 Console 显示 novel_delta 事件逐条接收
- [ ] 小说内容逐字显示
- [ ] 历史列表正确保存新生成的小说
- [ ] 数据库 t_epic_script 表有新记录
**问题与解决:**
(在此记录遇到的问题和解决方案)
EOF
git add docs/deployment-verification/2026-07-21-sse-streaming-fix.md
git commit -m "docs: 记录 SSE 流式传输修复的部署验证"
```
---
## Task 5: 测试用例执行
**Files:**
- 无文件修改,纯测试任务
- [ ] **Step 1: 执行测试用例 1 - 完整生成流程**
1. 输入心愿文本:"我想写一个关于时间旅行的故事"
2. 回答澄清问题(例如选择时代背景)
3. 确认大纲
4. 等待小说生成完成
5. **验证:**
- 前端逐字显示小说内容(观察 Console 日志)
- 后端日志显示事件逐条到达(`tail -f` 查看)
- 数据库中出现新记录
- 历史列表显示新生成的小说
- [ ] **Step 2: 执行测试用例 2 - 修改大纲后重新生成**
1. 输入心愿文本
2. 回答澄清问题
3. 点击"修改大纲",填写修改意见
4. 等待小说生成完成
5. **验证:** 同测试用例 1
- [ ] **Step 3: 执行测试用例 3 - 继续之前的创作**
1. 输入心愿文本
2. 回答澄清问题
3. 中断流程(关闭页面或刷新)
4. 重新进入页面,点击"继续创作"
5. 等待小说生成完成
6. **验证:** 同测试用例 1
- [ ] **Step 4: 记录测试结果**
```bash
cat > docs/test-results/2026-07-21-sse-streaming-test.md << 'EOF'
---
author: AI Assistant
created_at: 2026-07-21
purpose: 记录 SSE 流式传输修复的测试结果
---
# SSE 流式传输修复 - 测试结果
**测试时间:** 2026-07-21
## 测试用例 1:完整生成流程
- [ ] 前端逐字显示
- [ ] 后端日志显示事件逐条到达
- [ ] 数据库记录正确
- [ ] 历史列表显示
**测试结果:** PASS / FAIL
**问题描述:**(如果有)
## 测试用例 2:修改大纲后重新生成
- [ ] 前端逐字显示
- [ ] 后端日志显示事件逐条到达
- [ ] 数据库记录正确
- [ ] 历史列表显示
**测试结果:** PASS / FAIL
## 测试用例 3:继续之前的创作
- [ ] 前端逐字显示
- [ ] 后端日志显示事件逐条到达
- [ ] 数据库记录正确
- [ ] 历史列表显示
**测试结果:** PASS / FAIL
## 总结
(在此总结测试结果和发现的问题)
EOF
git add docs/test-results/2026-07-21-sse-streaming-test.md
git commit -m "docs: 记录 SSE 流式传输修复的测试结果"
```
---
## 完成标准
1. ✅ 所有后端日志显示事件逐条到达(时间间隔 > 100ms)
2. ✅ 前端 Console 显示 novel_delta 事件逐条接收
3. ✅ 小说内容在前端逐字显示,用户可以观察到文字逐个出现
4. ✅ 每次成功的小说生成都保存到数据库
5. ✅ 历史列表正确显示新生成的小说
6. ✅ 所有测试用例通过
---
## 风险评估与缓解
### 风险 1:上游服务不支持真正的流式传输
**检测方法:** 查看后端日志,如果所有 `novel_delta` 事件的时间戳都在同一毫秒,说明上游服务一次性发送了所有数据。
**缓解措施:** 联系上游服务提供者(`http://49.232.138.53:8010`),确认是否支持真正的流式传输。
### 风险 2nginx 配置不生效
**检测方法:** 使用 `curl -N` 测试 SSE 接口,观察是否逐条接收事件。
**缓解措施:** 检查 nginx error log,确认配置语法正确;检查是否有其他 location 块覆盖了 `/api/shortNovel/` 的配置。
### 风险 3SseEmitter flush 不工作
**检测方法:** 查看后端日志,确认事件处理时间戳和发送时间戳的差异。
**缓解措施:** 如果 `emitter.send(SseEmitter.event().comment(""))` 仍然不 flush,考虑更换为 `ResponseBodyEmitter``StreamingResponseBody`
@@ -0,0 +1,490 @@
# H5 模式 Stream 超时修复实施计划
> **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:** 修复小程序 H5 模式下 stream 请求 60 秒超时问题,使用 fetch + ReadableStream + AbortController 实现真正的长连接流式读取
**Architecture:** 在 `mini-program/src/services/shortNovel.js` 中实现双路径策略 - H5 环境使用浏览器原生 fetch API(支持自定义超时),小程序环境保持原有 uni.request 逻辑
**Tech Stack:** Vue 3 (UniApp), fetch API, ReadableStream, AbortController, TextDecoder
---
## 文件结构映射
**修改的文件:**
1. `mini-program/src/services/shortNovel.js` - 添加 H5 环境检测和 fetch 实现
**不创建新文件** - 所有修改都是在现有文件中添加 H5 路径实现
---
## Task 1: 添加环境检测常量和 H5 SSE 核心函数
**Files:**
- Modify: `mini-program/src/services/shortNovel.js`
- [ ] **Step 1: 在文件顶部添加环境检测常量**
打开 `mini-program/src/services/shortNovel.js`,在第 1 行(import 语句之后)添加:
```javascript
// 环境检测:H5 模式下存在 window.fetch
const isH5 = typeof window !== 'undefined' && typeof window.fetch === 'function'
// SSE 请求超时时间(毫秒)
const SSE_TIMEOUT_MS = 300000
```
- [ ] **Step 2: 添加 H5 SSE 事件解析函数**
`decodeChunk` 函数(第 112-140 行)之后、`consumeSseText` 函数之前,添加新的 `h5ConsumeSseText` 函数:
```javascript
/**
* H5 环境 SSE 事件解析
* 解析单个 SSE 事件块
* 事件格式:data: {"type":"status","session_id":"xxx","payload":{...}}
*/
function h5ConsumeSseText(text, onEvent, onError) {
const lines = text.split('\n')
let dataBuffer = ''
for (const line of lines) {
if (line.startsWith('data:')) {
dataBuffer += line.slice(5).trim()
} else if (line === '' && dataBuffer) {
// 空行表示事件结束
const dataStr = dataBuffer
dataBuffer = ''
if (dataStr === '[DONE]') return
try {
const event = JSON.parse(dataStr)
onEvent?.(event)
} catch (e) {
onError?.(`SSE 解析失败: ${e.message}`)
}
}
}
}
```
- [ ] **Step 3: 添加 H5 SSE 流式读取核心函数**
`h5ConsumeSseText` 函数之后,添加 `h5NovelStream` 函数:
```javascript
/**
* H5 环境 SSE 流式读取核心实现
* 使用浏览器原生 fetch + ReadableStream + AbortController
* @param {string} url - 请求 URL
* @param {Object} body - 请求体
* @param {Function} onEvent - 事件回调
* @param {Function} onError - 错误回调
* @returns {Object} 包含 abort 方法的对象
*/
function h5NovelStream(url, body, onEvent, onError) {
// 1. 创建 AbortController 用于超时控制
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), SSE_TIMEOUT_MS)
// 2. 发起 fetch 请求
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
...getAuthHeader()
},
body: JSON.stringify(body),
signal: controller.signal
}).then(response => {
// 3. 检查响应状态
if (!response.ok) {
clearTimeout(timeoutId)
onError?.(`请求失败: HTTP ${response.status}`)
return
}
// 4. 获取 ReadableStream 读取器
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
// 5. 循环读取流数据
function pump() {
return reader.read().then(({ done, value }) => {
if (done) {
clearTimeout(timeoutId)
// 处理缓冲区残留数据
if (buffer.trim()) h5ConsumeSseText(buffer, onEvent, onError)
return
}
// 6. 解码二进制块为文本
buffer += decoder.decode(value, { stream: true })
// 7. 解析完整的事件(按 \n\n 分隔)
const events = buffer.split('\n\n')
buffer = events.pop() // 最后一个可能不完整,保留到下次
for (const event of events) {
h5ConsumeSseText(event, onEvent, onError)
}
// 8. 继续读取
return pump()
})
}
return pump()
}).catch(err => {
clearTimeout(timeoutId)
if (err.name === 'AbortError') {
onError?.('请求超时(300秒)')
} else {
onError?.(err.message || '网络请求失败')
}
})
// 9. 返回 abort 接口
return {
abort: () => {
clearTimeout(timeoutId)
controller.abort()
}
}
}
```
---
## Task 2: 修改 startNovelStream 函数支持 H5 环境
**Files:**
- Modify: `mini-program/src/services/shortNovel.js:16-55`
- [ ] **Step 1: 在 startNovelStream 函数开头添加 H5 分支**
打开 `mini-program/src/services/shortNovel.js`,定位到第 16 行的 `startNovelStream` 函数,将其修改为:
```javascript
export const startNovelStream = ({ query, onEvent, onError }) => {
// H5 环境:使用原生 fetch + ReadableStream
if (isH5) {
return h5NovelStream(
`${getApiBaseUrl()}/shortNovel/stream`,
{ query },
onEvent,
onError
)
}
// 小程序环境:使用 uni.request
let chunkProcessed = false
const task = uni.request({
url: `${getApiBaseUrl()}/shortNovel/stream`,
method: 'POST',
data: { query },
header: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
...getAuthHeader()
},
enableChunked: true,
timeout: SSE_TIMEOUT_MS,
success: (res) => {
if (res.statusCode >= 400) {
onError?.(res.data?.message || '请求失败')
return
}
// 仅在 chunk 未处理时才处理完整 data(避免 H5 双重消费)
if (!chunkProcessed && typeof res.data === 'string' && res.data) {
consumeSseText(res.data, onEvent, onError)
}
},
fail: (error) => {
onError?.(error.errMsg || '网络请求失败')
}
})
task?.onChunkReceived?.((res) => {
chunkProcessed = true
try {
const text = decodeChunk(res.data)
consumeSseText(text, onEvent, onError)
} catch (error) {
onError?.(error.message || '流式解析失败')
}
})
return task
}
```
---
## Task 3: 修改 followupStream 函数支持 H5 环境
**Files:**
- Modify: `mini-program/src/services/shortNovel.js:68-106`
- [ ] **Step 1: 在 followupStream 函数开头添加 H5 分支**
打开 `mini-program/src/services/shortNovel.js`,定位到第 68 行的 `followupStream` 函数,将其修改为:
```javascript
export const followupStream = ({ sessionId, action, payload, originalQuery, onEvent, onError }) => {
// H5 环境:使用原生 fetch + ReadableStream
if (isH5) {
return h5NovelStream(
`${getApiBaseUrl()}/shortNovel/followup`,
{ sessionId, action, payload, originalQuery },
onEvent,
onError
)
}
// 小程序环境:使用 uni.request
let chunkProcessed = false
const task = uni.request({
url: `${getApiBaseUrl()}/shortNovel/followup`,
method: 'POST',
data: { sessionId, action, payload, originalQuery },
header: {
'Content-Type': 'application/json',
'Accept': 'text/event-stream',
...getAuthHeader()
},
enableChunked: true,
timeout: SSE_TIMEOUT_MS,
success: (res) => {
if (res.statusCode >= 400) {
onError?.(res.data?.message || '请求失败')
return
}
if (!chunkProcessed && typeof res.data === 'string' && res.data) {
consumeSseText(res.data, onEvent, onError)
}
},
fail: (error) => {
onError?.(error.errMsg || '网络请求失败')
}
})
task?.onChunkReceived?.((res) => {
chunkProcessed = true
try {
const text = decodeChunk(res.data)
consumeSseText(text, onEvent, onError)
} catch (error) {
onError?.(error.message || '流式解析失败')
}
})
return task
}
```
---
## Task 4: 构建验证
**Files:**
- 无文件修改,纯构建验证
- [ ] **Step 1: 构建小程序验证语法正确**
```bash
cd mini-program
npm run build:mp-weixin
```
预期输出:`DONE Build complete.`
如果构建失败,检查 `shortNovel.js` 的语法错误并修复。
- [ ] **Step 2: 启动 H5 开发服务器**
```bash
cd mini-program
npm run dev:h5
```
预期输出:服务器在 `http://localhost:5284` 启动
---
## Task 5: 浏览器验证 - H5 模式
**Files:**
- 无文件修改,纯验证任务
- [ ] **Step 1: 访问 H5 页面**
使用浏览器打开 `http://localhost:5284`,登录后进入"心愿实现"页面。
- [ ] **Step 2: 触发 stream 请求**
1. 输入心愿文本(例如:"我想写一个关于时间旅行的故事")
2. 点击生成按钮
3. **验证**:页面进入等待状态
- [ ] **Step 3: 检查 Network 面板**
打开浏览器 DevTools 的 Network 面板:
1. 找到 `/shortNovel/stream` 请求
2. **验证**
- 请求状态为 200(不是 `(failed)`
- 请求在大约 67 秒后仍然保持连接(不会 60 秒后中断)
- 收到 `status` 事件
- 收到 `clarification_card` 事件
- [ ] **Step 4: 检查 Console 面板**
打开 Console 面板,验证:
- 没有 "请求超时" 错误
- 没有 fetch 相关错误
- 正常接收到所有 SSE 事件
- [ ] **Step 5: 验证澄清卡片显示**
确认页面上正确显示澄清卡片,用户可以正常交互(选项可点击切换)。
---
## Task 6: 验证小程序模式不受影响
**Files:**
- 无文件修改,纯验证任务
- [ ] **Step 1: 在微信开发者工具中打开小程序**
1. 打开微信开发者工具
2. 导入 `mini-program/unpackage/dist/dev/mp-weixin` 目录
- [ ] **Step 2: 测试小说生成流程**
1. 进入"心愿实现"页面
2. 输入心愿文本
3. 完成澄清卡片、大纲确认等流程
4. **验证**:原有 uni.request 路径正常工作
- [ ] **Step 3: 检查 Network 请求**
在小程序调试器中查看网络请求:
- `/shortNovel/stream` 请求状态正常
- 事件正常接收
---
## Task 7: 验证服务器日志
**Files:**
- 无文件修改,纯验证任务
- [ ] **Step 1: 下载服务器日志**
```bash
cd G:/IdeaProjects/emotion-museun
python tools/download-server-log.py latest
```
- [ ] **Step 2: 搜索 Broken pipe 错误**
```bash
python tools/download-server-log.py grep "Broken pipe" 10
```
**验证**:H5 模式测试后,服务器日志中不再出现 `Broken pipe` 错误。
- [ ] **Step 3: 验证 SSE 事件正常转发**
```bash
python tools/download-server-log.py grep "ShortNovel SSE" 20
```
**验证**:日志显示完整的事件处理流程:
- 开始读取上游响应
- 处理 status 事件
- 处理 clarification_card 事件
- 完成读取上游响应
---
## Task 8: 提交修改
**Files:**
- Modify: `mini-program/src/services/shortNovel.js`
- [ ] **Step 1: 检查文件改动**
```bash
cd G:/IdeaProjects/emotion-museun
git diff mini-program/src/services/shortNovel.js
```
**验证**:diff 显示添加了 H5 路径实现,保持了原有的 uni.request 逻辑。
- [ ] **Step 2: 提交修改**
```bash
git add mini-program/src/services/shortNovel.js
git commit -m "fix: 修复 H5 模式 stream 请求 60 秒超时问题
- 在 H5 环境使用 fetch + ReadableStream + AbortController
- 支持自定义 300 秒超时
- 小程序环境保持原有 uni.request 逻辑不变"
```
---
## 完成标准
1. ✅ `shortNovel.js` 添加了 H5 路径实现(`h5NovelStream``h5ConsumeSseText`
2. ✅ `startNovelStream``followupStream` 根据环境自动分发
3. ✅ 小程序构建成功,无语法错误
4. ✅ H5 模式测试通过,stream 请求 67 秒后仍能正常接收事件
5. ✅ 浏览器 Network 面板不再显示 `(failed)` 状态
6. ✅ 小程序模式验证通过,原有功能不受影响
7. ✅ 服务器日志中 `Broken pipe` 错误消失
8. ✅ 修改提交到 git
---
## 风险评估与缓解
### 风险 1:浏览器兼容性
**检测方法**:在目标浏览器中运行 H5 开发服务器,触发 stream 请求
**缓解措施**:通过 `typeof window.fetch === 'function'` 检测,对不支持的环境降级或显示明确错误。
### 风险 2H5 环境检测失败
**检测方法**:如果 `isH5` 检测错误,可能在 H5 环境下走了小程序分支
**缓解措施**:检测条件同时检查 `typeof window !== 'undefined'``typeof window.fetch === 'function'`,确保只在现代浏览器中启用 H5 路径。
### 风险 3abort 接口不兼容
**检测方法**:检查 ScriptView.vue 是否调用 `task.abort()` 方法
**缓解措施**H5 实现返回 `{ abort: () => {...} }` 对象,与 uni.request 任务对象的 `abort` 方法对齐。如果 ScriptView.vue 使用其他属性,需要调整。
### 风险 4SSE 格式差异
**检测方法**:观察服务器发送的 SSE 事件格式
**缓解措施**:使用 `dataBuffer` 累积 `data:` 行,遇到空行才解析,符合 SSE 规范。如果某些事件不带空行分隔,可能需要调整解析逻辑。
---
## 自检清单
实施前请确认:
- [ ] 已阅读设计文档 `docs/superpowers/specs/2026-07-22-stream-timeout-fix-design.md`
- [ ] 已阅读 `mini-program/src/services/shortNovel.js` 当前实现
- [ ] 理解 H5 环境和 mp-weixin 环境的差异
- [ ] 准备好本地 H5 开发服务器(端口 5284)
@@ -0,0 +1,135 @@
# 详情页澄清选项卡显示不全修复 实施计划
> **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:** 修复后端 `forwardSse` 首次 stream 中第一个 clarification_card 因 sessionId 为 null 不累积的 bug,使详情页澄清选项卡数量与生成页完全一致。
**Architecture:** 在 `ShortNovelServiceImpl.forwardSse` 的事件循环中,每次解析 SSE 事件后,从事件**顶层**取 `session_id` 同步 `currentSessionId[0]`(与前端 `ScriptView.vue:1805` 取 sessionId 的方式对齐)。这样无论 status 事件是否在 payload 内携带 session_id,只要事件顶层有 session_id,首次 stream 的 `currentSessionId[0]` 就能被正确设置,首次 stream 里的 `clarification_card` 就能被累积。
**Tech Stack:** Java 17 / Spring Boot 2.7.18 / fastjson2 / OkHttp / MySQL / Maven / Python 部署脚本 / H5 端到端验收
## Global Constraints
- 编译命令:`mvn clean install -DskipTests`(禁止 `mvn clean compile`
- 部署命令:`python deploy.py backend`(根目录脚本)
- 部署前必须本地编译通过
- 部署后必须通过 H5 端到端验收(Console 0 新增错误 + Network 接口正常)
- 注释必须使用中文
- 禁止任何形式的 mock、兜底、默认值掩盖错误
- 只改后端 1 个方法(`forwardSse`),零前端改动,零数据库变更
---
## Task 1: 后端修复 forwardSse 顶层取 session_id
**Files:**
- Modify: `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java:304-308``forwardSse` 方法的事件循环内,status 处理之前)
**Interfaces:**
- Consumes: 上游 SSE 事件 JSON 结构(事件顶层 `session_id` 字段与 `type` 同级)
- Produces: `currentSessionId[0]` 在首次 stream 里也能被正确设置(不再是 null)
### 背景
- **根因**:上游 SSE 事件的 `session_id` 在事件**顶层**(与 `type` 同级),但后端 `forwardSse` 行 312-314 从 `payload.getString("session_id")` 取,取不到。导致首次 stream 的 `currentSessionId[0]` 保持 null`knownSessionId=null`)。
- **数据证据**DB 中 3 个有 clarification 的 conversation 全部是 `clarification_question=2 + clarification_answer=3`。第一个 `clarification_question``currentSessionId[0]==null` 不累积。
- **修复思路**:在 status 处理之前,从事件顶层取 `session_id` 同步 `currentSessionId[0]`(与前端 `ScriptView.vue:1805-1806` 逻辑对齐)。
### 步骤
- [ ] **Step 1: 在 forwardSse 事件循环中插入从顶层取 session_id 的代码**
打开 `server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java`,定位到 `forwardSse` 方法内的事件循环。当前代码(行 304-311):
```java
try {
JSONObject event = JSON.parseObject(dataStr);
String type = event.getString("type");
long eventTimestamp = System.currentTimeMillis();
log.info("[ShortNovel SSE] 处理事件: type={}, timestamp={}", type, eventTimestamp);
// 拦截 status 事件,缓存 sessionId → originalQuery 映射(供 followup 兜底)
if ("status".equals(type)) {
```
`log.info(...)` 之后、`// 拦截 status 事件` 注释之前,**插入以下 5 行代码**:
```java
// 从事件顶层取 session_id 同步 sessionId
// 上游事件结构与前端对齐:session_id 与 type 同级,不在 payload 内
// 修复:首次 stream 里 status 处理从 payload 取不到 session_id,导致 currentSessionId 保持 null
// 第一个 clarification_card 因 sessionId 为 null 不累积,详情页少显示第一个选项卡
String topSessionId = event.getString("session_id");
if (topSessionId != null && !topSessionId.isEmpty()) {
currentSessionId[0] = topSessionId;
}
```
插入后整体结构(行 304-320):
```java
try {
JSONObject event = JSON.parseObject(dataStr);
String type = event.getString("type");
long eventTimestamp = System.currentTimeMillis();
log.info("[ShortNovel SSE] 处理事件: type={}, timestamp={}", type, eventTimestamp);
// 从事件顶层取 session_id 同步 sessionId
// 上游事件结构与前端对齐:session_id 与 type 同级,不在 payload 内
// 修复:首次 stream 里 status 处理从 payload 取不到 session_id,导致 currentSessionId 保持 null
// 第一个 clarification_card 因 sessionId 为 null 不累积,详情页少显示第一个选项卡
String topSessionId = event.getString("session_id");
if (topSessionId != null && !topSessionId.isEmpty()) {
currentSessionId[0] = topSessionId;
}
// 拦截 status 事件,缓存 sessionId → originalQuery 映射(供 followup 兜底)
if ("status".equals(type)) {
```
- [ ] **Step 2: 本地编译验证**
Run: `cd server && mvn clean install -DskipTests`
Expected: `BUILD SUCCESS`,零错误。
- [ ] **Step 3: 部署到远程服务器**
Run: `python deploy.py backend`
Expected: 部署脚本输出"部署成功",服务重启完成。
如果部署脚本在 Windows 报 PATH 问题,手动执行:
```bash
scp server/target/emotion-single-1.0.0.jar root@101.200.208.45:/data/programs/emotion-museum/
ssh root@101.200.208.45 "cd /data/programs/emotion-museum && ./restart.sh emotion-museum-single"
```
- [ ] **Step 4: H5 端到端验收**
1. 启动 mini-program H5`python dev-services.py start mini-program`(端口 5180
2. 浏览器访问 `http://localhost:5180/#/pages/main/index?tab=script`
3. 手动完成一次新的小说生成(包含至少 2 轮澄清)
4. 生成过程中**记录生成页显示的选项卡数量 N**
5. 生成完成后,进入该剧本的详情页(通过列表页点"历史"进入 ScriptLibraryView,再点击对应剧本)
6. 验证详情页显示 **N 个澄清选项卡**(与生成页完全一致)
7. 浏览器 Console 检查:0 新增错误(既有的 `uni.getRecorderManager` 错误除外)
8. Network 面板:`listByConversation` 接口响应中 `clarification_question` 类型消息数量 = 生成页澄清轮数
- [ ] **Step 5: 提交代码**
```bash
cd G:\IdeaProjects\emotion-museun
git add server/src/main/java/com/emotion/service/impl/ShortNovelServiceImpl.java
git commit -m "fix(server): 修复 forwardSse 首次 stream 第一个 clarification_card 不累积的 bug
上游 SSE 事件的 session_id 在事件顶层(与 type 同级),后端原从 payload.session_id 取导致首次 stream 取不到,
currentSessionId 保持 null,第一个 clarification_card 不累积。
改为从事件顶层取 session_id 同步 currentSessionId(与前端 ScriptView.vue:1805 逻辑对齐)。
修复后新剧本详情页澄清选项卡数量与生成页完全一致。"
```
### 验收标准
- ✅ 本地 `mvn clean install` 编译通过
- ✅ 部署到远程服务器成功
- ✅ 新剧本详情页澄清选项卡数量 = 生成页澄清选项卡数量
- ✅ Console 0 新增错误
- ✅ 老剧本(3 个已有 conversation)保持现状,仍显示 2 个 card(数据已永久丢失,符合预期)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,605 @@
# 小说消息操作按钮恢复实施计划
> **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:** 恢复小程序小说生成页和详情页丢失的"复制、播放、折叠/展开"功能,每条 novel 消息下方添加 3 个操作按钮。
**Architecture:** 在 ScriptView.vue 和 ScriptDetailView.vue 中为 `kind === 'novel'` 的消息添加按钮组 UI(折叠/展开、复制、播放)。ScriptView 复用已有函数,ScriptDetailView 新增相同逻辑。按钮组样式统一,参考 MessageCard 的 result-actions 样式。
**Tech Stack:** Vue 3 Composition API / UniApp / Pinia / uni.setClipboardData / useTtsPlayer composable / H5 端到端验收
## Global Constraints
- 所有注释必须使用中文
- 样式单位必须使用 rpx(小程序移动端)
- 禁止任何形式的 mock、兜底、默认值掩盖错误
- 每个按钮必须有真实实现,不能有任何占位或 mock 功能
- 修改后必须通过 H5 端到端验收(Console 0 新增错误)
- 部署使用 `python deploy.py backend`(后端)或热加载(前端)
- 埋点必须完整触发:`script_message_collapse_toggle``script_message_copy_click``script_message_tts_click`
---
## Task 1: ScriptView 恢复按钮组 UI
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:180-187`novel 消息渲染部分)
- Modify: `mini-program/src/pages/main/ScriptView.vue:3500+`(样式部分)
**Interfaces:**
- Consumes: 已有函数 `getMessageDisplayContent(msg)``toggleMessageCollapse(msg)``copyMessageContent(msg)``playMessageTts(msg)``isMessageCollapsed(msg)`
- Produces: novel 消息下方显示 3 个按钮(折叠/展开、复制、播放),功能正常
### 背景
ScriptView.vue 中所有相关函数已存在(行 399-447),但模板中按钮组 UI 在 commit 2c778fa 中被删除。本任务恢复按钮组 UI。
### 步骤
- [ ] **Step 1: 定位 novel 消息渲染部分**
打开 `mini-program/src/pages/main/ScriptView.vue`,找到 `kind === 'novel'` 的消息渲染部分(约行 180-187):
```vue
<view v-else-if="msg.kind === 'novel'" class="chat-bubble system novel-bubble">
<Markdown :content="msg.content" />
</view>
```
- [ ] **Step 2: 修改 Markdown 渲染,使用折叠内容**
`<Markdown :content="msg.content" />` 改为:
```vue
<Markdown :content="getMessageDisplayContent(msg)" />
```
这样折叠时只显示前 200 字符。
- [ ] **Step 3: 添加折叠/展开按钮**
`<Markdown>` 下方添加折叠/展开按钮:
```vue
<view v-if="msg.content && msg.content.length > 200" class="collapse-toggle-row" @click="toggleMessageCollapse(msg)">
<text class="collapse-toggle-text">{{ isMessageCollapsed(msg) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(msg) }">
<view></view>
<view></view>
</view>
</view>
```
- [ ] **Step 4: 添加操作按钮组**
在折叠/展开按钮下方添加操作按钮组(复制、播放):
```vue
<view class="message-actions">
<button class="action-btn" @click="copyMessageContent(msg)">
<text>📋 复制</text>
</button>
<button class="action-btn primary" @click="playMessageTts(msg)">
<text class="action-icon">{{ ttsPlayer.playing.value ? 'Ⅱ' : '▶' }}</text>
<text>{{ ttsPlayer.playing.value ? '暂停' : '播放' }}</text>
</button>
</view>
```
注意:只有 2 个按钮(复制、播放),折叠/展开按钮已在上方独立显示。
- [ ] **Step 5: 添加按钮组样式**
`<style scoped>` 部分(约行 3500+)添加以下样式:
```css
/* 折叠/展开按钮行 */
.collapse-toggle-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 20rpx;
padding: 16rpx 0;
border-radius: 999rpx;
background: rgba(88, 28, 135, 0.12);
border: 1rpx solid rgba(192, 132, 252, 0.28);
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 700;
}
.collapse-toggle-row:active {
transform: scale(0.98);
opacity: 0.92;
}
.collapse-toggle-text {
line-height: 1;
}
/* 折叠箭头图标 */
.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;
}
/* 操作按钮组 */
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 20rpx;
}
.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;
}
```
- [ ] **Step 6: H5 验收**
启动 H5 并验收:
```bash
python dev-services.py start mini-program
```
浏览器访问 `http://localhost:5180/#/pages/main/index?tab=script`,进入一个已有小说的生成页:
验收项:
- ✅ 小说正文下方有折叠/展开按钮(内容超过 200 字时显示)
- ✅ 点击"展开全文":内容完整显示,按钮变为"收起全文"
- ✅ 点击"收起全文":内容截断显示前 200 字,按钮变为"展开全文"
- ✅ 小说正文下方有 2 个操作按钮:复制、播放
- ✅ 点击"复制"Toast 提示"已复制",剪贴板中有内容
- ✅ 点击"播放"TTS 开始播放,按钮变为"暂停"
- ✅ 浏览器 Console 检查:0 新增错误
- [ ] **Step 7: Commit**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat(mini-program): ScriptView 恢复小说消息操作按钮组
- 为 kind === 'novel' 的消息添加折叠/展开、复制、播放按钮
- 复用已有函数:getMessageDisplayContent、toggleMessageCollapse、copyMessageContent、playMessageTts
- 添加 .collapse-toggle-row、.message-actions、.action-btn 样式
- H5 验收通过,Console 0 新增错误"
```
### 验收标准
- ✅ 每条 novel 消息下方有折叠/展开按钮(内容超过 200 字时显示)
- ✅ 折叠/展开功能正常:折叠时显示前 200 字,展开时显示完整内容
- ✅ 每条 novel 消息下方有 2 个操作按钮:复制、播放
- ✅ 复制功能正常:点击后 Toast 提示"已复制"
- ✅ 播放功能正常:点击后 TTS 开始播放,按钮变为"暂停"
- ✅ Console 无新增错误
---
## Task 2: ScriptDetailView 新增函数和按钮组 UI
**Files:**
- Modify: `mini-program/src/pages/main/ScriptDetailView.vue:96-311`script 部分)
- Modify: `mini-program/src/pages/main/ScriptDetailView.vue:74-76`novel 消息渲染部分)
- Modify: `mini-program/src/pages/main/ScriptDetailView.vue:313+`(样式部分)
**Interfaces:**
- Consumes: `ttsPlayer` composable(已有)、`script` ref(已有)
- Produces: novel 消息下方显示 3 个按钮(折叠/展开、复制、播放),功能正常
### 背景
ScriptDetailView.vue 中完全没有相关函数和按钮组 UI。本任务新增所有函数 + 模板按钮组 + 样式。
### 步骤
- [ ] **Step 1: 新增 collapsedMessageIds ref**
打开 `mini-program/src/pages/main/ScriptDetailView.vue`,在 `<script setup>` 部分(约行 96-110)添加:
```javascript
const collapsedMessageIds = ref({})
```
放在 `const resultMessages = ref([])` 下方。
- [ ] **Step 2: 新增 isMessageCollapsed 函数**
`collapsedMessageIds` 下方添加:
```javascript
/**
* 判断消息是否折叠
*/
const isMessageCollapsed = (message) => {
return Boolean(collapsedMessageIds.value[message.id])
}
```
- [ ] **Step 3: 新增 getMessageDisplayContent 函数**
`isMessageCollapsed` 下方添加:
```javascript
/**
* 获取消息显示内容(折叠时截断)
*/
const getMessageDisplayContent = (message) => {
const content = String(message?.content || '')
if (!isMessageCollapsed(message)) return content
if (content.length <= 200) return content
return `${content.slice(0, 200)}...`
}
```
- [ ] **Step 4: 新增 toggleMessageCollapse 函数**
`getMessageDisplayContent` 下方添加:
```javascript
/**
* 切换消息折叠状态
*/
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value = {
...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 5: 新增 copyMessageContent 函数**
`toggleMessageCollapse` 下方添加:
```javascript
/**
* 复制消息内容到剪贴板
*/
const copyMessageContent = (message) => {
const content = String(message?.content || '')
if (!content.trim()) {
uni.showToast({ title: '暂无可复制内容', icon: 'none' })
return
}
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
analytics.track('script_message_copy_click', {
message_id: message?.id || '',
content_length: content.length
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 6: 新增 playMessageTts 函数**
`copyMessageContent` 下方添加:
```javascript
/**
* 播放消息 TTS 音频
*/
const playMessageTts = (message) => {
const scriptId = script.value?.id || ''
ttsPlayer.playSource(scriptId)
analytics.track('script_message_tts_click', {
message_id: message?.id || '',
script_id: scriptId
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 7: 定位 novel 消息渲染部分**
在模板中找到 `kind === 'novel'` 的消息渲染部分(约行 74-76):
```vue
<view v-else-if="msg.kind === 'novel'" class="chat-bubble system novel-bubble">
<Markdown :content="msg.content" />
</view>
```
- [ ] **Step 8: 修改 Markdown 渲染,使用折叠内容**
`<Markdown :content="msg.content" />` 改为:
```vue
<Markdown :content="getMessageDisplayContent(msg)" />
```
- [ ] **Step 9: 添加折叠/展开按钮**
`<Markdown>` 下方添加折叠/展开按钮:
```vue
<view v-if="msg.content && msg.content.length > 200" class="collapse-toggle-row" @click="toggleMessageCollapse(msg)">
<text class="collapse-toggle-text">{{ isMessageCollapsed(msg) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(msg) }">
<view></view>
<view></view>
</view>
</view>
```
- [ ] **Step 10: 添加操作按钮组**
在折叠/展开按钮下方添加操作按钮组(复制、播放):
```vue
<view class="message-actions">
<button class="action-btn" @click="copyMessageContent(msg)">
<text>📋 复制</text>
</button>
<button class="action-btn primary" @click="playMessageTts(msg)">
<text class="action-icon">{{ ttsPlayer.playing.value ? 'Ⅱ' : '▶' }}</text>
<text>{{ ttsPlayer.playing.value ? '暂停' : '播放' }}</text>
</button>
</view>
```
- [ ] **Step 11: 添加按钮组样式**
`<style scoped>` 部分(约行 313+)添加以下样式(与 ScriptView 完全一致):
```css
/* 折叠/展开按钮行 */
.collapse-toggle-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 20rpx;
padding: 16rpx 0;
border-radius: 999rpx;
background: rgba(88, 28, 135, 0.12);
border: 1rpx solid rgba(192, 132, 252, 0.28);
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 700;
}
.collapse-toggle-row:active {
transform: scale(0.98);
opacity: 0.92;
}
.collapse-toggle-text {
line-height: 1;
}
/* 折叠箭头图标 */
.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;
}
/* 操作按钮组 */
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 20rpx;
}
.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;
}
```
- [ ] **Step 12: H5 验收**
H5 服务应已在运行(端口 5180),浏览器访问详情页:
从列表页点击一个已有小说的剧本,进入详情页。
验收项:
- ✅ 小说正文下方有折叠/展开按钮(内容超过 200 字时显示)
- ✅ 点击"展开全文":内容完整显示,按钮变为"收起全文"
- ✅ 点击"收起全文":内容截断显示前 200 字,按钮变为"展开全文"
- ✅ 小说正文下方有 2 个操作按钮:复制、播放
- ✅ 点击"复制"Toast 提示"已复制",剪贴板中有内容
- ✅ 点击"播放"TTS 开始播放,按钮变为"暂停"
- ✅ 浏览器 Console 检查:0 新增错误
- [ ] **Step 13: Commit**
```bash
git add mini-program/src/pages/main/ScriptDetailView.vue
git commit -m "feat(mini-program): ScriptDetailView 新增小说消息操作按钮组
- 新增 collapsedMessageIds ref + 5 个函数(isMessageCollapsed、getMessageDisplayContent、toggleMessageCollapse、copyMessageContent、playMessageTts
- 为 kind === 'novel' 的消息添加折叠/展开、复制、播放按钮
- 添加 .collapse-toggle-row、.message-actions、.action-btn 样式
- H5 验收通过,Console 0 新增错误"
```
### 验收标准
- ✅ 每条 novel 消息下方有折叠/展开按钮(内容超过 200 字时显示)
- ✅ 折叠/展开功能正常:折叠时显示前 200 字,展开时显示完整内容
- ✅ 每条 novel 消息下方有 2 个操作按钮:复制、播放
- ✅ 复制功能正常:点击后 Toast 提示"已复制"
- ✅ 播放功能正常:点击后 TTS 开始播放,按钮变为"暂停"
- ✅ Console 无新增错误
- ✅ 埋点正常触发:`script_message_collapse_toggle``script_message_copy_click``script_message_tts_click`
---
## 整体验收
两个 Task 完成后,进行整体验收:
1. **生成页验收**
- 访问 `http://localhost:5180/#/pages/main/index?tab=script`
- 进入一个已有小说的生成页
- 验证折叠/展开、复制、播放功能
- Console 0 新增错误
2. **详情页验收**
- 从列表页点击一个已有小说的剧本,进入详情页
- 验证折叠/展开、复制、播放功能
- Console 0 新增错误
3. **样式一致性**
- 两个页面的按钮组样式完全一致
- 按钮布局、颜色、圆角、间距一致
4. **埋点验证**
- 浏览器 Console 中查看埋点日志
- 确认 `script_message_collapse_toggle``script_message_copy_click``script_message_tts_click` 正常触发
---
## 风险与注意事项
1. **TTS 播放的是整个小说**`ttsPlayer.playSource(scriptId)` 播放的是整个小说的音频,不是单条消息的音频。这是历史设计,保持一致。
2. **折叠阈值**:折叠时显示前 200 字符。这个阈值可以根据实际需求调整。
3. **样式一致性**:两个页面的按钮组样式必须保持一致,直接复制 ScriptView 的样式到 ScriptDetailView。
4. **性能考虑**`collapsedMessageIds` 是响应式对象,每次切换折叠状态都会触发重新渲染。对于大量消息的场景,可能需要优化(当前场景下消息数量有限,不构成问题)。
5. **热加载**:前端修改后利用热加载自动生效,不需要重启。如果热加载不生效,使用 `python dev-services.py restart mini-program`
---
## 提交历史
完成所有 Task 后,提交历史应为:
```
<hash> feat(mini-program): ScriptDetailView 新增小说消息操作按钮组
<hash> feat(mini-program): ScriptView 恢复小说消息操作按钮组
```
@@ -42,7 +42,7 @@ purpose: 设计并优化情绪博物馆跨平台服务管理脚本,修复硬
services:
backend:
name: "后端服务"
dir: "backend-single"
dir: "server"
type: "java"
port: 19089
health_check_path: "/actuator/health"
@@ -33,10 +33,10 @@ The current mini program already has the core functional modules:
The backend already has script generation support under:
- `backend-single/src/main/java/com/emotion/controller/EpicScriptController.java`
- `backend-single/src/main/java/com/emotion/service/EpicScriptService.java`
- `backend-single/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- `backend-single/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- `server/src/main/java/com/emotion/controller/EpicScriptController.java`
- `server/src/main/java/com/emotion/service/EpicScriptService.java`
- `server/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- `server/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
The missing product capability is a complete inspiration-mode flow. The backend currently exposes generic script creation, but it does not expose dedicated endpoints for inspiration recommendations, random inspiration, or one-sentence inspiration generation.
@@ -45,7 +45,7 @@ The missing product capability is a complete inspiration-mode flow. The backend
1. Redesign the mini-program pages to match the new prototype visual direction: dark cosmic background, purple glass panels, glowing primary actions, rounded bottom navigation, and stronger card hierarchy.
2. Keep the existing product functions largely unchanged.
3. Complete the functional flow from life events to script generation to script detail to path realization.
4. Add backend inspiration-mode APIs in `backend-single`.
4. Add backend inspiration-mode APIs in `server`.
5. Make the implementation modular enough to land in batches without leaving broken flows.
## Non-Goals
@@ -11,7 +11,7 @@ Add two complete but independently shippable capabilities:
The implementation should fit the current project shape:
- `backend-single`: Spring Boot 2.7, MyBatis Plus, MySQL, Redis.
- `server`: Spring Boot 2.7, MyBatis Plus, MySQL, Redis.
- `mini-program`: uni-app/Vue.
- `web-admin`: Vue, Element Plus, ECharts.
- Deployment target: `101.200.208.45` with passwordless SSH.
@@ -13,7 +13,7 @@
现有实现包含:
- `web-admin/src/views/aiconfig/AiConfigList.vue`:AI 配置列表、编辑、测试、测试后保存。
- `backend-single/src/main/java/com/emotion/entity/AiConfig.java`:对应 `t_ai_config`
- `server/src/main/java/com/emotion/entity/AiConfig.java`:对应 `t_ai_config`
- `AiConfigController``AiConfigService``AiConfigServiceImpl`:配置 CRUD、启用禁用、默认配置、测试后更新。
- `AiChatServiceImpl`:包含大量 Coze 专用调用、请求组装、SSE 解析、工作流调用和日志记录逻辑。
- `docs/dify平台接口.md`:Dify 平台接口文档,当前重点使用 `/chat-messages`
@@ -382,10 +382,10 @@ async function submitEndpointStreamTest() {
| 操作 | 文件 |
|------|------|
| 修改 | `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` |
| 修改 | `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java` |
| 修改 | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` |
| 修改 | `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` |
| 修改 | `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` |
| 修改 | `server/src/main/java/com/emotion/service/AiRuntimeService.java` |
| 修改 | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` |
| 修改 | `server/src/main/java/com/emotion/controller/AiRoutingController.java` |
| 修改 | `web-admin/src/types/aiconfig.ts` |
| 修改 | `web-admin/src/api/aiconfig.ts` |
| 修改 | `web-admin/src/views/aiconfig/AiRoutingList.vue` |
@@ -121,7 +121,7 @@ if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
|------|------|
| 修改 | `web-admin/src/views/aiconfig/AiRoutingList.vue` |
| 修改 | `web-admin/src/types/aiconfig.ts` |
| 修改 | `backend-single/src/main/java/com/emotion/service/ai/AiTemplateRenderer.java` |
| 修改 | `server/src/main/java/com/emotion/service/ai/AiTemplateRenderer.java` |
## 4. 风险
@@ -8,7 +8,7 @@ purpose: 为所有 Controller 及关联 DTO 补全 OpenAPI 中文注解
## 1. 目标
`backend-single/src/main/java/com/emotion/controller/` 下所有 39 个 Controller 及其关联的 Request/Response DTO 补全 OpenAPI 注解,使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
`server/src/main/java/com/emotion/controller/` 下所有 39 个 Controller 及其关联的 Request/Response DTO 补全 OpenAPI 注解,使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
## 2. 注解规范
@@ -31,7 +31,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
**保持不变的项**
- URL 路径(如 `/emotion-museum-admin/`)— 保持兼容
- 目录名(`web/``web-admin/``backend-single/`)— 避免构建断裂
- 目录名(`web/``web-admin/``server/`)— 避免构建断裂
- 数据库表名前缀(如 `emotion_record` → 保持)— 避免 Entity/Table 映射断裂
## 第 1 步:文档层(无风险)
@@ -43,7 +43,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- `README.md`
- `docs/superpowers/` 下所有 .md
- 各模块下的 `部署说明.md``技术方案.md``后端代码规范.md`
- `backend-single/部署说明.md``backend-single/后端项目结构.md`
- `server/部署说明.md``server/后端项目结构.md`
**操作**:批量 sed 替换中文 `情绪博物馆``开心星球`,英文 `Emotion Museum``Happy Planet``emotion-museum``happy-planet`
@@ -73,13 +73,13 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
**影响范围**:配置文件、部署脚本、nginx、systemd service
### 3.1 应用配置
- `backend-single/src/main/resources/application.yml``spring.application.name`
- `backend-single/src/main/resources/application-*.yml` — 同上
- `server/src/main/resources/application.yml``spring.application.name`
- `server/src/main/resources/application-*.yml` — 同上
- 日志路径、应用描述等配置中的 `emotion-museum`
### 3.2 部署脚本
- `deploy.sh``deploy.py`
- `backend-single/deploy.py``backend-single/deploy.sh`
- `server/deploy.py``server/deploy.sh`
- `web/deploy.sh``web/deploy.py`
- `web-admin/deploy.sh``web-admin/deploy.py`
- 各脚本中的路径引用、日志路径、提示文案
@@ -91,8 +91,8 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- jar 文件名、日志路径引用中的 `emotion-museum`
### 3.4 systemd Service
- `backend-single/asr-service/emotion-museum-asr.service``happy-planet-asr.service`
- `backend-single/tts-service/emotion-museum-tts.service``happy-planet-tts.service`
- `server/asr-service/emotion-museum-asr.service``happy-planet-asr.service`
- `server/tts-service/emotion-museum-tts.service``happy-planet-tts.service`
- 内部路径和描述中的 `emotion-museum`
### 3.5 工具脚本
@@ -118,7 +118,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
### 4.1 Java 包名重命名
**操作**
1. 移动目录:`backend-single/src/main/java/com/emotion/``backend-single/src/main/java/com/happyplanet/`
1. 移动目录:`server/src/main/java/com/emotion/``server/src/main/java/com/happyplanet/`
2. 更新 `pom.xml` 中的包名引用
3. 用 sed 批量替换所有 `.java` 文件中 `package com.emotion``package com.happyplanet`
4. 用 sed 批量替换所有 `.java` 文件中 `import com.emotion``import com.happyplanet`
@@ -126,7 +126,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
6. 处理编译错误(可能有遗漏的引用)
**测试目录**
- `backend-single/src/test/java/com/emotion/` 同样需要迁移
- `server/src/test/java/com/emotion/` 同样需要迁移
- `.java` 文件中的 `package``import` 语句
### 4.2 SQL 文件
@@ -139,7 +139,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- `USE emotion_museum``USE happy_planet`
### 4.3 其他底层引用
- `backend-single/create_api_tables.sql` 中的数据库名
- `server/create_api_tables.sql` 中的数据库名
- `.gitignore` 中的路径引用(如有)
**验证**
@@ -27,7 +27,7 @@ purpose: 设计接口管理页面的中文描述补全和测试面板表单化
### 3.1 Controller 注解补全
`backend-single/src/main/java/com/emotion/controller/` 下所有 Controller 补全注解:
`server/src/main/java/com/emotion/controller/` 下所有 Controller 补全注解:
**类级别注解:**
```java
@@ -247,9 +247,9 @@ path: '/api' + testForm.value.path // 例如 /api + /auth/login = /api/auth/log
| `web-admin/src/views/endpoint/EndpointList.vue` | 操作列加"测试"按钮,新增 `showTest` 方法 |
| `web-admin/src/views/endpoint/EndpointDetailDialog.vue` | 新增 `defaultTab` prop,测试面板改造:参数表单 + JSON 预填充 + 请求头展示 |
| `web-admin/src/api/endpoint.ts` | 类型定义无需修改(`operationId` 已存在) |
| `backend-single/src/main/java/com/emotion/controller/*.java` | 补全 @Tag/@Operation/@Parameter/@Schema 注解(按 4 个批次) |
| `backend-single/src/main/java/com/emotion/dto/request/**/*.java` | 补全 @Schema 注解(请求体参数描述来源) |
| `backend-single/src/main/java/com/emotion/dto/response/**/*.java` | 补全 @Schema 注解(响应体参数描述来源,可选) |
| `server/src/main/java/com/emotion/controller/*.java` | 补全 @Tag/@Operation/@Parameter/@Schema 注解(按 4 个批次) |
| `server/src/main/java/com/emotion/dto/request/**/*.java` | 补全 @Schema 注解(请求体参数描述来源) |
| `server/src/main/java/com/emotion/dto/response/**/*.java` | 补全 @Schema 注解(响应体参数描述来源,可选) |
## 5. 实施顺序
@@ -202,7 +202,7 @@ public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLog
### Request 对象
存放路径:`backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
存放路径:`server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
```java
@Schema(description = "AI 调用日志查询请求")
@@ -14,7 +14,7 @@ purpose: 在管理后台调用日志列表中增加"调用用户"列,显示用
### 1. 后端 — 实体新增字段
**文件**: `backend-single/src/main/java/com/emotion/entity/AiCallLog.java`
**文件**: `server/src/main/java/com/emotion/entity/AiCallLog.java`
`userId` 字段下方新增:
@@ -31,7 +31,7 @@ ALTER TABLE t_ai_call_log ADD COLUMN user_name VARCHAR(100) COMMENT '用户昵
### 3. 后端 — 日志保存时写入 userName
**文件**: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
**文件**: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
两处日志创建位置(第 68-73 行 `invokeStream` 方法、第 219-225 行 `invokeEndpointStream` 方法),在 `callLog.setUserId(...)` 之后补充:
@@ -27,7 +27,7 @@ The mini program currently supports phone number plus SMS code login only. The n
- `mini-program/src/pages/login/index.vue` contains the existing phone/SMS login UI and routes to main or onboarding based on profile presence.
- `mini-program/src/stores/app.js` already centralizes login, logout, token restore, and profile fetch behavior.
- `mini-program/src/services/auth.js` stores `access_token` and `refresh_token` from `AuthResponse`.
- `backend-single/src/main/java/com/emotion/controller/AuthController.java` exposes `/auth/login`, `/auth/refreshToken`, and token validation endpoints.
- `server/src/main/java/com/emotion/controller/AuthController.java` exposes `/auth/login`, `/auth/refreshToken`, and token validation endpoints.
- `t_user` already has `phone`, `avatar`, `nickname`, `third_party_id`, and `third_party_type`, which can support WeChat binding without a required schema change.
- The current backend issues JWT plus Redis-backed access/refresh tokens through `AuthServiceImpl`.
- The mini program manifest already has a WeChat appid: `wxaf2eaba72d28f0e4`.
@@ -167,7 +167,7 @@ if '//' in remote_conf or '..' in remote_conf:
### 不包含(明确排除)
- 不修改其他 deploy.py 子脚本(backend-single/web/web-admin/life-script
- 不修改其他 deploy.py 子脚本(server/web/web-admin/life-script
- 不改 `ssh_command()` / `run_ssh_args()` 底层
- 不增加 `--dry-run` 模式
- 不改 nginx 配置文件本身
@@ -51,7 +51,7 @@ alter table emotion_museum.t_user_profile
### 2. 实体类扩展
文件:`backend-single/src/main/java/com/emotion/entity/UserProfile.java`
文件:`server/src/main/java/com/emotion/entity/UserProfile.java`
新增字段(与数据库列名通过 `@TableField` 映射):
@@ -67,15 +67,15 @@ alter table emotion_museum.t_user_profile
以下三个文件新增相同 5 个字段:
- `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- `server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
`UserProfileResponse` 中的 `birthday` 使用 `@JsonFormat(pattern = "yyyy-MM-dd")` 注解,与现有日期字段保持一致。
### 4. Service 层
文件:`backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java`
文件:`server/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java`
无需改动。`BeanUtils.copyProperties``BeanUtil.copyProperties(..., CopyOptions.create().setIgnoreNullValue(true))` 会自动映射同名字段。
@@ -186,10 +186,10 @@ registrationData: {
| 文件 | 改动类型 | 说明 |
|------|----------|------|
| `sql/emotion_museum_ddl.sql` | 追加 | 5 个 ALTER TABLE 语句 |
| `backend-single/.../entity/UserProfile.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../request/.../UserProfileCreateRequest.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../request/.../UserProfileUpdateRequest.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../response/.../UserProfileResponse.java` | 修改 | 新增 5 个字段 |
| `server/.../entity/UserProfile.java` | 修改 | 新增 5 个字段 |
| `server/.../request/.../UserProfileCreateRequest.java` | 修改 | 新增 5 个字段 |
| `server/.../request/.../UserProfileUpdateRequest.java` | 修改 | 新增 5 个字段 |
| `server/.../response/.../UserProfileResponse.java` | 修改 | 新增 5 个字段 |
| `mini-program/src/services/userProfile.js` | 修改 | 双向转换增加 5 个字段 |
| `mini-program/src/stores/app.js` | 修改 | `registrationData` 扩展 |
| `mini-program/src/pages/main/RecordView.vue` | 修改 | 去除写死默认值和 sampleEvents |
@@ -90,14 +90,14 @@ purpose: 修复微信小程序登录 text/plain 错误(Spring RestTemplate 默
| # | 文件 | 操作 | 行数估算 | 目的 |
|---|---|---|---|---|
| 1 | `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java` | **重写** | 40 行 | 改用 `RestTemplateBuilder` + `BufferingClientHttpRequestFactory`:设置 User-Agent、超时、加 StringHttpMessageConverter、加 LoggingInterceptor、加 ErrorHandler |
| 2 | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | **重写** | 90 行 | `code2Session` 改用 `ResponseEntity<String>` + Jackson 二次解析;捕获所有失败场景的 raw body |
| 3 | `backend-single/src/main/resources/application.yml` | **修改** | 2 行 | 删除 `app-id``app-secret` 硬编码默认值(`${ENV_VAR:}` 空兜底) |
| 4 | `backend-single/src/main/java/com/emotion/exception/WechatApiException.java` | **新增** | 30 行 | 业务异常类(code 用 HTTP 风格状态码 + rawBody |
| 5 | `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | **新增** | 60 行 | ClientHttpRequestInterceptor:记录 method/URL/statusINFO+ 脱敏 bodyDEBUG |
| 6 | `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | **新增** | 30 行 | 4xx/5xx 不抛 RestClientException,重写 `hasError()` 显式声明 |
| 7 | `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | **修改** | 25 行 | 新增 `@ExceptionHandler(WechatApiException.class)` 返回脱敏的友好提示 |
| 8 | `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | **不动** | 0 行 | **保持纯 DTO**rawBody 不放在 DTO,只在异常对象和日志中) |
| 1 | `server/src/main/java/com/emotion/config/RestTemplateConfig.java` | **重写** | 40 行 | 改用 `RestTemplateBuilder` + `BufferingClientHttpRequestFactory`:设置 User-Agent、超时、加 StringHttpMessageConverter、加 LoggingInterceptor、加 ErrorHandler |
| 2 | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | **重写** | 90 行 | `code2Session` 改用 `ResponseEntity<String>` + Jackson 二次解析;捕获所有失败场景的 raw body |
| 3 | `server/src/main/resources/application.yml` | **修改** | 2 行 | 删除 `app-id``app-secret` 硬编码默认值(`${ENV_VAR:}` 空兜底) |
| 4 | `server/src/main/java/com/emotion/exception/WechatApiException.java` | **新增** | 30 行 | 业务异常类(code 用 HTTP 风格状态码 + rawBody |
| 5 | `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | **新增** | 60 行 | ClientHttpRequestInterceptor:记录 method/URL/statusINFO+ 脱敏 bodyDEBUG |
| 6 | `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | **新增** | 30 行 | 4xx/5xx 不抛 RestClientException,重写 `hasError()` 显式声明 |
| 7 | `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | **修改** | 25 行 | 新增 `@ExceptionHandler(WechatApiException.class)` 返回脱敏的友好提示 |
| 8 | `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | **不动** | 0 行 | **保持纯 DTO**rawBody 不放在 DTO,只在异常对象和日志中) |
**总计**:2 重写 + 3 新增 + 3 修改,约 280 行 Java + 2 行 YAML。
@@ -472,7 +472,7 @@ GlobalExceptionHandler.handleWechatApiException
| 步骤 | 命令 / 操作 | 期望 |
|---|---|---|
| 1. mvn 编译 | `mvn clean install -pl :backend-single -am -DskipTests` | 退出码 0 |
| 1. mvn 编译 | `mvn clean install -pl :server -am -DskipTests` | 退出码 0 |
| 2. 部署 | `python deploy.py backend` | 部署成功,重启 backend |
| 3. **WAF 验证**(修后 P1-7 | `tail -f /data/logs/emotion-museum/emotion-single.log \| grep "WeChatAPI"` | 看到 `[WeChatAPI] ---> GET https://api.weixin.qq.com/sns/jscode2session ...` 请求日志;`<--- 200` 响应日志 |
| 4. **UA 验证** | `grep "User-Agent" /data/logs/emotion-museum/emotion-single.log` | 看到 `Mozilla/5.0`(不再是 `Java/17.0.x` |
@@ -0,0 +1,67 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 明确小程序编辑资料页面在昵称未填时给出 Toast 提示的优化方案
---
# 小程序编辑资料页昵称必填提示优化
## 背景
当前小程序的编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)在昵称未填写时,点击保存按钮会直接静默返回,没有任何提示。用户无法判断是按钮失效、网络问题还是字段未填,体验较差。
## 目标
- 当用户点击保存按钮时,如果昵称未填写,必须给出明确的 Toast 提示「请填写昵称」。
- 保持改动最小,不影响现有保存流程和其他字段行为。
## 当前问题定位
`saveProfile` 方法中存在以下逻辑:
```javascript
if (!form.nickname.trim() || saving.value) return
```
`form.nickname` 为空或仅包含空白字符时,条件成立,方法直接 `return`,没有触发任何提示。
## 方案
采用「保存时校验 + Toast 提示」的轻量方案。
### 具体改动
**文件**`mini-program/src/pages/onboarding/index.vue`
**修改 `saveProfile` 方法**
1. 先判断 `saving.value`,避免重复提交。
2. 再判断昵称是否为空,为空时调用 `uni.showToast({ title: '请填写昵称', icon: 'none' })` 后返回。
改动后逻辑示例:
```javascript
const saveProfile = async () => {
if (saving.value) return
if (!form.nickname.trim()) {
uni.showToast({ title: '请填写昵称', icon: 'none' })
return
}
saving.value = true
// 原有保存逻辑保持不变
}
```
### 不改动的部分
- 不修改其他字段的校验规则。
- 不调整提示方式为字段高亮、弹窗或自动聚焦。
- 不新增失焦校验或实时输入校验。
## 验证标准
- 进入编辑资料页面,清空昵称后点击保存,页面顶部弹出 Toast「请填写昵称」。
- 填写昵称后点击保存,能正常提交并显示「已保存」。
- 浏览器 Console 无报错。
@@ -0,0 +1,81 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 去除编辑资料页默认昵称和剧本列表中的 mock/兜底数据
---
# 去除编辑资料页与剧本列表中的 mock/兜底数据
## 背景
项目代码中存在两处违反「禁止 mock 与兜底规则」的问题:
1. 编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)顶部预览卡片在没有昵称时,默认显示假名「Zoey」。
2. 剧本列表页面(`mini-program/src/pages/main/ScriptLibraryView.vue`)在 `store.scripts` 为空时,使用 `fallbackScripts` 数组展示 6 条写死 demo 数据;同时多个工具函数在无数据时返回固定的默认标签、字数、日期、进度等假值。
## 目标
- 没有昵称时,编辑资料页面顶部显示「未设置昵称」。
- 剧本列表没有数据时,不显示任何假数据,直接展示空状态。
- 清理剧本列表工具函数中的兜底默认值,确保「没有就是没有」。
## 改动点
### 1. 编辑资料页昵称空状态
**文件**`mini-program/src/pages/onboarding/index.vue:22`
**修改前**
```vue
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
```
**修改后**
```vue
<text class="hero-name">{{ form.nickname || '未设置昵称' }}</text>
```
### 2. 剧本列表删除 mock 数据与兜底值
**文件**`mini-program/src/pages/main/ScriptLibraryView.vue`
#### 2.1 删除 `fallbackScripts` 数组
删除第 178249 行的 `fallbackScripts` 常量及其 6 条 demo 数据。
#### 2.2 修改 `scripts` computed
**修改前**
```javascript
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
```
**修改后**
```javascript
const scripts = computed(() => store.scripts || [])
```
#### 2.3 清理工具函数默认值
| 函数 | 修改前 | 修改后 |
|---|---|---|
| `getTags` | `return [script.style || '逆袭成长', '都市', '事业', '热血']` | `return []` |
| `getWordCount` | `if (!count) return '3.1万字'` | 删除该分支,直接走后续逻辑(返回 `'0字'` |
| `getDateText` | 各分支使用 `|| '2025.05.10'``|| '2025.05.08'``|| '今天 21:30'` | 去掉兜底日期,返回空字符串 `''` |
| `getProgress` | `Number(script.progress || 28)` | `Number(script.progress || 0)` |
| `getChapterCount` | `script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))` | `script.chapterCount || script.chapters || 0` |
## 验证标准
1. 进入编辑资料页面,昵称输入框为空时,顶部预览卡片显示「未设置昵称」。
2. 剧本列表为空时,页面显示「还没有人生剧本」空状态,不展示任何 demo 剧本。
3. 剧本列表中的真实剧本数据展示不受影响(有数据时仍正常显示标题、标签、字数、日期、进度等)。
4. H5 模式下浏览器 Console 无新增报错。
5. 运行 `npm run build:h5` 编译通过。
@@ -0,0 +1,133 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 固定 ScriptView.vue 结果页顶部按钮,调整底部输入框边距,语音按钮改为麦克风图标
---
# ScriptView 对话结果页 UI 调整
## 背景
`ScriptView.vue`(剧本生成/对话结果页)中,结果页顶部左侧的「历史」按钮和右上角的「×」关闭按钮会随着页面内容滚动而移动,影响操作。底部输入框区域左右贴近屏幕边缘,视觉拥挤;语音按钮使用「语音」文字,不够简洁。
## 目标
1. 结果页顶部的历史按钮和关闭按钮在页面滚动时保持固定,不随内容滚动。
2. 底部输入框区域左右增加 24rpx 边距,输入框宽度适当压缩。
3. 语音按钮文字改为麦克风图标,颜色与当前主题一致。
4. 发送按钮保持当前大小和样式。
## 改动点
### 1. 顶部按钮固定
**文件**`mini-program/src/pages/main/ScriptView.vue`
将结果页顶部包裹层改为固定定位,并为下方内容增加顶部 padding 避免遮挡。
**结构示例**
```vue
<view class="result-header">
<view class="history-button" @click="openScriptLibrary">...</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
```
**样式示例**
```css
.result-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: 100rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
background: rgba(15, 7, 26, 0.85);
backdrop-filter: blur(18rpx);
-webkit-backdrop-filter: blur(18rpx);
}
```
同时给结果页内容区域增加顶部 padding(如 `padding-top: 100rpx` 或更大,以避开固定顶部栏和安全区)。
### 2. 底部输入框边距与压缩
**文件**`mini-program/src/pages/main/ScriptView.vue`
`.result-chat-bar` 增加左右内边距:
```css
.result-chat-bar {
/* 其他样式保持不变 */
padding: 0 24rpx;
}
```
输入框 `.result-input-shell` 保持 `flex: 1`,通过整体边距自然压缩可用宽度。如需进一步压缩,可增加左右 `margin: 0 10rpx`
语音按钮(86rpx × 76rpx)和发送按钮(92rpx × 76rpx)保持当前尺寸不变。
### 3. 语音按钮改为麦克风图标
**文件**`mini-program/src/pages/main/ScriptView.vue`
**修改前**
```vue
<view class="chat-voice-btn" ...>
<text>语音</text>
</view>
```
**修改后**
```vue
<view class="chat-voice-btn" ...>
<text class="voice-icon">🎤</text>
</view>
```
**样式**
```css
.chat-voice-btn {
/* 保持原有尺寸、背景、边框 */
width: 86rpx;
height: 76rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 26rpx;
background: rgba(88, 28, 135, 0.32);
border: 1rpx solid rgba(192, 132, 252, 0.3);
}
.voice-icon {
color: #e8ccff;
font-size: 32rpx;
}
.chat-voice-btn.pressing .voice-icon,
.chat-voice-btn.recognizing .voice-icon {
color: #fff;
}
```
如项目规范不允许使用 emoji,可改用 CSS 绘制的麦克风图标。
## 验证标准
1. 进入 ScriptView.vue 的剧本对话结果页,上下滚动内容,顶部历史按钮和关闭按钮始终保持在固定位置。
2. 底部输入框区域左右与屏幕边缘有 24rpx 边距。
3. 语音按钮显示为麦克风图标,默认颜色 `#e8ccff`,按下/识别时变为白色。
4. 发送按钮大小不变。
5. 输入框可以正常输入和发送消息。
6. 浏览器 Console 无新增报错。
7. 运行 `npm run build:h5` 编译通过。
@@ -0,0 +1,240 @@
---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"性格标签"与"兴趣爱好"两个模块的扩展设计:支持用户新增自定义标签、统一管理标签库、删除标签,并保证编辑页标签容器自适应撑开展示所有标签。
---
# 小程序编辑资料页:性格标签 / 兴趣爱好 标签库扩展设计
## 背景
当前小程序的编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)已具备性格标签和兴趣爱好的基础选择功能:
- 性格标签预设 11 个(理性、感性、乐观、独立、有创造力、坚韧、细腻、好奇、内敛、冒险、自由)。
- 兴趣爱好预设 10 个(阅读、旅行、音乐、写作、摄影、电影、运动、绘画、咖啡、游戏)。
- 兴趣爱好模块支持通过弹窗输入自定义标签。
- 性格标签模块 UI 上已有"+ 添加标签"占位,但未绑定事件。
- 两个模块均不支持删除标签,也没有统一的管理入口。
- 后端 `UserProfile` 实体只保存"用户已选中的标签"两个 JSON 字段(`personality_tags` / `hobbies`),不区分预设与自定义,也没有完整标签库的概念。
用户新增需求:性格标签和兴趣爱好两个模块均支持**用户自由新增自定义标签、保存、回显、删除**;编辑资料页的标签容器要**自适应撑开**,保证所有标签都能展示;**新增的标签展示在最前面**。
## 目标
1. 性格标签和兴趣爱好两个模块支持用户新增自定义标签、保存、回显、删除。
2. 新增的自定义标签展示在列表最前面。
3. 编辑资料页标签容器自适应撑开,不限行数,展示所有标签。
4. 提供独立的"管理标签"页面,支持统一删除(含预设与自定义)。
5. 老用户数据平滑迁移,不破坏现有已选标签。
6. 输入校验完整,避免重复、空值、超长。
## 非目标
- 不引入标签的分类、排序、分组等高级管理能力。
- 不做标签的全局共享、推荐、热门度等运营功能。
- 不修改后端存储架构(继续用 JSON 字段,不拆子表)。
- 不在编辑资料页做长按手势删除,统一收敛到管理页。
## 关键设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 新增标签输入方式 | `uni.showModal` 弹窗输入 | 与现有"自定义兴趣"交互保持一致,改动成本最低 |
| 删除入口 | 独立的"管理标签"页面 | 统一删除操作入口,避免编辑页交互过载;预设与自定义标签都能被删除 |
| 删除后的可见性 | 删除后从当前用户账号彻底移除 | 用户希望不再看到的标签就不会再出现 |
| 数据存储 | `UserProfile` 新增 `personality_tag_library` / `hobby_library` 两个 JSON 字段 | 后端需区分"用户已选中"与"用户可见的完整标签库",原有字段语义保留不变 |
| 预设 vs 自定义标签 | 前端维护一份内置预设数组;库里存的是用户专属合并后的字符串数组 | 预设标签库存在代码中便于维护,用户库字段只关心"当前账号可见的全部标签" |
| 编辑页标签顺序 | 新增的自定义标签 → 已选中的其他标签 → 其余未选中标签 | 严格保证"新增的标签展示在最前面",其他已选中标签紧随其后 |
## 后端改动
### 实体与数据库
`server` 模块:
- `UserProfile` 实体新增两个字段:
| 字段名 | Java 类型 | 数据库列 | 类型 | 说明 |
|---|---|---|---|---|
| `personalityTagLibrary` | `String` | `personality_tag_library` | `TEXT` | 用户的完整性格标签库,JSON 字符串(字符串数组) |
| `hobbyLibrary` | `String` | `hobby_library` | `TEXT` | 用户的完整兴趣爱好库,JSON 字符串(字符串数组) |
- 原 `personalityTags` / `hobbies` 字段保持不变,继续表示"用户已选中的标签"。
- 新增数据库迁移 SQL
```sql
ALTER TABLE user_profile
ADD COLUMN personality_tag_library TEXT DEFAULT NULL COMMENT '用户完整性格标签库(JSON数组)',
ADD COLUMN hobby_library TEXT DEFAULT NULL COMMENT '用户完整兴趣爱好库(JSON数组)';
```
### 接口变更
`UserProfileController` / `UserProfileService` 无需新增接口,复用现有:
- `PUT /user-profile/update`:入参 DTO 增加 `personalityTagLibrary` / `hobbyLibrary`(均为 `String`),直接持久化。
- `GET /user-profile/me`:返回体增加上述两个字段。
- `POST /user-profile/create`:同上。
### 数据兼容性
- 老用户(`personalityTagLibrary` 为空或 `null`)查询接口返回 `null`,由前端做兜底初始化。
- 后端不做兜底填充,严格遵守"没有就是没有"的禁止 mock 规则。
## 前端改动
### 数据层(`stores/app.js``services/userProfile.js`
- 请求返回的 `userProfile` 新增 `personalityTagLibrary` / `hobbyLibrary` 字段解析(JSON.parse)。
- `saveUserProfile` 提交时把两个库字段 `JSON.stringify` 后一并提交。
### 编辑资料页(`mini-program/src/pages/onboarding/index.vue`
#### 标签区结构(性格标签与兴趣爱好同构)
```
┌──────────────────────────────────────┐
│ 😊 性格标签 管理 > │ ← 标题行,右侧"管理"按钮跳转管理页
│ (最多选择5个) │
│ ┌────┐┌────┐┌────┐┌────┐ │
│ │ 理性 ││ 感性 ││ 乐观 ││ 独立 │... │ ← 标签网格,flex-wrap 自适应
│ └────┘└────┘└────┘└────┘ │
│ ┌───────┐ │
│ │ + 添加 │ │ ← 末尾占位,点击打开弹窗
│ └───────┘ │
└──────────────────────────────────────┘
```
- 容器使用 `display: flex; flex-wrap: wrap;`**高度随内容自适应撑开**。
- 标签顺序规则(严格保证"新增的标签展示在最前面"):
1. **新增的自定义标签**排在最前面(按创建时间倒序)。
2. **已选中的其他标签**(含预设与早期自定义)紧随其后。
3. **其余未选中的预设标签**排在最后。
- 样式沿用现有 `.tag-choice` / `.tag-choice.active`
- 点击标签:沿用 `toggleList(list, tag, 5)` 逻辑,超限 toast 提示。
#### 新增标签弹窗
- 点击" 添加标签"触发 `uni.showModal``editable: true`,占位符"请输入标签"。
- 校验规则:
- 非空、去除首尾空格。
- 长度 ≤ 8 个字符。
- 不能和当前库中任何标签完全相等(字符串完全一致比对)。
- 校验失败通过 `uni.showToast` 提示原因,不静默忽略。
- 校验通过后:
1. 新标签**插入到对应库数组的最前面**。
2. 若已选未满 5 个,自动加入已选数组;满则 toast 提示"已达上限,请先进入管理页删除其他标签",仍把新标签加入库,但不加入已选。
#### 保存流程
- 点击"保存"按钮时,把以下数据一起提交:
- `personalityTags`(已选中数组)
- `personalityTagLibrary`(完整库数组)
- `hobbies`(已选中数组)
- `hobbyLibrary`(完整库数组)
- 保存失败:toast 提示后端返回的真实错误信息,页面状态保留不变。
- 保存成功:toast 提示"已保存"350ms 后 `uni.navigateBack()`
### 管理页(新增独立页面)
路径:`mini-program/src/pages/onboarding/tag-manage.vue`
通过 `pages.json` 注册路由:`/pages/onboarding/tag-manage`
#### 入口与参数
- 编辑资料页的"管理"按钮跳转:`/pages/onboarding/tag-manage?type=personality``?type=hobby`
- 页面标题:根据 type 显示"管理性格标签"或"管理兴趣爱好"。
#### 页面结构
- 列表渲染当前库中全部标签(含剩余预设 + 自定义)。
- 每行结构:
```
┌────────────────────────────────────┐
│ 理性 × │
├────────────────────────────────────┤
│ 感性 × │
├────────────────────────────────────┤
│ ... │
└────────────────────────────────────┘
```
- **左滑删除**:使用 UniApp 的 `uni-swipe-action` 组件或自定义实现,向右滑动显示"删除"按钮。
- **点击 × 按钮**:同样触发删除。
- 删除前 `uni.showModal` 二次确认,提示"确定删除该标签吗?删除后不可恢复"。
- 删除后:
- 从库数组中移除该标签。
- 若该标签同时在已选数组中,一并移除。
- 页面列表立即刷新。
- 删除接口调用:删除动作本身不单独调用后端,仅在用户返回编辑资料页点击"保存"时统一提交。
#### 返回与数据同步
- 管理页的库数据存储在页面局部响应式变量中。
- 返回编辑资料页时,通过 `onUnload` 生命周期或全局 store 的 `updateRegistration` 把最新库数据回传给编辑资料页。
- 推荐方案:使用 `uni.$emit('tag-library-updated', { type, library })` 事件,编辑资料页在 `onShow` 中监听并合并。
## 数据初始化兼容方案
编辑资料页在 `onLoad` / `onShow` 时执行:
```
if (后端返回的 personalityTagLibrary 为空) {
库 = [...内置预设数组]
if (后端返回的 personalityTags 非空) {
// 把已选中的标签也合入库(老用户首次编辑时保留)
库 = 去重合并(库, 已选数组)
}
}
```
兴趣爱好同理。此逻辑保证老用户数据不丢失,新用户直接拿预设库。
## 错误处理
- 接口请求失败:按现有 `request.js` 约定,`try/catch` 返回 `{ success: false, error: 真实错误 }`,页面 toast 提示真实错误信息。**禁止本地 mock 成功响应、禁止兜底默认值**。
- 管理页删除:若保存时接口失败,本地已删除的标签状态**回滚**(恢复删除前的库内容),toast 提示失败原因。
- 网络超时:按 `request.js` 的现有超时策略处理。
## 文件清单
### 新增文件
- `mini-program/src/pages/onboarding/tag-manage.vue`:管理标签独立页面。
### 修改文件
**后端(`server/src/main/java/com/emotion/`):**
- `entity/UserProfile.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `controller/UserProfileController.java`:无需新增接口,入参 DTO 通过 `UserProfileUpdateRequest` / `UserProfileCreateRequest` 已支持新字段透传。
- `dto/request/userprofile/UserProfileUpdateRequest.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `dto/request/userprofile/UserProfileCreateRequest.java`:同上。
- `dto/response/userprofile/UserProfileResponse.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `service/UserProfileService.java``service/impl/UserProfileServiceImpl.java`:无业务逻辑改动,字段通过 MyBatis-Plus 自动映射持久化。
- 数据库迁移脚本:`server/src/main/resources/db/migration/2026-06-24-user-profile-tag-library.sql`,遵循现有 `YYYY-MM-DD-描述.sql` 命名约定。
**前端(`mini-program/`):**
- `src/pages/onboarding/index.vue`:重构标签区,新增管理入口、弹窗新增逻辑、自适应样式。
- `src/services/userProfile.js`:请求体与响应解析增加两个库字段。
- `src/stores/app.js``saveUserProfile` 提交时携带库字段。
- `src/pages.json`:注册 `tag-manage` 路由。
**不修改的文件:**
- `src/pages/main/MineView.vue`:展示区只读"已选中的标签"(沿用 `hobbies` 字段),不需要感知完整库,**无需修改**。
## 验收标准
1. 进入编辑资料页,性格标签、兴趣爱好两个区域的标签容器高度随内容自适应撑开,无截断。
2. 点击" 添加标签"或"+ 自定义兴趣",弹窗输入后,新标签出现在列表最前面。
3. 重复输入同名标签,toast 提示"标签已存在"。
4. 空输入、超长输入(>8 字符)分别 toast 提示对应原因。
5. 点击"管理"按钮,跳转到管理页,列表展示全部标签(预设 + 自定义)。
6. 管理页左滑或点 × 删除标签,二次确认后标签从列表消失。
7. 删除已在"已选"中的标签,返回编辑资料页后已选列表同步移除。
8. 保存后重新进入编辑资料页,新增的标签仍展示在最前面。
9. 老用户(无新字段)首次进入编辑资料页,已选标签保留,预设标签可见。
10. 接口失败时,页面保留原状态并 toast 真实错误,不静默成功。
@@ -0,0 +1,289 @@
---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"添加"按钮文字简化、字数限制收紧到4字、标签不换行、弹窗替换为深色太空主题自定义组件的改动设计。
---
# 小程序添加标签按钮简化与自定义弹窗设计
## 背景
当前小程序编辑资料页(`mini-program/src/pages/onboarding/index.vue`)和管理标签页(`mini-program/src/pages/onboarding/tag-manage.vue`)的"添加"按钮和弹窗存在以下问题:
- 编辑页性格标签的添加按钮文字是"+ 添加标签",兴趣爱好是"+ 添加兴趣",文字偏长,与紧凑的标签网格不协调。
- 添加标签的字数上限是 8 个字符,偏宽松,容易生成过长的标签破坏网格布局。
- 标签 `.tag-choice` / `.tag-text` 没有设置 `white-space: nowrap`,长标签会自动换行撑高单元格。
- 添加和删除确认都使用 `uni.showModal`,在 H5 模式下是 uni-app 默认的白底蓝按钮样式,与小程序整体的深色太空主题(深紫底、毛玻璃、紫色渐变、金色强调)严重不一致。
## 目标
1. 编辑页两个添加按钮文字统一简化为"+ 添加"。
2. 添加标签字数限制收紧到 4 个字。
3. 标签文字过长时不换行,用省略号截断。
4. 添加和删除确认弹窗替换为自定义弹窗组件,样式与深色太空主题保持一致。
## 非目标
- 不改标签的选择/取消选中逻辑、最多 5 个的限制。
- 不改后端接口或数据结构。
- 不改管理页的左滑删除手势。
- 不引入第三方 UI 组件库。
## 关键设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 添加按钮文字 | " 添加"(去掉"标签"/"兴趣"后缀) | 用户明确要求只保留"添加"两字 |
| 弹窗 title | 保持 `添加性格标签` / `添加兴趣爱好` | 按钮简、弹窗清,分开处理 |
| 字数限制 | 4 个字 | 用户明确要求 |
| toast 文案 | "内容不能超过4个字" | 用户指定 |
| 不换行实现 | `white-space: nowrap` + `overflow: hidden` + `text-overflow: ellipsis` | 标准三件套,标签固定高度不撑高 |
| 弹窗实现 | 新建自定义组件 `TagDialog.vue`,两种模式(input / confirm | 替换 `uni.showModal`,样式可控、深色主题统一 |
## 新增组件
### `mini-program/src/components/TagDialog.vue`
**职责**:统一的弹窗组件,支持输入模式和确认模式,样式与深色太空主题一致。
**Props**
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `visible` | `Boolean` | `false` | 是否显示弹窗 |
| `mode` | `String` | `'input'` | `'input'``'confirm'` |
| `title` | `String` | `''` | 弹窗标题 |
| `content` | `String` | `''` | confirm 模式的提示文案 |
| `placeholder` | `String` | `''` | input 模式的输入框占位符 |
| `modelValue` | `String` | `''` | input 模式的输入值(支持 v-model) |
**Emits**
| 事件 | 参数 | 说明 |
|---|---|---|
| `confirm` | input 模式:当前输入值;confirm 模式:无 | 点击确定按钮 |
| `cancel` | 无 | 点击取消按钮或遮罩层 |
| `update:modelValue` | 新值 | 输入框内容变化 |
| `update:visible` | `false` | 关闭弹窗 |
**模板结构**
```
┌────────────────────────────────┐
│ 遮罩层(半透明深色) │
│ ┌──────────────────────────┐ │
│ │ ✦ 添加性格标签 │ │ ← 标题 + 金色 ✦
│ │ ┌────────────────────┐ │ │
│ │ │ 输入框(深色底紫边)│ │ │ ← input 模式才有
│ │ └────────────────────┘ │ │
│ │ 确定删除「xxx」吗?... │ │ ← confirm 模式才有
│ │ │ │
│ │ [ 取消 ] [ 确定 ] │ │ ← 按钮
│ └──────────────────────────┘ │
└────────────────────────────────┘
```
**样式**(与小程序深色太空主题一致):
- 遮罩层:`rgba(3, 2, 13, 0.72)` 全屏覆盖
- 弹窗卡片:`glass-card` 毛玻璃 + `rgba(155, 110, 255, 0.14)` 紫色边框 + `22rpx` 圆角
- 标题:白色 `#fff` 加粗,配金色 `✦``#ffd58c`
- 输入框:深色底 `rgba(10, 12, 40, 0.66)` + 紫色边框 `rgba(151, 111, 255, 0.22)`
- 取消按钮:透明底 + 紫色边框
- 确定按钮:紫色渐变 `linear-gradient(180deg, #a855ff, #6b29ce)`
- 所有尺寸用 `rpx`
## 编辑资料页改动(`mini-program/src/pages/onboarding/index.vue`
### 模板
- 性格标签 panel 末尾占位:` 添加标签`` 添加`
- 兴趣爱好 panel 末尾占位:` 添加兴趣`` 添加`
- 在 `<scroll-view>` 之外、`</view>` 之前挂载 `<TagDialog>` 组件:
```html
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
```
### script setup
- 引入组件:`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态:
```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input') // 'input' 或 'confirm'
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null) // 记录当前弹窗对应的动作:{ kind: 'add'|'delete', type: 'personality'|'hobby', tag?: string }
```
- 重构 `addCustomTag`:改为打开弹窗(input 模式),不再调 `uni.showModal`
```javascript
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
dialogMode.value = 'input'
dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
dialogPlaceholder.value = '请输入标签(最多4个字)'
dialogInput.value = ''
dialogAction.value = { kind: 'add', type }
dialogVisible.value = true
}
```
- 新增 `onDialogConfirm`:处理弹窗确认(add / delete 两种动作):
```javascript
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
} else if (action.kind === 'delete') {
handleDeleteTag(action.type, action.tag)
}
dialogVisible.value = false
}
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
```
- 抽出 `handleAddTag`(原 `addCustomTag` 的 success 回调逻辑,字数限制改为 4):
```javascript
const handleAddTag = (type, rawValue) => {
const value = String(rawValue || '').trim()
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 4) {
uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
library.unshift(value)
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
```
### 样式
`.tag-choice` 增加:
```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```
## 管理页改动(`mini-program/src/pages/onboarding/tag-manage.vue`
### 模板
挂载 `<TagDialog>` 组件(confirm 模式):
```html
<TagDialog
v-model:visible="dialogVisible"
:mode="'confirm'"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
```
### script setup
- 引入组件:`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态:
```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```
- 重构 `confirmDelete`:改为打开弹窗(confirm 模式):
```javascript
const confirmDelete = (tag) => {
pendingTag.value = tag
dialogContent.value = `确定删除「${tag}」吗?删除后不可恢复`
dialogVisible.value = true
}
const onDialogConfirm = () => {
if (pendingTag.value) {
deleteTag(pendingTag.value)
pendingTag.value = null
}
dialogVisible.value = false
}
const onDialogCancel = () => {
pendingTag.value = null
dialogVisible.value = false
}
```
- `deleteTag` 函数保持不变(已经在 Task 6 实现)。
### 样式
`.tag-text` 增加:
```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```
## 文件清单
### 新增文件
- `mini-program/src/components/TagDialog.vue`:自定义弹窗组件。
### 修改文件
- `mini-program/src/pages/onboarding/index.vue`:按钮文字简化、字数限制 4、不换行样式、引入 TagDialog、重构 addCustomTag。
- `mini-program/src/pages/onboarding/tag-manage.vue`:引入 TagDialog、重构 confirmDelete、不换行样式。
## 验收标准
1. 编辑页性格标签和兴趣爱好的添加按钮只显示"+ 添加"。
2. 点击"+ 添加"弹出深色太空主题弹窗(毛玻璃卡片、紫色渐变、金色 ✦),不再是白底蓝按钮。
3. 输入 5 个字时 toast 提示"内容不能超过4个字"。
4. 输入 4 个字以内的新标签,确认后标签出现在列表最前面。
5. 标签文字过长时(如 4 字标签在窄屏),单元格内省略号截断,不换行撑高。
6. 管理页点 × 或左滑删除时,弹出深色主题确认弹窗,文案"确定删除「xxx」吗?删除后不可恢复"。
7. 弹窗的取消按钮和遮罩层点击都能关闭弹窗,不触发任何动作。
8. 弹窗在 H5 模式下样式与小程序深色太空主题一致。
@@ -0,0 +1,125 @@
---
author: claude
created_at: 2026-06-27
purpose: 统一小程序实现心愿页面中所有 AI 消息卡片的功能按钮,并修复 story-card 展开失效 bug
---
# AI 消息卡片功能按钮统一设计
## 背景
在小程序「实现心愿」页面(`mini-program/src/pages/main/ScriptView.vue`)中:
1. **第一个 AI 回复卡片**`story-card`)拥有完整的功能按钮:展开/收起、复制、换个方向、不像我、继续生成、播放。
2. **后续 AI 消息**`result-chat-list`)只有基础的展开/收起,缺少复制、换个方向、不像我、继续生成、播放。
3. **story-card 展开失效**:当用户触发过重新生成(`resultHasScriptReplies``true`)后,`<template v-if="!resultHasScriptReplies">` 会隐藏整个故事正文和底部展开按钮,导致点击展开无效果。
## 目标
- 每条 AI 回复消息卡片都拥有完整的功能按钮(展开/收起、复制、换个方向、不像我、继续生成、播放)
- 修复 story-card 展开失效的 bug
- 所有按钮功能正常可用
## 方案选择
采用 **方案 A:在现有单文件组件内修复 + 扩展**
- 不提取独立组件,保持现有文件结构
- 改动最小、风险低、不引入新文件
- 后续如有更复杂的差异化需求再升级
## 详细设计
### 1. 修复 story-card 展开失效
**根因**`ScriptView.vue` 第 192 行的条件包裹
```vue
<template v-if="!resultHasScriptReplies">
<scroll-view v-if="storyCollapsed" ...>
<text class="story-body">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">...</view>
</template>
```
`resultHasScriptReplies``true`(有 `kind: 'script'` 的消息)时,整个故事正文和底部展开按钮被隐藏。
**修复**:去掉 `<template v-if="!resultHasScriptReplies">` 条件包裹,让故事正文始终由 `storyCollapsed` 控制展开/收起。
**副作用**story-card 始终显示完整故事正文,与 result-chat-list 中的 script 消息内容重复。这是预期行为——两个卡片独立操作各自内容。
### 2. 给每条 assistant 消息加功能按钮组
**布局**:在每条 assistant 消息的内容 + 展开/收起按钮之后,追加一行网格按钮组,2 列布局(与 story-card 的 `.result-actions` 一致)。
**按钮清单**(按顺序):
| 按钮 | 作用 | 实现方式 |
|---|---|---|
| 复制 | 复制该消息正文到剪贴板 | `uni.setClipboardData` |
| 换个方向 | 进入「换方向」修订确认 | 复用现有 `changeDirection()` |
| 不像我 | 进入「不像我」修订确认 | 复用现有 `notLikeMe()` |
| 继续生成 | 收起故事 + 聚焦输入框 | 复用现有 `continueInChat()` |
| 播放 | 播放 TTS 音频 | 复用 `ttsPlayer.playSource`,传 `currentResult.id` |
**按钮条件**
- 展开/收起、复制:所有 assistant 消息都有
- 换个方向、不像我、继续生成、播放:所有 assistant 消息都有(本质上是全局操作,作用在 `currentResult` 上)
- 正在 `pending` 的 assistant 消息不显示按钮组(保持现有逻辑,`isAssistantMessage` 已排除 `pending`
### 3. 新增/修改的事件处理器
1. **`copyMessageContent(message)`**(新增):
- 复制 `message.content` 到剪贴板
- 空内容时 `showToast` 提示「暂无可复制内容」
- 埋点 `script_message_copy_click`
2. **`playMessageTts(message)`**(新增):
- 调用 `ttsPlayer.playSource(currentResult.value?.id || '')`
- 所有消息都播放同一个故事的 TTS(TTS 基于 `currentResult` 生成)
- 埋点 `script_message_tts_click`
3. **`toggleMessageCollapse(message)`**(修改):
- 补充埋点 `script_message_collapse_toggle`(与 `toggleStoryCollapse` 对齐)
### 4. 样式
为消息卡片内的按钮组新建 `.message-actions` 样式,复用 `.result-actions``.action-btn` 的视觉风格:
```css
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 16rpx;
}
```
按钮直接复用 `.action-btn` 类名,确保视觉与主卡片一致。展开/收起按钮(`.message-toggle`)保留现有金色调样式,与功能按钮组做视觉区分。
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| 空内容的 assistant 消息 | 复制按钮 `showToast` 提示「暂无可复制内容」;其他按钮正常可用 |
| pending 中的消息 | 不显示按钮组(现有逻辑) |
| user 消息 | 不显示按钮组 |
| 无 `currentResult` 时的播放 | 按钮仍可点击,`playSource` 会走内部兜底(传空 id |
## 修改范围
仅修改一个文件:`mini-program/src/pages/main/ScriptView.vue`
### 模板修改
1. 去掉第 192 行 `<template v-if="!resultHasScriptReplies">` 条件包裹
2. 在 `result-chat-list` 的 assistant 消息中添加功能按钮组
### 脚本修改
1. 新增 `copyMessageContent(message)` 方法
2. 新增 `playMessageTts(message)` 方法
3. 修改 `toggleMessageCollapse(message)` 补充埋点
### 样式修改
1. 新增 `.message-actions` 样式
@@ -0,0 +1,74 @@
---
author: peanu
created_at: 2026-06-27
purpose: 将 backend-single 目录重命名为 server 的设计方案
---
# 设计:backend-single 重命名为 server
## 背景
项目中的后端服务目录当前名为 `backend-single`,该名称是早期"单体服务"阶段的遗留命名。随着项目演进,这个名称已不再准确反映实际情况,且在日常开发中较长不便。将其重命名为更简洁的 `server` 可提升可读性和一致性。
## 决策
- **目录重命名**`backend-single/``server/`,使用 `git mv` 保留 Git 历史
- **Maven artifactId**`backend-single``server`JAR 文件随之变为 `server-1.0.0.jar`
- **历史文档**`docs/superpowers/` 下的历史设计和计划文档也全部更新,保持一致性
## 变更范围
### 第一层:目录和构建
| 操作 | 目标 |
|------|------|
| `git mv backend-single server` | 目录重命名 |
| `server/pom.xml` artifactId | `backend-single``server` |
| `server/pom.xml` name | `backend-single``server` |
| `server/deploy.py` JAR_NAME | `backend-single-1.0.0.jar``server-1.0.0.jar` |
| `server/deploy.sh` JAR_NAME | 同上 |
### 第二层:部署脚本
| 文件 | 变更内容 |
|------|---------|
| 根目录 `deploy.py` | 3 处 `backend-single``server`(注释、路径拼接、cwd |
| 根目录 `deploy.sh` | 4 处 `backend-single``server`(注释、存在性检查、cd |
| `.gitignore` | `backend-single/backend.err``server/backend.err` |
### 第三层:项目文档
| 文件 | 说明 |
|------|------|
| `CLAUDE.md` | 项目结构树(1 处)+ 后端命令段落(3 处) |
| `AGENTS.md` | 同 CLAUDE.md 的结构和命令段落 |
| `README.md` | 项目结构树(1 处) |
| `server/部署说明.md` | 自引用路径 |
| `快速部署参考.md` | 4 处部署路径引用 |
| `一键部署说明.md` | 5 处部署路径引用 |
### 第四层:前端和其他代码
| 文件 | 说明 |
|------|------|
| `web/src/types/auth.ts` | 第 5 行注释引用 |
| `web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md` | 4 处后端文件路径引用 |
### 第五层:历史文档批量替换
约 20 个 `docs/superpowers/specs/``docs/superpowers/plans/` 下的 `.md` 文件,将其中所有 `backend-single` 替换为 `server`
## 不在范围内
- **Java 包名**`com.emotion` 保持不变,不在本次重命名范围内
- **数据库名/表名**:不涉及数据库变更
- **`dev-services.py`**:该脚本自动发现 Spring Boot 服务,不直接引用 `backend-single` 字符串,无需修改
- **`deploy-server.sh`**:已存在的文件,不包含 `backend-single` 引用
## 验证标准
1. `mvn clean install -DskipTests``server/` 目录下编译成功
2. `server/deploy.py backend` 能正确找到 JAR 并部署
3. 根目录 `deploy.py` 能正确调用 `server/deploy.py`
4. 项目中不再有任何 `backend-single` 字符串残留(grep 验证)
5. Git 历史完整保留(`git log --follow server/pom.xml` 能看到旧历史)
@@ -0,0 +1,156 @@
---
author: claude
created_at: 2026-06-27
purpose: 规范本地服务管理:强制使用 dev-services.py 控制前后端服务,固定前端/H5 端口从 5178 开始累加,优化热加载避免无意义重启
---
# 本地服务管理规则优化设计
## 背景
当前项目使用 `dev-services.py` 管理本地前后端服务,但存在以下问题:
1. 前端/H5 端口分散配置在各自 `vite.config.*` 中,未统一管理
2. `dev-services.py` 端口冲突时自动递增,导致端口号不稳定
3. 用户已习惯使用 `dev-services.py`,但缺少"禁止无意义重启"的强制约束
4. 热加载场景下,开发者可能误重启支持热更新的前端服务,浪费时间
## 目标
1. 强制:启动/重启/停止本地前后端服务必须使用 `dev-services.py`
2. 固定:前端和 H5 服务端口从 5178 开始累加分配
3. 优化:`dev-services.py restart` 在无必须重启的变更时禁止重启
4. 一致:更新 CLAUDE.md 规则文档,与脚本行为对齐
## 方案选择
采用 **方案 Adev-services.py 内置固定端口表 + 修改各项目 vite 配置**
- 端口控制集中且与项目配置一致
- 与现有 `dev-services.py` 架构兼容
- 便于开发者直接查看配置文件了解端口
## 详细设计
### 1. 固定端口分配表
| 服务 | 目录 | 类型 | 固定端口 |
|---|---|---|---|
| Emotion-museum-web | `web/` | Vite | 5178 |
| Emotion-museum-admin | `web-admin/` | Vite | 5179 |
| Emotion-museum-uniapp | `mini-program/` | UniApp H5 | 5180 |
| Life-script 前端 | `life-script/` | Vite | 5181 |
后端服务端口保持现有配置不变。
### 2. dev-services.py 修改
#### 2.1 新增固定端口映射
```python
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
```
按项目目录名匹配。如果目录名不在映射中,仍按原有逻辑处理。
#### 2.2 修改端口分配逻辑
`assign_unique_ports()` 中:
- 对 `FIXED_FRONTEND_PORTS` 中定义的服务,强制使用固定端口
- 如果固定端口被非前端服务占用,报错退出
- 如果固定端口被另一个前端服务占用,报错退出(说明映射配置错误)
- 不再对前端服务自动递增端口
#### 2.3 新增 restart 热加载保护
定义"必须重启才能生效"的文件模式:
```python
RESTART_REQUIRED_PATTERNS = [
r'vite\.config\.(ts|js|mjs|cjs)$',
r'tsconfig\.json$',
r'\.env(\.[^/]*)?$',
r'package\.json$',
r'application.*\.ya?ml$',
r'pom\.xml$',
r'build\.gradle(\.kts)?$',
]
```
`restart` 命令执行前:
1. 获取 git 工作区中已修改的文件列表:`git diff --name-only`
2. 如果没有修改,提示"未检测到代码变更,无需重启"
3. 如果修改文件匹配 `RESTART_REQUIRED_PATTERNS`,允许正常 restart
4. 如果修改文件不匹配,拒绝 restart 并提示:
> 当前修改只涉及支持热加载的源码文件,无需重启。如需强制重启,请使用 `python dev-services.py restart --force`
#### 2.4 支持 `--force` 参数
`restart` 子命令解析中增加 `--force` 选项,允许用户绕过热加载保护强制重启。
### 3. 前端项目配置修改
| 文件 | 修改内容 |
|---|---|
| `web/vite.config.ts` | `server.port` 改为 5178 |
| `web/.env.development` | 添加 `VITE_PORT=5178` |
| `web-admin/vite.config.ts` | `server.port` 改为 5179 |
| `web-admin/.env.development` | `VITE_APP_PORT` 改为 5179 |
| `mini-program/vite.config.js` | `server.port` 改为 5180 |
| `mini-program/.env.development` | 添加 `VITE_PORT=5180` |
| `life-script/vite.config.js` | `server.port` 改为 5181 |
| `life-script/.env.development` | 添加 `VITE_PORT=5181`(不存在则创建) |
### 4. CLAUDE.md 规则更新
新增"本地服务管理规则"章节:
1. **必须使用 dev-services.py**
- 启动、重启、停止本地前后端服务必须使用 `python dev-services.py [命令] [服务名]`
- 禁止直接使用 `npm run dev``npm run dev:h5``mvn spring-boot:run` 等命令
2. **前端/H5 固定端口**
- `web`: 5178
- `web-admin`: 5179
- `mini-program`: 5180
- `life-script`: 5181
3. **禁止无意义重启**
- 只有修改必须重启才能生效的文件时,才允许执行 `restart`
- 修改源码文件(`.vue``.tsx``.ts``.js``.css``.scss` 等)时禁止 restart
- 确需强制重启时,使用 `python dev-services.py restart --force`
4. **热加载规则**
- 前端修改源码文件优先利用热加载
- 后端修改代码按现有规则处理(本地不启动后端,使用服务器验收)
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| 固定端口被占用 | `dev-services.py` 报错,要求先停止占用该端口的服务 |
| 无代码变更执行 restart | 提示无需重启 |
| 只修改 `.vue` 文件执行 restart | 拒绝重启,提示使用 `--force` 或无需重启 |
| 修改 `vite.config.ts` 执行 restart | 允许正常 restart |
| 多个服务同时 restart | 每个服务独立检测,符合规则的重启,不符合的跳过 |
| 用户传 `--force` | 跳过所有检测,强制重启 |
## 修改范围
| 文件 | 操作 |
|---|---|
| `dev-services.py` | 修改:添加固定端口映射、优化端口分配、新增热加载保护 |
| `web/vite.config.ts` | 修改:端口改为 5178 |
| `web/.env.development` | 修改/创建:添加 `VITE_PORT=5178` |
| `web-admin/vite.config.ts` | 修改:端口改为 5179 |
| `web-admin/.env.development` | 修改:`VITE_APP_PORT` 改为 5179 |
| `mini-program/vite.config.js` | 修改:端口改为 5180 |
| `mini-program/.env.development` | 修改/创建:添加 `VITE_PORT=5180` |
| `life-script/vite.config.js` | 修改:端口改为 5181 |
| `life-script/.env.development` | 修改/创建:添加 `VITE_PORT=5181` |
| `CLAUDE.md` | 修改:更新本地服务管理规则 |
@@ -0,0 +1,158 @@
---
author: claude
created_at: 2026-06-27
purpose: 提取 MessageCard 组件,统一所有 AI 消息卡片样式,按内容长度阈值(100 字)决定是否显示完整功能按钮
---
# MessageCard 组件统一设计
## 背景
在上一轮实现中,result-chat-list 中的 assistant 消息使用了 `.chat-bubble.system` 气泡样式,与 `story-card` 的卡片样式不一致。用户要求:
1. 所有 AI 回复消息卡片的样式、功能、布局、按钮等,都与第一次生成剧本的 `story-card` 完全一致
2. 内容 < 100 字的 assistant 消息:只显示消息内容,无功能按钮
3. 内容 ≥ 100 字的 assistant 消息:必须与 `story-card` 完全一致
## 目标
- 提取独立的 `MessageCard.vue` 组件
- story-card 和 result-chat-list 都复用该组件
- 按 100 字阈值自动切换显示模式
## 方案选择
采用 **方案 B:提取独立 MessageCard.vue 组件**
- 代码复用,维护性好
- 改动范围:新建 1 个组件 + 修改 ScriptView.vue
## 详细设计
### 1. 组件接口
**文件**`mini-program/src/components/MessageCard.vue`
**Props**
| Prop | 类型 | 必填 | 说明 |
|---|---|---|---|
| `content` | String | 是 | 消息/故事正文内容 |
| `title` | String | 否 | 卡片标题,默认 `'我的人生剧本'` |
| `tags` | Array | 否 | 标签列表 |
| `collapsed` | Boolean | 是 | 当前折叠状态 |
| `contentLength` | Number | 是 | 内容字数(用于阈值判断) |
| `isShortMessage` | Boolean | 是 | `contentLength < 100` 时为 true |
| `ttsIcon` | String | 否 | TTS 按钮图标(`▶``Ⅱ` |
| `ttsText` | String | 否 | TTS 按钮文字(`播放``暂停` |
**Events**
| 事件 | 说明 |
|---|---|
| `toggle-collapse` | 展开/收起 |
| `copy` | 复制内容 |
| `change-direction` | 换个方向 |
| `not-like-me` | 不像我 |
| `continue` | 继续生成 |
| `play-tts` | 播放 TTS |
### 2. 组件内部结构
**短消息(`isShortMessage = true`**
```vue
<view class="chat-bubble system">
<text>{{ content }}</text>
</view>
```
只显示纯文本气泡,无标题、无标签、无按钮。
**长消息(`isShortMessage = false`**
完整复用现有 `story-card` 的 HTML 结构:
- 标题(`.story-title`+ 标签(`.tag-row`
- 头部操作区:展开/收起按钮 + 复制按钮
- 正文:展开时用 scroll-view,收起时用 text
- 底部:展开全文/收起全文按钮
- 功能按钮组(2 列网格):换个方向、不像我、继续生成、播放
### 3. ScriptView.vue 中的使用
**替换现有 story-card**
```vue
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
```
**替换 result-chat-list 中的 assistant 消息**
```vue
<view v-for="message in resultMessages" :key="message.id">
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
```
### 4. 样式迁移
将以下 CSS 类从 `ScriptView.vue` 迁移到 `MessageCard.vue``<style scoped>` 中:
- `.story-card``.story-card.collapsed`
- `.story-head``.story-head-actions``.story-title-wrap`
- `.story-title``.tag-row``.tag`
- `.collapse-icon``.copy-card-btn``.collapse-row`
- `.collapse-chevron``.story-body``.story-body-scroll`
- `.result-actions``.action-btn`
`ScriptView.vue` 中删除这些已迁移的样式。
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| content 刚好 100 字 | `isShortMessage = false`,显示完整卡片 |
| content 为空 | 显示空卡片(与 story-card 现有行为一致) |
| pending 中的消息 | 不使用 MessageCard,保持 thinking-dots 渲染 |
| user 消息 | 不使用 MessageCard,保持 `.chat-bubble.user` 气泡 |
## 修改范围
| 文件 | 操作 |
|---|---|
| `mini-program/src/components/MessageCard.vue` | 新建 |
| `mini-program/src/pages/main/ScriptView.vue` | 修改(模板 + 脚本 + 样式) |
@@ -0,0 +1,207 @@
---
author: system
created_at: 2026-06-27
purpose: 短信登录开关与通用系统配置功能设计
---
# 短信登录开关与通用系统配置设计
## 背景
当前系统未接入真实短信服务商,短信验证码使用硬编码 `123456`。小程序登录页同时展示「微信一键登录」和「手机号+验证码登录」两种方式,SMS 相关元素始终可见。
需要在管理后台增加系统配置管理功能,通过「短信登录开关」控制小程序端是否展示手机号短信登录入口。短信未启用时,小程序登录页只显示微信授权登录。
## 方案选择
采用 **方案 AKey-Value 通用配置表**
- 新建 `t_system_config` 表,key-value 结构
- 通用性强,后续加新配置只需 INSERT 一行数据,不用改表结构
- 复杂度可控,管理后台根据 `value_type` 动态渲染对应控件
## 详细设计
### 一、数据库设计
#### 新建表 `t_system_config`
```sql
CREATE TABLE t_system_config (
id VARCHAR(64) PRIMARY KEY,
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置唯一标识',
config_value VARCHAR(500) NOT NULL DEFAULT '' COMMENT '配置值',
value_type VARCHAR(20) NOT NULL DEFAULT 'string' COMMENT '值类型: boolean/string/number/json',
config_group VARCHAR(50) NOT NULL DEFAULT 'system' COMMENT '配置分组: system/login/notification',
config_name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '显示名称',
description VARCHAR(300) DEFAULT '' COMMENT '配置说明',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序号',
is_visible TINYINT NOT NULL DEFAULT 1 COMMENT '是否在管理后台可见: 0-隐藏, 1-显示',
create_by VARCHAR(64) DEFAULT NULL,
create_time DATETIME DEFAULT NULL,
update_by VARCHAR(64) DEFAULT NULL,
update_time DATETIME DEFAULT NULL,
is_deleted TINYINT NOT NULL DEFAULT 0
) COMMENT = '系统配置表';
```
#### 初始数据
```sql
INSERT INTO t_system_config (id, config_key, config_value, value_type, config_group, config_name, description, sort_order, is_visible, create_time, update_time, is_deleted)
VALUES (
REPLACE(UUID(), '-', ''),
'sms_login_enabled',
'false',
'boolean',
'login',
'短信登录',
'是否启用手机号+短信验证码登录方式',
1,
1,
NOW(),
NOW(),
0
);
```
### 二、后端设计
#### 新增文件
| 层 | 文件 | 说明 |
|---|---|---|
| Entity | `SystemConfig.java` | 继承 `BaseEntity`,映射 `t_system_config` |
| Mapper | `SystemConfigMapper.java` | MyBatis-Plus 基础 CRUD |
| Service | `SystemConfigService.java` | 接口定义 |
| Service | `SystemConfigServiceImpl.java` | 业务逻辑实现 |
| Controller | `SystemConfigController.java` | 管理端接口,需 admin 登录 |
| DTO | `SystemConfigUpdateRequest.java` | 更新请求体(key + value |
| DTO | `LoginConfigResponse.java` | 返回给小程序的登录方式配置 |
#### 接口设计
**管理端接口(需要 admin token):**
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/admin/systemConfig/list` | 获取所有可见配置列表(`is_visible=1` |
| PUT | `/admin/systemConfig/update` | 批量更新配置值,传入 `List<SystemConfigUpdateRequest>` |
**公开接口(不需要登录,小程序登录页调用):**
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/auth/loginConfig` | 返回当前启用的登录方式 |
`/auth/loginConfig` 返回格式:
```json
{
"code": 200,
"data": {
"wechatLoginEnabled": true,
"smsLoginEnabled": false
}
}
```
#### 核心逻辑
1. `SystemConfigService` 提供 `getConfigValue(String key)` 方法
2. 内部做 Redis 缓存,缓存 key 为 `system_config:{config_key}`
3. 更新配置时同步清除对应缓存
4. `/auth/loginConfig` 读取 `sms_login_enabled` 配置值,组装 `LoginConfigResponse` 返回
5. 管理端更新接口接收 `List<SystemConfigUpdateRequest>`,支持一次提交多个配置变更
6. `/auth/loginConfig``WebMvcConfig` 白名单中放行(与 `/auth/login``/auth/wechat/login` 同级)
### 三、管理后台设计
#### 路由配置
`web-admin/src/router/index.ts` 中新增:
```ts
{
path: '/system',
component: Layout,
redirect: '/system/settings',
meta: { title: '系统设置', icon: 'Setting' },
children: [
{
path: 'settings',
name: 'SystemSettings',
component: () => import('@/views/system/SystemSettings.vue'),
meta: { title: '基础设置' }
}
]
}
```
#### 页面设计
**文件**`web-admin/src/views/system/SystemSettings.vue`
**页面结构**
- 顶部:页面标题「基础设置」+ 保存按钮
- 内容区:按 `config_group` 分组展示配置项
- 每个配置项根据 `value_type` 渲染不同控件:
- `boolean``el-switch` 开关
- `string``el-input` 输入框
- `number``el-input-number` 数字输入框
- 当前只有一个配置项:「短信登录」开关,下方显示描述文字
- 点击「保存」调用批量更新接口
#### API 封装
新增 `web-admin/src/api/systemConfig.ts`
- `getSystemConfigList()` → GET `/admin/systemConfig/list`
- `updateSystemConfig(list)` → PUT `/admin/systemConfig/update`
### 四、小程序登录页设计
#### 数据获取
在登录页 `onMounted` 时调用 `/auth/loginConfig` 接口:
```js
const loginConfig = ref({ wechatLoginEnabled: true, smsLoginEnabled: false })
onMounted(async () => {
try {
const res = await getLoginConfig()
loginConfig.value = res.data
} catch (e) {
// 接口失败时默认只显示微信登录
loginConfig.value = { wechatLoginEnabled: true, smsLoginEnabled: false }
}
})
```
#### 条件渲染
`v-if="loginConfig.smsLoginEnabled"` 控制以下元素的显示:
- 分隔线(「或使用手机号登录」)
- 手机号输入框
- 验证码输入框 + 获取验证码按钮
- 「开启旅程」手机号登录按钮
始终显示的元素:
- 品牌标题区域
- 「微信一键登录」按钮
- 底部协议文字
#### 代码处理
- 新增 `services/auth.js` 中的 `getLoginConfig()` 方法
- 短信相关的 `ref``phone``code``countdown` 等)和方法保留不动,只是 `v-if` 不渲染
- 不删除任何现有短信登录代码,后续启用短信时只需将配置改为 `true` 即可
## 验收标准
1. 管理后台出现「系统设置」菜单,可看到「短信登录」开关
2. 开关默认关闭(`false`
3. 小程序登录页在短信关闭时只显示微信登录按钮,无手机号表单
4. 管理后台将开关打开后,小程序刷新登录页可看到手机号+验证码表单
5. 后端编译通过,部署到服务器后验收正常
6. 浏览器 Console 无报错

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