28 KiB
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 + 依赖
go mod init 36wisdom
go get github.com/gogf/gf/v2@latest
go get github.com/glebarez/sqlite@latest
- Step 2: config.yml
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 嵌入)
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 初始化 + 数据目录)
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, 0o755); 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: 编译验证
go build ./...
Expected: 无输出(成功)。
- Step 7: Commit
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 张表名常量,含注释用途)
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(状态常量:内容状态 / 节点 / 终局评级 / 互动形态 / 徽章条件 / 积分原因 / 兑换状态 / 任务状态)
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(默认参数:教训分、积分档位、温故关卡数)
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: 编译 + 提交
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 为例):
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: 编译
go build ./...
- Step 3: 提交
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 为例):
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: 编译 + 冒烟启动
go build ./... && go run main.go &
curl -s http://localhost:8080/ping
Expected: pong;data/36wisdom.db 生成。
- Step 5: 提交
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)
{
"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: 冒烟验证
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: 提交
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)
// 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/registerPOST /api/parent/login。GoFrame v2 中控制器注册用s.Group("/api", middleware, func(g *ghttp.RouterGroup){ g.Bind(controller.Parent) })。 -
Step 4: 编译 + 提交
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 校验):
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: 冒烟测试
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: 提交
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
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: 冒烟
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: 提交
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
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 核心逻辑(纯函数可测):
// 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):
- 校验:level 存在、node 属于 level、option 属于 node、关卡已解锁(按解锁链递归校验)
- 取 next_node:决策节点 → 返回;终局节点 → 记 user_route_log(node/option/result_type)
- 终局结算(事务):结算判定(按上面规则)→ 写 user_progress(取最高星 + perfect + content_version)→ 写 point_log + child.points(delta≠0 时)→ 完美解锁下一关(strategy 内下一关 unlocked 由"前置完美"推导,无需改表)→ 计策卡(user_collection)→ 清用户缓存
- 并发防重入:
common.WithLock(ctx, "child:"+childId+":level:"+levelId, 10s, ...) - 返回 ChooseRes
- Step 4: 单元测试(
level_play_test.go,覆盖结算规则矩阵):
func TestSettleFinal(t *testing.T) {
// 未通关首次最佳 → +30 Cleared
// 未通关首次良好 → +10
// 未通关首次失败 → -10
// 连续失败第 3 次 → 0
// 余额不足 → 扣到 0
// 已通关补分支 → 0
// 全部终局到达 → Perfect
}
- Step 5: 运行测试
go test ./biz/service/ -run TestSettleFinal -v
Expected: PASS。
- Step 6: 冒烟(走通一关:首计第一关按最佳路线选 → 得 3 星 +30 分)
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: 提交
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: 全量编译 + 测试
go build ./... && go vet ./... && go test ./...
-
Step 2: 冒烟完整流程(脚本化 curl):注册 → 建孩子 → 列表 → 首计学堂 → 闯关(最佳路线 + 失败路线 + 补分支到完美)→ 解锁第二计 → 校验积分流水。
-
Step 3: 提交收尾
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 目标):
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 端联调
cd ui-src && npm run dev:h5
浏览器走通:注册 → 建孩子 → 地图 → 学堂 → 闯关(最佳 + 补分支到完美)→ 解锁第二计。
- Step 6: 提交
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*一致。