@@ -0,0 +1,786 @@
# M1 核心闯关实现计划(三十六计小课堂)
> **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: ** 完成 M1: Go+GoFrame 后端(19 张表、种子数据、家长/孩子体系、内容接口、分支闯关判分结算)+ uni-app 前端骨架,H5 端走通「家长注册 → 建孩子档案 → 计策学堂 → 分支闯关 → 完美解锁」全流程。
**Architecture: ** 严格按 CLAUDE.md 分层 controller → service → dao;路由由 dto `g.Meta` 声明;种子数据 `go:embed` JSON 启动导入;判分/结算核心逻辑在 service 层纯函数化以便单测。
**Tech Stack: ** Go 1.22+ / GoFrame v2 / SQLite( glebarez/sqlite 或 go-sqlite3) / uni-app Vue3( H5 先行)。
**Spec 依据: ** `技术设计.md` ( DDL 与规则权威来源)、`README.md` ( API 清单)。
---
## 文件结构(M1 创建)
```
go.mod, main.go, config.yml
common/base_dao.go, common/auth/auth.go
biz/consts/table_name.go, biz/consts/status.go, biz/consts/consts.go
biz/model/entity/( 19 张表,每表一文件)
biz/model/dto/( parent.go, child.go, strategy.go, level.go, points.go, prize.go, common.go)
biz/dao/( 19 个 dao 文件 + base 建表)
biz/service/seed/seed.go, seed/seed_36_ji.json( embed)
biz/service/( auth.go, parent.go, child.go, strategy.go, level.go, points.go, level_play.go)
biz/controller/( router.go, parent.go, child.go, strategy.go, level.go, points.go, prize.go)
ui-src/( uni-app Vue3 骨架:pages.json, manifest.json, main.js, App.vue, pages/, api/)
data/, workspace/(运行时,gitignore)
```
19 张表:parent, child, strategy, level, scene_node, node_option, element, user_progress, chapter_review, user_route_log, user_collection, badge, user_badge, point_log, prize, redemption, admin_user, life_task, life_task_log。字段定义以技术设计.md 第 3 节 DDL 为准,本计划不重复。
---
### Task 1: Go 工程骨架
**Files: **
- Create: `go.mod` , `config.yml` , `main.go` , `common/base_dao.go` , `.gitignore` (更新), `data/.gitkeep` , `workspace/.gitkeep`
- [ ] **Step 1: go.mod + 依赖 **
``` bash
go mod init 36wisdom
go get github.com/gogf/gf/v2@latest
go get github.com/glebarez/sqlite@latest
```
- [ ] **Step 2: config.yml **
``` yaml
server :
address : ":8080"
openapiPath : "/api.json"
swaggerPath : "/swagger"
logger :
level : "info"
database :
type : "sqlite"
link : "data/36wisdom.db"
cache :
ttl : 300
maxSize : 10000
pool :
defaultSize : 10
```
- [ ] **Step 3: common/base_dao.go ** ( DAO 基类,所有 dao 嵌入)
``` go
package common
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
type BaseDao struct { Table string }
func ( d * BaseDao ) Model ( ) * gdb . Model { return g . DB ( ) . Model ( d . Table ) }
func ( d * BaseDao ) InsertAndReturnId ( ctx context . Context , data gdb . Record ) ( int64 , error ) {
res , err := d . Model ( ) . Ctx ( ctx ) . Data ( data ) . Insert ( )
if err != nil {
return 0 , err
}
return res . LastInsertId ( )
}
func ( d * BaseDao ) GetOneByPk ( ctx context . Context , pk int64 ) ( gdb . Record , error ) {
return d . Model ( ) . Ctx ( ctx ) . WherePri ( pk ) . One ( )
}
func ( d * BaseDao ) UpdateByPk ( ctx context . Context , pk int64 , data gdb . Record ) error {
_ , err := d . Model ( ) . Ctx ( ctx ) . WherePri ( pk ) . Data ( data ) . Update ( )
return err
}
func ( d * BaseDao ) DeleteByPk ( ctx context . Context , pk int64 ) error {
_ , err := d . Model ( ) . Ctx ( ctx ) . WherePri ( pk ) . Delete ( )
return err
}
```
- [ ] **Step 4: main.go ** ( GoFrame 引导 + SQLite 初始化 + 数据目录)
``` go
package main
import (
"context"
_ "github.com/glebarez/sqlite"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"os"
"36wisdom/biz/controller"
)
func main ( ) {
var ctx context . Context = gctx . New ( )
for _ , dir := range [ ] string { "data" , "workspace" } {
if err := os . MkdirAll ( dir , 0 o755 ) ; err != nil {
g . Log ( ) . Fatal ( ctx , err )
}
}
s := g . Server ( )
controller . Register ( s )
s . Run ( )
}
```
- [ ] **Step 5: 更新 .gitignore ** 追加 `data/` 、`workspace/` 、`ui-src/dist/` 、`admin-src/dist/`
- [ ] **Step 6: 编译验证 **
``` bash
go build ./...
```
Expected: 无输出(成功)。
- [ ] **Step 7: Commit **
``` bash
git add go.mod go.sum config.yml main.go common/ .gitignore data/.gitkeep workspace/.gitkeep
git commit -m "feat: go 工程骨架(GoFrame + SQLite 引导)"
```
---
### Task 2: 常量集中(biz/consts)
**Files: **
- Create: `biz/consts/table_name.go` , `biz/consts/status.go` , `biz/consts/consts.go`
- [ ] **Step 1: table_name.go ** ( 19 张表名常量,含注释用途)
``` go
package consts
const (
TableParent = "parent"
TableChild = "child"
TableStrategy = "strategy"
TableLevel = "level"
TableSceneNode = "scene_node"
TableNodeOption = "node_option"
TableElement = "element"
TableUserProgress = "user_progress"
TableChapterReview = "chapter_review"
TableUserRouteLog = "user_route_log"
TableUserCollection = "user_collection"
TableBadge = "badge"
TableUserBadge = "user_badge"
TablePointLog = "point_log"
TablePrize = "prize"
TableRedemption = "redemption"
TableAdminUser = "admin_user"
TableLifeTask = "life_task"
TableLifeTaskLog = "life_task_log"
)
```
- [ ] **Step 2: status.go ** (状态常量:内容状态 / 节点 / 终局评级 / 互动形态 / 徽章条件 / 积分原因 / 兑换状态 / 任务状态)
``` go
package consts
// 内容通用状态
const (
StatusEnabled = 1
StatusDisabled = 0
)
// 节点类型
const (
NodeDecision = 1
NodeFinal = 2
)
// 终局评级
const (
ResultNone = 0
ResultFail = 1
ResultGood = 2
ResultBest = 3
)
// 互动形态
const (
InteractionOption = 1 // 选项选择
InteractionProp = 2 // 道具选择
InteractionOrder = 3 // 步骤排序
InteractionAction = 4 // 动作过关(跳跃/攀爬/躲避/奔跑,config 配子模式)
InteractionDrag = 5 // 拖拽放置
InteractionCatch = 6 // 接取收集
InteractionFind = 7 // 找线索
InteractionConnect = 8 // 连线配对
)
// 徽章条件类型
const (
BadgeCollect = 1 // 集卡数
BadgeSignIn = 2 // 连续签到
BadgeClear = 3 // 通关数
BadgePoints = 4 // 积分
BadgePerfect = 5 // 完美关卡数
BadgeReview = 6 // 复习完成数
)
// 积分原因
const (
ReasonLevel = 1 // 闯关结算
ReasonSignIn = 2 // 每日签到
ReasonRedeem = 3 // 兑换扣减
ReasonAdmin = 4 // 管理调整
ReasonReview = 5 // 章末温故
ReasonSummary = 6 // 智慧总结
ReasonTask = 7 // 生活任务
)
// 兑换状态
const (
RedeemPending = 1 // 待领取(虚拟)
RedeemWaiting = 2 // 待发货(实物)
RedeemShipped = 3 // 已发货
RedeemCanceled = 4 // 已取消
RedeemReceived = 5 // 已领取(家长确认)
)
// 生活任务状态
const (
TaskIssued = 1 // 已下发
TaskConfirming = 2 // 待家长确认
TaskConfirmed = 3 // 已确认
)
// 年龄段档位
const (
AgeGroup4_6 = "4-6"
AgeGroup6_8 = "6-8"
)
```
- [ ] **Step 3: consts.go ** (默认参数:教训分、积分档位、温故关卡数)
``` go
package consts
const (
PointsBest = 30
PointsGood = 10
PointsFail = - 10
PointsPerfect = 20
PointsReview = 10
PointsSummary = 5
PointsSignIn = 5
PointsTask = 20
FailNoDeductAfter = 2 // 同一关连续失败 N 次后不再扣分
ReviewLevelCount = 3 // 章末温故抽取计谋数(2-3,取 3 且不超过已学)
)
```
- [ ] **Step 4: 编译 + 提交 **
``` bash
go build ./... && git add biz/consts/ && git commit -m "feat: 常量集中(表名/状态/默认参数)"
```
---
### Task 3: entity( 19 张表映射)
**Files: **
- Create: `biz/model/entity/*.go` ( 19 文件,命名 `entity_parent.go` 等,按表名)
- [ ] **Step 1: 每表一个 entity 文件 ** 。模式(以 parent 为例):
``` go
package entity
import "github.com/gogf/gf/v2/os/gtime"
type Parent struct {
Id int64 ` json:"id" orm:"id" `
Openid string ` json:"openid" orm:"openid" `
Phone string ` json:"phone" orm:"phone" `
Password string ` json:"-" orm:"password" `
Nickname string ` json:"nickname" orm:"nickname" `
Avatar string ` json:"avatar" orm:"avatar" `
Status int ` json:"status" orm:"status" `
CreatedAt * gtime . Time ` json:"created_at" orm:"created_at" `
UpdatedAt * gtime . Time ` json:"updated_at" orm:"updated_at" `
}
```
字段列表严格对应技术设计.md 第 3 节 DDL(含 2026-08-13 迭代新增字段:strategy.teach_content/summary_*、child.daily_limit_minutes、level.content_version、scene_node.interaction_type/config、node_option.prop_id、user_progress.perfect、redemption 状态 5)。password 字段 json 序列化排除。
- [ ] **Step 2: 编译 **
``` bash
go build ./...
```
- [ ] **Step 3: 提交 **
``` bash
git add biz/model/entity/ && git commit -m "feat: 19 张表 entity 映射"
```
---
### Task 4: DAO 层 + 建表(19 个 dao 文件)
**Files: **
- Create: `biz/dao/*.go` ( 19 文件,命名 `dao_parent.go` 等)
- [ ] **Step 1: 每表一个 dao 文件 ** ,模式(以 parent 为例):
``` go
package dao
import (
"context"
"36wisdom/biz/consts"
"36wisdom/common"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
type parentDao struct { common . BaseDao }
var Parent = & parentDao { BaseDao : common . BaseDao { Table : consts . TableParent } }
func ( d * parentDao ) Init ( ctx context . Context ) {
_ , err := d . Model ( ) . Ctx ( ctx ) . Fields ( ` ... ` ) . Create ( )
...
}
```
- [ ] **Step 2: dao 初始化统一入口 `biz/dao/init.go` ** : `func InitTables(ctx)` 依序调用 19 个 dao 的建表(`CREATE TABLE IF NOT EXISTS` ,DDL 与索引照技术设计.md 第 3 节),strategy/level/scene_node/node_option/element 建表后执行无环校验(拓扑检测 scene_node 图,有环则日志 Fatal)。
- [ ] **Step 3: 启动接线 ** : main.go 的 g.Run 前调用 `dao.InitTables(ctx)` (用 `s.BindHandler("GET /ping", ...)` 先验证服务可用)。
- [ ] **Step 4: 编译 + 冒烟启动 **
``` bash
go build ./... && go run main.go &
curl -s http://localhost:8080/ping
```
Expected: `pong` ; `data/36wisdom.db` 生成。
- [ ] **Step 5: 提交 **
``` bash
git add biz/dao/ && git commit -m "feat: 19 张表 DAO 与建表初始化"
```
---
### Task 5: 种子数据(元素库 + 36 计决策树 + 奖品 + 徽章 + 管理员)
**Files: **
- Create: `biz/service/seed/seed.go` , `biz/service/seed/seed_36_ji.json`
- Modify: `main.go` (启动调用 EnsureSeeded)
- [ ] **Step 1: seed_36_ji.json 结构 ** (go:embed,数组式按依赖序:elements → strategies → levels → nodes → options → prizes → badges → admin)
``` json
{
"elements" : [
{ "e_type" : 1 , "name" : "操场" , "description" : "孩子们游戏的地方" } ,
{ "e_type" : 2 , "name" : "小明" , "description" : "爱踢足球的小男孩" } ,
{ "e_type" : 3 , "name" : "足球" , "description" : "圆圆的皮球" }
] ,
"strategies" : [
{
"name" : "声东击西" ,
"pinyin" : "shēng dōng jī xī" ,
"group_no" : 1 , "group_name" : "胜战计" ,
"meaning" : "假装要往东边去,其实是要往西边" ,
"teach_content" : "小明想从右边突破,但他先假装往左边跑……" ,
"summary_q" : "哪个说法总结了「声东击西」?" ,
"summary_options" : "[{\"text\":\"假装往东其实往西\",\"correct\":true},{\"text\":\"一直往东跑\"},{\"text\":\"站在中间不动\"}]" ,
"sort_order" : 1 ,
"unlock_before" : null ,
"levels" : [
{
"title" : "足球场上的假动作" , "scene_name" : "操场" , "age_group" : "4-6" ,
"scene_content" : "比赛最后一分钟,小明带着球冲向球门……" ,
"nodes" : [
{ "title" : "被挡住了" , "character_name" : "小明" , "content" : "大个子队员挡在面前,硬冲会被抢走球,怎么办?" ,
"interaction_type" : 1 , "is_entry" : 1 , "options" : [
{ "text" : "假装向右跑,骗他转身" , "prop_name" : "足球" , "next_index" : 1 , "feedback" : "好主意!他向右追,你就从左边冲过去" } ,
{ "text" : "硬冲过去" , "next_index" : 2 , "feedback" : "硬冲被拦住了,球丢了……" }
] } ,
{ "content" : "成功了!趁防守转身,你从左边突破射门!" , "node_type" : 2 , "result_type" : 3 } ,
{ "content" : "球被抢走了……再试一次,换种办法!" , "node_type" : 2 , "result_type" : 1 }
]
}
]
}
] ,
"prizes" : [
{ "name" : "小军师徽章" , "p_type" : 1 , "points_cost" : 50 , "stock" : -1 } ,
{ "name" : "计策卡册" , "p_type" : 2 , "points_cost" : 200 , "stock" : 10 }
] ,
"badges" : [
{ "name" : "初出茅庐" , "cond_type" : 3 , "cond_value" : 1 } ,
{ "name" : "小将军" , "cond_type" : 3 , "cond_value" : 10 }
] ,
"admin" : { "username" : "admin" , "password" : "admin123" }
}
```
节点引用用 `next_index` (数组下标)+ 后台导入时解析为 next_node_id; `node_type` 缺省=1 决策节点;元素通过 `scene_name` /`character_name` /`prop_name` 引用元素库。
- [ ] **Step 2: 内容创作要求 ** :M1 必须内置**全部 36 计**(胜战计/敌战计/攻战计/混战计/并战计/败战计各 6 计),每计至少 1 个情境关卡、每关 5-8 节点(≥2 个决策节点、≥2 个终局,至少含 1 个最佳终局与 1 个非最佳终局),现代场景(操场/超市/公园/家庭/教室/游乐场…),内容遵循技术设计.md「决策树骨架」范式。剩余章节的补充情境(每计 2-3 关)M2 由后台/种子迭代补入。
- [ ] **Step 3: seed.go 实现 EnsureSeeded(ctx) ** :查 strategy 表非空即跳过;空则事务内按依赖序插入(元素先入库取 id,按 name 索引;节点 `next_index` 转为 `next_node_id` );管理员密码 bcrypt 哈希;全程包事务,失败回滚并日志 Fatal。
- [ ] **Step 4: main.go 接线 ** : `dao.InitTables(ctx)` 后调用 `seed.EnsureSeeded(ctx)` 。
- [ ] **Step 5: 冒烟验证 **
``` bash
rm -f data/36wisdom.db && go run main.go &
sqlite3 data/36wisdom.db "SELECT count(*) FROM strategy; SELECT count(*) FROM scene_node;"
```
Expected: `36` 、节点总数 ≥180。
- [ ] **Step 6: 提交 **
``` bash
git add biz/service/seed/ main.go && git commit -m "feat: 种子数据(36 计决策树/元素库/奖品/徽章/管理员)"
```
---
### Task 6: 鉴权中间件(家长 token)
**Files: **
- Create: `common/auth/auth.go` , `biz/service/auth.go` , `biz/controller/router.go` (骨架)
- Modify: `main.go`
- [ ] **Step 1: token 生成与校验 ** (自签 HS256 JWT,密钥在 config.yml `auth.secret` ;依赖 `github.com/golang-jwt/jwt/v5` )
``` go
// common/auth/auth.go
package auth
func GenerateToken ( secret string , uid int64 , role string , expireSeconds int ) ( string , error )
func ParseToken ( secret , tokenString string ) ( uid int64 , role string , err error )
```
- [ ] **Step 2: 中间件 ** : `common/auth.Middleware(secret)` 返回 gin 风格 HandlerFunc;从 `Authorization: Bearer <token>` 解析,注入 gctx 键 `authUid` /`authRole` ; role=admin 的接口额外校验 role。config.yml 加 `auth: { secret: "...", expire: 604800 }` 。
- [ ] **Step 3: router.go ** 注册分组:`/api` (家长鉴权组)、`/api/admin` (admin 鉴权组,M1 先注册登录接口骨架)、公开 `POST /api/parent/register` `POST /api/parent/login` 。GoFrame v2 中控制器注册用 `s.Group("/api", middleware, func(g *ghttp.RouterGroup){ g.Bind(controller.Parent) })` 。
- [ ] **Step 4: 编译 + 提交 **
``` bash
go build ./... && git add common/auth/ biz/service/auth.go biz/controller/ config.yml main.go && git commit -m "feat: 家长 token 鉴权中间件与路由分组"
```
---
### Task 7: 家长/孩子接口
**Files: **
- Create: `biz/model/dto/parent.go` , `biz/model/dto/child.go` , `biz/service/parent.go` , `biz/service/child.go` , `biz/controller/parent.go` , `biz/controller/child.go`
- [ ] **Step 1: dto ** ( g.Meta 声明路由,v tag 校验):
``` go
type RegisterReq struct {
g . Meta ` path:"/api/parent/register" method:"post" summary:"家长注册" `
Phone string ` v:"required|length:11,11|regex:^1[3-9][0-9] { 9}$" `
Password string ` v:"required|length:6,32" `
Nickname string ` v:"required|length:1,32" `
}
type RegisterRes struct { Token string ; ParentId int64 }
type LoginReq struct {
g . Meta ` path:"/api/parent/login" method:"post" `
Phone string ` v:"required" `
Password string ` v:"required" `
}
type LoginRes struct { Token string ; ParentId int64 }
type ChildCreateReq struct {
g . Meta ` path:"/api/parent/child/create" method:"post" `
Nickname string ` v:"required|length:1,32" `
AgeGroup string ` v:"required|in:4-6,6-8" `
}
type ChildCreateRes struct { ChildId int64 }
type ChildUpdateReq struct {
g . Meta ` path:"/api/parent/child/update" method:"post" `
ChildId int64 ` v:"required" `
Nickname string ` v:"length:1,32" `
AgeGroup string ` v:"in:4-6,6-8" `
DailyLimitMinutes int ` v:"min:0|max:600" `
}
type ChildListReq struct {
g . Meta ` path:"/api/parent/child/list" method:"get" `
}
type ChildListItem struct {
ChildId int64 ; Nickname string ; AgeGroup string ; Avatar string
Points int ; Level int ; DailyLimitMinutes int
}
type ChildListRes struct { List [ ] ChildListItem }
```
- [ ] **Step 2: service ** :Register(查重手机号 → 插入 → 生成 token)、Login(校验 bcrypt → token)、ChildCreate(校验 parent 归属自动带 parent_id)、ChildUpdate(校验归属 + 必填项非空校验)、ChildList(含 points)。积分、等级派生(完美关卡数 → 称号映射函数放 `biz/service/level.go` )。
- [ ] **Step 3: controller ** :方法签名 `(ctx, *dto.XxxReq) (*dto.XxxRes, error)` ,从 ctx 取 authUid 传给 service。
- [ ] **Step 4: 冒烟测试 **
``` bash
curl -s -X POST localhost:8080/api/parent/register -H 'Content-Type: application/json' -d '{"phone":"13800000000","password":"pass123456","nickname":"爸爸"}'
curl -s -X POST localhost:8080/api/parent/child/create -H " Authorization: Bearer $TOKEN " -H 'Content-Type: application/json' -d '{"nickname":"小宝","age_group":"4-6"}'
```
Expected: token 与 child_id 正常返回;错误参数返回 400 校验错误。
- [ ] **Step 5: 提交 **
``` bash
git add biz/model/dto/ biz/service/parent.go biz/service/child.go biz/service/level.go biz/controller/ && git commit -m "feat: 家长注册登录与孩子档案接口"
```
---
### Task 8: 内容接口(strategy / level + 缓存)
**Files: **
- Create: `biz/model/dto/strategy.go` , `biz/model/dto/level.go` , `biz/service/strategy.go` , `biz/service/level.go` , `biz/controller/strategy.go` , `biz/controller/level.go`
- [ ] **Step 1: dto **
``` go
type StrategyListReq struct { g . Meta ` path:"/api/strategy/list" method:"get" ` }
type StrategyItem struct {
StrategyId int64 ; Name string ; Pinyin string ; GroupNo int ; GroupName string
Meaning string ; Icon string ; SortOrder int
Stars int // 本计总星数
PerfectCount int // 已完美关卡数
TotalLevels int
Unlocked bool ; UnlockReason string
}
type StrategyListRes struct { List [ ] StrategyItem }
type StrategyDetailReq struct { g . Meta ` path:"/api/strategy/detail" method:"get" ` ; StrategyId int64 ` v:"required" ` }
type LevelBrief struct { LevelId int64 ; Title string ; AgeGroup string ; SceneName string ; Stars int ; Perfect bool ; Unlocked bool ; ContentVersion int ; ProgressVersion int }
type StrategyDetailRes struct {
StrategyId int64 ; Name string ; Pinyin string ; Meaning string ; GroupName string
TeachContent string ; TeachImage string ; TeachAudio string
SummaryQ string ; SummaryOptions string
Levels [ ] LevelBrief
}
type LevelDetailReq struct { g . Meta ` path:"/api/level/detail" method:"get" ` ; LevelId int64 ` v:"required" ` }
type ElementVO struct { Id int64 ; EType int ; Name string ; Image string ; Audio string ; Description string }
type OptionVO struct { OptionId int64 ; Text string ; Prop * ElementVO }
type NodeVO struct {
NodeId int64 ; Title string ; Content string ; Image string ; Audio string
Character * ElementVO ; InteractionType int ; Config string
NodeType int ; ResultType int
Options [ ] OptionVO
}
type LevelDetailRes struct {
LevelId int64 ; Title string ; Scene * ElementVO ; SceneContent string ; SceneImage string ; SceneAudio string
Entry NodeVO ; TotalFinals int ; Perfect bool ; Stars int
}
```
- [ ] **Step 2: service 聚合规则 ** (严格单表 SQL + 内存组装,无 JOIN):
- strategy/list: strategy 全量(缓存,TTL 300s) + 本用户 user_progress 按 level IN 取星(≤100 分批)→ 按 strategy 分组 → 解锁判断(unlock_before 为空 或 前置计完美关卡数=前置计关卡总数);content_version 不一致的关卡标记"可重新挑战"
- level/detail: level + scene_node + node_option + element 分表取回内存组装成树(节点拓扑排序按 sort_order),校验入口唯一;返回入口节点 + 终局总数(result_type>0 的节点数)+ 用户 perfect/stars
- **缓存**: strategy/level 内容缓存(gdb.CacheOption 或 gcache),**写操作(后台)后清对应缓存**——M1 后台未建,种子导入后清一次
- 年龄段过滤:`4-6` 孩子只看 age_group='4-6' 关卡
- [ ] **Step 3: controller ** 组装 DTO( entity → DTO 映射在 controller)。
- [ ] **Step 4: 冒烟 **
``` bash
curl -s "localhost:8080/api/strategy/list?child_id=1" -H " Authorization: Bearer $TOKEN "
curl -s "localhost:8080/api/level/detail?level_id=1" -H " Authorization: Bearer $TOKEN "
```
Expected: 36 计列表(首计 unlocked=true,其余 false);关卡返回入口节点与选项。
- [ ] **Step 5: 提交 **
``` bash
git add biz/model/dto/strategy.go biz/model/dto/level.go biz/service/strategy.go biz/service/level.go biz/controller/strategy.go biz/controller/level.go && git commit -m "feat: 内容接口(计策/关卡/元素,含缓存与解锁判定)"
```
---
### Task 9: 闯关判分与结算(核心)
**Files: **
- Create: `biz/service/level_play.go` , `biz/controller/level_play.go` , `biz/model/dto/level_play.go` , `biz/service/level_play_test.go`
- [ ] **Step 1: dto **
``` go
type ChooseReq struct {
g . Meta ` path:"/api/level/choose" method:"post" `
LevelId int64 ` v:"required" `
NodeId int64 ` v:"required" `
OptionId int64 ` v:"required" `
}
type ChooseRes struct {
Next * NodeVO // 下一节点(决策节点)
Final * FinalSettle // 终局结算(终局节点)
}
type FinalSettle struct {
ResultType int ; Stars int ; ScoreDelta int
Cleared bool // 本关是否首次通关(到最佳终局)
Perfect bool // 是否达成完美
UnlockNext bool // 是否解锁下一关
CollectionUnlocked bool // 计策卡解锁
NewLevel int // 成长等级(升级时非 0)
BalanceAfter int
}
```
- [ ] **Step 2: service 核心逻辑(纯函数可测) ** :
``` go
// SettleFinal 纯函数:给定历史状态返回结算结果(便于单测)
type SettleState struct {
LevelCleared bool // 该关已通关(到过最佳终局)
FailStreak int // 连续失败次数
ReachedFinals map [ int ] bool // 已到达终局 result_type 集合
TotalFinals int
BalanceAfter int
}
func SettleFinal ( s SettleState , resultType int ) FinalSettle
```
规则(技术设计 4.3/4.4):
- 首次到达终局才结算:`ReachedFinals` 含该 result_type 则 delta=0
- 未通关:Best +30 / Good +10 / Fail -10(连续失败 ≥2 次后不再扣,且余额不为负)
- 已通关后补分支:delta=0(只记完成度)
- 最佳终局 → Cleared=true;全部 result_type 到达 → Perfect=true(解锁下一关 +20)
- 计策卡:本计所有关卡 Perfect 后解锁
- 成长等级:完美关卡数 → 等级(biz/service/level.go 映射)
- [ ] **Step 3: service 流程(Choose) ** :
1. 校验:level 存在、node 属于 level、option 属于 node、关卡已解锁(按解锁链递归校验)
2. 取 next_node:决策节点 → 返回;终局节点 → 记 user_route_log( node/option/result_type)
3. 终局结算(事务):结算判定(按上面规则)→ 写 user_progress(取最高星 + perfect + content_version)→ 写 point_log + child.points( delta≠0 时)→ 完美解锁下一关(strategy 内下一关 unlocked 由"前置完美"推导,无需改表)→ 计策卡(user_collection)→ 清用户缓存
4. 并发防重入:`common.WithLock(ctx, "child:"+childId+":level:"+levelId, 10s, ...)`
5. 返回 ChooseRes
- [ ] **Step 4: 单元测试 ** ( `level_play_test.go` ,覆盖结算规则矩阵):
``` go
func TestSettleFinal ( t * testing . T ) {
// 未通关首次最佳 → +30 Cleared
// 未通关首次良好 → +10
// 未通关首次失败 → -10
// 连续失败第 3 次 → 0
// 余额不足 → 扣到 0
// 已通关补分支 → 0
// 全部终局到达 → Perfect
}
```
- [ ] **Step 5: 运行测试 **
``` bash
go test ./biz/service/ -run TestSettleFinal -v
```
Expected: PASS。
- [ ] **Step 6: 冒烟 ** (走通一关:首计第一关按最佳路线选 → 得 3 星 +30 分)
``` bash
curl -s "localhost:8080/api/level/detail?level_id=1" -H " Authorization: Bearer $TOKEN "
# 沿入口节点选项选 next_index 为最佳终局的路径逐次 choose
curl -s -X POST localhost:8080/api/level/choose -H " Authorization: Bearer $TOKEN " -H 'Content-Type: application/json' -d '{"level_id":1,"node_id":N,"option_id":M}'
```
Expected: 终局返回 Stars=3、ScoreDelta=30。
- [ ] **Step 7: 提交 **
``` bash
git add biz/service/level_play.go biz/service/level_play_test.go biz/controller/level_play.go biz/model/dto/level_play.go && git commit -m "feat: 分支闯关判分与终局结算(含单测)"
```
---
### Task 10: 统一验证
- [ ] **Step 1: 全量编译 + 测试 **
``` bash
go build ./... && go vet ./... && go test ./...
```
- [ ] **Step 2: 冒烟完整流程 ** (脚本化 curl):注册 → 建孩子 → 列表 → 首计学堂 → 闯关(最佳路线 + 失败路线 + 补分支到完美)→ 解锁第二计 → 校验积分流水。
- [ ] **Step 3: 提交收尾 **
``` bash
git add -A && git commit -m "chore: M1 后端验证通过"
```
---
### Task 11: uni-app 前端骨架(H5 先行)
**Files: **
- Create: `ui-src/` ( package.json, vite.config.js, index.html, src/main.js, src/App.vue, src/pages.json, src/manifest.json, src/pages/login/login.vue, src/pages/children/children.vue, src/pages/map/map.vue, src/pages/play/play.vue, src/pages/result/result.vue, src/api/request.js, src/api/index.js, src/store/user.js)
- [ ] **Step 1: 工程初始化 ** (最小 uni-app Vue3 Vite 工程,H5 目标):
``` bash
cd ui-src && npm init -y && npm install vue@3 @dcloudio/uni-app@latest @dcloudio/uni-h5@latest vite@5 @vitejs/plugin-vue@latest
```
- [ ] **Step 2: 基础配置 ** : `pages.json` 注册 5 个页面(login → children → map → play → result);`manifest.json` ( appid 占位、h5 配置);vite.config.js 代理 `/api` → `http://localhost:8080` 。
- [ ] **Step 3: api/request.js ** : uni.request 封装(baseURL `/api` 、token 注入 `Authorization: Bearer` 、401 跳登录、统一错误 toast)。
- [ ] **Step 4: 页面 ** (M1 交互可用、样式从简但卡通化配色):
- login:手机号 + 密码 + 昵称注册 / 登录
- children:孩子列表 + 新建孩子(年龄段选择)
- map: strategy/list 渲染六组地图网格,计策卡图标 + 星星 + 锁定态;点解锁章节 → 学堂弹层(strategy/detail 的 teach_content + 音频播放按钮)→ 进入 play
- play: level/detail 渲染入口节点 → 选项列表(选择类互动;M1 只实现 interaction_type=1 选项选择,其余类型显示"敬请期待"占位并禁用)→ 点击选项调 choose → 展示 feedback → 下一节点;终局时展示结算(星/分/完美/解锁动画)→ result
- result:结算页(星数动画、积分变化、计策卡获得、下一关解锁提示)
- [ ] **Step 5: H5 端联调 **
``` bash
cd ui-src && npm run dev:h5
```
浏览器走通:注册 → 建孩子 → 地图 → 学堂 → 闯关(最佳 + 补分支到完美)→ 解锁第二计。
- [ ] **Step 6: 提交 **
``` bash
cd .. && git add ui-src/ && git commit -m "feat: uni-app 前端骨架(登录/孩子/地图/闯关/结算)"
```
---
## Self-Review 记录
- **Spec 覆盖**:技术设计 3 节 19 表 ✓(Task 3/4)、4.1 种子 ✓(Task 5)、4.3 学习闭环/判分/完美/温故——M1 覆盖判分与完美(温故与总结在 M2)✓、4.8 鉴权 ✓(Task 6)、API 清单 M1 范围 ✓。M2 内容(温故/总结/任务/徽章/签到/兑换/后台)不在本计划,属 M2 计划。
- **占位符扫描**:seed JSON 给出完整结构与示例(声东击西整树),其余 35 计在实现时按同范式创作(内容创作任务,结构已定义)。
- **类型一致性**: `FinalSettle` /`SettleFinal` /`NodeVO` /`OptionVO` 在 Task 8/9 间一致;积分常量与 settle 规则引用 `consts.Points*` 一致。