43 KiB
技术设计
《三十六计小课堂》实现细节与技术决策。规范见 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.id,type=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.id,type=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.id,type=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(生活践行任务)
游戏内践行(情境闯关)之外的真实生活践行:每章完美后任务下发,家长引导孩子实践,家长确认为权威入口(天然防刷)。
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);
CREATE TABLE life_task_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
child_id INTEGER NOT NULL,
task_id INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 1, -- 1 已下发 2 待家长确认 3 已确认
confirm_photo TEXT, -- 家长确认时上传的照片(可选)
confirmed_at DATETIME,
created_at DATETIME,
UNIQUE(child_id, task_id)
);
积分与奖品
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, -- 关联业务 id(level/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打进二进制(biz/service/seed/),含:元素库(场景约 15 / 人物约 10 / 道具约 20)、36 计全部内容、每计 1-3 个现代情境关卡(决策树:节点 + 分支选项 + 元素关联)、初始奖品、初始徽章、默认管理员账号 - 启动时在 dao 层(各表
init()内已有CREATE TABLE IF NOT EXISTS)之后,service 层执行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 分钟),匹配低龄注意力,地图随时进出。
流程(儿童逐步决策,每步即时反馈):
GET /api/level/detail返回关卡入口节点与情境信息POST /api/level/choose(level_id + node_id + option_id):- 服务端校验:node 属于 level、option 属于 node、关卡已解锁;校验通过返回下一节点内容
- 若下一节点是决策节点:继续选择
- 若下一节点是终局节点:返回终局评级与结算结果(星星、积分),本次闯关结束
- 每步选择后前端展示该选项的即时点评(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.WithLock(key=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_review(UNIQUE(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_log 按 child_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 流水线(后续里程碑):与拼音同一"写时生成"流水线——TTS(如火山/微软)在初始化与后台保存时异步生成音频落
workspace/uploads/audio/,表存相对路径;文件名{表}_{内容id}_{文本hash}.mp3保证幂等(文本未变重复保存不重生成);生成失败不阻塞内容落库,音频缺失时前端自动降级为浏览器朗读(前端零改动) - 静态资源: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.vue(touchstart/move/end 拖拽)/ StepSort.vue(拖拽排序)/ LinkMatch.vue(SVG 画线 + 点选配对);统一接口 (config) → {success: bool},play.vue 按 interaction_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 接入(本机 oMLX,OpenAI 兼容):
- Endpoint:
POST http://127.0.0.1:18080/v1/chat/completions(本地无鉴权,model=Qwen3.5-9B-MLX-4bit),配置在config.yml的genasset段(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.mjs(sharp,devDependency)转 PNG@2x 后复制入ui-src/static/generated/{scene,char,prop,strategy}/,随五端打包;小程序<image>不支持 SVG,客户端统一用 PNG - 入库回填:
element.image←/static/generated/...png;node_option.feedback_pros/cons← 生成的点评;未回填字段前端按 visual.js 兜底
风格约定(prompt 约束):简笔卡通、粗线条、暖色调(米黄/橙/青绿)、画面无文字、适合 4-8 岁;场景含环境主体物(操场有滑梯/球门),角色为 Q 版全身(儿童形象),道具为单一主体图标。
对比点评与反馈展示:
- 存储:
node_option加feedback_pros(好处)/feedback_cons(不足·错过的更好选择)两列(DDL 见 3 节),儿童化语言 20-40 字 - 展示:反馈弹层三段式——「你的选择」→「✓ 好处」→「✗ 对比」(如"如果选 B,会更快找到帮手");触控互动成功出口点评=好处+鼓励,失败出口=坏处+温柔引导(不指责)
- 路线回顾:结算页展示整局选择路径(前端 play 过程本地累积:每步「选项文本 + 好处一句话」),孩子回看"一路的选择与价值";
user_route_log仍照常服务端落库
动效与动画:正确/失败/结算动画为 CSS 代码实现(弹跳/彩带/星星弹出),不属于素材生成范畴,不走 LLM;角色表情用立绘 + 前端 emoji 徽章叠加(😊/😢/😮 角标),避免每角色生成 3 张表情变体。
4.9 缓存与并发
- 读接口(strategy 列表、level 详情、prize 列表、badge 列表)走
gdb.CacheOption,TTL 取database.cache.ttl - 写操作后清对应缓存:内容修改(后台)→ 清内容缓存;判分/兑换 → 清用户相关缓存
- 内容型缓存为全量列表,可后台修改时全局失效;用户数据(进度/积分)不加缓存或短 TTL,避免一致性负担
- 并行点:种子初始化无并行需求;批量节点/选项加载(≤100 分批 IN)主 goroutine 串行即可,无需 grpool
- 锁:兑换/签到等防重入场景用
common.WithLock(内存锁即可,单实例部署),key 用child:{id}:{业务}
4.10 鉴权与安全
- 前台家长账号体系:微信小程序用
openid静默登录(code2session 换 openid);H5/App 手机号注册 + token(gf_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 已确认)。防刷:UNIQUE(child_id, task_id) + 家长确认为唯一权威入口。
成长等级:按完美关卡数映射称号(规则表在 biz/consts,如小学徒 → 小书童 → 小军师 → 军师 → 大将军),派生值不落库——结算接口计算并返回新等级,前端播升级动画;家长报告展示当前称号。
兑换仪式感:虚拟奖品兑换成功即庆祝动画;实物奖品兑换成功即弹"庆祝 + 请爸爸妈妈帮你领取"(预期管理),后台发货生成兑换码后家长在家长中心"确认领取"(状态 5 已领取),孩子端下次打开提示"奖品到啦"。
使用时长管理:家长端设置每日限额(child.daily_limit_minutes,0=不限);前端本地计时,达限额弹休息提醒(护眼);单次会话 = 一关(5-10 分钟)天然切分学习时段。服务端仅存设置与展示(报告"今日学习时长"按当日活动估算),不强制中断——低龄应用以家长监督为主。
4.13 游戏化 UI 设计(M1.5:全面游戏化展示与交互)
低龄儿童界面原则:图 > 字、大 > 小、动 > 静、声 > 默。闯关页与结算页全面游戏化,全部用 CSS 动画 + WebAudio 合成音效实现,不引入新依赖、不依赖 LLM 生成动效。
1. 场景沉浸:闯关页顶部全宽场景横幅——背景用 scene.image(genasset 生成的场景插画 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 双出口(成功进决策树 / 失败进失败终局),与多分支选项(多出口、各带独立点评)语义不兼容,故决策节点不套触控组件。
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 分钟) |