feat: 内容接口(计策/关卡/元素,含缓存与解锁判定)
计策列表/详情、关卡详情(决策树内存组装),内容查询走缓存, 按孩子年龄段过滤关卡,解锁按前置计全部关卡完美判定。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user