Files
36Wisdom/技术设计.md
2026-08-14 16:11:10 +08:00

53 KiB
Raw Permalink Blame History

技术设计

《三十六计小课堂》实现细节与技术决策。规范见 CLAUDE.md,功能总览见 README.md。

1. 技术选型

选型 理由
前端前台 uni-app (Vue 3 + Vite) 一套代码发布 Android / iOS / 平板 / 微信小程序 / H5;Vue 3 语法与团队技术栈一致;小程序支持生态最成熟
前端后台 Vue 3 + Element Plus 后台为桌面 Web 场景,EP 组件成熟;独立工程 admin-src/,与 uni-app 前台互不影响
后端 Go + GoFrame v2 团队既有规范(CLAUDE.md 分层),性能与部署简单
数据库 SQLite(单文件) 单机部署、文件型备份、零运维;写操作串行化规避并发锁
缓存 gcache 内存 / 可选 redis 读查询缓存(TTL 来自 database.cache.ttl),写后清缓存
部署 Docker Compose 单机 前后端一体单端口,数据目录挂载持久化
文件存储 本地磁盘 workspace/ 图片/音频资源,HTTP 静态托管
朗读语音 TTS 生成音频文件(预留) 低龄档朗读;首期可先接入在线 TTS 或人工录制,音频文件落 workspace

不采用:Flutter(小程序支持不成熟)、Taro(需转 React)、原生双端(维护成本翻倍)。

2. 总体架构

uni-app 前台(5端) ─┐
Vue3+EP 后台 ──────┼── HTTP/JSON ──> Go 服务 ──> SQLite
                   │                   │
                   └── 静态资源(图片/音频) <── workspace/
  • 路由前缀:前台 /api/,后台 /api/admin/(管理员鉴权)
  • 种子数据:启动时检测 strategy 表为空则从 Go embed 的 JSON 导入,后台运营可直接改库迭代
  • 资源访问:上传文件存 workspace/uploads/,数据库存相对路径,静态路由映射

3. 数据库设计(DDL

表名集中在 biz/consts/table_name.go,状态常量在 biz/consts/status.go

parent(家长)与 child(孩子档案)

家长注册登录(微信 openid 或手机号),下挂多个孩子档案。学习主体是孩子:进度、积分、计策卡、兑换均按 child 维度,家长负责注册、查看报告与确认实物奖品。

CREATE TABLE parent (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  openid        TEXT UNIQUE,              -- 微信小程序 openid
  phone         TEXT UNIQUE,              -- 手机号(H5/App 注册)
  password      TEXT,                     -- bcrypt 哈希(手机号注册时)
  nickname      TEXT,
  avatar        TEXT,                     -- 头像文件相对路径
  status        INTEGER NOT NULL DEFAULT 1,
  created_at    DATETIME,
  updated_at    DATETIME
);
CREATE INDEX idx_parent_openid ON parent(openid);

CREATE TABLE child (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  parent_id     INTEGER NOT NULL,
  nickname      TEXT,
  avatar        TEXT,
  age_group     TEXT NOT NULL DEFAULT '4-6',  -- 年龄段档位:4-6 / 6-8
  points        INTEGER NOT NULL DEFAULT 0,   -- 积分余额(冗余,以 point_log 为准,写时同事务)
  daily_limit_minutes INTEGER NOT NULL DEFAULT 0, -- 每日学习限额(0=不限,家长端设置,前端本地计时)
  status        INTEGER NOT NULL DEFAULT 1,
  created_at    DATETIME,
  updated_at    DATETIME
);
CREATE INDEX idx_child_parent ON child(parent_id);

后续各表中 child_id 均指孩子(原 user 维度表统一改为 child 语义)。

strategy(计策)

CREATE TABLE strategy (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  name          TEXT NOT NULL,            -- 计策名,如「声东击西」
  pinyin        TEXT NOT NULL,            -- 拼音 zhu sheng dong ji xi
  group_no      INTEGER NOT NULL,         -- 六套分组:1胜战 2敌战 3攻战 4混战 5并战 6败战
  group_name    TEXT NOT NULL,
  meaning       TEXT NOT NULL,            -- 儿童语言释义
  meaning_pinyin TEXT,                    -- 释义拼音(逐字对齐 JSON,见 4.6)
  teach_content TEXT,                     -- 计策学堂:儿童化讲解(名称/释义/使用时机)
  teach_content_pinyin TEXT,
  teach_image   TEXT,
  teach_audio   TEXT,
  summary_q     TEXT,                     -- 智慧总结反思题
  summary_q_pinyin TEXT,
  summary_options TEXT,                   -- 反思题选项 JSON(含正确答案)
  summary_options_pinyin TEXT,
  summary_audio TEXT,
  icon          TEXT,                     -- 计策卡图标
  sort_order    INTEGER NOT NULL,         -- 组内排序
  unlock_before INTEGER,                  -- 解锁前置:NULL=首计或按进度解锁
  status        INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_strategy_group ON strategy(group_no, sort_order);

解锁规则:unlock_before 为空或前置计策已通关(服务端校验)。

level(关卡,每计 3-5 关)

CREATE TABLE level (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  strategy_id   INTEGER NOT NULL,
  title         TEXT NOT NULL,            -- 场景标题,如「足球场上的假动作」
  scene_id      INTEGER,                  -- 环境场景元素(element.idtype=1),整关发生场所
  scene_content TEXT NOT NULL,            -- 情境描述(含拼音标注格式)
  scene_content_pinyin TEXT,              -- 情境描述拼音(逐字对齐 JSON)
  scene_image   TEXT,
  scene_audio   TEXT,                     -- 情境朗读音频
  age_group     TEXT NOT NULL DEFAULT '4-6',  -- 内容年龄段标签,按孩子档位过滤
  content_version INTEGER NOT NULL DEFAULT 1, -- 内容版本:节点/选项改动时 +1
  sort_order    INTEGER NOT NULL,
  status        INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_level_strategy ON level(strategy_id, sort_order);

scene_node(情境节点)与 node_option(分支选项)

闯关以决策树组织:每关(level)一个情境,一棵树。scene_node 为节点(决策节点或终局节点),node_option 为决策节点的分支选项,选项通过 next_node_id 指向下一节点;多条不同分支可汇聚到同一终局节点(正确路线不唯一)。后台保存时校验有向无环(防死循环)。

CREATE TABLE scene_node (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  level_id      INTEGER NOT NULL,
  title         TEXT,                         -- 节点标题(可选)
  character_id  INTEGER,                      -- 当前人物元素(element.idtype=2),冲突主体
  content       TEXT NOT NULL,                -- 情境描述(含拼音标注格式)
  content_pinyin TEXT,                        -- 节点文本拼音(逐字对齐 JSON)
  image         TEXT,
  audio         TEXT,                         -- 情境朗读音频
  node_type     INTEGER NOT NULL DEFAULT 1,   -- 1 决策节点 2 终局节点
  interaction_type INTEGER NOT NULL DEFAULT 1, -- 互动形态:1选项选择 2道具选择 3步骤排序 4动作过关 5拖拽放置 6接取收集 7找线索 8连线配对
  config        TEXT,                         -- 互动配置 JSON:动作过关子模式(jump/climb/dodge/run)+难度(速度/障碍密度)等
  result_type   INTEGER NOT NULL DEFAULT 0,   -- 终局评级:0 非终局 1 失败 2 良好 3 最佳
  is_entry      INTEGER NOT NULL DEFAULT 0,   -- 是否入口节点(每关一个)
  sort_order    INTEGER NOT NULL DEFAULT 1,
  status        INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_node_level ON scene_node(level_id);

CREATE TABLE node_option (
  id             INTEGER PRIMARY KEY AUTOINCREMENT,
  node_id        INTEGER NOT NULL,            -- 所属决策节点
  text           TEXT NOT NULL,               -- 选项文本(行为描述)
  text_pinyin    TEXT,                        -- 选项文本拼音(逐字对齐 JSON)
  prop_id        INTEGER,                     -- 使用道具元素(element.idtype=3,可空)
  audio          TEXT,
  next_node_id   INTEGER,                     -- 指向下一节点(同 level 内)
  feedback       TEXT NOT NULL,               -- 选择后的即时点评(儿童语言)
  feedback_pros  TEXT,                        -- 对比点评:这个选择的好处(生成管线填充)
  feedback_cons  TEXT,                        -- 对比点评:这个选择的不足/错过的更好选择
  feedback_pinyin TEXT,
  feedback_audio TEXT,
  sort_order     INTEGER NOT NULL DEFAULT 1,
  status         INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_option_node ON node_option(node_id);

element(元素库:场景 / 人物 / 道具)

元素是决策树的结构性骨架(见 4.3 内容建模规范),统一一张表按类型复用,后台一个入口管理。

CREATE TABLE element (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  e_type        INTEGER NOT NULL,             -- 1 场景(环境) 2 人物 3 道具
  name          TEXT NOT NULL,
  name_pinyin   TEXT,                         -- 元素名拼音(逐字对齐 JSON)
  image         TEXT,
  audio         TEXT,                         -- 元素名称朗读
  description   TEXT,                         -- 儿童语言介绍(道具点击提示等)
  description_pinyin TEXT,
  status        INTEGER NOT NULL DEFAULT 1,
  sort_order    INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_element_type ON element(e_type);

约束:决策节点(node_type=1)至少 2 个选项;终局节点(node_type=2)无选项;每关恰好 1 个入口节点;next_node_id 必须指向同 level 节点;保存/加载时拓扑校验无环;选项 prop_id、节点 character_id、关卡 scene_id 须指向存在的元素。

进度与成就

CREATE TABLE user_progress (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  child_id      INTEGER NOT NULL,
  level_id      INTEGER NOT NULL,
  stars         INTEGER NOT NULL DEFAULT 0,   -- 0-3
  score         INTEGER NOT NULL DEFAULT 0,   -- 本次得分
  perfect       INTEGER NOT NULL DEFAULT 0,   -- 1=本关所有终局都到达过(完美,解锁下一关)
  content_version INTEGER NOT NULL DEFAULT 1, -- 通关时关卡版本(后台改内容后版本不一致,标记"可重新挑战")
  completed_at  DATETIME,
  UNIQUE(child_id, level_id)
);

CREATE TABLE chapter_review (                -- 章末温故:每章完美后随机抽旧关复习,完成解锁下一章
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  child_id      INTEGER NOT NULL,
  strategy_id   INTEGER NOT NULL,            -- 温故发生在哪一章
  level_ids     TEXT NOT NULL,               -- 抽中的复习关卡 JSON 快照,如 [1,3,5]
  status        INTEGER NOT NULL DEFAULT 1,  -- 1 进行中 2 已完成
  created_at    DATETIME,
  UNIQUE(child_id, strategy_id)
);

CREATE TABLE user_route_log (                -- 决策路径流水:每步选择一条,终局一条
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  child_id      INTEGER NOT NULL,
  level_id      INTEGER NOT NULL,
  node_id       INTEGER NOT NULL,
  option_id     INTEGER NOT NULL DEFAULT 0,  -- 0=终局记录
  result_type   INTEGER NOT NULL DEFAULT 0,  -- 终局评级(仅终局记录行)
  created_at    DATETIME
);
CREATE INDEX idx_route_log_child ON user_route_log(child_id, level_id, created_at);

CREATE TABLE user_collection (                -- 计策卡:计策单元全关卡通关后解锁
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id       INTEGER NOT NULL,
  strategy_id   INTEGER NOT NULL,
  unlocked_at   DATETIME,
  UNIQUE(user_id, strategy_id)
);

CREATE TABLE badge (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  name          TEXT NOT NULL,
  icon          TEXT,
  cond_type     INTEGER NOT NULL,             -- 1集卡数 2连续签到 3通关数 4积分 5完美关卡数 6复习完成数
  cond_value    INTEGER NOT NULL,             -- 达成阈值
  status        INTEGER NOT NULL DEFAULT 1
);

CREATE TABLE user_badge (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id       INTEGER NOT NULL,
  badge_id      INTEGER NOT NULL,
  earned_at     DATETIME,
  UNIQUE(user_id, badge_id)
);

life_task(生活践行任务)

游戏内践行(情境闯关)之外的真实生活践行:每章完美后任务下发,家长引导孩子实践,家长确认为权威入口(天然防刷)。孩子 × 任务的执行状态记录(原 life_task_log)无读写引用,已连同建表与库内数据整套移除(启动时 DROP),M2 实现时按孩子 × 任务唯一约束重建。

CREATE TABLE life_task (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  strategy_id   INTEGER NOT NULL,            -- 关联计策(该章完美后下发)
  title         TEXT NOT NULL,               -- 任务名,如「引开小猫」
  description   TEXT NOT NULL,               -- 给孩子看的任务描述
  guide         TEXT NOT NULL,               -- 给家长的引导语
  reward_points INTEGER NOT NULL DEFAULT 20, -- 家长确认后奖励积分
  status        INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_life_task_strategy ON life_task(strategy_id);

积分与奖品

point_log 为 child 的附属流水表,建表由主表 child 的 dao 托管(豁免规则见 CLAUDE.md「分层文件对齐与代码模式」)。

CREATE TABLE point_log (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id       INTEGER NOT NULL,
  change        INTEGER NOT NULL,             -- 正负变动
  reason_type   INTEGER NOT NULL,             -- 1闯关 2签到 3兑换扣减 4管理调整
  ref_id        INTEGER,                      -- 关联业务 idlevel/prize
  balance_after INTEGER NOT NULL,
  created_at    DATETIME
);
CREATE INDEX idx_point_log_user ON point_log(user_id, created_at);

CREATE TABLE prize (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  name          TEXT NOT NULL,
  description   TEXT,
  icon          TEXT,
  p_type        INTEGER NOT NULL DEFAULT 1,   -- 1虚拟(即时到账) 2实物(家长确认)
  points_cost   INTEGER NOT NULL,
  stock         INTEGER NOT NULL DEFAULT 0,   -- -1 不限量
  status        INTEGER NOT NULL DEFAULT 1,
  sort_order    INTEGER NOT NULL DEFAULT 1
);

CREATE TABLE redemption (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id       INTEGER NOT NULL,
  prize_id      INTEGER NOT NULL,
  points_cost   INTEGER NOT NULL,
  status        INTEGER NOT NULL DEFAULT 1,   -- 1待领取(虚拟) 2待发货(实物) 3已发货 4已取消 5已领取(家长确认)
  code          TEXT,                         -- 兑换码(实物发货后生成)
  created_at    DATETIME,
  updated_at    DATETIME
);
CREATE INDEX idx_redemption_user ON redemption(user_id, created_at);

admin_user(后台管理员)

CREATE TABLE admin_user (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  username    TEXT UNIQUE NOT NULL,
  password    TEXT NOT NULL,                 -- bcrypt 哈希
  status      INTEGER NOT NULL DEFAULT 1,
  created_at  DATETIME
);

4. 关键设计

4.1 种子数据初始化

  • 种子 JSON 用 go:embed 打进二进制(根级 seed/ 目录,非表分层目录:跨表启动导入不属任何业务表,禁放 biz/service/),含:元素库(场景约 15 / 人物约 10 / 道具约 20)、36 计全部内容、每计 1-3 个现代情境关卡(决策树:节点 + 分支选项 + 元素关联)、初始奖品、初始徽章、默认管理员账号
  • 启动时先建表(main.go 依次调各 dao 的 InitCREATE TABLE IF NOT EXISTS 幂等),随后执行 seed.EnsureSeeded():查 strategy 空表则按序插入,插入包事务;随后对全部内容调用标注服务生成拼音(见 4.6),TTS 音频异步生成,均只执行一次
  • 后台运营迭代 = 后台改内容 + 可选导出种子;不覆盖用户数据表

4.2 年龄段分级

  • 两档:4-6(启蒙)/ 6-8(进阶)
  • 内容层:level.age_group 标签,列表接口按孩子档位过滤(采用"关卡带最低档位标签"策略:4-6 孩子只看 4-6 关卡,6-8 孩子两档都看,后续可加独立进阶关卡)
  • 表现层:前端按档位开关拼音与朗读(低龄档默认全量开启)与互动形态(4-6 档仅选择类 + 低速操作类,6-8 档全开放)

4.3 分支决策闯关(POST /api/level/choose

决策树骨架(元素结构):元素是决定决策走向与分支的核心要素,内容按统一骨架构建:

结构 元素 说明
关卡 环境场景(level.scene_id 冲突发生的场所,整关背景
决策节点 人物(scene_node.character_id 节点中冲突的主体("谁"遇到问题)
选项 道具(node_option.prop_id 该行为使用的道具("用什么"),可空
分支 next_node_id 该行为在环境中的后果

内容写作范式:决策节点 = 环境中人物遇到的冲突;选项 = 对谁、用什么道具、做什么行为;分支 = 行为的后果。LLM 生成初稿与人工审核均按此骨架;道具应与环境匹配(后台保存时校验元素存在,环境匹配为内容软规范)。元素不参与判分(分支由决策树显式定义),只决定决策的表达与走向解释。

互动形态(interaction_type:决策节点的交互方式不限于选项点击,按 8 个大类组织:1 选项选择 / 2 道具选择 / 3 步骤排序 / 4 动作过关 / 5 拖拽放置 / 6 接取收集 / 7 找线索 / 8 连线配对。其中动作过关类(4)是一套平台动作组件,子模式由节点 config 配置:跳跃(jump,跳过陷阱坑/河沟抄近路)、攀爬(climb,翻墙越障)、障碍躲避(dodge,躲避巡逻)、操作奔跑(run,追击/逃回)——同一组件,地形与目标配置不同,实现成本一份、玩法多样;难度(速度/障碍密度)同样走 config 并按年龄段过滤(4-6 档低速,6-8 档全速)。操作类节点(4-6)复用选项表预置"成功/失败"两个出口text=成功了/没成功,next 指向对应分支,feedback 写互动点评),choose 接口与决策树模型零改动;前端按 interaction_type + config 渲染对应互动组件(uni-app 触摸/CSS 动画实现,无需游戏引擎),互动结束后按结果提交对应选项。枚举可扩展。

学习闭环:每章按「计策学堂(认知)→ 情境闯关(践行)→ 完美 → 章末温故 → 智慧总结(反思)」推进:进入章节先看学堂(strategy.teach_,图文 + 音频约 1 分钟)→ 逐关闯关;完美且温故完成后出现智慧总结一题(strategy.summary_,答对 +5 分每章一次,答错展示正确解释);随后解锁下一章。单次会话 = 一关(5-10 分钟),匹配低龄注意力,地图随时进出。

流程(儿童逐步决策,每步即时反馈):

  1. GET /api/level/detail 返回关卡入口节点与情境信息
  2. POST /api/level/chooselevel_id + node_id + option_id):
    • 服务端校验:node 属于 level、option 属于 node、关卡已解锁;校验通过返回下一节点内容
    • 若下一节点是决策节点:继续选择
    • 若下一节点是终局节点:返回终局评级与结算结果(星星、积分),本次闯关结束
  3. 每步选择后前端展示该选项的即时点评(feedback)

判分规则(答案以服务端为准,防篡改):

  • 终局评级 → 星数:最佳=3 星、良好=2 星、其他结果=0 星
  • 多条路线均可到达最佳终局(决策树分支汇聚),任何路线拿到最佳都算"正确领悟"
  • 结算时机:首次到达某终局时结算,重复路线不重复结算(防刷分)
    • 该关未通关时:最佳 +30 / 良好 +10 / 其他结果 -10(教训分,余额扣至 0 不为负;同一关卡连续 2 次到达失败终局后不再扣分,防重复挫败;扣分事件前端可爱化表达"没找到妙计,-10 军粮"
    • 该关已通关后补分支:不结算积分,只记完成度(探索无压力,避免"强制走错路还扣分"的矛盾)
  • 通关 = 到达任一最佳终局(3 星、发积分);完美 = 本关所有终局都到达过(解锁下一关、+20 分、钻石标记)
  • 单关完美后检查:该计策全部关卡是否完美 → 是则解锁计策卡(写 user_collection

结算写库(同事务):user_progress(取最高星 + perfect 标记 + 记录通关时 content_version)、point_log + child.points 同步、user_collection 解锁。并发防重入:同一孩子同一关结算用 common.WithLockkey=child:{id}:level:{id})。

路径记录:每次 choose(含终局)写一条 user_route_log——支撑家长学习报告与运营内容分析(哪个节点卡点、哪些路线无人走);写失败仅记日志不阻断主流程(学习数据非关键路径)。

再玩一次:已通关关卡解锁校验放行,可随时重玩换路线(复用同一 choose 流程);已通关后不结算积分/星级,纯探索其他分支;完美判定由 user_route_log 统计(该关 distinct 终局节点数 = 终局总数)。

章末温故(章节间复习):每章(计)完美后、解锁下一章前出现温故环节——POST /api/review/start 服务端从之前已完美章节随机抽 2-3 个计谋、每计抽 1 个已通关关卡(选项顺序打乱,防位置记忆)返回;孩子逐关走同一 choose 流程(已通关不计分);全部到达终局后 POST /api/review/complete 校验:写 chapter_reviewUNIQUE(child_id, strategy_id) 防重复)、解锁下一章、发放复习奖励积分(+10)。末章无下一章不触发。后续可运营"复习专用新场景关卡",将温故升级为真迁移测试。

内容版本:后台保存节点/选项改动时 level.content_version + 1;地图接口对 progress.content_version < level.content_version 的关卡标记"内容已更新,可重新挑战",星级保留、重玩可刷新。

4.4 积分规则

来源 分值
未通关首次到达最佳终局 +30 分(3 星)
未通关首次到达良好终局 +10 分(2 星)
未通关首次到达其他终局 -10 分(教训分,余额扣至 0 不为负)
完美(全终局到达) +20 分
章末温故完成 +10 分(每章一次)
智慧总结答对 +5 分(每章一次)
生活践行任务(家长确认) +20 分(每任务一次)
每日签到 +5 分(按日期去重,同日重复请求直接忽略)
兑换 扣减奖品积分(校验余额与库存)
管理调整 后台手动 ±

签到去重:point_logchild_id + reason_type=2 + 当天日期 查重,重复直接返回"已签到"。

4.5 奖品兑换

  • 虚拟奖品:余额校验 → 库存校验 → 扣积分 → 写 redemption(状态=待领取)→ 发放入库(如徽章/皮肤字段),同事务
  • 实物奖品:扣积分 → 写 redemption(状态=待发货)→ 兑换成功即弹庆祝动画"请爸爸妈妈帮你领取";后台发货后生成兑换码(code)→ 家长在家长中心"确认领取"(状态=5 已领取)→ 孩子端下次打开提示"奖品到啦"
  • 库存扣减:事务内 UPDATE prize SET stock=stock-1 WHERE id=? AND stock>0,行级原子扣减防超卖(SQLite 单写者天然串行,database is locked 风险由重试兜底)

4.6 拼音与朗读(写时一次性标注,读时零成本)

拼音与音频都遵循"写时转换"原则:内容量小(约 300 节点)但读请求量大(所有端所有用户),标注做一次入库,前端读取零计算、五端一致。

  • 拼音入库:种子 JSON 只存原文 → 初始化时由 common/pinyin.go 标注服务生成拼音 → 随原文一并入库(各内容表存 *_pinyin 字段,见 3 节 DDL);后台改文案保存时调用同一标注函数自动重标,仍是写时一次性
  • 拼音格式*_pinyin逐字对齐的 JSON 数组字符串(如 ["shēng","dōng","jī","xī","。",""]),下标与原文字符一一对应(标点拼音为空串或原文标点);前端 JSON.parse 后与原文 zip,渲染 <ruby> 注音,无需分词对齐
  • 标注算法:按多音字词表最长匹配切分原文——命中词表(成语整体、专名)的片段整体转拼音,未命中片段逐字转(github.com/mozillazg/go-pinyin,StyleTone 带声调符号);词表在 biz/consts 集中维护(36 计成语整体拼音 + 教育专名 override),保证准确性
  • 标注幂等:文本未变不重标(标注函数纯函数、无副作用);已存在库的旧数据在启动时补标一次(扫描 *_pinyin 为空的记录)
  • 朗读音频(M1.5 降级方案):audio 字段全空且无 TTS 账号时——前端降级朗读H5 端优先播文件(uni.createInnerAudioContext,有 *_audio 字段就播),无文件则用浏览器原生 SpeechSynthesis(zh-CN)朗读该文本,零成本立刻可听;朗读按钮统一封装 ui-src/src/utils/speech.js
  • TTS 音频文件管线:与拼音同一"写时生成"原则,但走离线管线 cmd/gentss/(Edge TTS 神经网络语音,见 4.16),一次性生成 mp3 回填各 *_audio 字段;生成失败不阻塞内容落库,音频缺失时前端自动降级为浏览器朗读(前端零改动)
  • 静态资源Go 静态路由托管 workspace/uploads/,URL 直接进前端;元素素材(场景/人物/道具图片与名称音频)同样按"写时一次性"生成与上传,随内容接口返回

4.7 触控互动组件(M1.5:互动形态 2/3/5/7/8

设计 4.3 定义 8 种互动形态,M1 仅实现 1 选项选择。M1.5 补齐触控 5 种:2 道具选择、3 步骤排序、5 拖拽放置、7 找线索、8 连线配对(4 动作过关、6 接取收集留 M3)。实现遵循「互动入口节点 + 成功/失败双出口」模型,后端 choose 接口与判分零改动。

互动入口节点模型:每关在决策树入口前插入一个互动节点(interaction_type 2/3/5/7/8 按关卡轮换),其选项表预置两个出口——成功(next 指向原入口节点)与失败(next 指向本关首个失败终局节点);is_entry 标记迁移到互动节点(仍满足"每关恰好 1 个入口"约束)。互动组件判定结果后提交对应出口选项,走既有 choose → 终局结算链路。失败即进失败终局(正常判分 -10,连续失败保护生效);成功进原决策树,原树结构零改动、完美逻辑不受影响。

config 数据(程序化生成,不手写内容):全部由该关元素派生——道具列表 = 本关所有选项的 prop_id 去重;正确答案 = 最佳分支选项的道具(best_prop)。种子初始化时 seed.go 为每关生成互动节点与 config

类型 config.kind 组件玩法 判定
2 道具选择 prop_pick 展示道具卡,点选一个 选中 = best_prop
7 找线索 find_spot 场景中若干"可疑点"(道具卡排布),点击寻找 点中 = best_prop
5 拖拽放置 drag_place 把道具拖到目标框 放下的是 best_prop
3 步骤排序 step_sort 拖拽排序道具(正确顺序 = 本关选项 sort_order 序) 全序正确
8 连线配对 link_match 左列人物 ↔ 右列道具,两两连线 人物-道具配对与选项 prop 关联一致

config JSON 结构(scene_node.config,前端解析渲染):{"kind":"prop_pick","items":[{"id":1,"name":"画","e_type":3},...],"answer":3,"persons":[...]}link_match 含 persons + 答案对)。选项文本预置「成功 / 没成功」,feedback 写互动点评。

前端组件ui-src/src/components/interactions/uni-app touch 事件 + CSS/SVG,无游戏引擎):PropPick.vue(点选)/ FindSpot.vue(点选定位)/ DragPlace.vuetouchstart/move/end 拖拽)/ StepSort.vue(拖拽排序)/ LinkMatch.vue(SVG 画线 + 点选配对);统一接口 (config) → {success: bool}play.vueinteraction_type 分发渲染,完成后提交对应出口选项。失败出口提交后同普通终局显示结算(无星),成功出口继续决策树。

视觉素材(M1.5 生成管线):场景插画、角色立绘、道具图标、计策卡图标由离线生成管线(见 4.8)产出 PNG 打包入客户端,element.image/static/generated/... 相对路径;生成前以 ui-src/src/utils/visual.js 的 emoji + 渐变色映射兜底(未生成素材的元素按名称映射渲染,组件零改动)。

4.8 视觉素材与对比点评生成管线(M1.5:oMLX 离线一次性生成)

儿童画面的场景插画、角色立绘、道具图标、计策卡图标与"每个选择的对比点评"均为内容型数据,离线生成一次、打包/入库后运行时零生成依赖——不占线上成本、不依赖线上 LLM 可用性。生成工具 cmd/genasset/(独立 Go 程序,仅开发机运行,不进入服务运行链路)。

LLM 接入(本机 oMLXOpenAI 兼容)

  • EndpointPOST http://127.0.0.1:18080/v1/chat/completions(本地无鉴权,model=Qwen3.5-9B-MLX-4bit),配置在 config.ymlgenasset 段(endpoint/model/timeout/max_tokens),可切换任意 OpenAI 兼容服务
  • 必须关闭思维链:请求体带 chat_template_kwargs: {"enable_thinking": false}(Qwen3.5 思考文本默认混入 content,破坏 JSON/SVG 解析)
  • 生成预算 max_tokens=24576;本地 9B 模型约 5 tok/s,单张 SVG 需 1-3 分钟,HTTP 超时 600s、失败重试 3 次(调用模式参照 rag-local 项目 chat_service.go
  • 生成输出一律要求 JSON(SVG 代码包在 JSON 字段中返回),解析失败重试

生成清单与幂等

  • 场景插画 SVG(每场景 1 张,level.scene_id 去重)、角色立绘 SVG(每角色 1 张,scene_node.character_id 去重)、道具图标 SVG(每道具 1 个,node_option.prop_id 去重)、计策卡图标 SVG(36 张)
  • 对比点评:每个决策选项 2 条(feedback_pros / feedback_cons),含触控互动成功/失败出口
  • 产物先落 workspace/genasset/SVG 源),已存在即跳过(--force 重生成);SVG 经 ui-src/scripts/svg2png.mjssharpdevDependency)转 PNG@2x 后复制入 ui-src/static/generated/{scene,char,prop,strategy}/,随五端打包;小程序 <image> 不支持 SVG,客户端统一用 PNG
  • 入库回填:element.image/static/generated/...pngnode_option.feedback_pros/cons ← 生成的点评;未回填字段前端按 visual.js 兜底

风格约定(prompt 约束):简笔卡通、粗线条、暖色调(米黄/橙/青绿)、画面无文字、适合 4-8 岁;场景含环境主体物(操场有滑梯/球门),角色为 Q 版全身(儿童形象),道具为单一主体图标。

对比点评与反馈展示

  • 存储:node_optionfeedback_pros(好处)/ feedback_cons(不足·错过的更好选择)两列(DDL 见 3 节),儿童化语言 20-40 字
  • 展示:反馈弹层三段式——「你的选择」→「✓ 好处」→「✗ 对比」(如"如果选 B,会更快找到帮手");触控互动成功出口点评=好处+鼓励,失败出口=坏处+温柔引导(不指责)
  • 路线回顾:结算页展示整局选择路径(前端 play 过程本地累积:每步「选项文本 + 好处一句话」),孩子回看"一路的选择与价值";user_route_log 仍照常服务端落库

动效与动画:正确/失败/结算动画为 CSS 代码实现(弹跳/彩带/星星弹出),不属于素材生成范畴,不走 LLM;角色表情用立绘 + 前端 emoji 徽章叠加(😊/😢/😮 角标),避免每角色生成 3 张表情变体。

4.9 缓存与并发

  • 读接口(strategy 列表、level 详情、prize 列表、badge 列表)走 gdb.CacheOptionTTL 取 database.cache.ttl
  • 写操作后清对应缓存:内容修改(后台)→ 清内容缓存;判分/兑换 → 清用户相关缓存
  • 内容型缓存为全量列表,可后台修改时全局失效;用户数据(进度/积分)不加缓存或短 TTL,避免一致性负担
  • 并行点:种子初始化无并行需求;批量节点/选项加载(≤100 分批 IN)主 goroutine 串行即可,无需 grpool
  • 锁:兑换/签到等防重入场景用 common.WithLock(内存锁即可,单实例部署),key 用 child:{id}:{业务}

4.10 鉴权与安全

  • 前台家长账号体系:微信小程序用 openid 静默登录(code2session 换 openid);H5/App 手机号注册 + tokengf_token 或自签 JWT,config 配置)。登录后持 parent token
  • 孩子维度接口(闯关/积分/兑换/报告)请求带 child_id,中间件校验归属(child.parent_id == token.parent_id),防越权访问他人孩子数据
  • 后台:管理员账号 + 独立 token,中间件校验 admin_user 身份
  • 判分、兑换等写接口一律服务端校验,前端只做展示
  • 上传接口(后台)限制文件类型与大小(图片/音频白名单)

4.11 家长中心与学习报告

  • 孩子档案:家长创建/修改多个孩子(年龄段、昵称),接口见 README API 清单
  • 学习报告(GET /api/parent/report):按孩子聚合——已通关计策数、星星总数、最近学习时间、各计策完成状态、复习/探索徽章获得情况
  • 路径摘要:按 user_route_log 统计孩子最近 N 关的终局分布(家长可看到"孩子在哪条路线上卡住/选择了失败分支")
  • 实物奖品确认:redemption 列表按孩子展示,家长凭兑换码领取
  • 数据全部单表 SQL + 应用层内存组装(遵循无 JOIN 约束),报告接口低频调用可短缓存

4.12 践行与成长体系

生活践行任务(知行合一):章完美后任务下发(life_task 按 strategy 关联)→ 孩子端显示"和爸爸妈妈一起做"卡片 → 家长查看引导语、带孩子实践 → 家长确认(POST /api/task/confirm,可选照片)→ 积分到账 + 状态流转(1 已下发 → 2 待家长确认 → 3 已确认)。防刷:家长确认为唯一权威入口,M2 实现时按 child × task 唯一约束建执行记录表(原 life_task_log 无读写引用,已整套移除)。

成长等级:按完美关卡数映射称号(规则表在 biz/consts,如小学徒 → 小书童 → 小军师 → 军师 → 大将军),派生值不落库——结算接口计算并返回新等级,前端播升级动画;家长报告展示当前称号。

兑换仪式感:虚拟奖品兑换成功即庆祝动画;实物奖品兑换成功即弹"庆祝 + 请爸爸妈妈帮你领取"(预期管理),后台发货生成兑换码后家长在家长中心"确认领取"(状态 5 已领取),孩子端下次打开提示"奖品到啦"。

使用时长管理:家长端设置每日限额(child.daily_limit_minutes0=不限);前端本地计时,达限额弹休息提醒(护眼);单次会话 = 一关(5-10 分钟)天然切分学习时段。服务端仅存设置与展示(报告"今日学习时长"按当日活动估算),不强制中断——低龄应用以家长监督为主。

4.13 游戏化 UI 设计(M1.5:全面游戏化展示与交互)

低龄儿童界面原则:图 > 字、大 > 小、动 > 静、声 > 默。闯关页与结算页全面游戏化,全部用 CSS 动画 + WebAudio 合成音效实现,不引入新依赖、不依赖 LLM 生成动效。

1. 场景沉浸:闯关页顶部全宽场景横幅——背景用 scene.imagegenasset 生成的场景插画 PNG),无素材时用 visual.js 的 emoji + 渐变兜底;场景横幅带缓慢漂浮装饰(CSS 动画飘动的云朵/星星 emoji,纯前端装饰不依赖素材)。

2. 角色剧场:角色立绘 character.image 卡片化展示(圆角大卡片 + 淡入弹跳进场),表情状态用 emoji 徽章叠加在立绘角标(😊 开心 / 😢 难过 / 😮 惊讶 / 🤔 思考),由当前节点的 result_type/互动成败切换,只做 1 张立绘 + 4 个 emoji 角标,不生成多表情素材。

3. 选项卡片化:选项从文字行改为大卡片——渐变彩色边框 + 圆形字母徽章(A/B/C 大字号)+ 道具图标(option.prop.image 或 emoji 兜底)+ 文本;按压/点击有缩放回弹动效(:active transform + 松手回弹)。

4. 反馈演出:选择后反馈弹层按阶段演出——弹入缩放动画 → 点评内容分段依次显现(你的选择 → ✓ 好处 → ✗ 对比,每段 200ms 淡入错峰);正确选择弹层绿色系、失败选择暖色系;底部「继续」按钮呼吸光效。触控互动成功/失败走同一演出(成功撒小星星粒子、失败温柔 shake)。

5. 进度与成就感:关卡顶部显示步骤进度条(当前节点/总节点数,圆点 + 连线,前进时圆点点亮动画);连续选择正确触发连击(🔥 ×2 提示,本地计数,不落库)。

6. 声音utils/sound.js 用 WebAudio 合成短音效(不依赖音频文件):点击 blip、正确 chime(上扬三音)、失败 soft buzz(低音柔和)、完成 fanfare、金币 coin——全部短促低龄友好;声音开关存本地(默认开)。小程序端 WebAudio 受限时静默降级(无音效,功能不受影响)。

7. 结算狂欢:结算页星星逐个弹出(现有 pop-in)+ 顶部彩带 confetti 粒子(CSS 动画,12 个 emoji/彩色纸屑随机下落旋转);解锁徽章/计策卡/升级时对应卡片带光效浮现 + fanfare 音效;路线回顾卡片逐条滑入(左滑入 + 好处文字)。

8. 地图页路线图:关卡地图改为纵向路线图(计策卡图标为驿站节点,直线/曲线连接,当前关高亮呼吸光效,已通关点亮、未解锁灰显 + 锁形 emoji);解锁新关时新节点弹跳入场。计策卡图标用 genasset 生成的 strategy.icon PNG,未生成用 emoji 兜底。

9. 触控皮肤:5 个互动组件沿用各自交互逻辑,视觉统一升级——道具选择/找线索:道具卡片化(图标大图 + 名称);步骤排序:可拖动的彩色圆角块(拖起浮起 + 阴影,松手吸附到槽位);拖拽放置:目标框虚线高亮(拖入时变实线 + 变色);连线配对:左侧人物彩色圆卡、右侧道具卡,连线用 4 色轮换区分。

视觉一致性:配色固定暖色系(米黄底 #fdf6ec、橙 #ff6b35 主色、青绿 #4ecdc4 点缀),圆角统一 24rpx 大圆角、字号 ≥28rpx、行高 ≥1.8;所有素材图 CSS object-fit: cover + 圆角,缺图一律 emoji 兜底不破版。

三幕剧场化:闯关整体编排为三幕小剧场——① 开场演出:进入关卡先播全屏覆盖层 SceneTheater(旁白朗读 + 字幕逐字淡入 + 角色入场滑入 + 道具飞入,可跳过),演出结束才显示首个决策节点;② 决策演出:普通决策节点一律动作卡 ActionCard(图标/拼音/听读/按压动画,即第 3 条选项卡片化的承载形态),config 互动节点用触控组件(第 9 条);选择后先播分支演出过渡(场景晃动 0.6s)再弹对比点评(第 4 条反馈演出);③ 结局演出:通关结算走彩带 CSS 粒子(第 7 条结算狂欢)+ 庆祝标题,未通关弹鼓励语。

互动形态判定:config 互动节点用触控组件、普通决策节点一律动作卡——触控组件是 success/fail 双出口(成功进决策树 / 失败进失败终局),与多分支选项(多出口、各带独立点评)语义不兼容,故决策节点不套触控组件。

4.14 互动剧情演绎 v2(台词流演绎)

4.13 三幕小剧场之上,把"动作卡 + 弹窗点评"升级为剧情流演绎:节点台词逐句播出,决策选项直接嵌入剧情流,角色回应以气泡融入剧情(无弹窗),闯关从"做题"变为"演一幕小短剧"。

数据模型

  • scene_node 新增 script 列(JSON 台词流),元素结构 {speaker, text, pinyin, emotion}:speaker 为角色名或「旁白」,text 为台词,pinyin 为逐字注音(导入时common.AnnotatePinyin 生成,不入草稿),emotion 为表情(normal/happy/sad/think/surprise,驱动角色立绘角标切换)
  • 每节点 3-6 句,每句 8-30 字,至少 1 句旁白;**决策节点末句为角色提问(以?结尾)**衔接选项;终局节点末句为角色总结(不得是旁白)
  • 决策树模型零改动:level/detail 接口透出 scriptscene_node/node_option/result_type 与 level/choose 接口均不变

前端演绎(StoryPlayer + play.vue

  • StoryPlayer 逐句演绎:一句一气泡,朗读 onEnd 驱动下一句同步推进(朗读慢时逐字高亮等朗读);文本逐字高亮约 260ms/字引导跟读;emotion 驱动角色表情切换;单句 20s 无输出判 TTS 无声自动跳过(兜底防卡死);右上角「跳过」整段快进
  • play.vue 剧情流:script 非空节点走 StoryPlayer:key="curNode.node_id" 重建组件保证状态干净,@done 后显示选项)→ ActionCard/触控选项 → 角色回应气泡融入剧情流;script 为空回退 v1 动作卡 + 弹窗点评呈现
  • 反馈剧情化:对比点评(pros/cons)从弹层改为角色回应气泡,随剧情流逐条呈现(✓ 好处 / ✗ 对比),无弹窗打断

剧本生成管线(genasset

  • go run ./cmd/genasset --only=scripts:按 prompt 约束(决策/终局节点语义、3-6 句、末句规则)调用本机 oMLX 生成草稿,落 workspace/scripts/{level_id}/{node_id}.jsonvalidateScript 校验(3-6 句、每句 8-30 字、speaker 合法、≥1 句旁白、决策末句?/终局末句角色总结),不合格重试
  • 人工精修草稿 → go run ./cmd/genasset --import-scripts:重新校验 + AnnotatePinyin 注音 + 回填 scene_node.script
  • 幂等:仅处理 script 为空的节点;草稿已存在跳过(防重跑覆盖人工精修,--force 覆盖);--strategy 限定计策分批执行
  • 试点:第 1 计 8 节点已导入精修,全量 36 计剧本由主会话按计分批生成

4.15 分层文件对齐(沿革)

对齐规则(每表一组文件、豁免与归属、非表文件处置)见 CLAUDE.md「分层文件对齐与代码模式」,本节只记沿革:

  • 13 张表补全M1 时仅 5 个模块(parent/child/strategy/level/level_play)有完整分层,其余 15 张只有 entity/dao;一次补齐为每表一组文件,6 张在用表的跨表 DAO 直调收拢到各自 service;补齐的表先只建门面结构,不挂 g.Meta 不注册路由(路由静态,按需暴露)
  • 收口(2026-08-14level_play(闯关流程)并入 level 表文件、表初始化列表并入 main.go、service/auth.go 并入 common/auth——各层严格 = 17 表文件(18 表 − point_log 豁免)

4.16 音频生成管线(Edge TTS 神经网络语音)

朗读音频由离线管线 cmd/gentss/ 一次性生成(独立 Go 程序,仅开发机运行),逐句合成 mp3 落静态目录并回填各 *_audio 字段——运行时零生成依赖,五端通用(小程序/App 不支持 speechSynthesis,文件是唯一朗读通道)。

协议要点(实现对照 edge-tts 7.2.8 源码,本机已验证):

  • 端点为微软 Edge 朗读私有 WebSocketwss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=6A5AA1D4EAFF4E9FB37E23D68491D6F4&ConnectionId=<uuid去横线>&Sec-MS-GEC=<token>&Sec-MS-GEC-Version=<version>
  • Sec-MS-GEC 纯算法生成(无 HTTP 预请求):ticks = UTC秒 + 11644473600 → ticks -= ticks % 3005 分钟窗)→ ticks ×= 1e9/100Windows 100ns 刻度)→ SHA256(取整字符串 + TrustedClientToken).hex().upper()403 时读响应 Date 头算时钟偏移累加后重试
  • 握手头:Origin: chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold + Cookie: muid=<32位大写hex> + Edge UA + permessage-deflate
  • 连接后发两帧文本:Path:speech.config(输出格式 audio-24khz-48kbitrate-mono-mp3)→ Path:ssml<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'><voice name='{voice}'><prosody rate='{rate}'>{文本}</prosody></voice></speak>,文本先剔除控制字符再转义 & < >
  • 收帧:二进制帧前 2 字节大端 header 长度,Path:audio 载荷拼接为 mp3;文本帧 Path:turn.end 结束(turn.start/response/audio.metadata 忽略);每句一个连接串行最稳,超长文本按 4096 字节分块多轮连接后拼接
  • tts: 配置段:voice(默认 zh-CN-XiaoxiaoNeural/ rate / versionChromium 版本号,协议漂移时调)/ concurrency / timeout / retries

生成范围与路径规则URL 前缀 /static/generated/audio/,与 PNG 素材同挂载):

内容源 文件路径 回填字段
script 台词行(每行) audio/{level}/{node}/{line_idx}.mp3 script JSON 行内 audio(整列 Update
节点内容 scene_node.content audio/{level}/{node}/content.mp3 scene_node.audio
开场 level.scene_content audio/{level}/scene.mp3 level.scene_audio
选项文本 node_option.text audio/{level}/{node}/opt_{option_id}.mp3 node_option.audio
学堂 strategy.teach_content audio/teach/{strategy_id}.mp3 strategy.teach_audio

幂等:mp3 文件已存在且对应字段非空 → 跳过;--force 重生成覆盖;回填单条 Update 无事务(沿用 genasset 先例)。脚本行音频回填:整列读 script JSON(行内无 audio 的行逐个合成),合成成功后整列重写 JSON 落库,避免逐行 Update。

降级链:文件缺失 → H5 浏览器 speechSynthesis 兜底(speech.js 既有逻辑);文件存在 → 五端一律播 mp3。

缓存与预取(弱网策略,不做全量打包与真离线)

  • 音频是内容资产不是应用代码:不打包进客户端(小程序包体积硬限制、App 无必要),按需拉取 + 本地缓存;H5 关卡内容本身依赖网络接口,完全离线非目标
  • 后端:/static 响应加 Cache-Control: public, max-age=31536000, immutable(生成资产路径不可变,安全)——H5 每文件只下载一次,重复播放零网络
  • 前端:StoryPlayer 播第 N 句时预载第 N+1 句(speech.js prefetchH5 用 new Audio().preload='auto'App/小程序用 createInnerAudioContext 预载)——单句 mp3 仅 20-50KB,句间衔接不卡
  • App/小程序本地文件缓存(首播落沙盒/文件系统)留 M4 多端发布时做

4.17 后台管理(/api/admin,管理员鉴权)

路由与鉴权/api/admin 分组——login 公开,其余全部挂 auth.Middleware(secret, consts.RoleAdmin)RoleAdmin="admin" 首次投入使用)。admin 接口用独立 adminXxx 控制器结构体(各表 controller 文件内并列定义,同文件多结构体不破坏分层文件对齐),只绑 admin 组,不泄露进前台组。

登录username + bcrypt 密码校验(admin_user 表,种子默认 admin/admin123)→ auth.GenerateToken(secret, uid, RoleAdmin, expire)expire 读 auth.expire 配置(≤0 回退常量)。

缓存失效(后台改内容 → 前台立即可见):gcache 无按前缀删除,用 gdb Core.ClearCache(ctx, table)KeyStrings 全量取 key → 按 SelectCache:<table>@ 前缀过滤 → 批量删,gf v2.10.2 源码确认)。common/content_cache.goInvalidateContentCache 统一失效 8 个内容表(strategy/level/scene_node/node_option/element/prize/badge/life_task),内容写方法提交后调用一次。理由:strategy 列表 / level 详情是跨表内存组装(level→node→option→element),单表失效易漏;内存 key 总量几十级,全清成本可忽略。用户数据(parent/child/progress/redemptionadmin 查询不设缓存,无失效义务。

内容 CRUD:五件套 list/create/update/disable/enable,删除一律软删除(status=0 下架,无物理删除——user_progress/redemption 引用 id,下架即前台消失、可逆)。内容写时一次性拼音重标(复用 common.AnnotatePinyinstrategy.name 用 ToPinyinPlain);node/option 及 level 自身改动在事务内 content_version+1gdb.Raw("content_version+1") 原子自增,前台按版本号提示重新挑战)。校验:level 须 strategy 存在、node 须 level 存在、option 须 node 存在且 next_node_id 同关(内存环检测:沿 next 走 ≤ 关节点数步)、character_id/prop_id>0 时元素存在、is_entry=1 时同事务清同关其他入口。

兑换状态机1待领取/2待发货 → ship({1,2}→3 已发货,code 缺省 36ZH-+大写8位随机) → receive(3→5 已领取)cancel({1,2}→4 已取消,事务内退积分:读 child.points → UPDATE + 插入 point_log{reason_type=ReasonAdmin=4})。状态校验在事务内读行防并发竞态。

关卡卡点统计:5 条单表 SQL + 内存组装——level、scene_node、node_option(≤100 分批 IN)、user_route_log GROUP BY node_id,option_id,result_type 聚合、user_progress GROUP BY perfect;不缓存(实时)。dao Init 补 idx_route_log_level(level_id)idx_progress_level(level_id)

上传multipart → workspace/uploads/{image|audio}/YYYYMMDD_<随机>.ext(运行时数据与代码分离,不落 git 跟踪的 ui-src/static),/uploads 静态挂载(不带 immutable 头);白名单 png/jpg/jpeg/webp/gif/mp3/wav/m4a/ogg(扩展名 + http.DetectContentType 双校验),图片 ≤10MB、音频 ≤20MB。后台上传素材供 H5/Web 引用;多端发布时素材走打包/CDN 策略另行决定。

5. 开发计划

里程碑 内容 验收标准
M1 骨架与闯关核心 前后端工程初始化、数据模型(家长/孩子/决策树/元素库/路径表/内容版本)+ 种子数据、家长注册登录与孩子档案、计策学堂、关卡地图、关卡详情(含元素)、分支决策与终局结算(积分/完美/计策卡/解锁流转)、进度与路径记录 H5 端可完整走通「家长注册 → 建孩子档案 → 学堂 → 分支闯关 → 多路线拿最佳终局 → 完美解锁下一计」
M1.5 互动体验补齐(进行中) 拼音标注写时入库(全量 36 计)、朗读降级(浏览器语音,文件优先)、视觉素材离线生成管线(oMLX qwen 生成场景插画/角色立绘/道具图标/计策卡图标,SVG→PNG 打包入客户端)、对比点评(每个选项好处/坏处,反馈三段式 + 结算路线回顾)、场景/角色/道具视觉呈现、触控互动 5 种(2 道具选择 / 3 步骤排序 / 5 拖拽放置 / 7 找线索 / 8 连线配对,互动入口节点模型,后端 choose 零改动)、游戏化 UI(场景沉浸 / 角色剧场 / 选项卡片化 / 反馈演出 / 进度成就感 / WebAudio 音效 / 结算狂欢 / 地图路线图) 闯关不再是纯文字问答:场景有背景插画有朗读、文本有拼音注音、选项道具图标可视化、角色立绘 + 表情反馈、每个选择都有好处/坏处解释、结算路线回顾、每关入口先玩一个触控互动(成功进决策树 / 失败进失败终局);界面全面游戏化——选项大卡片、反馈演出动画、音效反馈、结算彩带动画、地图路线图;全量 36 关可玩
M2 游戏化与成长 签到、徽章、章末温故、智慧总结、生活践行任务、时长管理、接取收集互动(6) 全激励闭环可用,温故/总结/生活任务闭环生效,时长守护可用,接取收集互动可用
M3 兑换与后台 奖品兑换、兑换记录与发货(含家长确认领取与庆祝动画)、后台内容/奖品/徽章/生活任务/家长与孩子管理、学习报告、节点卡点统计、动作过关组件(跳跃/攀爬/躲避/奔跑) 后台改内容前台即时生效(内容版本标记可重新挑战),实物兑换流程走通(确认领取闭环),家长报告与卡点统计可看,动作过关互动多端可用
M4 多端发布 H5 内测 → 微信小程序提审 → Android/iOS/平板打包、平板布局适配 五端全部可运行

6. 风险与备选方案

风险 影响 应对
36 计决策树内容策划量大 M1 依赖内容质量 首期每计先 1 个情境(每关 5-8 节点,内容量 36×7≈250 节点),种子 JSON 单独文件便于内容团队迭代;可引入 LLM 生成决策树初稿 + 人工审核(common 已有 LLM 编排能力)
SQLite 并发写锁 兑换/判分高峰期 database is locked 写操作串行化 + 重试兜底;数据量小(单家庭应用),单实例部署下风险可控;必要时切 WAL 模式
uni-app 多端差异 小程序/App 渲染差异、音频自动播放限制、操作型互动组件(触摸/CSS 动画)端差异 组件层封装平台差异;互动组件先 H5/小程序验证再 App 适配;音频走用户手势触发播放
小程序审核与儿童合规 儿童类目需备案/资质、按《儿童个人信息网络保护规定》做家长授权与最小化收集 家长账号注册即家长授权入口,注册协议写明;提前准备合规材料;H5 端先行上线验证
朗读音频版权/生成成本 TTS 语音质量影响体验 首期接入商用 TTS,文件落库可随时人工替换
积分防刷 重复挑战刷分 首次到达终局才结算 + 服务端判分 + 兑换校验余额,日志留痕
低龄使用健康 单次/每日使用超时伤眼 前端本地计时 + 家长端限额设置 + 单次会话=一关(5-10 分钟)