323 lines
9.1 KiB
Go
323 lines
9.1 KiB
Go
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
|
|
MeaningPinyin string
|
|
GroupName string
|
|
TeachContent string
|
|
TeachContentPinyin 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(),
|
|
MeaningPinyin: rec["meaning_pinyin"].String(),
|
|
GroupName: rec["group_name"].String(),
|
|
TeachContent: rec["teach_content"].String(),
|
|
TeachContentPinyin: rec["teach_content_pinyin"].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("group_no ASC, 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 := UserProgress.ListByChildLevelIds(ctx, childId, levelIds)
|
|
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 := Element.ListEnabledByIds(ctx, ids)
|
|
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
|
|
}
|