chore: M1 后端验证通过

This commit is contained in:
2026-08-13 12:49:36 +08:00
parent 414c7de007
commit ae79c13834
4 changed files with 794 additions and 24 deletions
+5 -24
View File
@@ -31,43 +31,24 @@
│ HTTP / JSON │
└────────┬────────┘
┌────────▼────────┐
│ Go + GoFrame
controller → service → dao
│ 鉴权中间件 · 文件解析 · 缓存 │
Go + GoFrame │
后端服务
└────────┬────────┘
┌────────▼────────┐
│ SQLite (data/) │ ← 种子数据启动初始化,后台可迭代
└─────────────────┘
```
- **后端**Go + GoFrame分层 `controller → service → dao`,详见 CLAUDE.md
- **后端**Go + GoFrame分层与代码规范遵循 CLAUDE.md
- **前端前台**uni-app (Vue 3),一套代码五端,生产产物由后端托管(H5 端)或独立打包发布
- **前端后台**Vue 3 + Element Plus 独立 Web 工程,产物由后端托管
- **数据库**:SQLite 单文件,运行时数据在 `data/`,上传文件在 `workspace/`,均不提交 git
- **部署**Docker Compose 单机,前后端一体单端口,数据目录挂载持久化
## 目录结构
| 目录 | 职责 |
|---|---|
| common/ | 通用层:HTTP 服务与鉴权、文件解析、中文分词、向量 JSON、DAO 基类、查询缓存、协程池 |
| biz/consts/ | 常量集中地:表名、状态、内容类型、协程池默认大小 |
| biz/model/ | entity(表结构)、dto(请求/响应 + 路由)、domain(领域模型) |
| biz/dao/ | 单表数据访问,每表一个文件 |
| biz/service/ | 业务逻辑:规则校验、事务、跨表组装、积分/兑换、闯关判分 |
| biz/controller/ | 接口层:接收参数、调用 service、组装返回值 |
| ui-src/ | uni-app 前台工程(五端) |
| admin-src/ | 管理后台 Web 工程 |
| data/ | SQLite 数据库(运行时数据,不提交 git) |
| workspace/ | 上传文件(图片/音频,不提交 git) |
- **目录结构**:后端业务代码在 `biz/`(分层职责见 CLAUDE.md),前端前台 `ui-src/`uni-app)、后台 `admin-src/`Web
## 数据流
1. 前端调用 HTTP APIJSON,经鉴权中间件 → controller
2. controller 接收参数(DTO `v` tag 自动校验)→ 调用 service
3. service 执行业务规则(判分、积分计算、兑换校验)→ 调用 dao
4. dao 单表访问 SQLite,读查询走缓存,写操作后清对应缓存
5. 返回值经 controller 组装 DTO 回前端
前端调用 HTTP APIJSON)→ controller 接收参数 → service 执行业务(判分、积分、兑换)→ dao 访问 SQLite;内容型数据(计策/关卡/元素/奖品)读接口走缓存,后台修改后失效;返回值组装后回前端。种子数据在服务启动时自动初始化,后台运营可直接迭代。
## 功能模块
@@ -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:** 完成 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 依据:** `技术设计.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.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 + 依赖**
```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, 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: 编译验证**
```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: entity19 张表映射)
**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/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: 冒烟**
```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_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`,覆盖结算规则矩阵):
```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:孩子列表 + 新建孩子(年龄段选择)
- mapstrategy/list 渲染六组地图网格,计策卡图标 + 星星 + 锁定态;点解锁章节 → 学堂弹层(strategy/detail 的 teach_content + 音频播放按钮)→ 进入 play
- playlevel/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*` 一致。
+1
View File
@@ -5,6 +5,7 @@ go 1.26.1
require (
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
github.com/gogf/gf/v2 v2.10.2
github.com/golang-jwt/jwt/v5 v5.3.1
golang.org/x/crypto v0.55.0
)
+2
View File
@@ -23,6 +23,8 @@ github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 h1:KLS68SWS2W749x7e+eCCOO3U
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2/go.mod h1:uLcsu73PfpyhRc0Jq0gGAWQjN1tyGU9iBRrYgt/lu7g=
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=