From 285fa01471ea7c018579dec811ebc0a05cdf2a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Fri, 14 Aug 2026 12:13:02 +0800 Subject: [PATCH] 1 --- CLAUDE.md | 5 +- README.md | 1 - biz/consts/status.go | 7 - biz/consts/table_name.go | 1 - biz/controller/admin_user.go | 34 ++ biz/controller/badge.go | 49 ++ biz/controller/chapter_review.go | 36 ++ biz/controller/element.go | 39 ++ biz/controller/level.go | 32 +- biz/controller/level_play.go | 43 -- biz/controller/life_task.go | 50 ++ biz/controller/node_option.go | 53 +++ biz/controller/prize.go | 52 +++ biz/controller/redemption.go | 37 ++ biz/controller/router.go | 30 -- biz/controller/scene_node.go | 58 +++ biz/controller/user_badge.go | 34 ++ biz/controller/user_collection.go | 34 ++ biz/controller/user_progress.go | 50 ++ biz/controller/user_route_log.go | 37 ++ biz/dao/dao_badge.go | 3 +- biz/dao/dao_child.go | 13 +- biz/dao/dao_life_task_log.go | 29 -- biz/dao/dao_point_log.go | 29 -- biz/dao/dao_prize.go | 3 +- biz/dao/init.go | 40 -- biz/model/dto/admin_user.go | 15 + biz/model/dto/badge.go | 24 + biz/model/dto/chapter_review.go | 17 + biz/model/dto/element.go | 20 + biz/model/dto/level.go | 26 ++ biz/model/dto/level_play.go | 29 -- biz/model/dto/life_task.go | 25 + biz/model/dto/node_option.go | 31 ++ biz/model/dto/prize.go | 27 ++ biz/model/dto/redemption.go | 18 + biz/model/dto/scene_node.go | 36 ++ biz/model/dto/user_badge.go | 15 + biz/model/dto/user_collection.go | 15 + biz/model/dto/user_progress.go | 29 ++ biz/model/dto/user_route_log.go | 19 + biz/model/entity/entity_life_task_log.go | 14 - biz/model/entity/entity_point_log.go | 14 - biz/service/admin_user.go | 18 + biz/service/auth.go | 27 -- biz/service/badge.go | 25 + biz/service/chapter_review.go | 29 ++ biz/service/child.go | 6 +- biz/service/element.go | 20 + biz/service/level.go | 393 +++++++++++++++- biz/service/level_play.go | 428 ------------------ .../{level_play_test.go => level_test.go} | 0 biz/service/life_task.go | 25 + biz/service/node_option.go | 39 ++ biz/service/parent.go | 5 +- biz/service/prize.go | 25 + biz/service/redemption.go | 29 ++ biz/service/scene_node.go | 33 ++ biz/service/strategy.go | 7 +- biz/service/user_badge.go | 27 ++ biz/service/user_collection.go | 36 ++ biz/service/user_progress.go | 78 ++++ biz/service/user_route_log.go | 33 ++ common/auth/auth.go | 5 + common/middleware.go | 19 + main.go | 43 +- ui-src/static/generated/char/char_2.png | Bin 0 -> 37499 bytes ui-src/static/generated/prop/prop_3.png | Bin 0 -> 28208 bytes ui-src/static/generated/scene/scene_1.png | Bin 0 -> 25143 bytes .../static/generated/strategy/strategy_1.png | Bin 0 -> 51204 bytes 技术设计.md | 28 +- 71 files changed, 1805 insertions(+), 746 deletions(-) create mode 100644 biz/controller/admin_user.go create mode 100644 biz/controller/badge.go create mode 100644 biz/controller/chapter_review.go create mode 100644 biz/controller/element.go delete mode 100644 biz/controller/level_play.go create mode 100644 biz/controller/life_task.go create mode 100644 biz/controller/node_option.go create mode 100644 biz/controller/prize.go create mode 100644 biz/controller/redemption.go delete mode 100644 biz/controller/router.go create mode 100644 biz/controller/scene_node.go create mode 100644 biz/controller/user_badge.go create mode 100644 biz/controller/user_collection.go create mode 100644 biz/controller/user_progress.go create mode 100644 biz/controller/user_route_log.go delete mode 100644 biz/dao/dao_life_task_log.go delete mode 100644 biz/dao/dao_point_log.go delete mode 100644 biz/dao/init.go create mode 100644 biz/model/dto/admin_user.go create mode 100644 biz/model/dto/badge.go create mode 100644 biz/model/dto/chapter_review.go create mode 100644 biz/model/dto/element.go delete mode 100644 biz/model/dto/level_play.go create mode 100644 biz/model/dto/life_task.go create mode 100644 biz/model/dto/node_option.go create mode 100644 biz/model/dto/prize.go create mode 100644 biz/model/dto/redemption.go create mode 100644 biz/model/dto/scene_node.go create mode 100644 biz/model/dto/user_badge.go create mode 100644 biz/model/dto/user_collection.go create mode 100644 biz/model/dto/user_progress.go create mode 100644 biz/model/dto/user_route_log.go delete mode 100644 biz/model/entity/entity_life_task_log.go delete mode 100644 biz/model/entity/entity_point_log.go create mode 100644 biz/service/admin_user.go delete mode 100644 biz/service/auth.go create mode 100644 biz/service/badge.go create mode 100644 biz/service/chapter_review.go create mode 100644 biz/service/element.go delete mode 100644 biz/service/level_play.go rename biz/service/{level_play_test.go => level_test.go} (100%) create mode 100644 biz/service/life_task.go create mode 100644 biz/service/node_option.go create mode 100644 biz/service/prize.go create mode 100644 biz/service/redemption.go create mode 100644 biz/service/scene_node.go create mode 100644 biz/service/user_badge.go create mode 100644 biz/service/user_collection.go create mode 100644 biz/service/user_progress.go create mode 100644 biz/service/user_route_log.go create mode 100644 common/middleware.go create mode 100644 ui-src/static/generated/char/char_2.png create mode 100644 ui-src/static/generated/prop/prop_3.png create mode 100644 ui-src/static/generated/scene/scene_1.png create mode 100644 ui-src/static/generated/strategy/strategy_1.png diff --git a/CLAUDE.md b/CLAUDE.md index 50544da..65b5e7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,8 @@ ## 分层文件对齐与代码模式(硬性要求) -- 每张业务表对应一组 `entity / dao / service / controller / dto` 文件,数量严格对齐;虚拟表(向量 vec0 / FTS5)不建独立分层文件,由主表 dao 统一管理 +- 每张业务表对应一组 `entity / dao / service / controller / dto` 文件,数量严格对齐(核验方式:每层目录文件数 = 分层表数,分层表数 = 总表数 − 豁免表数);虚拟表(向量 vec0 / FTS5)与**流水/记录类表(如 point_log)豁免分层对齐**:不建任何独立分层文件(含 entity/dao),建表由主表 dao 统一管理(同虚拟表模式),由使用方 service 事务内直写,禁止为只写不读的审计表造分层门面;无任何读写引用的死表连表带分层整套删除,启动时 DROP 库内残留表与代码保持一致 +- **非表文件一律不进业务分层目录**:路由注册与中间件装配、表初始化列表(建表 + 死表 DROP)直接写在 `main.go`;鉴权等跨模块通用能力放 `common/`;跨表业务流程归入所属表文件(如闯关 Choose 属 level 表)——分层目录出现非表文件即违反对齐,禁止以非表名开独立分层文件 - **不建 parser/rag 等技术目录**:纯技术能力(文档解析、中文分词、向量序列化)平铺在 `common/`;业务编排(分块、检索、工作流)归入对应 service 文件 - entity:每文件一张表,`orm` 标签与列名一致,时间字段用 `*gtime.Time`,只做表映射 - dao:单例 `var Xxx = &xxxDao{}`,`init()` 内 `CREATE TABLE IF NOT EXISTS` + 索引 + 迁移;通用 CRUD 复用 `common/base_dao.go`(InsertAndReturnId / GetOneByPk / UpdateByPk / DeleteByPk) @@ -61,7 +62,7 @@ ## 数据访问规范(硬性要求) -- **事务**:涉及多张表的增删改操作必须包数据库事务,禁止逐表裸调用。事务放 dao 层方法内,service 层负责编排;`tx.Begin` 后必须用 `defer` 防护已提交后的二次 Rollback +- **事务**:涉及多张表的增删改操作必须包数据库事务,禁止逐表裸调用。**事务必须在 service 层**(`g.DB().Transaction` 包裹与事务内写方法如 `XxxInTx`,service 持有 tx 句柄编排多表),dao 层只做单表无状态 CRUD,不持事务;`tx.Begin` 后必须用 `defer` 防护已提交后的二次 Rollback - **SQL 单表约束**:每个 SQL 只允许访问一张表,禁止 JOIN 与跨表子查询(`IN (SELECT ...)` / `EXISTS`);跨表数据一律拆为多条单表 SQL + 应用层内存组装——先取外键 id 列表,再对目标表 `IN` 查询;`IN` 参数须按 ≤100 分批(SQLite 变量数上限 999) - **禁止 N+1 查询**:禁止在循环中逐条查库。循环场景一律改为批处理——一次 `ListByXxx` 取回后按外键在内存分组 - **缓存一致性**:DAO 查询走缓存(TTL 来自 `database.cache.ttl`),写操作后必须清对应缓存 diff --git a/README.md b/README.md index 7add10d..1eee49a 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,6 @@ | user_badge | 孩子已获徽章 | | point_log | 积分流水:变动、原因类型、余额快照 | | life_task | 生活践行任务:关联计策、任务描述、家长引导语、确认奖励积分 | -| life_task_log | 生活任务完成记录:孩子 × 任务、家长确认状态 | | prize | 奖品:积分价格、库存、类型(虚拟/实物) | | redemption | 兑换记录:孩子 × 奖品、状态流转、兑换码 | | admin_user | 后台管理员账号 | diff --git a/biz/consts/status.go b/biz/consts/status.go index f1fbaa4..72f15b5 100644 --- a/biz/consts/status.go +++ b/biz/consts/status.go @@ -62,13 +62,6 @@ const ( RedeemReceived = 5 // 已领取(家长确认) ) -// 生活任务状态(life_task_log.status) -const ( - TaskIssued = 1 // 已下发 - TaskConfirming = 2 // 待家长确认 - TaskConfirmed = 3 // 已确认 -) - // 年龄段档位(child.age_group / level.age_group) const ( AgeGroup4_6 = "4-6" diff --git a/biz/consts/table_name.go b/biz/consts/table_name.go index 3368ca1..3f8be0c 100644 --- a/biz/consts/table_name.go +++ b/biz/consts/table_name.go @@ -20,5 +20,4 @@ const ( TableRedemption = "redemption" TableAdminUser = "admin_user" TableLifeTask = "life_task" - TableLifeTaskLog = "life_task_log" ) diff --git a/biz/controller/admin_user.go b/biz/controller/admin_user.go new file mode 100644 index 0000000..2e49d99 --- /dev/null +++ b/biz/controller/admin_user.go @@ -0,0 +1,34 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type adminUser struct{} + +var AdminUser = &adminUser{} + +func (c *adminUser) Get(ctx context.Context, req *dto.AdminUserGetReq) (*dto.AdminUserGetRes, error) { + rec, err := service.AdminUser.GetByUsername(ctx, req.Username) + if err != nil { + return nil, err + } + res := &dto.AdminUserGetRes{} + if !rec.IsEmpty() { + res.AdminUser = adminUserItem(rec) + } + return res, nil +} + +func adminUserItem(r gdb.Record) dto.AdminUserItem { + return dto.AdminUserItem{ + Id: r["id"].Int64(), + Username: r["username"].String(), + Status: r["status"].Int(), + } +} diff --git a/biz/controller/badge.go b/biz/controller/badge.go new file mode 100644 index 0000000..cd0a0e2 --- /dev/null +++ b/biz/controller/badge.go @@ -0,0 +1,49 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type badge struct{} + +var Badge = &badge{} + +func (c *badge) Get(ctx context.Context, req *dto.BadgeGetReq) (*dto.BadgeGetRes, error) { + rec, err := service.Badge.GetByPk(ctx, req.Id) + if err != nil { + return nil, err + } + res := &dto.BadgeGetRes{} + if !rec.IsEmpty() { + res.Badge = badgeItem(rec) + } + return res, nil +} + +func (c *badge) List(ctx context.Context, req *dto.BadgeListReq) (*dto.BadgeListRes, error) { + recs, err := service.Badge.ListEnabled(ctx) + if err != nil { + return nil, err + } + res := &dto.BadgeListRes{List: make([]dto.BadgeItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, badgeItem(r)) + } + return res, nil +} + +func badgeItem(r gdb.Record) dto.BadgeItem { + return dto.BadgeItem{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Icon: r["icon"].String(), + CondType: r["cond_type"].Int(), + CondValue: r["cond_value"].Int(), + Status: r["status"].Int(), + } +} diff --git a/biz/controller/chapter_review.go b/biz/controller/chapter_review.go new file mode 100644 index 0000000..d578660 --- /dev/null +++ b/biz/controller/chapter_review.go @@ -0,0 +1,36 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type chapterReview struct{} + +var ChapterReview = &chapterReview{} + +func (c *chapterReview) ListByChild(ctx context.Context, req *dto.ChapterReviewListReq) (*dto.ChapterReviewListRes, error) { + recs, err := service.ChapterReview.ListByChild(ctx, req.ChildId) + if err != nil { + return nil, err + } + res := &dto.ChapterReviewListRes{List: make([]dto.ChapterReviewItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, chapterReviewItem(r)) + } + return res, nil +} + +func chapterReviewItem(r gdb.Record) dto.ChapterReviewItem { + return dto.ChapterReviewItem{ + Id: r["id"].Int64(), + ChildId: r["child_id"].Int64(), + StrategyId: r["strategy_id"].Int64(), + LevelIds: r["level_ids"].String(), + Status: r["status"].Int(), + } +} diff --git a/biz/controller/element.go b/biz/controller/element.go new file mode 100644 index 0000000..71c1e92 --- /dev/null +++ b/biz/controller/element.go @@ -0,0 +1,39 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type element struct{} + +var Element = &element{} + +func (c *element) ListByIds(ctx context.Context, req *dto.ElementListReq) (*dto.ElementListRes, error) { + recs, err := service.Element.ListEnabledByIds(ctx, req.Ids) + if err != nil { + return nil, err + } + res := &dto.ElementListRes{List: make([]dto.ElementItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, elementItem(r)) + } + return res, nil +} + +func elementItem(r gdb.Record) dto.ElementItem { + return dto.ElementItem{ + Id: r["id"].Int64(), + EType: r["e_type"].Int(), + Name: r["name"].String(), + Image: r["image"].String(), + Audio: r["audio"].String(), + Description: r["description"].String(), + Status: r["status"].Int(), + SortOrder: r["sort_order"].Int(), + } +} diff --git a/biz/controller/level.go b/biz/controller/level.go index 889e609..6dafb7f 100644 --- a/biz/controller/level.go +++ b/biz/controller/level.go @@ -33,7 +33,7 @@ func (c *level) Detail(ctx context.Context, req *dto.LevelDetailReq) (*dto.Level return res, nil } -func elementVO(e *service.Element) *dto.ElementVO { +func elementVO(e *service.ElementVO) *dto.ElementVO { if e == nil { return nil } @@ -77,3 +77,33 @@ func nodeVO(n *service.Node) dto.NodeVO { } return vo } + +func (c *level) Choose(ctx context.Context, req *dto.ChooseReq) (*dto.ChooseRes, error) { + node, settle, err := service.Level.Choose(ctx, auth.GetUid(ctx), req.ChildId, req.LevelId, req.NodeId, req.OptionId) + if err != nil { + return nil, err + } + res := &dto.ChooseRes{} + if node != nil { + vo := nodeVO(node) + res.Next = &vo + } + if settle != nil { + res.Final = &dto.FinalSettle{ + ResultType: settle.ResultType, + Stars: settle.Stars, + ScoreDelta: settle.ScoreDelta, + Cleared: settle.Cleared, + Perfect: settle.Perfect, + UnlockNext: settle.UnlockNext, + CollectionUnlocked: settle.CollectionUnlocked, + NewLevel: settle.NewLevel, + BalanceAfter: settle.BalanceAfter, + } + if settle.FinalNode != nil { + vo := nodeVO(settle.FinalNode) + res.Final.FinalNode = &vo + } + } + return res, nil +} diff --git a/biz/controller/level_play.go b/biz/controller/level_play.go deleted file mode 100644 index 7c6e8a6..0000000 --- a/biz/controller/level_play.go +++ /dev/null @@ -1,43 +0,0 @@ -package controller - -import ( - "context" - - "36wisdom/biz/model/dto" - "36wisdom/biz/service" - "36wisdom/common/auth" -) - -type levelPlay struct{} - -var LevelPlay = &levelPlay{} - -func (c *levelPlay) Choose(ctx context.Context, req *dto.ChooseReq) (*dto.ChooseRes, error) { - node, settle, err := service.LevelPlay.Choose(ctx, auth.GetUid(ctx), req.ChildId, req.LevelId, req.NodeId, req.OptionId) - if err != nil { - return nil, err - } - res := &dto.ChooseRes{} - if node != nil { - vo := nodeVO(node) - res.Next = &vo - } - if settle != nil { - res.Final = &dto.FinalSettle{ - ResultType: settle.ResultType, - Stars: settle.Stars, - ScoreDelta: settle.ScoreDelta, - Cleared: settle.Cleared, - Perfect: settle.Perfect, - UnlockNext: settle.UnlockNext, - CollectionUnlocked: settle.CollectionUnlocked, - NewLevel: settle.NewLevel, - BalanceAfter: settle.BalanceAfter, - } - if settle.FinalNode != nil { - vo := nodeVO(settle.FinalNode) - res.Final.FinalNode = &vo - } - } - return res, nil -} diff --git a/biz/controller/life_task.go b/biz/controller/life_task.go new file mode 100644 index 0000000..e60fef7 --- /dev/null +++ b/biz/controller/life_task.go @@ -0,0 +1,50 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type lifeTask struct{} + +var LifeTask = &lifeTask{} + +func (c *lifeTask) Get(ctx context.Context, req *dto.LifeTaskGetReq) (*dto.LifeTaskGetRes, error) { + rec, err := service.LifeTask.GetByPk(ctx, req.Id) + if err != nil { + return nil, err + } + res := &dto.LifeTaskGetRes{} + if !rec.IsEmpty() { + res.LifeTask = lifeTaskItem(rec) + } + return res, nil +} + +func (c *lifeTask) List(ctx context.Context, req *dto.LifeTaskListReq) (*dto.LifeTaskListRes, error) { + recs, err := service.LifeTask.ListEnabled(ctx) + if err != nil { + return nil, err + } + res := &dto.LifeTaskListRes{List: make([]dto.LifeTaskItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, lifeTaskItem(r)) + } + return res, nil +} + +func lifeTaskItem(r gdb.Record) dto.LifeTaskItem { + return dto.LifeTaskItem{ + Id: r["id"].Int64(), + StrategyId: r["strategy_id"].Int64(), + Title: r["title"].String(), + Description: r["description"].String(), + Guide: r["guide"].String(), + RewardPoints: r["reward_points"].Int(), + Status: r["status"].Int(), + } +} diff --git a/biz/controller/node_option.go b/biz/controller/node_option.go new file mode 100644 index 0000000..4c4cb5e --- /dev/null +++ b/biz/controller/node_option.go @@ -0,0 +1,53 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type nodeOption struct{} + +var NodeOption = &nodeOption{} + +func (c *nodeOption) Get(ctx context.Context, req *dto.NodeOptionGetReq) (*dto.NodeOptionGetRes, error) { + rec, err := service.NodeOption.GetInNode(ctx, req.OptionId, req.NodeId) + if err != nil { + return nil, err + } + res := &dto.NodeOptionGetRes{} + if !rec.IsEmpty() { + res.NodeOption = nodeOptionItem(rec) + } + return res, nil +} + +func (c *nodeOption) ListByNode(ctx context.Context, req *dto.NodeOptionListReq) (*dto.NodeOptionListRes, error) { + recs, err := service.NodeOption.ListEnabledByNode(ctx, req.NodeId) + if err != nil { + return nil, err + } + res := &dto.NodeOptionListRes{List: make([]dto.NodeOptionItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, nodeOptionItem(r)) + } + return res, nil +} + +func nodeOptionItem(r gdb.Record) dto.NodeOptionItem { + return dto.NodeOptionItem{ + Id: r["id"].Int64(), + NodeId: r["node_id"].Int64(), + Text: r["text"].String(), + PropId: r["prop_id"].Int64(), + Audio: r["audio"].String(), + NextNodeId: r["next_node_id"].Int64(), + Feedback: r["feedback"].String(), + FeedbackAudio: r["feedback_audio"].String(), + SortOrder: r["sort_order"].Int(), + Status: r["status"].Int(), + } +} diff --git a/biz/controller/prize.go b/biz/controller/prize.go new file mode 100644 index 0000000..18ce0bb --- /dev/null +++ b/biz/controller/prize.go @@ -0,0 +1,52 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type prize struct{} + +var Prize = &prize{} + +func (c *prize) Get(ctx context.Context, req *dto.PrizeGetReq) (*dto.PrizeGetRes, error) { + rec, err := service.Prize.GetByPk(ctx, req.Id) + if err != nil { + return nil, err + } + res := &dto.PrizeGetRes{} + if !rec.IsEmpty() { + res.Prize = prizeItem(rec) + } + return res, nil +} + +func (c *prize) List(ctx context.Context, req *dto.PrizeListReq) (*dto.PrizeListRes, error) { + recs, err := service.Prize.ListEnabled(ctx) + if err != nil { + return nil, err + } + res := &dto.PrizeListRes{List: make([]dto.PrizeItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, prizeItem(r)) + } + return res, nil +} + +func prizeItem(r gdb.Record) dto.PrizeItem { + return dto.PrizeItem{ + Id: r["id"].Int64(), + Name: r["name"].String(), + Description: r["description"].String(), + Icon: r["icon"].String(), + PType: r["p_type"].Int(), + PointsCost: r["points_cost"].Int(), + Stock: r["stock"].Int(), + Status: r["status"].Int(), + SortOrder: r["sort_order"].Int(), + } +} diff --git a/biz/controller/redemption.go b/biz/controller/redemption.go new file mode 100644 index 0000000..65774d1 --- /dev/null +++ b/biz/controller/redemption.go @@ -0,0 +1,37 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type redemption struct{} + +var Redemption = &redemption{} + +func (c *redemption) ListByUser(ctx context.Context, req *dto.RedemptionListReq) (*dto.RedemptionListRes, error) { + recs, err := service.Redemption.ListByUser(ctx, req.UserId) + if err != nil { + return nil, err + } + res := &dto.RedemptionListRes{List: make([]dto.RedemptionItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, redemptionItem(r)) + } + return res, nil +} + +func redemptionItem(r gdb.Record) dto.RedemptionItem { + return dto.RedemptionItem{ + Id: r["id"].Int64(), + UserId: r["user_id"].Int64(), + PrizeId: r["prize_id"].Int64(), + PointsCost: r["points_cost"].Int(), + Status: r["status"].Int(), + Code: r["code"].String(), + } +} diff --git a/biz/controller/router.go b/biz/controller/router.go deleted file mode 100644 index 7396a05..0000000 --- a/biz/controller/router.go +++ /dev/null @@ -1,30 +0,0 @@ -package controller - -import ( - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/net/ghttp" - "github.com/gogf/gf/v2/os/gctx" - - "36wisdom/common/auth" -) - -// Register 注册全部路由分组;各业务控制器在对应任务中挂载。 -func Register(s *ghttp.Server) { - s.Use(ghttp.MiddlewareHandlerResponse) - - s.BindHandler("GET:/ping", func(r *ghttp.Request) { - r.Response.Write("pong") - }) - - secret := g.Cfg().MustGet(gctx.New(), "auth.secret").String() - - // 前台:公开接口(注册/登录),鉴权接口按模块挂载 - s.Group("/api", func(g *ghttp.RouterGroup) { - g.Bind(Parent) - - g.Group("/", func(g *ghttp.RouterGroup) { - g.Middleware(auth.Middleware(secret, "")) - g.Bind(Child, Strategy, Level, LevelPlay) - }) - }) -} diff --git a/biz/controller/scene_node.go b/biz/controller/scene_node.go new file mode 100644 index 0000000..85c1ffa --- /dev/null +++ b/biz/controller/scene_node.go @@ -0,0 +1,58 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type sceneNode struct{} + +var SceneNode = &sceneNode{} + +func (c *sceneNode) Get(ctx context.Context, req *dto.SceneNodeGetReq) (*dto.SceneNodeGetRes, error) { + rec, err := service.SceneNode.GetInLevel(ctx, req.NodeId, req.LevelId) + if err != nil { + return nil, err + } + res := &dto.SceneNodeGetRes{} + if !rec.IsEmpty() { + res.SceneNode = sceneNodeItem(rec) + } + return res, nil +} + +func (c *sceneNode) ListByLevel(ctx context.Context, req *dto.SceneNodeListReq) (*dto.SceneNodeListRes, error) { + recs, err := service.SceneNode.ListEnabledByLevel(ctx, req.LevelId) + if err != nil { + return nil, err + } + res := &dto.SceneNodeListRes{List: make([]dto.SceneNodeItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, sceneNodeItem(r)) + } + return res, nil +} + +func sceneNodeItem(r gdb.Record) dto.SceneNodeItem { + return dto.SceneNodeItem{ + Id: r["id"].Int64(), + LevelId: r["level_id"].Int64(), + Title: r["title"].String(), + CharacterId: r["character_id"].Int64(), + Content: r["content"].String(), + Image: r["image"].String(), + Audio: r["audio"].String(), + NodeType: r["node_type"].Int(), + InteractionType: r["interaction_type"].Int(), + Config: r["config"].String(), + Script: r["script"].String(), + ResultType: r["result_type"].Int(), + IsEntry: r["is_entry"].Int(), + SortOrder: r["sort_order"].Int(), + Status: r["status"].Int(), + } +} diff --git a/biz/controller/user_badge.go b/biz/controller/user_badge.go new file mode 100644 index 0000000..b1dd35c --- /dev/null +++ b/biz/controller/user_badge.go @@ -0,0 +1,34 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type userBadge struct{} + +var UserBadge = &userBadge{} + +func (c *userBadge) ListByUser(ctx context.Context, req *dto.UserBadgeListReq) (*dto.UserBadgeListRes, error) { + recs, err := service.UserBadge.ListByUser(ctx, req.UserId) + if err != nil { + return nil, err + } + res := &dto.UserBadgeListRes{List: make([]dto.UserBadgeItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, userBadgeItem(r)) + } + return res, nil +} + +func userBadgeItem(r gdb.Record) dto.UserBadgeItem { + return dto.UserBadgeItem{ + Id: r["id"].Int64(), + UserId: r["user_id"].Int64(), + BadgeId: r["badge_id"].Int64(), + } +} diff --git a/biz/controller/user_collection.go b/biz/controller/user_collection.go new file mode 100644 index 0000000..e882899 --- /dev/null +++ b/biz/controller/user_collection.go @@ -0,0 +1,34 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type userCollection struct{} + +var UserCollection = &userCollection{} + +func (c *userCollection) ListByUser(ctx context.Context, req *dto.UserCollectionListReq) (*dto.UserCollectionListRes, error) { + recs, err := service.UserCollection.ListByUser(ctx, req.UserId) + if err != nil { + return nil, err + } + res := &dto.UserCollectionListRes{List: make([]dto.UserCollectionItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, userCollectionItem(r)) + } + return res, nil +} + +func userCollectionItem(r gdb.Record) dto.UserCollectionItem { + return dto.UserCollectionItem{ + Id: r["id"].Int64(), + UserId: r["user_id"].Int64(), + StrategyId: r["strategy_id"].Int64(), + } +} diff --git a/biz/controller/user_progress.go b/biz/controller/user_progress.go new file mode 100644 index 0000000..4a7114b --- /dev/null +++ b/biz/controller/user_progress.go @@ -0,0 +1,50 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type userProgress struct{} + +var UserProgress = &userProgress{} + +func (c *userProgress) GetByChildLevel(ctx context.Context, req *dto.UserProgressGetReq) (*dto.UserProgressGetRes, error) { + rec, err := service.UserProgress.GetByChildLevel(ctx, req.ChildId, req.LevelId) + if err != nil { + return nil, err + } + res := &dto.UserProgressGetRes{} + if !rec.IsEmpty() { + res.UserProgress = userProgressItem(rec) + } + return res, nil +} + +func (c *userProgress) ListByChild(ctx context.Context, req *dto.UserProgressListReq) (*dto.UserProgressListRes, error) { + recs, err := service.UserProgress.ListByChildLevelIds(ctx, req.ChildId, req.LevelIds) + if err != nil { + return nil, err + } + res := &dto.UserProgressListRes{List: make([]dto.UserProgressItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, userProgressItem(r)) + } + return res, nil +} + +func userProgressItem(r gdb.Record) dto.UserProgressItem { + return dto.UserProgressItem{ + Id: r["id"].Int64(), + ChildId: r["child_id"].Int64(), + LevelId: r["level_id"].Int64(), + Stars: r["stars"].Int(), + Score: r["score"].Int(), + Perfect: r["perfect"].Int(), + ContentVersion: r["content_version"].Int(), + } +} diff --git a/biz/controller/user_route_log.go b/biz/controller/user_route_log.go new file mode 100644 index 0000000..7bbead9 --- /dev/null +++ b/biz/controller/user_route_log.go @@ -0,0 +1,37 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type userRouteLog struct{} + +var UserRouteLog = &userRouteLog{} + +func (c *userRouteLog) ListByChildLevel(ctx context.Context, req *dto.UserRouteLogListReq) (*dto.UserRouteLogListRes, error) { + recs, err := service.UserRouteLog.ListFinalsByChildLevel(ctx, req.ChildId, req.LevelId) + if err != nil { + return nil, err + } + res := &dto.UserRouteLogListRes{List: make([]dto.UserRouteLogItem, 0, len(recs))} + for _, r := range recs { + res.List = append(res.List, userRouteLogItem(r)) + } + return res, nil +} + +func userRouteLogItem(r gdb.Record) dto.UserRouteLogItem { + return dto.UserRouteLogItem{ + Id: r["id"].Int64(), + ChildId: r["child_id"].Int64(), + LevelId: r["level_id"].Int64(), + NodeId: r["node_id"].Int64(), + OptionId: r["option_id"].Int64(), + ResultType: r["result_type"].Int(), + } +} diff --git a/biz/dao/dao_badge.go b/biz/dao/dao_badge.go index 26ec854..fe1014c 100644 --- a/biz/dao/dao_badge.go +++ b/biz/dao/dao_badge.go @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS badge ( cond_type INTEGER NOT NULL, cond_value INTEGER NOT NULL, status INTEGER NOT NULL DEFAULT 1 -);`) +); +CREATE INDEX IF NOT EXISTS idx_badge_status ON badge(status);`) return err } diff --git a/biz/dao/dao_child.go b/biz/dao/dao_child.go index 7825e3a..fd6d472 100644 --- a/biz/dao/dao_child.go +++ b/biz/dao/dao_child.go @@ -27,6 +27,17 @@ CREATE TABLE IF NOT EXISTS child ( created_at DATETIME, updated_at DATETIME ); -CREATE INDEX IF NOT EXISTS idx_child_parent ON child(parent_id);`) +CREATE INDEX IF NOT EXISTS idx_child_parent ON child(parent_id); +-- point_log 为 child 的附属流水表(纯表,不建独立分层文件),建表由主表 child 托管(同虚拟表模式) +CREATE TABLE IF NOT EXISTS point_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + change INTEGER NOT NULL, + reason_type INTEGER NOT NULL, + ref_id INTEGER, + balance_after INTEGER NOT NULL, + created_at DATETIME +); +CREATE INDEX IF NOT EXISTS idx_point_log_user ON point_log(user_id, created_at);`) return err } diff --git a/biz/dao/dao_life_task_log.go b/biz/dao/dao_life_task_log.go deleted file mode 100644 index 2069301..0000000 --- a/biz/dao/dao_life_task_log.go +++ /dev/null @@ -1,29 +0,0 @@ -package dao - -import ( - "context" - - "github.com/gogf/gf/v2/frame/g" - - "36wisdom/biz/consts" - "36wisdom/common" -) - -type lifeTaskLogDao struct{ common.BaseDao } - -var LifeTaskLog = &lifeTaskLogDao{BaseDao: common.BaseDao{Table: consts.TableLifeTaskLog}} - -func (d *lifeTaskLogDao) Init(ctx context.Context) error { - _, err := g.DB().Exec(ctx, ` -CREATE TABLE IF NOT EXISTS life_task_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - child_id INTEGER NOT NULL, - task_id INTEGER NOT NULL, - status INTEGER NOT NULL DEFAULT 1, - confirm_photo TEXT, - confirmed_at DATETIME, - created_at DATETIME, - UNIQUE(child_id, task_id) -);`) - return err -} diff --git a/biz/dao/dao_point_log.go b/biz/dao/dao_point_log.go deleted file mode 100644 index db05e22..0000000 --- a/biz/dao/dao_point_log.go +++ /dev/null @@ -1,29 +0,0 @@ -package dao - -import ( - "context" - - "github.com/gogf/gf/v2/frame/g" - - "36wisdom/biz/consts" - "36wisdom/common" -) - -type pointLogDao struct{ common.BaseDao } - -var PointLog = &pointLogDao{BaseDao: common.BaseDao{Table: consts.TablePointLog}} - -func (d *pointLogDao) Init(ctx context.Context) error { - _, err := g.DB().Exec(ctx, ` -CREATE TABLE IF NOT EXISTS point_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - change INTEGER NOT NULL, - reason_type INTEGER NOT NULL, - ref_id INTEGER, - balance_after INTEGER NOT NULL, - created_at DATETIME -); -CREATE INDEX IF NOT EXISTS idx_point_log_user ON point_log(user_id, created_at);`) - return err -} diff --git a/biz/dao/dao_prize.go b/biz/dao/dao_prize.go index 5d308fb..58063aa 100644 --- a/biz/dao/dao_prize.go +++ b/biz/dao/dao_prize.go @@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS prize ( stock INTEGER NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 1, sort_order INTEGER NOT NULL DEFAULT 1 -);`) +); +CREATE INDEX IF NOT EXISTS idx_prize_status ON prize(status, sort_order);`) return err } diff --git a/biz/dao/init.go b/biz/dao/init.go deleted file mode 100644 index b55193e..0000000 --- a/biz/dao/init.go +++ /dev/null @@ -1,40 +0,0 @@ -package dao - -import ( - "context" - - "github.com/gogf/gf/v2/frame/g" -) - -type tableInitializer func(ctx context.Context) error - -var tableInitializers = []tableInitializer{ - Parent.Init, - Child.Init, - Strategy.Init, - Level.Init, - SceneNode.Init, - NodeOption.Init, - Element.Init, - UserProgress.Init, - ChapterReview.Init, - UserRouteLog.Init, - UserCollection.Init, - Badge.Init, - UserBadge.Init, - PointLog.Init, - Prize.Init, - Redemption.Init, - AdminUser.Init, - LifeTask.Init, - LifeTaskLog.Init, -} - -// InitTables 创建全部业务表与索引(IF NOT EXISTS 幂等)。 -func InitTables(ctx context.Context) { - for _, init := range tableInitializers { - if err := init(ctx); err != nil { - g.Log().Fatal(ctx, err) - } - } -} diff --git a/biz/model/dto/admin_user.go b/biz/model/dto/admin_user.go new file mode 100644 index 0000000..4ac7c44 --- /dev/null +++ b/biz/model/dto/admin_user.go @@ -0,0 +1,15 @@ +package dto + +type AdminUserItem struct { + Id int64 `json:"id"` + Username string `json:"username"` + Status int `json:"status"` +} + +type AdminUserGetReq struct { + Username string `v:"required" json:"username"` +} + +type AdminUserGetRes struct { + AdminUser AdminUserItem `json:"admin_user"` +} diff --git a/biz/model/dto/badge.go b/biz/model/dto/badge.go new file mode 100644 index 0000000..027a11c --- /dev/null +++ b/biz/model/dto/badge.go @@ -0,0 +1,24 @@ +package dto + +type BadgeItem struct { + Id int64 `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + CondType int `json:"cond_type"` + CondValue int `json:"cond_value"` + Status int `json:"status"` +} + +type BadgeGetReq struct { + Id int64 `v:"required" json:"id"` +} + +type BadgeGetRes struct { + Badge BadgeItem `json:"badge"` +} + +type BadgeListReq struct{} + +type BadgeListRes struct { + List []BadgeItem `json:"list"` +} diff --git a/biz/model/dto/chapter_review.go b/biz/model/dto/chapter_review.go new file mode 100644 index 0000000..21abc9b --- /dev/null +++ b/biz/model/dto/chapter_review.go @@ -0,0 +1,17 @@ +package dto + +type ChapterReviewItem struct { + Id int64 `json:"id"` + ChildId int64 `json:"child_id"` + StrategyId int64 `json:"strategy_id"` + LevelIds string `json:"level_ids"` + Status int `json:"status"` +} + +type ChapterReviewListReq struct { + ChildId int64 `v:"required" json:"child_id"` +} + +type ChapterReviewListRes struct { + List []ChapterReviewItem `json:"list"` +} diff --git a/biz/model/dto/element.go b/biz/model/dto/element.go new file mode 100644 index 0000000..d90ae3e --- /dev/null +++ b/biz/model/dto/element.go @@ -0,0 +1,20 @@ +package dto + +type ElementItem struct { + Id int64 `json:"id"` + EType int `json:"e_type"` + Name string `json:"name"` + Image string `json:"image"` + Audio string `json:"audio"` + Description string `json:"description"` + Status int `json:"status"` + SortOrder int `json:"sort_order"` +} + +type ElementListReq struct { + Ids []int64 `v:"required" json:"ids"` +} + +type ElementListRes struct { + List []ElementItem `json:"list"` +} diff --git a/biz/model/dto/level.go b/biz/model/dto/level.go index dea1a7b..6172931 100644 --- a/biz/model/dto/level.go +++ b/biz/model/dto/level.go @@ -57,3 +57,29 @@ type LevelDetailRes struct { Perfect bool `json:"perfect"` Stars int `json:"stars"` } + +type ChooseReq struct { + g.Meta `path:"/level/choose" method:"post" summary:"分支闯关选择"` + ChildId int64 `v:"required" json:"child_id"` + LevelId int64 `v:"required" json:"level_id"` + NodeId int64 `v:"required" json:"node_id"` + OptionId int64 `v:"required" json:"option_id"` +} + +type ChooseRes struct { + Next *NodeVO `json:"next"` + Final *FinalSettle `json:"final"` +} + +type FinalSettle struct { + ResultType int `json:"result_type"` + Stars int `json:"stars"` + ScoreDelta int `json:"score_delta"` + Cleared bool `json:"cleared"` + Perfect bool `json:"perfect"` + UnlockNext bool `json:"unlock_next"` + CollectionUnlocked bool `json:"collection_unlocked"` + NewLevel int `json:"new_level"` + BalanceAfter int `json:"balance_after"` + FinalNode *NodeVO `json:"final_node"` +} diff --git a/biz/model/dto/level_play.go b/biz/model/dto/level_play.go deleted file mode 100644 index 6c3e703..0000000 --- a/biz/model/dto/level_play.go +++ /dev/null @@ -1,29 +0,0 @@ -package dto - -import "github.com/gogf/gf/v2/frame/g" - -type ChooseReq struct { - g.Meta `path:"/level/choose" method:"post" summary:"分支闯关选择"` - ChildId int64 `v:"required" json:"child_id"` - LevelId int64 `v:"required" json:"level_id"` - NodeId int64 `v:"required" json:"node_id"` - OptionId int64 `v:"required" json:"option_id"` -} - -type ChooseRes struct { - Next *NodeVO `json:"next"` - Final *FinalSettle `json:"final"` -} - -type FinalSettle struct { - ResultType int `json:"result_type"` - Stars int `json:"stars"` - ScoreDelta int `json:"score_delta"` - Cleared bool `json:"cleared"` - Perfect bool `json:"perfect"` - UnlockNext bool `json:"unlock_next"` - CollectionUnlocked bool `json:"collection_unlocked"` - NewLevel int `json:"new_level"` - BalanceAfter int `json:"balance_after"` - FinalNode *NodeVO `json:"final_node"` -} diff --git a/biz/model/dto/life_task.go b/biz/model/dto/life_task.go new file mode 100644 index 0000000..4eff68c --- /dev/null +++ b/biz/model/dto/life_task.go @@ -0,0 +1,25 @@ +package dto + +type LifeTaskItem struct { + Id int64 `json:"id"` + StrategyId int64 `json:"strategy_id"` + Title string `json:"title"` + Description string `json:"description"` + Guide string `json:"guide"` + RewardPoints int `json:"reward_points"` + Status int `json:"status"` +} + +type LifeTaskGetReq struct { + Id int64 `v:"required" json:"id"` +} + +type LifeTaskGetRes struct { + LifeTask LifeTaskItem `json:"life_task"` +} + +type LifeTaskListReq struct{} + +type LifeTaskListRes struct { + List []LifeTaskItem `json:"list"` +} diff --git a/biz/model/dto/node_option.go b/biz/model/dto/node_option.go new file mode 100644 index 0000000..96f060b --- /dev/null +++ b/biz/model/dto/node_option.go @@ -0,0 +1,31 @@ +package dto + +type NodeOptionItem struct { + Id int64 `json:"id"` + NodeId int64 `json:"node_id"` + Text string `json:"text"` + PropId int64 `json:"prop_id"` + Audio string `json:"audio"` + NextNodeId int64 `json:"next_node_id"` + Feedback string `json:"feedback"` + FeedbackAudio string `json:"feedback_audio"` + SortOrder int `json:"sort_order"` + Status int `json:"status"` +} + +type NodeOptionGetReq struct { + OptionId int64 `v:"required" json:"option_id"` + NodeId int64 `v:"required" json:"node_id"` +} + +type NodeOptionGetRes struct { + NodeOption NodeOptionItem `json:"node_option"` +} + +type NodeOptionListReq struct { + NodeId int64 `v:"required" json:"node_id"` +} + +type NodeOptionListRes struct { + List []NodeOptionItem `json:"list"` +} diff --git a/biz/model/dto/prize.go b/biz/model/dto/prize.go new file mode 100644 index 0000000..53f248e --- /dev/null +++ b/biz/model/dto/prize.go @@ -0,0 +1,27 @@ +package dto + +type PrizeItem struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + PType int `json:"p_type"` + PointsCost int `json:"points_cost"` + Stock int `json:"stock"` + Status int `json:"status"` + SortOrder int `json:"sort_order"` +} + +type PrizeGetReq struct { + Id int64 `v:"required" json:"id"` +} + +type PrizeGetRes struct { + Prize PrizeItem `json:"prize"` +} + +type PrizeListReq struct{} + +type PrizeListRes struct { + List []PrizeItem `json:"list"` +} diff --git a/biz/model/dto/redemption.go b/biz/model/dto/redemption.go new file mode 100644 index 0000000..991f99b --- /dev/null +++ b/biz/model/dto/redemption.go @@ -0,0 +1,18 @@ +package dto + +type RedemptionItem struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + PrizeId int64 `json:"prize_id"` + PointsCost int `json:"points_cost"` + Status int `json:"status"` + Code string `json:"code"` +} + +type RedemptionListReq struct { + UserId int64 `v:"required" json:"user_id"` +} + +type RedemptionListRes struct { + List []RedemptionItem `json:"list"` +} diff --git a/biz/model/dto/scene_node.go b/biz/model/dto/scene_node.go new file mode 100644 index 0000000..e01ed24 --- /dev/null +++ b/biz/model/dto/scene_node.go @@ -0,0 +1,36 @@ +package dto + +type SceneNodeItem struct { + Id int64 `json:"id"` + LevelId int64 `json:"level_id"` + Title string `json:"title"` + CharacterId int64 `json:"character_id"` + Content string `json:"content"` + Image string `json:"image"` + Audio string `json:"audio"` + NodeType int `json:"node_type"` + InteractionType int `json:"interaction_type"` + Config string `json:"config"` + Script string `json:"script"` + ResultType int `json:"result_type"` + IsEntry int `json:"is_entry"` + SortOrder int `json:"sort_order"` + Status int `json:"status"` +} + +type SceneNodeGetReq struct { + NodeId int64 `v:"required" json:"node_id"` + LevelId int64 `v:"required" json:"level_id"` +} + +type SceneNodeGetRes struct { + SceneNode SceneNodeItem `json:"scene_node"` +} + +type SceneNodeListReq struct { + LevelId int64 `v:"required" json:"level_id"` +} + +type SceneNodeListRes struct { + List []SceneNodeItem `json:"list"` +} diff --git a/biz/model/dto/user_badge.go b/biz/model/dto/user_badge.go new file mode 100644 index 0000000..315a87d --- /dev/null +++ b/biz/model/dto/user_badge.go @@ -0,0 +1,15 @@ +package dto + +type UserBadgeItem struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + BadgeId int64 `json:"badge_id"` +} + +type UserBadgeListReq struct { + UserId int64 `v:"required" json:"user_id"` +} + +type UserBadgeListRes struct { + List []UserBadgeItem `json:"list"` +} diff --git a/biz/model/dto/user_collection.go b/biz/model/dto/user_collection.go new file mode 100644 index 0000000..54fd858 --- /dev/null +++ b/biz/model/dto/user_collection.go @@ -0,0 +1,15 @@ +package dto + +type UserCollectionItem struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + StrategyId int64 `json:"strategy_id"` +} + +type UserCollectionListReq struct { + UserId int64 `v:"required" json:"user_id"` +} + +type UserCollectionListRes struct { + List []UserCollectionItem `json:"list"` +} diff --git a/biz/model/dto/user_progress.go b/biz/model/dto/user_progress.go new file mode 100644 index 0000000..6fe50bd --- /dev/null +++ b/biz/model/dto/user_progress.go @@ -0,0 +1,29 @@ +package dto + +type UserProgressItem struct { + Id int64 `json:"id"` + ChildId int64 `json:"child_id"` + LevelId int64 `json:"level_id"` + Stars int `json:"stars"` + Score int `json:"score"` + Perfect int `json:"perfect"` + ContentVersion int `json:"content_version"` +} + +type UserProgressGetReq struct { + ChildId int64 `v:"required" json:"child_id"` + LevelId int64 `v:"required" json:"level_id"` +} + +type UserProgressGetRes struct { + UserProgress UserProgressItem `json:"user_progress"` +} + +type UserProgressListReq struct { + ChildId int64 `v:"required" json:"child_id"` + LevelIds []int64 `v:"required" json:"level_ids"` +} + +type UserProgressListRes struct { + List []UserProgressItem `json:"list"` +} diff --git a/biz/model/dto/user_route_log.go b/biz/model/dto/user_route_log.go new file mode 100644 index 0000000..66576f2 --- /dev/null +++ b/biz/model/dto/user_route_log.go @@ -0,0 +1,19 @@ +package dto + +type UserRouteLogItem struct { + Id int64 `json:"id"` + ChildId int64 `json:"child_id"` + LevelId int64 `json:"level_id"` + NodeId int64 `json:"node_id"` + OptionId int64 `json:"option_id"` + ResultType int `json:"result_type"` +} + +type UserRouteLogListReq struct { + ChildId int64 `v:"required" json:"child_id"` + LevelId int64 `v:"required" json:"level_id"` +} + +type UserRouteLogListRes struct { + List []UserRouteLogItem `json:"list"` +} diff --git a/biz/model/entity/entity_life_task_log.go b/biz/model/entity/entity_life_task_log.go deleted file mode 100644 index b634b57..0000000 --- a/biz/model/entity/entity_life_task_log.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import "github.com/gogf/gf/v2/os/gtime" - -// LifeTaskLog 生活任务完成记录:孩子 × 任务,家长确认状态流转。 -type LifeTaskLog struct { - Id int64 `json:"id" orm:"id"` - ChildId int64 `json:"child_id" orm:"child_id"` - TaskId int64 `json:"task_id" orm:"task_id"` - Status int `json:"status" orm:"status"` - ConfirmPhoto string `json:"confirm_photo" orm:"confirm_photo"` - ConfirmedAt *gtime.Time `json:"confirmed_at" orm:"confirmed_at"` - CreatedAt *gtime.Time `json:"created_at" orm:"created_at"` -} diff --git a/biz/model/entity/entity_point_log.go b/biz/model/entity/entity_point_log.go deleted file mode 100644 index 741f6a3..0000000 --- a/biz/model/entity/entity_point_log.go +++ /dev/null @@ -1,14 +0,0 @@ -package entity - -import "github.com/gogf/gf/v2/os/gtime" - -// PointLog 积分流水:变动、原因类型、余额快照。 -type PointLog struct { - Id int64 `json:"id" orm:"id"` - UserId int64 `json:"user_id" orm:"user_id"` - Change int `json:"change" orm:"change"` - ReasonType int `json:"reason_type" orm:"reason_type"` - RefId int64 `json:"ref_id" orm:"ref_id"` - BalanceAfter int `json:"balance_after" orm:"balance_after"` - CreatedAt *gtime.Time `json:"created_at" orm:"created_at"` -} diff --git a/biz/service/admin_user.go b/biz/service/admin_user.go new file mode 100644 index 0000000..de0d04f --- /dev/null +++ b/biz/service/admin_user.go @@ -0,0 +1,18 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/dao" +) + +type adminUser struct{} + +var AdminUser = &adminUser{} + +// GetByUsername 按用户名取管理员账号(含密码列,登录校验用)。 +func (s *adminUser) GetByUsername(ctx context.Context, username string) (gdb.Record, error) { + return dao.AdminUser.Model().Ctx(ctx).Where("username", username).One() +} diff --git a/biz/service/auth.go b/biz/service/auth.go deleted file mode 100644 index 981fdde..0000000 --- a/biz/service/auth.go +++ /dev/null @@ -1,27 +0,0 @@ -package service - -import ( - "context" - - "github.com/gogf/gf/v2/frame/g" - - "36wisdom/biz/consts" - "36wisdom/common/auth" -) - -// authSvc 鉴权服务:token 签发与解析,密钥统一来自配置 auth.secret。 -type authSvc struct{} - -var Auth = &authSvc{} - -func (s *authSvc) GenerateToken(ctx context.Context, uid int64, role string) (string, error) { - return auth.GenerateToken(secret(ctx), uid, role, consts.AuthExpireSeconds) -} - -func (s *authSvc) ParseToken(ctx context.Context, tokenString string) (int64, string, error) { - return auth.ParseToken(secret(ctx), tokenString) -} - -func secret(ctx context.Context) string { - return g.Cfg().MustGet(ctx, "auth.secret").String() -} diff --git a/biz/service/badge.go b/biz/service/badge.go new file mode 100644 index 0000000..de9cfd3 --- /dev/null +++ b/biz/service/badge.go @@ -0,0 +1,25 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type badge struct{} + +var Badge = &badge{} + +// GetByPk 徽章定义。 +func (s *badge) GetByPk(ctx context.Context, id int64) (gdb.Record, error) { + return dao.Badge.GetOneByPk(ctx, id) +} + +// ListEnabled 启用徽章定义(内容缓存)。 +func (s *badge) ListEnabled(ctx context.Context) ([]gdb.Record, error) { + return dao.Badge.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("status", consts.StatusEnabled).Order("id ASC").All() +} diff --git a/biz/service/chapter_review.go b/biz/service/chapter_review.go new file mode 100644 index 0000000..5c60cf2 --- /dev/null +++ b/biz/service/chapter_review.go @@ -0,0 +1,29 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + + "36wisdom/biz/dao" +) + +type chapterReview struct{} + +var ChapterReview = &chapterReview{} + +// ListByChild 孩子温故记录。 +func (s *chapterReview) ListByChild(ctx context.Context, childId int64) ([]gdb.Record, error) { + return dao.ChapterReview.Model().Ctx(ctx).Where("child_id", childId).Order("id DESC").All() +} + +// Insert 创建温故记录。 +func (s *chapterReview) Insert(ctx context.Context, childId, strategyId int64, levelIds string) (int64, error) { + return dao.ChapterReview.InsertAndReturnId(ctx, g.Map{ + "child_id": childId, + "strategy_id": strategyId, + "level_ids": levelIds, + "status": 1, + }) +} diff --git a/biz/service/child.go b/biz/service/child.go index 11b5cf9..0e4cae5 100644 --- a/biz/service/child.go +++ b/biz/service/child.go @@ -80,11 +80,7 @@ func (s *child) List(ctx context.Context, parentId int64) ([]*ChildItem, error) } perfectMap := make(map[int64]int, len(recs)) - rows, err := dao.UserProgress.Model().Ctx(ctx). - Fields("child_id", "COUNT(*) AS cnt"). - Where("perfect", 1). - WhereIn("child_id", childIds). - Group("child_id").All() + rows, err := UserProgress.CountPerfectByChildIds(ctx, childIds) if err != nil { return nil, err } diff --git a/biz/service/element.go b/biz/service/element.go new file mode 100644 index 0000000..dfd9ece --- /dev/null +++ b/biz/service/element.go @@ -0,0 +1,20 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type element struct{} + +var Element = &element{} + +// ListEnabledByIds 按 id 批量取启用元素(内容缓存)。 +func (s *element) ListEnabledByIds(ctx context.Context, ids []int64) ([]gdb.Record, error) { + return dao.Element.Model().Ctx(ctx).Cache(contentCache(ctx)). + WhereIn("id", ids).Where("status", consts.StatusEnabled).All() +} diff --git a/biz/service/level.go b/biz/service/level.go index 315ab19..fdac19c 100644 --- a/biz/service/level.go +++ b/biz/service/level.go @@ -2,12 +2,16 @@ package service import ( "context" + "fmt" + "time" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" "36wisdom/biz/consts" "36wisdom/biz/dao" + "36wisdom/common" ) type level struct{} @@ -39,8 +43,8 @@ func LevelOf(perfectCount int) (level int, title string) { return } -// Element 元素 VO(场景/人物/道具)。 -type Element struct { +// ElementVO 元素 VO(场景/人物/道具)。 +type ElementVO struct { Id int64 EType int Name string @@ -58,7 +62,7 @@ type Option struct { FeedbackPros string FeedbackCons string Audio string - Prop *Element + Prop *ElementVO } // Node 关卡节点 VO(决策/终局)。 @@ -69,7 +73,7 @@ type Node struct { ContentPinyin string Image string Audio string - Character *Element + Character *ElementVO InteractionType int Config string Script string @@ -82,7 +86,7 @@ type Node struct { type LevelDetail struct { LevelId int64 Title string - Scene *Element + Scene *ElementVO SceneContent string SceneContentPinyin string SceneImage string @@ -111,9 +115,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) ( return nil, gerror.New("关卡不属于当前年龄段") } - nodeRecs, err := dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)). - Where("level_id", levelId).Where("status", consts.StatusEnabled). - Order("sort_order ASC").All() + nodeRecs, err := SceneNode.ListEnabledByLevel(ctx, levelId) if err != nil { return nil, err } @@ -124,9 +126,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) ( optionRecs := []gdb.Record{} if len(nodeIds) > 0 { - optionRecs, err = dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)). - WhereIn("node_id", nodeIds).Where("status", consts.StatusEnabled). - Order("sort_order ASC").All() + optionRecs, err = NodeOption.ListEnabledByNodeIds(ctx, nodeIds) if err != nil { return nil, err } @@ -137,8 +137,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) ( return nil, err } - progressRec, err := dao.UserProgress.Model().Ctx(ctx). - Where("child_id", childId).Where("level_id", levelId).One() + progressRec, err := UserProgress.GetByChildLevel(ctx, childId, levelId) if err != nil { return nil, err } @@ -173,7 +172,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) ( } // elementsOf 汇总关卡涉及的场景/人物/道具元素,批量查询后按 id 索引。 -func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs []gdb.Record) (map[int64]*Element, error) { +func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs []gdb.Record) (map[int64]*ElementVO, error) { ids := []int64{levelRec["scene_id"].Int64()} for _, n := range nodeRecs { if cid := n["character_id"].Int64(); cid > 0 { @@ -187,17 +186,16 @@ func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs [ } ids = uniqueInt64(ids) - m := make(map[int64]*Element, len(ids)) + m := make(map[int64]*ElementVO, len(ids)) if len(ids) == 0 { return m, nil } - recs, err := dao.Element.Model().Ctx(ctx).Cache(contentCache(ctx)). - WhereIn("id", ids).Where("status", consts.StatusEnabled).All() + recs, err := Element.ListEnabledByIds(ctx, ids) if err != nil { return nil, err } for _, r := range recs { - m[r["id"].Int64()] = &Element{ + m[r["id"].Int64()] = &ElementVO{ Id: r["id"].Int64(), EType: r["e_type"].Int(), Name: r["name"].String(), @@ -211,7 +209,7 @@ func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs [ } // buildNode 组装单个节点的选项与人物/道具元素。 -func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]*Element) *Node { +func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]*ElementVO) *Node { node := &Node{ NodeId: nodeRec["id"].Int64(), Title: nodeRec["title"].String(), @@ -242,3 +240,360 @@ func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]* } return node } + +// SettleState 结算输入:该关历史状态(由进度 + 路径流水推导,锁内读取)。 +type SettleState struct { + LevelCleared bool // 该关已通关(到过最佳终局) + FailStreak int // 连续失败终局次数(非失败终局打断连续) + ReachedFinals map[int64]bool // 已到达终局节点 id 集合 + TotalFinals int // 该关终局节点总数 + PerfectAwarded bool // 完美奖励是否已发放(进度表为准) + BalanceAfter int // 结算前余额 +} + +// FinalSettle 结算结果(service 层领域值,controller 映射为 dto)。 +type FinalSettle struct { + ResultType int + Stars int + ScoreDelta int + Cleared bool + Perfect bool + UnlockNext bool + CollectionUnlocked bool + NewLevel int + BalanceAfter int + FinalNode *Node // 终局节点(含 script,v2.1 前端演绎结局台词后跳结算) +} + +// SettleFinal 纯函数判分(技术设计 4.3/4.4,可单测): +// - 首次到达某终局才结算,重复到达 delta=0 +// - 未通关:最佳 +30 / 良好 +10 / 失败 -10(连续失败 ≥2 次后不再扣;余额扣至 0 不为负) +// - 已通关后补分支:只记完成度不结算积分 +// - 终局数集齐 → 完美(+20);最佳终局 → 通关 +func SettleFinal(s SettleState, finalNodeId int64, resultType int) FinalSettle { + f := FinalSettle{ResultType: resultType, BalanceAfter: s.BalanceAfter} + switch resultType { + case consts.ResultBest: + f.Stars = 3 + case consts.ResultGood: + f.Stars = 2 + default: + f.Stars = 0 + } + + newNode := !s.ReachedFinals[finalNodeId] + if newNode && !s.LevelCleared { + switch resultType { + case consts.ResultBest: + f.ScoreDelta = consts.PointsBest + case consts.ResultGood: + f.ScoreDelta = consts.PointsGood + default: + if s.FailStreak < consts.FailNoDeductAfter { + f.ScoreDelta = consts.PointsFail + } + } + } + if f.ScoreDelta < 0 && -f.ScoreDelta > s.BalanceAfter { + f.ScoreDelta = -s.BalanceAfter + } + + f.Cleared = s.LevelCleared || resultType == consts.ResultBest + + reached := len(s.ReachedFinals) + if newNode { + reached++ + } + // 完美以进度表标记为准发放一次(日志为尽力而为的记录,可能漂移) + if reached == s.TotalFinals && s.TotalFinals > 0 && !s.PerfectAwarded { + f.Perfect = true + f.ScoreDelta += consts.PointsPerfect + } + + f.BalanceAfter = s.BalanceAfter + f.ScoreDelta + return f +} + +// Choose 分支闯关:校验 node/option 归属与解锁 → 决策节点返回下一节点; +// 终局节点在锁内结算(事务写进度/积分/计策卡)。路径流水尽力而为,失败不阻断。 +func (s *level) Choose(ctx context.Context, parentUid, childId, levelId, nodeId, optionId int64) (*Node, *FinalSettle, error) { + child, err := getChildOf(ctx, parentUid, childId) + if err != nil { + return nil, nil, err + } + + levelRec, err := dao.Level.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(levelId).One() + if err != nil { + return nil, nil, err + } + if levelRec.IsEmpty() || levelRec["status"].Int() != consts.StatusEnabled { + return nil, nil, gerror.New("关卡不存在") + } + if levelRec["age_group"].String() != child["age_group"].String() { + return nil, nil, gerror.New("关卡不属于当前年龄段") + } + + nodeRec, err := SceneNode.GetInLevel(ctx, nodeId, levelId) + if err != nil { + return nil, nil, err + } + if nodeRec.IsEmpty() { + return nil, nil, gerror.New("节点不存在") + } + + optionRec, err := NodeOption.GetInNode(ctx, optionId, nodeId) + if err != nil { + return nil, nil, err + } + if optionRec.IsEmpty() { + return nil, nil, gerror.New("选项不存在") + } + + nextRec, err := SceneNode.GetInLevel(ctx, optionRec["next_node_id"].Int64(), levelId) + if err != nil { + return nil, nil, err + } + if nextRec.IsEmpty() { + return nil, nil, gerror.New("目标节点不存在") + } + + progressRec, err := UserProgress.GetByChildLevel(ctx, childId, levelId) + if err != nil { + return nil, nil, err + } + if !s.levelUnlocked(ctx, childId, child, levelRec, progressRec) { + return nil, nil, gerror.New("关卡未解锁") + } + + if nextRec["result_type"].Int() == consts.ResultNone { + s.logRoute(ctx, childId, levelId, nodeId, optionId, consts.ResultNone) + node, err := nodeOf(ctx, levelRec, nextRec) + if err != nil { + return nil, nil, err + } + return node, nil, nil + } + + // v2.1:终局节点剧本先于结算构建(只读查询失败则未结算,无副作用) + finalNode, err := nodeOf(ctx, levelRec, nextRec) + if err != nil { + return nil, nil, err + } + // 终局:路径流水在锁内结算后记录,避免状态读到本次到达导致重复结算误判 + settle, err := common.WithLock(ctx, fmt.Sprintf("child:%d:level:%d", childId, levelId), 10*time.Second, 3, 200*time.Millisecond, func() (*FinalSettle, error) { + return s.settle(ctx, child, levelRec, nextRec, nodeId, optionId) + }) + if err != nil { + return nil, nil, err + } + settle.FinalNode = finalNode + return nil, settle, nil +} + +// levelUnlocked 关卡解锁判定:本关已通关可重玩,否则按解锁链校验计策解锁。 +func (s *level) levelUnlocked(ctx context.Context, childId int64, child gdb.Record, levelRec, progressRec gdb.Record) bool { + if progressRec["stars"].Int() >= 3 { + return true + } + strategyRec, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)). + WherePri(levelRec["strategy_id"].Int64()).One() + if err != nil { + return false + } + unlocked, _ := Strategy.unlockState(ctx, childId, child["age_group"].String(), strategyRec) + return unlocked +} + +// logRoute 路径流水(尽力而为,写失败不阻断主流程)。 +func (s *level) logRoute(ctx context.Context, childId, levelId, nodeId, optionId int64, resultType int) { + _ = UserRouteLog.Append(ctx, childId, levelId, nodeId, optionId, resultType) +} + +// nodeOf 组装下一决策节点(选项 + 元素)。 +func nodeOf(ctx context.Context, levelRec, nodeRec gdb.Record) (*Node, error) { + optionRecs, err := NodeOption.ListEnabledByNode(ctx, nodeRec["id"].Int64()) + if err != nil { + return nil, err + } + elements, err := elementsOf(ctx, levelRec, []gdb.Record{nodeRec}, optionRecs) + if err != nil { + return nil, err + } + return buildNode(nodeRec, optionRecs, elements), nil +} + +// settle 终局结算(调用方持锁):锁内重读状态 → 纯函数判分 → 事务写库 → 记录本次终局路径 → 派生结果。 +func (s *level) settle(ctx context.Context, child gdb.Record, levelRec, finalRec gdb.Record, nodeId, optionId int64) (*FinalSettle, error) { + childId := child["id"].Int64() + levelId := levelRec["id"].Int64() + + state, err := s.settleState(ctx, childId, levelId) + if err != nil { + return nil, err + } + f := SettleFinal(state, finalRec["id"].Int64(), finalRec["result_type"].Int()) + + if f.ScoreDelta != 0 || f.Cleared || f.Perfect { + if err = s.commitSettle(ctx, childId, levelRec, &f); err != nil { + return nil, err + } + } + s.logRoute(ctx, childId, levelId, nodeId, optionId, finalRec["result_type"].Int()) + s.deriveExtras(ctx, child, levelRec, &f) + return &f, nil +} + +// settleState 锁内重读该关历史状态:终局数、已到达终局、连续失败、余额。 +func (s *level) settleState(ctx context.Context, childId, levelId int64) (SettleState, error) { + state := SettleState{ReachedFinals: map[int64]bool{}} + + progressRec, err := UserProgress.GetByChildLevel(ctx, childId, levelId) + if err != nil { + return state, err + } + state.LevelCleared = progressRec["stars"].Int() >= 3 + state.PerfectAwarded = progressRec["perfect"].Int() == 1 + + childRec, err := dao.Child.Model().Ctx(ctx).WherePri(childId).One() + if err != nil { + return state, err + } + state.BalanceAfter = childRec["points"].Int() + + totalFinals, err := SceneNode.CountFinalsByLevel(ctx, levelId) + if err != nil { + return state, err + } + state.TotalFinals = totalFinals + + logs, err := UserRouteLog.ListFinalsByChildLevel(ctx, childId, levelId) + if err != nil { + return state, err + } + // 日志记录的是选择节点(node_id)+ 选项;终局节点经选项的 next_node_id 解析 + optionIds := make([]int64, 0, len(logs)) + for _, l := range logs { + optionIds = append(optionIds, l["option_id"].Int64()) + } + nextOf := map[int64]int64{} + if len(optionIds) > 0 { + opts, err := NodeOption.ListByOptionIds(ctx, optionIds) + if err != nil { + return state, err + } + for _, o := range opts { + nextOf[o["id"].Int64()] = o["next_node_id"].Int64() + } + } + for _, l := range logs { + if nid, ok := nextOf[l["option_id"].Int64()]; ok { + state.ReachedFinals[nid] = true + } + } + for i := len(logs) - 1; i >= 0; i-- { + if logs[i]["result_type"].Int() == consts.ResultFail { + state.FailStreak++ + } else { + break + } + } + return state, nil +} + +// commitSettle 结算事务:进度(最高星 + 完美标记 + 内容版本)、积分流水与余额、计策卡。 +func (s *level) commitSettle(ctx context.Context, childId int64, levelRec gdb.Record, f *FinalSettle) error { + levelId := levelRec["id"].Int64() + return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + stars := f.Stars + perfect := 0 + if f.Perfect { + perfect = 1 + } + if err := UserProgress.UpsertInTx(ctx, tx, childId, levelId, stars, perfect, levelRec["content_version"].Int()); err != nil { + return err + } + + if f.ScoreDelta != 0 { + childRec, err := tx.Model(consts.TableChild).Ctx(ctx).WherePri(childId).One() + if err != nil { + return err + } + balance := childRec["points"].Int() + f.ScoreDelta + if _, err = tx.Model(consts.TableChild).Ctx(ctx).Data(g.Map{"points": balance}).WherePri(childId).Update(); err != nil { + return err + } + if _, err = tx.Model(consts.TablePointLog).Ctx(ctx).Data(g.Map{ + "user_id": childId, + "change": f.ScoreDelta, + "reason_type": consts.ReasonLevel, + "ref_id": levelId, + "balance_after": balance, + }).Insert(); err != nil { + return err + } + f.BalanceAfter = balance + } + + return s.collectIfAllPerfect(ctx, tx, childId, levelRec, f) + }) +} + +// collectIfAllPerfect 本计全部关卡完美 → 写计策卡(防重复:已存在则跳过)。 +func (s *level) collectIfAllPerfect(ctx context.Context, tx gdb.TX, childId int64, levelRec gdb.Record, f *FinalSettle) error { + if !f.Perfect { + return nil + } + strategyId := levelRec["strategy_id"].Int64() + levels, err := tx.Model(consts.TableLevel).Ctx(ctx). + Where("strategy_id", strategyId).Where("status", consts.StatusEnabled).All() + if err != nil { + return err + } + for _, lv := range levels { + rec, err := UserProgress.GetInTx(ctx, tx, childId, lv["id"].Int64()) + if err != nil { + return err + } + if rec.IsEmpty() || rec["perfect"].Int() != 1 { + return nil + } + } + exists, err := UserCollection.ExistsInTx(ctx, tx, childId, strategyId) + if err != nil { + return err + } + if exists { + return nil + } + if err = UserCollection.InsertInTx(ctx, tx, childId, strategyId); err != nil { + return err + } + f.CollectionUnlocked = true + return nil +} + +// deriveExtras 结算后派生:计策卡解锁提示、下一计解锁、成长等级。 +func (s *level) deriveExtras(ctx context.Context, child gdb.Record, levelRec gdb.Record, f *FinalSettle) { + childId := child["id"].Int64() + + if f.CollectionUnlocked { + if next, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("unlock_before", levelRec["strategy_id"].Int64()).One(); err == nil && !next.IsEmpty() { + f.UnlockNext = true + } + } + + before, err := UserProgress.CountPerfectByChild(ctx, childId) + if err != nil { + return + } + after := before + if f.Perfect { + after++ + } + oldLevel, _ := LevelOf(before) + newLevel, _ := LevelOf(after) + if oldLevel != newLevel { + f.NewLevel = newLevel + } +} diff --git a/biz/service/level_play.go b/biz/service/level_play.go deleted file mode 100644 index c8ef7da..0000000 --- a/biz/service/level_play.go +++ /dev/null @@ -1,428 +0,0 @@ -package service - -import ( - "context" - "fmt" - "time" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" - - "36wisdom/biz/consts" - "36wisdom/biz/dao" - "36wisdom/common" -) - -type levelPlay struct{} - -var LevelPlay = &levelPlay{} - -// SettleState 结算输入:该关历史状态(由进度 + 路径流水推导,锁内读取)。 -type SettleState struct { - LevelCleared bool // 该关已通关(到过最佳终局) - FailStreak int // 连续失败终局次数(非失败终局打断连续) - ReachedFinals map[int64]bool // 已到达终局节点 id 集合 - TotalFinals int // 该关终局节点总数 - PerfectAwarded bool // 完美奖励是否已发放(进度表为准) - BalanceAfter int // 结算前余额 -} - -// FinalSettle 结算结果(service 层领域值,controller 映射为 dto)。 -type FinalSettle struct { - ResultType int - Stars int - ScoreDelta int - Cleared bool - Perfect bool - UnlockNext bool - CollectionUnlocked bool - NewLevel int - BalanceAfter int - FinalNode *Node // 终局节点(含 script,v2.1 前端演绎结局台词后跳结算) -} - -// SettleFinal 纯函数判分(技术设计 4.3/4.4,可单测): -// - 首次到达某终局才结算,重复到达 delta=0 -// - 未通关:最佳 +30 / 良好 +10 / 失败 -10(连续失败 ≥2 次后不再扣;余额扣至 0 不为负) -// - 已通关后补分支:只记完成度不结算积分 -// - 终局数集齐 → 完美(+20);最佳终局 → 通关 -func SettleFinal(s SettleState, finalNodeId int64, resultType int) FinalSettle { - f := FinalSettle{ResultType: resultType, BalanceAfter: s.BalanceAfter} - switch resultType { - case consts.ResultBest: - f.Stars = 3 - case consts.ResultGood: - f.Stars = 2 - default: - f.Stars = 0 - } - - newNode := !s.ReachedFinals[finalNodeId] - if newNode && !s.LevelCleared { - switch resultType { - case consts.ResultBest: - f.ScoreDelta = consts.PointsBest - case consts.ResultGood: - f.ScoreDelta = consts.PointsGood - default: - if s.FailStreak < consts.FailNoDeductAfter { - f.ScoreDelta = consts.PointsFail - } - } - } - if f.ScoreDelta < 0 && -f.ScoreDelta > s.BalanceAfter { - f.ScoreDelta = -s.BalanceAfter - } - - f.Cleared = s.LevelCleared || resultType == consts.ResultBest - - reached := len(s.ReachedFinals) - if newNode { - reached++ - } - // 完美以进度表标记为准发放一次(日志为尽力而为的记录,可能漂移) - if reached == s.TotalFinals && s.TotalFinals > 0 && !s.PerfectAwarded { - f.Perfect = true - f.ScoreDelta += consts.PointsPerfect - } - - f.BalanceAfter = s.BalanceAfter + f.ScoreDelta - return f -} - -// Choose 分支闯关:校验 node/option 归属与解锁 → 决策节点返回下一节点; -// 终局节点在锁内结算(事务写进度/积分/计策卡)。路径流水尽力而为,失败不阻断。 -func (s *levelPlay) Choose(ctx context.Context, parentUid, childId, levelId, nodeId, optionId int64) (*Node, *FinalSettle, error) { - child, err := getChildOf(ctx, parentUid, childId) - if err != nil { - return nil, nil, err - } - - levelRec, err := dao.Level.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(levelId).One() - if err != nil { - return nil, nil, err - } - if levelRec.IsEmpty() || levelRec["status"].Int() != consts.StatusEnabled { - return nil, nil, gerror.New("关卡不存在") - } - if levelRec["age_group"].String() != child["age_group"].String() { - return nil, nil, gerror.New("关卡不属于当前年龄段") - } - - nodeRec, err := dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)). - Where("id", nodeId).Where("level_id", levelId).Where("status", consts.StatusEnabled).One() - if err != nil { - return nil, nil, err - } - if nodeRec.IsEmpty() { - return nil, nil, gerror.New("节点不存在") - } - - optionRec, err := dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)). - Where("id", optionId).Where("node_id", nodeId).Where("status", consts.StatusEnabled).One() - if err != nil { - return nil, nil, err - } - if optionRec.IsEmpty() { - return nil, nil, gerror.New("选项不存在") - } - - nextRec, err := dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)). - Where("id", optionRec["next_node_id"].Int64()).Where("level_id", levelId).Where("status", consts.StatusEnabled).One() - if err != nil { - return nil, nil, err - } - if nextRec.IsEmpty() { - return nil, nil, gerror.New("目标节点不存在") - } - - progressRec, err := dao.UserProgress.Model().Ctx(ctx). - Where("child_id", childId).Where("level_id", levelId).One() - if err != nil { - return nil, nil, err - } - if !s.levelUnlocked(ctx, childId, child, levelRec, progressRec) { - return nil, nil, gerror.New("关卡未解锁") - } - - if nextRec["result_type"].Int() == consts.ResultNone { - s.logRoute(ctx, childId, levelId, nodeId, optionId, consts.ResultNone) - node, err := nodeOf(ctx, levelRec, nextRec) - if err != nil { - return nil, nil, err - } - return node, nil, nil - } - - // v2.1:终局节点剧本先于结算构建(只读查询失败则未结算,无副作用) - finalNode, err := nodeOf(ctx, levelRec, nextRec) - if err != nil { - return nil, nil, err - } - // 终局:路径流水在锁内结算后记录,避免状态读到本次到达导致重复结算误判 - settle, err := common.WithLock(ctx, fmt.Sprintf("child:%d:level:%d", childId, levelId), 10*time.Second, 3, 200*time.Millisecond, func() (*FinalSettle, error) { - return s.settle(ctx, child, levelRec, nextRec, nodeId, optionId) - }) - if err != nil { - return nil, nil, err - } - settle.FinalNode = finalNode - return nil, settle, nil -} - -// levelUnlocked 关卡解锁判定:本关已通关可重玩,否则按解锁链校验计策解锁。 -func (s *levelPlay) levelUnlocked(ctx context.Context, childId int64, child gdb.Record, levelRec, progressRec gdb.Record) bool { - if progressRec["stars"].Int() >= 3 { - return true - } - strategyRec, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)). - WherePri(levelRec["strategy_id"].Int64()).One() - if err != nil { - return false - } - unlocked, _ := Strategy.unlockState(ctx, childId, child["age_group"].String(), strategyRec) - return unlocked -} - -// logRoute 路径流水(尽力而为,写失败不阻断主流程)。 -func (s *levelPlay) logRoute(ctx context.Context, childId, levelId, nodeId, optionId int64, resultType int) { - _, _ = dao.UserRouteLog.InsertAndReturnId(ctx, g.Map{ - "child_id": childId, - "level_id": levelId, - "node_id": nodeId, - "option_id": optionId, - "result_type": resultType, - }) -} - -// nodeOf 组装下一决策节点(选项 + 元素)。 -func nodeOf(ctx context.Context, levelRec, nodeRec gdb.Record) (*Node, error) { - optionRecs, err := dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)). - Where("node_id", nodeRec["id"].Int64()).Where("status", consts.StatusEnabled). - Order("sort_order ASC").All() - if err != nil { - return nil, err - } - elements, err := elementsOf(ctx, levelRec, []gdb.Record{nodeRec}, optionRecs) - if err != nil { - return nil, err - } - return buildNode(nodeRec, optionRecs, elements), nil -} - -// settle 终局结算(调用方持锁):锁内重读状态 → 纯函数判分 → 事务写库 → 记录本次终局路径 → 派生结果。 -func (s *levelPlay) settle(ctx context.Context, child gdb.Record, levelRec, finalRec gdb.Record, nodeId, optionId int64) (*FinalSettle, error) { - childId := child["id"].Int64() - levelId := levelRec["id"].Int64() - - state, err := s.settleState(ctx, childId, levelId) - if err != nil { - return nil, err - } - f := SettleFinal(state, finalRec["id"].Int64(), finalRec["result_type"].Int()) - - if f.ScoreDelta != 0 || f.Cleared || f.Perfect { - if err = s.commitSettle(ctx, childId, levelRec, &f); err != nil { - return nil, err - } - } - s.logRoute(ctx, childId, levelId, nodeId, optionId, finalRec["result_type"].Int()) - s.deriveExtras(ctx, child, levelRec, &f) - return &f, nil -} - -// settleState 锁内重读该关历史状态:终局数、已到达终局、连续失败、余额。 -func (s *levelPlay) settleState(ctx context.Context, childId, levelId int64) (SettleState, error) { - state := SettleState{ReachedFinals: map[int64]bool{}} - - progressRec, err := dao.UserProgress.Model().Ctx(ctx). - Where("child_id", childId).Where("level_id", levelId).One() - if err != nil { - return state, err - } - state.LevelCleared = progressRec["stars"].Int() >= 3 - state.PerfectAwarded = progressRec["perfect"].Int() == 1 - - childRec, err := dao.Child.Model().Ctx(ctx).WherePri(childId).One() - if err != nil { - return state, err - } - state.BalanceAfter = childRec["points"].Int() - - nodeRecs, err := dao.SceneNode.Model().Ctx(ctx). - Where("level_id", levelId).Where("status", consts.StatusEnabled).All() - if err != nil { - return state, err - } - for _, n := range nodeRecs { - if n["result_type"].Int() > 0 { - state.TotalFinals++ - } - } - - logs, err := dao.UserRouteLog.Model().Ctx(ctx). - Where("child_id", childId).Where("level_id", levelId). - WhereGT("result_type", 0).Order("id ASC").All() - if err != nil { - return state, err - } - // 日志记录的是选择节点(node_id)+ 选项;终局节点经选项的 next_node_id 解析 - optionIds := make([]int64, 0, len(logs)) - for _, l := range logs { - optionIds = append(optionIds, l["option_id"].Int64()) - } - nextOf := map[int64]int64{} - if len(optionIds) > 0 { - opts, err := dao.NodeOption.Model().Ctx(ctx).WhereIn("id", optionIds).All() - if err != nil { - return state, err - } - for _, o := range opts { - nextOf[o["id"].Int64()] = o["next_node_id"].Int64() - } - } - for _, l := range logs { - if nid, ok := nextOf[l["option_id"].Int64()]; ok { - state.ReachedFinals[nid] = true - } - } - for i := len(logs) - 1; i >= 0; i-- { - if logs[i]["result_type"].Int() == consts.ResultFail { - state.FailStreak++ - } else { - break - } - } - return state, nil -} - -// commitSettle 结算事务:进度(最高星 + 完美标记 + 内容版本)、积分流水与余额、计策卡。 -func (s *levelPlay) commitSettle(ctx context.Context, childId int64, levelRec gdb.Record, f *FinalSettle) error { - levelId := levelRec["id"].Int64() - return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { - progressRec, err := tx.Model(consts.TableUserProgress).Ctx(ctx). - Where("child_id", childId).Where("level_id", levelId).One() - if err != nil { - return err - } - stars := f.Stars - perfect := 0 - if f.Perfect { - perfect = 1 - } - if !progressRec.IsEmpty() { - if stars < progressRec["stars"].Int() { - stars = progressRec["stars"].Int() - } - perfect = progressRec["perfect"].Int() | perfect - } - progressData := g.Map{ - "stars": stars, - "perfect": perfect, - "content_version": levelRec["content_version"].Int(), - "completed_at": gtime.Now(), - } - if progressRec.IsEmpty() { - progressData["child_id"] = childId - progressData["level_id"] = levelId - _, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(progressData).Insert() - } else { - _, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(progressData). - Where("child_id", childId).Where("level_id", levelId).Update() - } - if err != nil { - return err - } - - if f.ScoreDelta != 0 { - childRec, err := tx.Model(consts.TableChild).Ctx(ctx).WherePri(childId).One() - if err != nil { - return err - } - balance := childRec["points"].Int() + f.ScoreDelta - if _, err = tx.Model(consts.TableChild).Ctx(ctx).Data(g.Map{"points": balance}).WherePri(childId).Update(); err != nil { - return err - } - if _, err = tx.Model(consts.TablePointLog).Ctx(ctx).Data(g.Map{ - "user_id": childId, - "change": f.ScoreDelta, - "reason_type": consts.ReasonLevel, - "ref_id": levelId, - "balance_after": balance, - }).Insert(); err != nil { - return err - } - f.BalanceAfter = balance - } - - return s.collectIfAllPerfect(ctx, tx, childId, levelRec, f) - }) -} - -// collectIfAllPerfect 本计全部关卡完美 → 写计策卡(防重复:已存在则跳过)。 -func (s *levelPlay) collectIfAllPerfect(ctx context.Context, tx gdb.TX, childId int64, levelRec gdb.Record, f *FinalSettle) error { - if !f.Perfect { - return nil - } - strategyId := levelRec["strategy_id"].Int64() - levels, err := tx.Model(consts.TableLevel).Ctx(ctx). - Where("strategy_id", strategyId).Where("status", consts.StatusEnabled).All() - if err != nil { - return err - } - for _, lv := range levels { - rec, err := tx.Model(consts.TableUserProgress).Ctx(ctx). - Where("child_id", childId).Where("level_id", lv["id"].Int64()).One() - if err != nil { - return err - } - if rec.IsEmpty() || rec["perfect"].Int() != 1 { - return nil - } - } - exists, err := tx.Model(consts.TableUserCollection).Ctx(ctx). - Where("user_id", childId).Where("strategy_id", strategyId).Count() - if err != nil { - return err - } - if exists > 0 { - return nil - } - if _, err = tx.Model(consts.TableUserCollection).Ctx(ctx).Data(g.Map{ - "user_id": childId, - "strategy_id": strategyId, - }).Insert(); err != nil { - return err - } - f.CollectionUnlocked = true - return nil -} - -// deriveExtras 结算后派生:计策卡解锁提示、下一计解锁、成长等级。 -func (s *levelPlay) deriveExtras(ctx context.Context, child gdb.Record, levelRec gdb.Record, f *FinalSettle) { - childId := child["id"].Int64() - - if f.CollectionUnlocked { - if next, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)). - Where("unlock_before", levelRec["strategy_id"].Int64()).One(); err == nil && !next.IsEmpty() { - f.UnlockNext = true - } - } - - before, err := dao.UserProgress.Model().Ctx(ctx). - Where("child_id", childId).Where("perfect", 1).Count() - if err != nil { - return - } - after := before - if f.Perfect { - after++ - } - oldLevel, _ := LevelOf(before) - newLevel, _ := LevelOf(after) - if oldLevel != newLevel { - f.NewLevel = newLevel - } -} diff --git a/biz/service/level_play_test.go b/biz/service/level_test.go similarity index 100% rename from biz/service/level_play_test.go rename to biz/service/level_test.go diff --git a/biz/service/life_task.go b/biz/service/life_task.go new file mode 100644 index 0000000..67dfedb --- /dev/null +++ b/biz/service/life_task.go @@ -0,0 +1,25 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type lifeTask struct{} + +var LifeTask = &lifeTask{} + +// GetByPk 生活践行任务。 +func (s *lifeTask) GetByPk(ctx context.Context, id int64) (gdb.Record, error) { + return dao.LifeTask.GetOneByPk(ctx, id) +} + +// ListEnabled 启用任务(内容缓存)。 +func (s *lifeTask) ListEnabled(ctx context.Context) ([]gdb.Record, error) { + return dao.LifeTask.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("status", consts.StatusEnabled).Order("id ASC").All() +} diff --git a/biz/service/node_option.go b/biz/service/node_option.go new file mode 100644 index 0000000..c155ce9 --- /dev/null +++ b/biz/service/node_option.go @@ -0,0 +1,39 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type nodeOption struct{} + +var NodeOption = &nodeOption{} + +// GetInNode 节点下启用选项(按选项 id + 节点 id 精确查询)。 +func (s *nodeOption) GetInNode(ctx context.Context, optionId, nodeId int64) (gdb.Record, error) { + return dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("id", optionId).Where("node_id", nodeId).Where("status", consts.StatusEnabled).One() +} + +// ListEnabledByNode 节点下启用选项,按 sort_order 升序(内容缓存)。 +func (s *nodeOption) ListEnabledByNode(ctx context.Context, nodeId int64) ([]gdb.Record, error) { + return dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("node_id", nodeId).Where("status", consts.StatusEnabled). + Order("sort_order ASC").All() +} + +// ListEnabledByNodeIds 批量节点下启用选项,按 sort_order 升序(内容缓存)。 +func (s *nodeOption) ListEnabledByNodeIds(ctx context.Context, nodeIds []int64) ([]gdb.Record, error) { + return dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)). + WhereIn("node_id", nodeIds).Where("status", consts.StatusEnabled). + Order("sort_order ASC").All() +} + +// ListByOptionIds 按选项 id 批量取(终局判定用,不缓存)。 +func (s *nodeOption) ListByOptionIds(ctx context.Context, optionIds []int64) ([]gdb.Record, error) { + return dao.NodeOption.Model().Ctx(ctx).WhereIn("id", optionIds).All() +} diff --git a/biz/service/parent.go b/biz/service/parent.go index a03604e..3a10984 100644 --- a/biz/service/parent.go +++ b/biz/service/parent.go @@ -9,6 +9,7 @@ import ( "36wisdom/biz/consts" "36wisdom/biz/dao" + "36wisdom/common/auth" ) type parent struct{} @@ -38,7 +39,7 @@ func (s *parent) Register(ctx context.Context, phone, password, nickname string) if err != nil { return 0, "", err } - token, err = Auth.GenerateToken(ctx, parentId, consts.RoleParent) + token, err = auth.GenerateToken(auth.Secret(ctx), parentId, consts.RoleParent, consts.AuthExpireSeconds) return parentId, token, err } @@ -55,6 +56,6 @@ func (s *parent) Login(ctx context.Context, phone, password string) (parentId in return 0, "", gerror.New("手机号或密码错误") } parentId = rec["id"].Int64() - token, err = Auth.GenerateToken(ctx, parentId, consts.RoleParent) + token, err = auth.GenerateToken(auth.Secret(ctx), parentId, consts.RoleParent, consts.AuthExpireSeconds) return parentId, token, err } diff --git a/biz/service/prize.go b/biz/service/prize.go new file mode 100644 index 0000000..e2f234a --- /dev/null +++ b/biz/service/prize.go @@ -0,0 +1,25 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type prize struct{} + +var Prize = &prize{} + +// GetByPk 奖品定义。 +func (s *prize) GetByPk(ctx context.Context, id int64) (gdb.Record, error) { + return dao.Prize.GetOneByPk(ctx, id) +} + +// ListEnabled 可兑换奖品(启用态,按 sort_order 升序,内容缓存)。 +func (s *prize) ListEnabled(ctx context.Context) ([]gdb.Record, error) { + return dao.Prize.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("status", consts.StatusEnabled).Order("sort_order ASC").All() +} diff --git a/biz/service/redemption.go b/biz/service/redemption.go new file mode 100644 index 0000000..467455c --- /dev/null +++ b/biz/service/redemption.go @@ -0,0 +1,29 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + + "36wisdom/biz/dao" +) + +type redemption struct{} + +var Redemption = &redemption{} + +// ListByUser 孩子兑换记录(按创建时间倒序)。 +func (s *redemption) ListByUser(ctx context.Context, userId int64) ([]gdb.Record, error) { + return dao.Redemption.Model().Ctx(ctx).Where("user_id", userId).Order("id DESC").All() +} + +// Insert 创建兑换记录。 +func (s *redemption) Insert(ctx context.Context, userId, prizeId int64, pointsCost int) (int64, error) { + return dao.Redemption.InsertAndReturnId(ctx, g.Map{ + "user_id": userId, + "prize_id": prizeId, + "points_cost": pointsCost, + "status": 1, + }) +} diff --git a/biz/service/scene_node.go b/biz/service/scene_node.go new file mode 100644 index 0000000..f162ff9 --- /dev/null +++ b/biz/service/scene_node.go @@ -0,0 +1,33 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type sceneNode struct{} + +var SceneNode = &sceneNode{} + +// GetInLevel 关卡内启用节点(按节点 id + 关卡 id 精确查询)。 +func (s *sceneNode) GetInLevel(ctx context.Context, nodeId, levelId int64) (gdb.Record, error) { + return dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("id", nodeId).Where("level_id", levelId).Where("status", consts.StatusEnabled).One() +} + +// ListEnabledByLevel 关卡内启用节点,按 sort_order 升序(内容缓存)。 +func (s *sceneNode) ListEnabledByLevel(ctx context.Context, levelId int64) ([]gdb.Record, error) { + return dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("level_id", levelId).Where("status", consts.StatusEnabled). + Order("sort_order ASC").All() +} + +// CountFinalsByLevel 关卡内终局节点数(result_type > 0,不缓存)。 +func (s *sceneNode) CountFinalsByLevel(ctx context.Context, levelId int64) (int, error) { + return dao.SceneNode.Model().Ctx(ctx). + Where("level_id", levelId).Where("status", consts.StatusEnabled).WhereGT("result_type", 0).Count() +} diff --git a/biz/service/strategy.go b/biz/service/strategy.go index d674b33..05c1ce1 100644 --- a/biz/service/strategy.go +++ b/biz/service/strategy.go @@ -265,9 +265,7 @@ func (s *strategy) progressOfLevels(ctx context.Context, childId int64, levelIds if len(levelIds) == 0 { return m, nil } - recs, err := dao.UserProgress.Model().Ctx(ctx). - Where("child_id", childId). - WhereIn("level_id", levelIds).All() + recs, err := UserProgress.ListByChildLevelIds(ctx, childId, levelIds) if err != nil { return nil, err } @@ -284,8 +282,7 @@ func (s *strategy) elementNames(ctx context.Context, ids []int64) (map[int64]str if len(ids) == 0 { return m, nil } - recs, err := dao.Element.Model().Ctx(ctx).Cache(contentCache(ctx)). - WhereIn("id", ids).Where("status", consts.StatusEnabled).All() + recs, err := Element.ListEnabledByIds(ctx, ids) if err != nil { return nil, err } diff --git a/biz/service/user_badge.go b/biz/service/user_badge.go new file mode 100644 index 0000000..473ffe1 --- /dev/null +++ b/biz/service/user_badge.go @@ -0,0 +1,27 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + + "36wisdom/biz/dao" +) + +type userBadge struct{} + +var UserBadge = &userBadge{} + +// ListByUser 孩子已获徽章。 +func (s *userBadge) ListByUser(ctx context.Context, userId int64) ([]gdb.Record, error) { + return dao.UserBadge.Model().Ctx(ctx).Where("user_id", userId).Order("id ASC").All() +} + +// Insert 记录孩子获得徽章。 +func (s *userBadge) Insert(ctx context.Context, userId, badgeId int64) (int64, error) { + return dao.UserBadge.InsertAndReturnId(ctx, g.Map{ + "user_id": userId, + "badge_id": badgeId, + }) +} diff --git a/biz/service/user_collection.go b/biz/service/user_collection.go new file mode 100644 index 0000000..18d8f3c --- /dev/null +++ b/biz/service/user_collection.go @@ -0,0 +1,36 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type userCollection struct{} + +var UserCollection = &userCollection{} + +// ExistsInTx 事务内判断孩子是否已收集该计策。 +func (s *userCollection) ExistsInTx(ctx context.Context, tx gdb.TX, userId, strategyId int64) (bool, error) { + n, err := tx.Model(consts.TableUserCollection).Ctx(ctx). + Where("user_id", userId).Where("strategy_id", strategyId).Count() + return n > 0, err +} + +// InsertInTx 事务内写入计策卡收集。 +func (s *userCollection) InsertInTx(ctx context.Context, tx gdb.TX, userId, strategyId int64) error { + _, err := tx.Model(consts.TableUserCollection).Ctx(ctx).Data(g.Map{ + "user_id": userId, + "strategy_id": strategyId, + }).Insert() + return err +} + +// ListByUser 孩子已收集计策列表。 +func (s *userCollection) ListByUser(ctx context.Context, userId int64) ([]gdb.Record, error) { + return dao.UserCollection.Model().Ctx(ctx).Where("user_id", userId).All() +} diff --git a/biz/service/user_progress.go b/biz/service/user_progress.go new file mode 100644 index 0000000..7e17297 --- /dev/null +++ b/biz/service/user_progress.go @@ -0,0 +1,78 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type userProgress struct{} + +var UserProgress = &userProgress{} + +// GetByChildLevel 孩子某关进度(不缓存,随闯关更新)。 +func (s *userProgress) GetByChildLevel(ctx context.Context, childId, levelId int64) (gdb.Record, error) { + return dao.UserProgress.Model().Ctx(ctx). + Where("child_id", childId).Where("level_id", levelId).One() +} + +// ListByChildLevelIds 孩子多关进度(不缓存)。 +func (s *userProgress) ListByChildLevelIds(ctx context.Context, childId int64, levelIds []int64) ([]gdb.Record, error) { + return dao.UserProgress.Model().Ctx(ctx). + Where("child_id", childId). + WhereIn("level_id", levelIds).All() +} + +// CountPerfectByChild 孩子完美通关关卡数。 +func (s *userProgress) CountPerfectByChild(ctx context.Context, childId int64) (int, error) { + return dao.UserProgress.Model().Ctx(ctx). + Where("child_id", childId).Where("perfect", 1).Count() +} + +// CountPerfectByChildIds 批量孩子的完美通关数(按 child_id 分组)。 +func (s *userProgress) CountPerfectByChildIds(ctx context.Context, childIds []int64) ([]gdb.Record, error) { + return dao.UserProgress.Model().Ctx(ctx). + Fields("child_id", "COUNT(*) AS cnt"). + Where("perfect", 1). + WhereIn("child_id", childIds). + Group("child_id").All() +} + +// GetInTx 事务内读孩子某关进度。 +func (s *userProgress) GetInTx(ctx context.Context, tx gdb.TX, childId, levelId int64) (gdb.Record, error) { + return tx.Model(consts.TableUserProgress).Ctx(ctx). + Where("child_id", childId).Where("level_id", levelId).One() +} + +// UpsertInTx 事务内合并写入进度:星星取历史最高、完美取并集;不存在则插入。 +func (s *userProgress) UpsertInTx(ctx context.Context, tx gdb.TX, childId, levelId int64, stars, perfect, contentVersion int) error { + rec, err := tx.Model(consts.TableUserProgress).Ctx(ctx). + Where("child_id", childId).Where("level_id", levelId).One() + if err != nil { + return err + } + if stars < rec["stars"].Int() { + stars = rec["stars"].Int() + } + perfect = rec["perfect"].Int() | perfect + data := g.Map{ + "stars": stars, + "perfect": perfect, + "content_version": contentVersion, + "completed_at": gtime.Now(), + } + if rec.IsEmpty() { + data["child_id"] = childId + data["level_id"] = levelId + _, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(data).Insert() + } else { + _, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(data). + Where("child_id", childId).Where("level_id", levelId).Update() + } + return err +} diff --git a/biz/service/user_route_log.go b/biz/service/user_route_log.go new file mode 100644 index 0000000..4784cd3 --- /dev/null +++ b/biz/service/user_route_log.go @@ -0,0 +1,33 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + + "36wisdom/biz/dao" +) + +type userRouteLog struct{} + +var UserRouteLog = &userRouteLog{} + +// Append 追加一条决策路径流水。 +func (s *userRouteLog) Append(ctx context.Context, childId, levelId, nodeId, optionId int64, resultType int) error { + _, err := dao.UserRouteLog.InsertAndReturnId(ctx, g.Map{ + "child_id": childId, + "level_id": levelId, + "node_id": nodeId, + "option_id": optionId, + "result_type": resultType, + }) + return err +} + +// ListFinalsByChildLevel 孩子某关的终局路径(result_type > 0,按 id 升序,不缓存)。 +func (s *userRouteLog) ListFinalsByChildLevel(ctx context.Context, childId, levelId int64) ([]gdb.Record, error) { + return dao.UserRouteLog.Model().Ctx(ctx). + Where("child_id", childId).Where("level_id", levelId). + WhereGT("result_type", 0).Order("id ASC").All() +} diff --git a/common/auth/auth.go b/common/auth/auth.go index 77c2723..e007123 100644 --- a/common/auth/auth.go +++ b/common/auth/auth.go @@ -31,6 +31,11 @@ func GenerateToken(secret string, uid int64, role string, expireSeconds int) (st return claims.SignedString([]byte(secret)) } +// Secret 鉴权密钥(config.yml auth.secret),token 签发与解析共用入口。 +func Secret(ctx context.Context) string { + return g.Cfg().MustGet(ctx, "auth.secret").String() +} + // ParseToken 校验并解析 token,返回 uid 与 role。 func ParseToken(secret, tokenString string) (uid int64, role string, err error) { token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) { diff --git a/common/middleware.go b/common/middleware.go new file mode 100644 index 0000000..cea6a40 --- /dev/null +++ b/common/middleware.go @@ -0,0 +1,19 @@ +package common + +import ( + "net/http" + "strings" + + "github.com/gogf/gf/v2/net/ghttp" +) + +// MiddlewareNoiseSilence 静默浏览器/DevTools 噪音探测路径(favicon、well-known),避免访问日志刷 404 +func MiddlewareNoiseSilence(r *ghttp.Request) { + p := r.URL.Path + if p == "/favicon.ico" || strings.HasPrefix(p, "/.well-known/") { + r.Response.WriteHeader(http.StatusNoContent) + r.Exit() + return + } + r.Middleware.Next() +} diff --git a/main.go b/main.go index b21d59f..ffe9b44 100644 --- a/main.go +++ b/main.go @@ -7,11 +7,14 @@ import ( _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gctx" "36wisdom/biz/controller" "36wisdom/biz/dao" "36wisdom/biz/service/seed" + "36wisdom/common" + "36wisdom/common/auth" ) func main() { @@ -21,11 +24,28 @@ func main() { g.Log().Fatal(ctx, err) } } - dao.InitTables(ctx) + initTables(ctx) seed.EnsureSeeded(ctx) s := g.Server() - controller.Register(s) + // 路由注册与中间件装配(静态,不属业务表分层) + s.Use(common.MiddlewareNoiseSilence) + s.Use(ghttp.MiddlewareHandlerResponse) + s.BindHandler("GET:/ping", func(r *ghttp.Request) { + r.Response.Write("pong") + }) + + secret := g.Cfg().MustGet(ctx, "auth.secret").String() + + // 前台:公开接口(注册/登录),鉴权接口按模块挂载 + s.Group("/api", func(g *ghttp.RouterGroup) { + g.Bind(controller.Parent) + + g.Group("/", func(g *ghttp.RouterGroup) { + g.Middleware(auth.Middleware(secret, "")) + g.Bind(controller.Child, controller.Strategy, controller.Level) + }) + }) // 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口) // 注:根路径须用 SetServerRoot——AddStaticPath("/", ...) 因前缀防误匹配守卫只服务根路径, // /assets 等子路径全部 404;素材目录 ui-src/static 以 /static 前缀单独挂载 @@ -37,3 +57,22 @@ func main() { } s.Run() } + +// initTables 建表与死表清理(各 dao 的 Init 幂等;life_task_log 无读写引用整套移除, +// 库内残留表启动时 DROP 与代码保持一致)。 +func initTables(ctx context.Context) { + inits := []func(ctx context.Context) error{ + dao.Parent.Init, dao.Child.Init, dao.Strategy.Init, dao.Level.Init, dao.SceneNode.Init, + dao.NodeOption.Init, dao.Element.Init, dao.UserProgress.Init, dao.ChapterReview.Init, + dao.UserRouteLog.Init, dao.UserCollection.Init, dao.Badge.Init, dao.UserBadge.Init, + dao.Prize.Init, dao.Redemption.Init, dao.AdminUser.Init, dao.LifeTask.Init, + } + for _, init := range inits { + if err := init(ctx); err != nil { + g.Log().Fatal(ctx, err) + } + } + if _, err := g.DB().Exec(ctx, "DROP TABLE IF EXISTS life_task_log"); err != nil { + g.Log().Fatal(ctx, err) + } +} diff --git a/ui-src/static/generated/char/char_2.png b/ui-src/static/generated/char/char_2.png new file mode 100644 index 0000000000000000000000000000000000000000..4d5791d590dc3de36028ee4373d2d1bdb1b5e484 GIT binary patch literal 37499 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 literal 0 HcmV?d00001 diff --git a/ui-src/static/generated/prop/prop_3.png b/ui-src/static/generated/prop/prop_3.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d995956f4b52a24ac174b182dbbae00e2cc685 GIT binary patch literal 28208 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 literal 0 HcmV?d00001 diff --git a/ui-src/static/generated/strategy/strategy_1.png b/ui-src/static/generated/strategy/strategy_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b2e75a3c2eb464859f0500664e8c0bc61b0c2620 GIT binary patch literal 51204 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 literal 0 HcmV?d00001 diff --git a/技术设计.md b/技术设计.md index 3c70804..f8165f6 100644 --- a/技术设计.md +++ b/技术设计.md @@ -246,9 +246,9 @@ CREATE TABLE user_badge ( ); ``` -### life_task / life_task_log(生活践行任务) +### life_task(生活践行任务) -游戏内践行(情境闯关)之外的真实生活践行:每章完美后任务下发,家长引导孩子实践,家长确认为权威入口(天然防刷)。 +游戏内践行(情境闯关)之外的真实生活践行:每章完美后任务下发,家长引导孩子实践,家长确认为权威入口(天然防刷)。孩子 × 任务的执行状态记录(原 life_task_log)无读写引用,已连同建表与库内数据整套移除(启动时 DROP),M2 实现时按孩子 × 任务唯一约束重建。 ```sql CREATE TABLE life_task ( @@ -261,21 +261,12 @@ CREATE TABLE life_task ( status INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX idx_life_task_strategy ON life_task(strategy_id); - -CREATE TABLE life_task_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - child_id INTEGER NOT NULL, - task_id INTEGER NOT NULL, - status INTEGER NOT NULL DEFAULT 1, -- 1 已下发 2 待家长确认 3 已确认 - confirm_photo TEXT, -- 家长确认时上传的照片(可选) - confirmed_at DATETIME, - created_at DATETIME, - UNIQUE(child_id, task_id) -); ``` ### 积分与奖品 +point_log 为 child 的附属流水表,建表由主表 child 的 dao 托管(豁免规则见 CLAUDE.md「分层文件对齐与代码模式」)。 + ```sql CREATE TABLE point_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -330,7 +321,7 @@ CREATE TABLE admin_user ( ### 4.1 种子数据初始化 - 种子 JSON 用 `go:embed` 打进二进制(`biz/service/seed/`),含:元素库(场景约 15 / 人物约 10 / 道具约 20)、36 计全部内容、每计 1-3 个现代情境关卡(决策树:节点 + 分支选项 + 元素关联)、初始奖品、初始徽章、默认管理员账号 -- 启动时在 dao 层(各表 `init()` 内已有 `CREATE TABLE IF NOT EXISTS`)之后,service 层执行 `seed.EnsureSeeded()`:查 `strategy` 空表则按序插入,插入包事务;随后对全部内容调用标注服务生成拼音(见 4.6),TTS 音频异步生成,均只执行一次 +- 启动时先建表(main.go 依次调各 dao 的 `Init`,`CREATE TABLE IF NOT EXISTS` 幂等),随后执行 `seed.EnsureSeeded()`:查 `strategy` 空表则按序插入,插入包事务;随后对全部内容调用标注服务生成拼音(见 4.6),TTS 音频异步生成,均只执行一次 - 后台运营迭代 = 后台改内容 + 可选导出种子;不覆盖用户数据表 ### 4.2 年龄段分级 @@ -491,7 +482,7 @@ config JSON 结构(`scene_node.config`,前端解析渲染):`{"kind":"pro ### 4.12 践行与成长体系 -**生活践行任务**(知行合一):章完美后任务下发(life_task 按 strategy 关联)→ 孩子端显示"和爸爸妈妈一起做"卡片 → 家长查看引导语、带孩子实践 → 家长确认(POST /api/task/confirm,可选照片)→ 积分到账 + 状态流转(1 已下发 → 2 待家长确认 → 3 已确认)。防刷:UNIQUE(child_id, task_id) + 家长确认为唯一权威入口。 +**生活践行任务**(知行合一):章完美后任务下发(life_task 按 strategy 关联)→ 孩子端显示"和爸爸妈妈一起做"卡片 → 家长查看引导语、带孩子实践 → 家长确认(POST /api/task/confirm,可选照片)→ 积分到账 + 状态流转(1 已下发 → 2 待家长确认 → 3 已确认)。防刷:家长确认为唯一权威入口,M2 实现时按 child × task 唯一约束建执行记录表(原 life_task_log 无读写引用,已整套移除)。 **成长等级**:按完美关卡数映射称号(规则表在 biz/consts,如小学徒 → 小书童 → 小军师 → 军师 → 大将军),**派生值不落库**——结算接口计算并返回新等级,前端播升级动画;家长报告展示当前称号。 @@ -547,6 +538,13 @@ config JSON 结构(`scene_node.config`,前端解析渲染):`{"kind":"pro - 幂等:仅处理 `script` 为空的节点;草稿已存在跳过(防重跑覆盖人工精修,`--force` 覆盖);`--strategy` 限定计策分批执行 - 试点:第 1 计 8 节点已导入精修,全量 36 计剧本由主会话按计分批生成 +### 4.15 分层文件对齐(沿革) + +对齐规则(每表一组文件、豁免与归属、非表文件处置)见 CLAUDE.md「分层文件对齐与代码模式」,本节只记沿革: + +- **13 张表补全**:M1 时仅 5 个模块(parent/child/strategy/level/level_play)有完整分层,其余 15 张只有 entity/dao;一次补齐为每表一组文件,6 张在用表的跨表 DAO 直调收拢到各自 service;补齐的表先只建门面结构,不挂 g.Meta 不注册路由(路由静态,按需暴露) +- **收口(2026-08-14)**:level_play(闯关流程)并入 level 表文件、表初始化列表并入 main.go、service/auth.go 并入 common/auth——各层严格 = 17 表文件(18 表 − point_log 豁免) + ## 5. 开发计划 | 里程碑 | 内容 | 验收标准 |