1
This commit is contained in:
@@ -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`),写操作后必须清对应缓存
|
||||
|
||||
@@ -103,7 +103,6 @@
|
||||
| user_badge | 孩子已获徽章 |
|
||||
| point_log | 积分流水:变动、原因类型、余额快照 |
|
||||
| life_task | 生活践行任务:关联计策、任务描述、家长引导语、确认奖励积分 |
|
||||
| life_task_log | 生活任务完成记录:孩子 × 任务、家长确认状态 |
|
||||
| prize | 奖品:积分价格、库存、类型(虚拟/实物) |
|
||||
| redemption | 兑换记录:孩子 × 奖品、状态流转、兑换码 |
|
||||
| admin_user | 后台管理员账号 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -20,5 +20,4 @@ const (
|
||||
TableRedemption = "redemption"
|
||||
TableAdminUser = "admin_user"
|
||||
TableLifeTask = "life_task"
|
||||
TableLifeTaskLog = "life_task_log"
|
||||
)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
+31
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+12
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
+374
-19
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -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. 开发计划
|
||||
|
||||
| 里程碑 | 内容 | 验收标准 |
|
||||
|
||||
Reference in New Issue
Block a user