Files
36Wisdom/docs/superpowers/plans/2026-08-13-m1-core.md
T
2026-08-13 12:49:36 +08:00

28 KiB
Raw Blame History

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: 完成 M1Go+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 / SQLiteglebarez/sqlite 或 go-sqlite3/ uni-app Vue3H5 先行)。

Spec 依据: 技术设计.mdDDL 与规则权威来源)、README.mdAPI 清单)。


文件结构(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.jsonembed
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.goDAO 基类,所有 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.goGoFrame 引导 + 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.go19 张表名常量,含注释用途)

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: entity19 张表映射)

Files:

  • Create: biz/model/entity/*.go19 文件,命名 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/*.go19 文件,命名 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.gofunc 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: pongdata/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_idnode_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/authRolerole=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: 编译 + 提交

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: dtog.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/liststrategy 全量(缓存,TTL 300s+ 本用户 user_progress 按 level IN 取星(≤100 分批)→ 按 strategy 分组 → 解锁判断(unlock_before 为空 或 前置计完美关卡数=前置计关卡总数);content_version 不一致的关卡标记"可重新挑战"

  • level/detaillevel + 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 组装 DTOentity → 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

  1. 校验:level 存在、node 属于 level、option 属于 node、关卡已解锁(按解锁链递归校验)
  2. 取 next_node:决策节点 → 返回;终局节点 → 记 user_route_lognode/option/result_type
  3. 终局结算(事务):结算判定(按上面规则)→ 写 user_progress(取最高星 + perfect + content_version)→ 写 point_log + child.pointsdelta≠0 时)→ 完美解锁下一关(strategy 内下一关 unlocked 由"前置完美"推导,无需改表)→ 计策卡(user_collection)→ 清用户缓存
  4. 并发防重入:common.WithLock(ctx, "child:"+childId+":level:"+levelId, 10s, ...)
  5. 返回 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.jsonappid 占位、h5 配置);vite.config.js 代理 /apihttp://localhost:8080

  • Step 3: api/request.jsuni.request 封装(baseURL /api、token 注入 Authorization: Bearer、401 跳登录、统一错误 toast)。

  • Step 4: 页面(M1 交互可用、样式从简但卡通化配色):

  • login:手机号 + 密码 + 昵称注册 / 登录

  • children:孩子列表 + 新建孩子(年龄段选择)

  • mapstrategy/list 渲染六组地图网格,计策卡图标 + 星星 + 锁定态;点解锁章节 → 学堂弹层(strategy/detail 的 teach_content + 音频播放按钮)→ 进入 play

  • playlevel/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* 一致。