Files
36Wisdom/biz/service/level.go
T
2026-08-14 16:11:10 +08:00

789 lines
23 KiB
Go

package service
import (
"context"
"fmt"
"math"
"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/biz/model/domain"
"36wisdom/biz/model/dto"
"36wisdom/biz/model/entity"
"36wisdom/common"
"36wisdom/common/auth"
)
type level struct{}
var Level = &level{}
// 成长等级:按完美关卡数映射称号(技术设计.md 4.10,派生值不落库)。
var levelTitles = []struct {
MinPerfect int
Title string
}{
{MinPerfect: 0, Title: "小学徒"},
{MinPerfect: 5, Title: "小书童"},
{MinPerfect: 10, Title: "小军师"},
{MinPerfect: 20, Title: "军师"},
{MinPerfect: 30, Title: "大将军"},
}
// LevelOf 按完美关卡数返回等级(1-5)与称号。
func LevelOf(perfectCount int) (level int, title string) {
level = 1
title = levelTitles[0].Title
for i, lt := range levelTitles {
if perfectCount >= lt.MinPerfect {
level = i + 1
title = lt.Title
}
}
return
}
// Detail 关卡详情:节点/选项/元素分表取回,内存组装决策树(入口节点)。
func (s *level) Detail(ctx context.Context, req *dto.LevelDetailReq) (*dto.LevelDetailRes, error) {
child, err := getChildOf(ctx, auth.GetUid(ctx), req.ChildId)
if err != nil {
return nil, err
}
levelRec, err := dao.Level.GetByPkCached(ctx, req.LevelId)
if err != nil {
return nil, err
}
if levelRec == nil || levelRec.Status != consts.StatusEnabled {
return nil, gerror.New("关卡不存在")
}
if levelRec.AgeGroup != child.AgeGroup {
return nil, gerror.New("关卡不属于当前年龄段")
}
nodeRecs, err := dao.SceneNode.ListEnabledByLevelCached(ctx, req.LevelId)
if err != nil {
return nil, err
}
nodeIds := make([]int64, 0, len(nodeRecs))
for _, n := range nodeRecs {
nodeIds = append(nodeIds, n.Id)
}
var optionRecs []*entity.NodeOption
if len(nodeIds) > 0 {
optionRecs, err = dao.NodeOption.ListEnabledByNodeIdsCached(ctx, nodeIds)
if err != nil {
return nil, err
}
}
elements, err := elementsOf(ctx, levelRec, nodeRecs, optionRecs)
if err != nil {
return nil, err
}
progressRec, err := dao.UserProgress.GetByChildLevel(ctx, req.ChildId, req.LevelId)
if err != nil {
return nil, err
}
var entryRec *entity.SceneNode
totalFinals := 0
for _, n := range nodeRecs {
if n.IsEntry == 1 {
entryRec = n
}
if n.ResultType > 0 {
totalFinals++
}
}
if entryRec == nil {
return nil, gerror.New("关卡入口缺失")
}
res := &dto.LevelDetailRes{
LevelId: levelRec.Id,
Title: levelRec.Title,
Scene: elements[levelRec.SceneId],
SceneContent: levelRec.SceneContent,
SceneContentPinyin: levelRec.SceneContentPinyin,
SceneImage: levelRec.SceneImage,
SceneAudio: levelRec.SceneAudio,
Entry: *buildNode(entryRec, optionRecs, elements),
TotalFinals: totalFinals,
}
if progressRec != nil {
res.Perfect = progressRec.Perfect == 1
res.Stars = progressRec.Stars
}
return res, nil
}
// elementsOf 汇总关卡涉及的场景/人物/道具元素,批量查询后按 id 索引。
func elementsOf(ctx context.Context, levelRec *entity.Level, nodeRecs []*entity.SceneNode, optionRecs []*entity.NodeOption) (map[int64]*dto.ElementVO, error) {
ids := []int64{levelRec.SceneId}
for _, n := range nodeRecs {
if cid := n.CharacterId; cid > 0 {
ids = append(ids, cid)
}
}
for _, o := range optionRecs {
if pid := o.PropId; pid > 0 {
ids = append(ids, pid)
}
}
ids = uniqueInt64(ids)
m := make(map[int64]*dto.ElementVO, len(ids))
if len(ids) == 0 {
return m, nil
}
recs, err := dao.Element.ListEnabledByIdsCached(ctx, ids)
if err != nil {
return nil, err
}
for _, r := range recs {
m[r.Id] = &dto.ElementVO{
Id: r.Id,
EType: r.EType,
Name: r.Name,
NamePinyin: r.NamePinyin,
Image: r.Image,
Audio: r.Audio,
Description: r.Description,
}
}
return m, nil
}
// buildNode 组装单个节点的选项与人物/道具元素。
func buildNode(nodeRec *entity.SceneNode, optionRecs []*entity.NodeOption, elements map[int64]*dto.ElementVO) *dto.NodeVO {
node := &dto.NodeVO{
NodeId: nodeRec.Id,
Title: nodeRec.Title,
Content: nodeRec.Content,
ContentPinyin: nodeRec.ContentPinyin,
Image: nodeRec.Image,
Audio: nodeRec.Audio,
Character: elements[nodeRec.CharacterId],
InteractionType: nodeRec.InteractionType,
Config: nodeRec.Config,
Script: nodeRec.Script,
NodeType: nodeRec.NodeType,
ResultType: nodeRec.ResultType,
Options: make([]dto.OptionVO, 0),
}
for _, o := range optionRecs {
if o.NodeId != node.NodeId {
continue
}
node.Options = append(node.Options, dto.OptionVO{
OptionId: o.Id,
Text: o.Text,
TextPinyin: o.TextPinyin,
FeedbackPros: o.FeedbackPros,
FeedbackCons: o.FeedbackCons,
Audio: o.Audio,
Prop: elements[o.PropId],
})
}
return node
}
// SettleFinal 纯函数判分(技术设计 4.3/4.4,可单测):
// - 首次到达某终局才结算,重复到达 delta=0
// - 未通关:最佳 +30 / 良好 +10 / 失败 -10(连续失败 ≥2 次后不再扣;余额扣至 0 不为负)
// - 已通关后补分支:只记完成度不结算积分
// - 终局数集齐 → 完美(+20);最佳终局 → 通关
func SettleFinal(s domain.SettleState, finalNodeId int64, resultType int) dto.FinalSettle {
f := dto.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, req *dto.ChooseReq) (*dto.ChooseRes, error) {
child, err := getChildOf(ctx, auth.GetUid(ctx), req.ChildId)
if err != nil {
return nil, err
}
levelRec, err := dao.Level.GetByPkCached(ctx, req.LevelId)
if err != nil {
return nil, err
}
if levelRec == nil || levelRec.Status != consts.StatusEnabled {
return nil, gerror.New("关卡不存在")
}
if levelRec.AgeGroup != child.AgeGroup {
return nil, gerror.New("关卡不属于当前年龄段")
}
nodeRec, err := dao.SceneNode.GetInLevelCached(ctx, req.NodeId, req.LevelId)
if err != nil {
return nil, err
}
if nodeRec == nil {
return nil, gerror.New("节点不存在")
}
optionRec, err := dao.NodeOption.GetInNodeCached(ctx, req.OptionId, req.NodeId)
if err != nil {
return nil, err
}
if optionRec == nil {
return nil, gerror.New("选项不存在")
}
nextRec, err := dao.SceneNode.GetInLevelCached(ctx, optionRec.NextNodeId, req.LevelId)
if err != nil {
return nil, err
}
if nextRec == nil {
return nil, gerror.New("目标节点不存在")
}
progressRec, err := dao.UserProgress.GetByChildLevel(ctx, req.ChildId, req.LevelId)
if err != nil {
return nil, err
}
if !s.levelUnlocked(ctx, req.ChildId, child, levelRec, progressRec) {
return nil, gerror.New("关卡未解锁")
}
if nextRec.ResultType == consts.ResultNone {
s.logRoute(ctx, req.ChildId, req.LevelId, req.NodeId, req.OptionId, consts.ResultNone)
node, err := nodeOf(ctx, levelRec, nextRec)
if err != nil {
return nil, err
}
return &dto.ChooseRes{Next: node}, nil
}
// v2.1:终局节点剧本先于结算构建(只读查询失败则未结算,无副作用)
finalNode, err := nodeOf(ctx, levelRec, nextRec)
if err != nil {
return nil, err
}
// 终局:路径流水在锁内结算后记录,避免状态读到本次到达导致重复结算误判
settle, err := common.WithLock(ctx, fmt.Sprintf("child:%d:level:%d", req.ChildId, req.LevelId), 10*time.Second, 3, 200*time.Millisecond, func() (*dto.FinalSettle, error) {
return s.settle(ctx, child, levelRec, nextRec, req.NodeId, req.OptionId)
})
if err != nil {
return nil, err
}
settle.FinalNode = finalNode
return &dto.ChooseRes{Final: settle}, nil
}
// levelUnlocked 关卡解锁判定:本关已通关可重玩,否则按解锁链校验计策解锁。
func (s *level) levelUnlocked(ctx context.Context, childId int64, child *entity.Child, levelRec *entity.Level, progressRec *entity.UserProgress) bool {
if progressRec != nil && progressRec.Stars >= 3 {
return true
}
strategyRec, err := dao.Strategy.GetByPkCached(ctx, levelRec.StrategyId)
if err != nil || strategyRec == nil {
return false
}
unlocked, _ := Strategy.unlockState(ctx, childId, child.AgeGroup, strategyRec)
return unlocked
}
// logRoute 路径流水(尽力而为,写失败仅记录日志不阻断主流程)。
func (s *level) logRoute(ctx context.Context, childId, levelId, nodeId, optionId int64, resultType int) {
if _, err := dao.UserRouteLog.InsertAndReturnId(ctx, g.Map{
"child_id": childId,
"level_id": levelId,
"node_id": nodeId,
"option_id": optionId,
"result_type": resultType,
}); err != nil {
g.Log().Warning(ctx, "记录路径流水失败", err)
}
}
// nodeOf 组装下一决策节点(选项 + 元素)。
func nodeOf(ctx context.Context, levelRec *entity.Level, nodeRec *entity.SceneNode) (*dto.NodeVO, error) {
optionRecs, err := dao.NodeOption.ListEnabledByNodeCached(ctx, nodeRec.Id)
if err != nil {
return nil, err
}
elements, err := elementsOf(ctx, levelRec, []*entity.SceneNode{nodeRec}, optionRecs)
if err != nil {
return nil, err
}
return buildNode(nodeRec, optionRecs, elements), nil
}
// settle 终局结算(调用方持锁):锁内重读状态 → 纯函数判分 → 事务写库 → 记录本次终局路径 → 派生结果。
func (s *level) settle(ctx context.Context, child *entity.Child, levelRec *entity.Level, finalRec *entity.SceneNode, nodeId, optionId int64) (*dto.FinalSettle, error) {
childId := child.Id
levelId := levelRec.Id
state, err := s.settleState(ctx, childId, levelId)
if err != nil {
return nil, err
}
f := SettleFinal(state, finalRec.Id, finalRec.ResultType)
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.ResultType)
s.deriveExtras(ctx, child, levelRec, &f)
return &f, nil
}
// settleState 锁内重读该关历史状态:终局数、已到达终局、连续失败、余额。
func (s *level) settleState(ctx context.Context, childId, levelId int64) (domain.SettleState, error) {
state := domain.SettleState{ReachedFinals: map[int64]bool{}}
progressRec, err := dao.UserProgress.GetByChildLevel(ctx, childId, levelId)
if err != nil {
return state, err
}
if progressRec != nil {
state.LevelCleared = progressRec.Stars >= 3
state.PerfectAwarded = progressRec.Perfect == 1
}
childRec, err := dao.Child.GetByPk(ctx, childId)
if err != nil {
return state, err
}
if childRec != nil {
state.BalanceAfter = childRec.Points
}
totalFinals, err := dao.SceneNode.CountFinalsByLevel(ctx, levelId)
if err != nil {
return state, err
}
state.TotalFinals = totalFinals
logs, err := dao.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.OptionId)
}
nextOf := map[int64]int64{}
if len(optionIds) > 0 {
opts, err := dao.NodeOption.ListByOptionIds(ctx, optionIds)
if err != nil {
return state, err
}
for _, o := range opts {
nextOf[o.Id] = o.NextNodeId
}
}
for _, l := range logs {
if nid, ok := nextOf[l.OptionId]; ok {
state.ReachedFinals[nid] = true
}
}
for i := len(logs) - 1; i >= 0; i-- {
if logs[i].ResultType == consts.ResultFail {
state.FailStreak++
} else {
break
}
}
return state, nil
}
// commitSettle 结算事务:进度(最高星 + 完美标记 + 内容版本)、积分流水与余额、计策卡。
func (s *level) commitSettle(ctx context.Context, childId int64, levelRec *entity.Level, f *dto.FinalSettle) error {
levelId := levelRec.Id
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 := dao.UserProgress.UpsertInTx(ctx, tx, childId, levelId, stars, perfect, levelRec.ContentVersion); err != nil {
return err
}
if f.ScoreDelta != 0 {
childRec, err := dao.Child.GetByPkInTx(ctx, tx, childId)
if err != nil {
return err
}
if childRec == nil {
return gerror.New("孩子不存在")
}
balance := childRec.Points + 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 *entity.Level, f *dto.FinalSettle) error {
if !f.Perfect {
return nil
}
strategyId := levelRec.StrategyId
levels, err := dao.Level.ListEnabledByStrategyInTx(ctx, tx, strategyId)
if err != nil {
return err
}
for _, lv := range levels {
rec, err := dao.UserProgress.GetInTx(ctx, tx, childId, lv.Id)
if err != nil {
return err
}
if rec == nil || rec.Perfect != 1 {
return nil
}
}
exists, err := dao.UserCollection.ExistsInTx(ctx, tx, childId, strategyId)
if err != nil {
return err
}
if exists {
return nil
}
if err = dao.UserCollection.InsertInTx(ctx, tx, childId, strategyId); err != nil {
return err
}
f.CollectionUnlocked = true
return nil
}
// deriveExtras 结算后派生:计策卡解锁提示、下一计解锁、成长等级。
func (s *level) deriveExtras(ctx context.Context, child *entity.Child, levelRec *entity.Level, f *dto.FinalSettle) {
childId := child.Id
if f.CollectionUnlocked {
if next, err := dao.Strategy.GetByUnlockBefore(ctx, levelRec.StrategyId); err == nil && next != nil {
f.UnlockNext = true
}
}
before, err := dao.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
}
}
// ---------- 后台管理 ----------
type adminLevel struct{}
var AdminLevel = &adminLevel{}
// List 指定计策下全部关卡(含下架,后台全量展示),按序号排序。
func (s *adminLevel) List(ctx context.Context, req *dto.AdminLevelListReq) (*dto.AdminLevelListRes, error) {
recs, err := dao.Level.ListByStrategyId(ctx, req.StrategyId)
if err != nil {
return nil, err
}
ids := make([]int64, 0, len(recs))
sceneIds := make([]int64, 0, len(recs))
for _, r := range recs {
ids = append(ids, r.Id)
sceneIds = append(sceneIds, r.SceneId)
}
counts, err := dao.SceneNode.CountByLevelIds(ctx, ids)
if err != nil {
return nil, err
}
names, err := sceneNames(ctx, sceneIds)
if err != nil {
return nil, err
}
items := make([]*dto.AdminLevelItem, 0, len(recs))
for _, r := range recs {
items = append(items, &dto.AdminLevelItem{
Id: r.Id, StrategyId: r.StrategyId, Title: r.Title,
SceneId: r.SceneId, SceneName: names[r.SceneId],
SceneContent: r.SceneContent, SceneImage: r.SceneImage,
SceneAudio: r.SceneAudio, AgeGroup: r.AgeGroup,
ContentVersion: r.ContentVersion, SortOrder: r.SortOrder,
Status: r.Status, NodeCount: counts[r.Id],
})
}
return &dto.AdminLevelListRes{List: items}, nil
}
// sceneNames 批量元素名(含下架,后台全量展示)。
func sceneNames(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.ListByIds(ctx, ids)
if err != nil {
return nil, err
}
for _, r := range recs {
m[r.Id] = r.Name
}
return m, nil
}
// checkStrategy 计策存在性校验(含下架,历史关卡允许继续维护)。
func checkStrategy(ctx context.Context, strategyId int64) error {
rec, err := dao.Strategy.GetByPk(ctx, strategyId)
if err != nil {
return err
}
if rec == nil {
return gerror.New("计策不存在")
}
return nil
}
// Create 新增关卡:场景文案拼音标注,初始 content_version=1,清内容缓存。
func (s *adminLevel) Create(ctx context.Context, req *dto.AdminLevelCreateReq) (*dto.AdminLevelCreateRes, error) {
if err := checkStrategy(ctx, req.StrategyId); err != nil {
return nil, err
}
id, err := dao.Level.InsertAndReturnId(ctx, g.Map{
"strategy_id": req.StrategyId, "title": req.Title,
"scene_id": req.SceneId, "scene_content": req.SceneContent,
"scene_content_pinyin": common.AnnotatePinyin(req.SceneContent),
"scene_image": req.SceneImage, "scene_audio": req.SceneAudio,
"age_group": req.AgeGroup, "content_version": 1,
"sort_order": req.SortOrder, "status": consts.StatusEnabled,
})
if err != nil {
return nil, err
}
common.InvalidateContentCache(ctx, consts.TableLevel)
return &dto.AdminLevelCreateRes{Id: id}, nil
}
// Update 编辑关卡:全字段覆盖,拼音重标,content_version+1(前台据此可重新挑战),清内容缓存。
func (s *adminLevel) Update(ctx context.Context, req *dto.AdminLevelUpdateReq) (*dto.AdminLevelUpdateRes, error) {
rec, err := dao.Level.GetByPk(ctx, req.Id)
if err != nil {
return nil, err
}
if rec == nil {
return nil, gerror.New("关卡不存在")
}
if err = checkStrategy(ctx, req.StrategyId); err != nil {
return nil, err
}
if err = dao.Level.UpdateByPk(ctx, req.Id, g.Map{
"strategy_id": req.StrategyId, "title": req.Title,
"scene_id": req.SceneId, "scene_content": req.SceneContent,
"scene_content_pinyin": common.AnnotatePinyin(req.SceneContent),
"scene_image": req.SceneImage, "scene_audio": req.SceneAudio,
"age_group": req.AgeGroup, "sort_order": req.SortOrder,
"content_version": gdb.Raw("content_version+1"),
}); err != nil {
return nil, err
}
common.InvalidateContentCache(ctx, consts.TableLevel)
return &dto.AdminLevelUpdateRes{}, nil
}
// Disable 下架关卡(软删除):内容可见性变化同样 bump content_version。
func (s *adminLevel) Disable(ctx context.Context, req *dto.AdminLevelDisableReq) (*dto.AdminLevelDisableRes, error) {
if err := s.setStatus(ctx, req.Id, consts.StatusDisabled); err != nil {
return nil, err
}
return &dto.AdminLevelDisableRes{}, nil
}
// Enable 上架关卡。
func (s *adminLevel) Enable(ctx context.Context, req *dto.AdminLevelEnableReq) (*dto.AdminLevelEnableRes, error) {
if err := s.setStatus(ctx, req.Id, consts.StatusEnabled); err != nil {
return nil, err
}
return &dto.AdminLevelEnableRes{}, nil
}
// setStatus 上下架(软删除):bump content_version。
func (s *adminLevel) setStatus(ctx context.Context, id int64, status int) error {
rec, err := dao.Level.GetByPk(ctx, id)
if err != nil {
return err
}
if rec == nil {
return gerror.New("关卡不存在")
}
if err = dao.Level.UpdateByPk(ctx, id, g.Map{
"status": status,
"content_version": gdb.Raw("content_version+1"),
}); err != nil {
return err
}
common.InvalidateContentCache(ctx, consts.TableLevel)
return nil
}
// ---------- 关卡统计 ----------
type optKey struct{ nodeId, optionId int64 }
type adminStats struct{}
var AdminStats = &adminStats{}
// Level 关卡统计:5 条单表 SQL + 内存组装,不缓存。
func (s *adminStats) Level(ctx context.Context, req *dto.AdminStatsLevelReq) (*dto.AdminStatsLevelRes, error) {
levelRec, err := dao.Level.GetByPk(ctx, req.LevelId)
if err != nil {
return nil, err
}
if levelRec == nil {
return nil, gerror.New("关卡不存在")
}
players, perfectCount, err := dao.UserProgress.ProgressStatsByLevel(ctx, req.LevelId)
if err != nil {
return nil, err
}
stats := &dto.AdminStatsLevelRes{Players: players, PerfectCount: perfectCount}
if players > 0 {
stats.PerfectRate = math.Round(float64(perfectCount)*1000/float64(players)) / 10
}
nodes, err := dao.SceneNode.ListByLevel(ctx, req.LevelId)
if err != nil {
return nil, err
}
nodeIds := make([]int64, 0, len(nodes))
for _, n := range nodes {
nodeIds = append(nodeIds, n.Id)
}
opts, err := dao.NodeOption.ListByNodeIds(ctx, nodeIds)
if err != nil {
return nil, err
}
optionByKey := make(map[optKey]*dto.AdminStatsOption, len(opts))
for _, o := range opts {
os := &dto.AdminStatsOption{OptionId: o.Id, NodeId: o.NodeId, Text: o.Text}
stats.Options = append(stats.Options, os)
optionByKey[optKey{os.NodeId, os.OptionId}] = os
}
logs, err := dao.UserRouteLog.RouteStatsByLevel(ctx, req.LevelId)
if err != nil {
return nil, err
}
nodeById := make(map[int64]*dto.AdminStatsNode, len(nodes))
for _, l := range logs {
cnt := l.Cnt
ns := nodeById[l.NodeId]
if ns == nil {
ns = &dto.AdminStatsNode{NodeId: l.NodeId}
nodeById[l.NodeId] = ns
}
ns.ReachCount += cnt
switch l.ResultType {
case consts.ResultNone:
ns.None += cnt
case consts.ResultFail:
ns.Fail += cnt
case consts.ResultGood:
ns.Good += cnt
case consts.ResultBest:
ns.Best += cnt
}
if l.OptionId > 0 {
if os := optionByKey[optKey{l.NodeId, l.OptionId}]; os != nil {
switch l.ResultType {
case consts.ResultNone:
os.ChooseCount += cnt
case consts.ResultFail:
os.Fail += cnt
case consts.ResultGood:
os.Good += cnt
case consts.ResultBest:
os.Best += cnt
}
}
}
}
for _, n := range nodes {
ns := nodeById[n.Id]
if ns == nil {
ns = &dto.AdminStatsNode{NodeId: n.Id}
}
ns.Content = n.Content
ns.NodeType = n.NodeType
stats.Nodes = append(stats.Nodes, ns)
}
return stats, nil
}