From d2887bf8a37de1b7e385362f37e51989b9d6cc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=96=8C?= <259278618@qq.com> Date: Thu, 13 Aug 2026 12:26:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=86=85=E5=AE=B9=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=88=E8=AE=A1=E7=AD=96/=E5=85=B3=E5=8D=A1/=E5=85=83?= =?UTF-8?q?=E7=B4=A0=EF=BC=8C=E5=90=AB=E7=BC=93=E5=AD=98=E4=B8=8E=E8=A7=A3?= =?UTF-8?q?=E9=94=81=E5=88=A4=E5=AE=9A=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 计策列表/详情、关卡详情(决策树内存组装),内容查询走缓存, 按孩子年龄段过滤关卡,解锁按前置计全部关卡完美判定。 Co-Authored-By: Claude Opus 4.7 --- biz/controller/level.go | 71 ++++++++ biz/controller/router.go | 2 +- biz/controller/strategy.go | 73 +++++++++ biz/model/dto/level.go | 51 ++++++ biz/model/dto/strategy.go | 60 +++++++ biz/service/child.go | 13 ++ biz/service/level.go | 202 +++++++++++++++++++++++ biz/service/strategy.go | 321 +++++++++++++++++++++++++++++++++++++ 8 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 biz/controller/level.go create mode 100644 biz/controller/strategy.go create mode 100644 biz/model/dto/level.go create mode 100644 biz/model/dto/strategy.go create mode 100644 biz/service/strategy.go diff --git a/biz/controller/level.go b/biz/controller/level.go new file mode 100644 index 0000000..16e85b2 --- /dev/null +++ b/biz/controller/level.go @@ -0,0 +1,71 @@ +package controller + +import ( + "context" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" + "36wisdom/common/auth" +) + +type level struct{} + +var Level = &level{} + +func (c *level) Detail(ctx context.Context, req *dto.LevelDetailReq) (*dto.LevelDetailRes, error) { + detail, err := service.Level.Detail(ctx, auth.GetUid(ctx), req.ChildId, req.LevelId) + if err != nil { + return nil, err + } + res := &dto.LevelDetailRes{ + LevelId: detail.LevelId, + Title: detail.Title, + Scene: elementVO(detail.Scene), + SceneContent: detail.SceneContent, + SceneImage: detail.SceneImage, + SceneAudio: detail.SceneAudio, + Entry: nodeVO(detail.Entry), + TotalFinals: detail.TotalFinals, + Perfect: detail.Perfect, + Stars: detail.Stars, + } + return res, nil +} + +func elementVO(e *service.Element) *dto.ElementVO { + if e == nil { + return nil + } + return &dto.ElementVO{ + Id: e.Id, + EType: e.EType, + Name: e.Name, + Image: e.Image, + Audio: e.Audio, + Description: e.Description, + } +} + +func nodeVO(n *service.Node) dto.NodeVO { + vo := dto.NodeVO{ + NodeId: n.NodeId, + Title: n.Title, + Content: n.Content, + Image: n.Image, + Audio: n.Audio, + Character: elementVO(n.Character), + InteractionType: n.InteractionType, + Config: n.Config, + NodeType: n.NodeType, + ResultType: n.ResultType, + Options: make([]dto.OptionVO, 0, len(n.Options)), + } + for _, o := range n.Options { + vo.Options = append(vo.Options, dto.OptionVO{ + OptionId: o.OptionId, + Text: o.Text, + Prop: elementVO(o.Prop), + }) + } + return vo +} diff --git a/biz/controller/router.go b/biz/controller/router.go index c910b01..0e20127 100644 --- a/biz/controller/router.go +++ b/biz/controller/router.go @@ -24,7 +24,7 @@ func Register(s *ghttp.Server) { g.Group("/", func(g *ghttp.RouterGroup) { g.Middleware(auth.Middleware(secret, "")) - g.Bind(Child) + g.Bind(Child, Strategy, Level) }) }) } diff --git a/biz/controller/strategy.go b/biz/controller/strategy.go new file mode 100644 index 0000000..0a1a498 --- /dev/null +++ b/biz/controller/strategy.go @@ -0,0 +1,73 @@ +package controller + +import ( + "context" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" + "36wisdom/common/auth" +) + +type strategy struct{} + +var Strategy = &strategy{} + +func (c *strategy) List(ctx context.Context, req *dto.StrategyListReq) (*dto.StrategyListRes, error) { + items, err := service.Strategy.List(ctx, auth.GetUid(ctx), req.ChildId) + if err != nil { + return nil, err + } + res := &dto.StrategyListRes{List: make([]dto.StrategyItem, 0, len(items))} + for _, it := range items { + res.List = append(res.List, dto.StrategyItem{ + StrategyId: it.StrategyId, + Name: it.Name, + Pinyin: it.Pinyin, + GroupNo: it.GroupNo, + GroupName: it.GroupName, + Meaning: it.Meaning, + Icon: it.Icon, + SortOrder: it.SortOrder, + Stars: it.Stars, + PerfectCount: it.PerfectCount, + TotalLevels: it.TotalLevels, + Unlocked: it.Unlocked, + UnlockReason: it.UnlockReason, + }) + } + return res, nil +} + +func (c *strategy) Detail(ctx context.Context, req *dto.StrategyDetailReq) (*dto.StrategyDetailRes, error) { + detail, err := service.Strategy.Detail(ctx, auth.GetUid(ctx), req.ChildId, req.StrategyId) + if err != nil { + return nil, err + } + res := &dto.StrategyDetailRes{ + StrategyId: detail.StrategyId, + Name: detail.Name, + Pinyin: detail.Pinyin, + Meaning: detail.Meaning, + GroupName: detail.GroupName, + TeachContent: detail.TeachContent, + TeachImage: detail.TeachImage, + TeachAudio: detail.TeachAudio, + SummaryQ: detail.SummaryQ, + SummaryOptions: detail.SummaryOptions, + Levels: make([]dto.LevelBrief, 0, len(detail.Levels)), + } + for _, lv := range detail.Levels { + res.Levels = append(res.Levels, dto.LevelBrief{ + LevelId: lv.LevelId, + Title: lv.Title, + AgeGroup: lv.AgeGroup, + SceneName: lv.SceneName, + Stars: lv.Stars, + Perfect: lv.Perfect, + Unlocked: lv.Unlocked, + ContentVersion: lv.ContentVersion, + ProgressVersion: lv.ProgressVersion, + }) + } + return res, nil +} diff --git a/biz/model/dto/level.go b/biz/model/dto/level.go new file mode 100644 index 0000000..777c4a5 --- /dev/null +++ b/biz/model/dto/level.go @@ -0,0 +1,51 @@ +package dto + +import "github.com/gogf/gf/v2/frame/g" + +type LevelDetailReq struct { + g.Meta `path:"/level/detail" method:"get" summary:"关卡详情"` + LevelId int64 `v:"required" json:"level_id"` + ChildId int64 `v:"required" json:"child_id"` +} + +type ElementVO 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"` +} + +type OptionVO struct { + OptionId int64 `json:"option_id"` + Text string `json:"text"` + Prop *ElementVO `json:"prop"` +} + +type NodeVO struct { + NodeId int64 `json:"node_id"` + Title string `json:"title"` + Content string `json:"content"` + Image string `json:"image"` + Audio string `json:"audio"` + Character *ElementVO `json:"character"` + InteractionType int `json:"interaction_type"` + Config string `json:"config"` + NodeType int `json:"node_type"` + ResultType int `json:"result_type"` + Options []OptionVO `json:"options"` +} + +type LevelDetailRes struct { + LevelId int64 `json:"level_id"` + Title string `json:"title"` + Scene *ElementVO `json:"scene"` + SceneContent string `json:"scene_content"` + SceneImage string `json:"scene_image"` + SceneAudio string `json:"scene_audio"` + Entry NodeVO `json:"entry"` + TotalFinals int `json:"total_finals"` + Perfect bool `json:"perfect"` + Stars int `json:"stars"` +} diff --git a/biz/model/dto/strategy.go b/biz/model/dto/strategy.go new file mode 100644 index 0000000..b11bd57 --- /dev/null +++ b/biz/model/dto/strategy.go @@ -0,0 +1,60 @@ +package dto + +import "github.com/gogf/gf/v2/frame/g" + +type StrategyListReq struct { + g.Meta `path:"/strategy/list" method:"get" summary:"计策列表"` + ChildId int64 `v:"required" json:"child_id"` +} + +type StrategyItem struct { + StrategyId int64 `json:"strategy_id"` + Name string `json:"name"` + Pinyin string `json:"pinyin"` + GroupNo int `json:"group_no"` + GroupName string `json:"group_name"` + Meaning string `json:"meaning"` + Icon string `json:"icon"` + SortOrder int `json:"sort_order"` + Stars int `json:"stars"` + PerfectCount int `json:"perfect_count"` + TotalLevels int `json:"total_levels"` + Unlocked bool `json:"unlocked"` + UnlockReason string `json:"unlock_reason"` +} + +type StrategyListRes struct { + List []StrategyItem `json:"list"` +} + +type StrategyDetailReq struct { + g.Meta `path:"/strategy/detail" method:"get" summary:"计策详情"` + StrategyId int64 `v:"required" json:"strategy_id"` + ChildId int64 `v:"required" json:"child_id"` +} + +type LevelBrief struct { + LevelId int64 `json:"level_id"` + Title string `json:"title"` + AgeGroup string `json:"age_group"` + SceneName string `json:"scene_name"` + Stars int `json:"stars"` + Perfect bool `json:"perfect"` + Unlocked bool `json:"unlocked"` + ContentVersion int `json:"content_version"` + ProgressVersion int `json:"progress_version"` +} + +type StrategyDetailRes struct { + StrategyId int64 `json:"strategy_id"` + Name string `json:"name"` + Pinyin string `json:"pinyin"` + Meaning string `json:"meaning"` + GroupName string `json:"group_name"` + TeachContent string `json:"teach_content"` + TeachImage string `json:"teach_image"` + TeachAudio string `json:"teach_audio"` + SummaryQ string `json:"summary_q"` + SummaryOptions string `json:"summary_options"` + Levels []LevelBrief `json:"levels"` +} diff --git a/biz/service/child.go b/biz/service/child.go index 5763cd6..11b5cf9 100644 --- a/biz/service/child.go +++ b/biz/service/child.go @@ -3,6 +3,7 @@ package service import ( "context" + "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" @@ -14,6 +15,18 @@ type child struct{} var Child = &child{} +// getChildOf 校验孩子归属当前家长并返回档案记录。 +func getChildOf(ctx context.Context, parentUid, childId int64) (gdb.Record, error) { + rec, err := dao.Child.Model().Ctx(ctx).WherePri(childId).One() + if err != nil { + return nil, err + } + if rec.IsEmpty() || rec["parent_id"].Int64() != parentUid { + return nil, gerror.New("孩子档案不存在") + } + return rec, nil +} + // ChildItem 孩子列表项(含派生值:完美关卡数)。 type ChildItem struct { ChildId int64 diff --git a/biz/service/level.go b/biz/service/level.go index 7e1df0a..81f2e69 100644 --- a/biz/service/level.go +++ b/biz/service/level.go @@ -1,5 +1,19 @@ package service +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type level struct{} + +var Level = &level{} + // 成长等级:按完美关卡数映射称号(技术设计.md 4.10,派生值不落库)。 var levelTitles = []struct { MinPerfect int @@ -24,3 +38,191 @@ func LevelOf(perfectCount int) (level int, title string) { } return } + +// Element 元素 VO(场景/人物/道具)。 +type Element struct { + Id int64 + EType int + Name string + Image string + Audio string + Description string +} + +// Option 分支选项 VO。 +type Option struct { + OptionId int64 + Text string + Prop *Element +} + +// Node 关卡节点 VO(决策/终局)。 +type Node struct { + NodeId int64 + Title string + Content string + Image string + Audio string + Character *Element + InteractionType int + Config string + NodeType int + ResultType int + Options []*Option +} + +// LevelDetail 关卡详情:场景 + 入口节点决策树 + 用户进度。 +type LevelDetail struct { + LevelId int64 + Title string + Scene *Element + SceneContent string + SceneImage string + SceneAudio string + Entry *Node + TotalFinals int + Perfect bool + Stars int +} + +// Detail 关卡详情:节点/选项/元素分表取回,内存组装决策树(入口节点)。 +func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) (*LevelDetail, error) { + child, err := getChildOf(ctx, parentUid, childId) + if err != nil { + return nil, err + } + + levelRec, err := dao.Level.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(levelId).One() + if err != nil { + return nil, err + } + if levelRec.IsEmpty() || levelRec["status"].Int() != consts.StatusEnabled { + return nil, gerror.New("关卡不存在") + } + if levelRec["age_group"].String() != child["age_group"].String() { + 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() + if err != nil { + return nil, err + } + nodeIds := make([]int64, 0, len(nodeRecs)) + for _, n := range nodeRecs { + nodeIds = append(nodeIds, n["id"].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() + if err != nil { + return nil, err + } + } + + elements, err := s.elementsOf(ctx, levelRec, nodeRecs, optionRecs) + if err != nil { + return nil, err + } + + progressRec, err := dao.UserProgress.Model().Ctx(ctx). + Where("child_id", childId).Where("level_id", levelId).One() + if err != nil { + return nil, err + } + + var entryRec gdb.Record + totalFinals := 0 + for _, n := range nodeRecs { + if n["is_entry"].Int() == 1 { + entryRec = n + } + if n["result_type"].Int() > 0 { + totalFinals++ + } + } + if entryRec.IsEmpty() { + return nil, gerror.New("关卡入口缺失") + } + + return &LevelDetail{ + LevelId: levelRec["id"].Int64(), + Title: levelRec["title"].String(), + Scene: elements[levelRec["scene_id"].Int64()], + SceneContent: levelRec["scene_content"].String(), + SceneImage: levelRec["scene_image"].String(), + SceneAudio: levelRec["scene_audio"].String(), + Entry: buildNode(entryRec, optionRecs, elements), + TotalFinals: totalFinals, + Perfect: progressRec["perfect"].Int() == 1, + Stars: progressRec["stars"].Int(), + }, nil +} + +// elementsOf 汇总关卡涉及的场景/人物/道具元素,批量查询后按 id 索引。 +func (s *level) elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs []gdb.Record) (map[int64]*Element, error) { + ids := []int64{levelRec["scene_id"].Int64()} + for _, n := range nodeRecs { + if cid := n["character_id"].Int64(); cid > 0 { + ids = append(ids, cid) + } + } + for _, o := range optionRecs { + if pid := o["prop_id"].Int64(); pid > 0 { + ids = append(ids, pid) + } + } + ids = uniqueInt64(ids) + + m := make(map[int64]*Element, 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() + if err != nil { + return nil, err + } + for _, r := range recs { + m[r["id"].Int64()] = &Element{ + 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(), + } + } + return m, nil +} + +// buildNode 组装单个节点的选项与人物/道具元素。 +func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]*Element) *Node { + node := &Node{ + NodeId: nodeRec["id"].Int64(), + Title: nodeRec["title"].String(), + Content: nodeRec["content"].String(), + Image: nodeRec["image"].String(), + Audio: nodeRec["audio"].String(), + Character: elements[nodeRec["character_id"].Int64()], + InteractionType: nodeRec["interaction_type"].Int(), + Config: nodeRec["config"].String(), + NodeType: nodeRec["node_type"].Int(), + ResultType: nodeRec["result_type"].Int(), + } + for _, o := range optionRecs { + if o["node_id"].Int64() != node.NodeId { + continue + } + node.Options = append(node.Options, &Option{ + OptionId: o["id"].Int64(), + Text: o["text"].String(), + Prop: elements[o["prop_id"].Int64()], + }) + } + return node +} diff --git a/biz/service/strategy.go b/biz/service/strategy.go new file mode 100644 index 0000000..7e86cea --- /dev/null +++ b/biz/service/strategy.go @@ -0,0 +1,321 @@ +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" +) + +type strategy struct{} + +var Strategy = &strategy{} + +// StrategyItem 计策列表项:内容 + 本用户进度 + 解锁状态。 +type StrategyItem struct { + StrategyId int64 + Name string + Pinyin string + GroupNo int + GroupName string + Meaning string + Icon string + SortOrder int + Stars int + PerfectCount int + TotalLevels int + Unlocked bool + UnlockReason string +} + +// StrategyDetail 计策详情:内容 + 关卡列表(含进度)。 +type StrategyDetail struct { + StrategyId int64 + Name string + Pinyin string + Meaning string + GroupName string + TeachContent string + TeachImage string + TeachAudio string + SummaryQ string + SummaryOptions string + Levels []*LevelBrief +} + +// LevelBrief 关卡概要:进度 + 解锁 + 内容版本(客户端据此判断可重新挑战)。 +type LevelBrief struct { + LevelId int64 + Title string + AgeGroup string + SceneName string + Stars int + Perfect bool + Unlocked bool + ContentVersion int + ProgressVersion int +} + +// contentCache 内容查询缓存选项:TTL 来自配置 database.cache.ttl。 +// 内容仅在后台维护,M1 无写操作;后续后台变更后须清对应缓存。 +func contentCache(ctx context.Context) gdb.CacheOption { + ttl := g.Cfg().MustGet(ctx, "database.cache.ttl").Int() + if ttl <= 0 { + ttl = 300 + } + return gdb.CacheOption{Duration: time.Duration(ttl) * time.Second} +} + +// List 计策列表:内容带缓存,进度不带;解锁按前置计全部关卡完美判定。 +func (s *strategy) List(ctx context.Context, parentUid, childId int64) ([]*StrategyItem, error) { + child, err := getChildOf(ctx, parentUid, childId) + if err != nil { + return nil, err + } + childAge := child["age_group"].String() + + strategies, err := s.listAll(ctx) + if err != nil { + return nil, err + } + + strategyIds := make([]int64, 0, len(strategies)) + for _, r := range strategies { + strategyIds = append(strategyIds, r["id"].Int64()) + } + levels, err := s.listLevels(ctx, strategyIds, childAge) + if err != nil { + return nil, err + } + progress, err := s.progressOfLevels(ctx, childId, levelIdsOf(levels)) + if err != nil { + return nil, err + } + + // 按计策分组统计,同时维护解锁依赖(线性链:unlock_before 指向前置计)。 + stats := make(map[int64]*StrategyItem, len(strategies)) + for _, r := range strategies { + it := &StrategyItem{ + StrategyId: r["id"].Int64(), + Name: r["name"].String(), + Pinyin: r["pinyin"].String(), + GroupNo: r["group_no"].Int(), + GroupName: r["group_name"].String(), + Meaning: r["meaning"].String(), + Icon: r["icon"].String(), + SortOrder: r["sort_order"].Int(), + } + stats[it.StrategyId] = it + } + for _, lv := range levels { + st := stats[lv["strategy_id"].Int64()] + st.TotalLevels++ + p := progress[lv["id"].Int64()] + st.Stars += p["stars"].Int() + if p["perfect"].Int() == 1 { + st.PerfectCount++ + } + } + + nameOf := make(map[int64]string, len(strategies)) + for _, r := range strategies { + nameOf[r["id"].Int64()] = r["name"].String() + } + for _, r := range strategies { + it := stats[r["id"].Int64()] + if prevId := r["unlock_before"].Int64(); prevId == 0 { + it.Unlocked = true + } else if prev, ok := stats[prevId]; ok && prev.PerfectCount == prev.TotalLevels && prev.TotalLevels > 0 { + it.Unlocked = true + } else { + it.UnlockReason = fmt.Sprintf("完成《%s》全部关卡解锁", nameOf[prevId]) + } + } + + items := make([]*StrategyItem, 0, len(strategies)) + for _, r := range strategies { + items = append(items, stats[r["id"].Int64()]) + } + return items, nil +} + +// Detail 计策详情:含关卡列表与解锁状态。 +func (s *strategy) Detail(ctx context.Context, parentUid, childId, strategyId int64) (*StrategyDetail, error) { + child, err := getChildOf(ctx, parentUid, childId) + if err != nil { + return nil, err + } + childAge := child["age_group"].String() + + rec, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(strategyId).One() + if err != nil { + return nil, err + } + if rec.IsEmpty() || rec["status"].Int() != consts.StatusEnabled { + return nil, gerror.New("计策不存在") + } + + levels, err := s.listLevels(ctx, []int64{strategyId}, childAge) + if err != nil { + return nil, err + } + progress, err := s.progressOfLevels(ctx, childId, levelIdsOf(levels)) + if err != nil { + return nil, err + } + unlocked, _ := s.unlockState(ctx, childId, childAge, rec) + + sceneNames, err := s.elementNames(ctx, sceneIdsOf(levels)) + if err != nil { + return nil, err + } + + detail := &StrategyDetail{ + StrategyId: rec["id"].Int64(), + Name: rec["name"].String(), + Pinyin: rec["pinyin"].String(), + Meaning: rec["meaning"].String(), + GroupName: rec["group_name"].String(), + TeachContent: rec["teach_content"].String(), + TeachImage: rec["teach_image"].String(), + TeachAudio: rec["teach_audio"].String(), + SummaryQ: rec["summary_q"].String(), + SummaryOptions: rec["summary_options"].String(), + } + for _, lv := range levels { + p := progress[lv["id"].Int64()] + detail.Levels = append(detail.Levels, &LevelBrief{ + LevelId: lv["id"].Int64(), + Title: lv["title"].String(), + AgeGroup: lv["age_group"].String(), + SceneName: sceneNames[lv["scene_id"].Int64()], + Stars: p["stars"].Int(), + Perfect: p["perfect"].Int() == 1, + Unlocked: unlocked, + ContentVersion: lv["content_version"].Int(), + ProgressVersion: p["content_version"].Int(), + }) + } + return detail, nil +} + +// unlockState 计策解锁判定:unlock_before 为空直接解锁,否则前置计全部关卡完美。 +func (s *strategy) unlockState(ctx context.Context, childId int64, childAge string, strategyRec gdb.Record) (bool, string) { + prevId := strategyRec["unlock_before"].Int64() + if prevId == 0 { + return true, "" + } + prevName := "" + if r, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(prevId).One(); err == nil { + prevName = r["name"].String() + } + perfect, total := 0, 0 + if levels, err := s.listLevels(ctx, []int64{prevId}, childAge); err == nil { + total = len(levels) + if progress, err := s.progressOfLevels(ctx, childId, levelIdsOf(levels)); err == nil { + for _, lv := range levels { + if progress[lv["id"].Int64()]["perfect"].Int() == 1 { + perfect++ + } + } + } + } + if total > 0 && perfect == total { + return true, "" + } + return false, fmt.Sprintf("完成《%s》全部关卡解锁", prevName) +} + +// listAll 全部启用计策(内容缓存)。 +func (s *strategy) listAll(ctx context.Context) ([]gdb.Record, error) { + recs, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)). + Where("status", consts.StatusEnabled).Order("sort_order ASC").All() + if err != nil { + return nil, err + } + return recs, nil +} + +// listLevels 指定计策下、匹配年龄段且启用的关卡(内容缓存)。 +func (s *strategy) listLevels(ctx context.Context, strategyIds []int64, ageGroup string) ([]gdb.Record, error) { + recs, err := dao.Level.Model().Ctx(ctx).Cache(contentCache(ctx)). + WhereIn("strategy_id", strategyIds). + Where("status", consts.StatusEnabled). + Where("age_group", ageGroup). + Order("sort_order ASC").All() + if err != nil { + return nil, err + } + return recs, nil +} + +// progressOfLevels 孩子的关卡进度(不缓存,随闯关更新)。 +func (s *strategy) progressOfLevels(ctx context.Context, childId int64, levelIds []int64) (map[int64]gdb.Record, error) { + m := make(map[int64]gdb.Record, len(levelIds)) + if len(levelIds) == 0 { + return m, nil + } + recs, err := dao.UserProgress.Model().Ctx(ctx). + Where("child_id", childId). + WhereIn("level_id", levelIds).All() + if err != nil { + return nil, err + } + for _, r := range recs { + m[r["level_id"].Int64()] = r + } + return m, nil +} + +// elementNames 批量取元素名(内容缓存)。 +func (s *strategy) elementNames(ctx context.Context, ids []int64) (map[int64]string, error) { + m := make(map[int64]string, len(ids)) + ids = uniqueInt64(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() + if err != nil { + return nil, err + } + for _, r := range recs { + m[r["id"].Int64()] = r["name"].String() + } + return m, nil +} + +func levelIdsOf(levels []gdb.Record) []int64 { + ids := make([]int64, 0, len(levels)) + for _, lv := range levels { + ids = append(ids, lv["id"].Int64()) + } + return ids +} + +func sceneIdsOf(levels []gdb.Record) []int64 { + ids := make([]int64, 0, len(levels)) + for _, lv := range levels { + ids = append(ids, lv["scene_id"].Int64()) + } + return ids +} + +func uniqueInt64(in []int64) []int64 { + seen := make(map[int64]struct{}, len(in)) + out := make([]int64, 0, len(in)) + for _, v := range in { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +}