Files
36Wisdom/biz/service/strategy.go
T
adminandClaude Opus 4.7 414c7de007 feat: 分支闯关判分与终局结算(含单测)
choose 校验节点/选项归属与解锁链,终局在 common.WithLock 内按
技术设计 4.3/4.4 结算:首次到达才结算、连续失败限扣、余额不为负、
完美集齐全终局 +20 并解锁计策卡与下一计;路径流水尽力而为。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-13 12:47:01 +08:00

322 lines
9.0 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
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("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 := 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
}