From ae79c13834935ac90808c56b9a17845b2b597928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Thu, 13 Aug 2026 12:49:36 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20M1=20=E5=90=8E=E7=AB=AF=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 +- docs/superpowers/plans/2026-08-13-m1-core.md | 786 +++++++++++++++++++ go.mod | 1 + go.sum | 2 + 4 files changed, 794 insertions(+), 24 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-13-m1-core.md diff --git a/README.md b/README.md index 9f5add1..b603e6a 100644 --- a/README.md +++ b/README.md @@ -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 API(JSON),经鉴权中间件 → controller -2. controller 接收参数(DTO `v` tag 自动校验)→ 调用 service -3. service 执行业务规则(判分、积分计算、兑换校验)→ 调用 dao -4. dao 单表访问 SQLite,读查询走缓存,写操作后清对应缓存 -5. 返回值经 controller 组装 DTO 回前端 +前端调用 HTTP API(JSON)→ controller 接收参数 → service 执行业务(判分、积分、兑换)→ dao 访问 SQLite;内容型数据(计策/关卡/元素/奖品)读接口走缓存,后台修改后失效;返回值组装后回前端。种子数据在服务启动时自动初始化,后台运营可直接迭代。 ## 功能模块 diff --git a/docs/superpowers/plans/2026-08-13-m1-core.md b/docs/superpowers/plans/2026-08-13-m1-core.md new file mode 100644 index 0000000..e2a94cc --- /dev/null +++ b/docs/superpowers/plans/2026-08-13-m1-core.md @@ -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, 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: 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 ` 解析,注入 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*` 一致。 diff --git a/go.mod b/go.mod index d32fba1..bc28703 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 51b3ee6..f764e96 100644 --- a/go.sum +++ b/go.sum @@ -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=