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

399 lines
12 KiB
Go

package service
import (
"context"
"fmt"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"36wisdom/biz/consts"
"36wisdom/biz/dao"
"36wisdom/biz/model/dto"
"36wisdom/biz/model/entity"
"36wisdom/common"
"36wisdom/common/auth"
)
type strategy struct{}
var Strategy = &strategy{}
// List 计策列表:内容带缓存,进度不带;解锁按前置计全部关卡完美判定。
func (s *strategy) List(ctx context.Context, req *dto.StrategyListReq) (*dto.StrategyListRes, error) {
child, err := getChildOf(ctx, auth.GetUid(ctx), req.ChildId)
if err != nil {
return nil, err
}
strategies, err := dao.Strategy.ListEnabled(ctx)
if err != nil {
return nil, err
}
strategyIds := make([]int64, 0, len(strategies))
for _, r := range strategies {
strategyIds = append(strategyIds, r.Id)
}
levels, err := dao.Level.ListEnabledByStrategyIds(ctx, strategyIds, child.AgeGroup)
if err != nil {
return nil, err
}
progress, err := s.progressOfLevels(ctx, req.ChildId, levelIdsOf(levels))
if err != nil {
return nil, err
}
// 按计策分组统计,同时维护解锁依赖(线性链:unlock_before 指向前置计)。
stats := make(map[int64]*dto.StrategyItem, len(strategies))
for _, r := range strategies {
stats[r.Id] = &dto.StrategyItem{
StrategyId: r.Id,
Name: r.Name,
Pinyin: r.Pinyin,
GroupNo: r.GroupNo,
GroupName: r.GroupName,
Meaning: r.Meaning,
Icon: r.Icon,
SortOrder: r.SortOrder,
}
}
for _, lv := range levels {
st := stats[lv.StrategyId]
st.TotalLevels++
if p := progress[lv.Id]; p != nil {
st.Stars += p.Stars
if p.Perfect == 1 {
st.PerfectCount++
}
}
}
nameOf := make(map[int64]string, len(strategies))
for _, r := range strategies {
nameOf[r.Id] = r.Name
}
for _, r := range strategies {
it := stats[r.Id]
if r.UnlockBefore == 0 {
it.Unlocked = true
} else if prev, ok := stats[r.UnlockBefore]; ok && prev.TotalLevels > 0 && prev.PerfectCount == prev.TotalLevels {
it.Unlocked = true
} else {
it.UnlockReason = fmt.Sprintf("完成《%s》全部关卡解锁", nameOf[r.UnlockBefore])
}
}
items := make([]dto.StrategyItem, 0, len(strategies))
for _, r := range strategies {
items = append(items, *stats[r.Id])
}
return &dto.StrategyListRes{List: items}, nil
}
// Detail 计策详情:含关卡列表与解锁状态。
func (s *strategy) Detail(ctx context.Context, req *dto.StrategyDetailReq) (*dto.StrategyDetailRes, error) {
child, err := getChildOf(ctx, auth.GetUid(ctx), req.ChildId)
if err != nil {
return nil, err
}
rec, err := dao.Strategy.GetByPkCached(ctx, req.StrategyId)
if err != nil {
return nil, err
}
if rec == nil || rec.Status != consts.StatusEnabled {
return nil, gerror.New("计策不存在")
}
levels, err := dao.Level.ListEnabledByStrategyIds(ctx, []int64{req.StrategyId}, child.AgeGroup)
if err != nil {
return nil, err
}
progress, err := s.progressOfLevels(ctx, req.ChildId, levelIdsOf(levels))
if err != nil {
return nil, err
}
unlocked, _ := s.unlockState(ctx, req.ChildId, child.AgeGroup, rec)
sceneNames, err := s.elementNames(ctx, sceneIdsOf(levels))
if err != nil {
return nil, err
}
detail := &dto.StrategyDetailRes{
StrategyId: rec.Id,
Name: rec.Name,
Pinyin: rec.Pinyin,
Meaning: rec.Meaning,
MeaningPinyin: rec.MeaningPinyin,
GroupName: rec.GroupName,
TeachContent: rec.TeachContent,
TeachContentPinyin: rec.TeachContentPinyin,
TeachImage: rec.TeachImage,
TeachAudio: rec.TeachAudio,
SummaryQ: rec.SummaryQ,
SummaryOptions: rec.SummaryOptions,
Levels: make([]dto.LevelBrief, 0, len(levels)),
}
for _, lv := range levels {
stars, perfect, progressVersion := 0, false, 0
if p := progress[lv.Id]; p != nil {
stars, perfect, progressVersion = p.Stars, p.Perfect == 1, p.ContentVersion
}
detail.Levels = append(detail.Levels, dto.LevelBrief{
LevelId: lv.Id,
Title: lv.Title,
AgeGroup: lv.AgeGroup,
SceneName: sceneNames[lv.SceneId],
Stars: stars,
Perfect: perfect,
Unlocked: unlocked,
ContentVersion: lv.ContentVersion,
ProgressVersion: progressVersion,
})
}
return detail, nil
}
// unlockState 计策解锁判定:unlock_before 为空直接解锁,否则前置计全部关卡完美。
func (s *strategy) unlockState(ctx context.Context, childId int64, childAge string, strategyRec *entity.Strategy) (bool, string) {
prevId := strategyRec.UnlockBefore
if prevId == 0 {
return true, ""
}
prevName := ""
if r, err := dao.Strategy.GetByPkCached(ctx, prevId); err == nil && r != nil {
prevName = r.Name
}
perfect, total := 0, 0
if levels, err := dao.Level.ListEnabledByStrategyIds(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 p := progress[lv.Id]; p != nil && p.Perfect == 1 {
perfect++
}
}
}
}
if total > 0 && perfect == total {
return true, ""
}
return false, fmt.Sprintf("完成《%s》全部关卡解锁", prevName)
}
// progressOfLevels 孩子的关卡进度(不缓存,随闯关更新)。
func (s *strategy) progressOfLevels(ctx context.Context, childId int64, levelIds []int64) (map[int64]*entity.UserProgress, error) {
recs, err := dao.UserProgress.ListByChildLevelIds(ctx, childId, levelIds)
if err != nil {
return nil, err
}
m := make(map[int64]*entity.UserProgress, len(recs))
for _, r := range recs {
m[r.LevelId] = 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.ListEnabledByIdsCached(ctx, ids)
if err != nil {
return nil, err
}
for _, r := range recs {
m[r.Id] = r.Name
}
return m, nil
}
func levelIdsOf(levels []*entity.Level) []int64 {
ids := make([]int64, 0, len(levels))
for _, lv := range levels {
ids = append(ids, lv.Id)
}
return ids
}
func sceneIdsOf(levels []*entity.Level) []int64 {
ids := make([]int64, 0, len(levels))
for _, lv := range levels {
ids = append(ids, lv.SceneId)
}
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
}
// ---------- 后台管理 ----------
type adminStrategy struct{}
var AdminStrategy = &adminStrategy{}
// List 全部计策(含启用关卡数),按分组 + 序号排序。
func (s *adminStrategy) List(ctx context.Context, req *dto.AdminStrategyListReq) (*dto.AdminStrategyListRes, error) {
recs, err := dao.Strategy.ListAll(ctx)
if err != nil {
return nil, err
}
ids := make([]int64, 0, len(recs))
for _, r := range recs {
ids = append(ids, r.Id)
}
counts, err := dao.Level.CountEnabledByStrategyIds(ctx, ids)
if err != nil {
return nil, err
}
items := make([]*dto.AdminStrategyItem, 0, len(recs))
for _, r := range recs {
items = append(items, &dto.AdminStrategyItem{
Id: r.Id, Name: r.Name, Pinyin: r.Pinyin,
GroupNo: r.GroupNo, GroupName: r.GroupName,
Meaning: r.Meaning, TeachContent: r.TeachContent,
TeachImage: r.TeachImage, TeachAudio: r.TeachAudio,
SummaryQ: r.SummaryQ, SummaryOptions: r.SummaryOptions,
SummaryAudio: r.SummaryAudio, Icon: r.Icon,
SortOrder: r.SortOrder, UnlockBefore: r.UnlockBefore,
Status: r.Status, LevelCount: counts[r.Id],
})
}
return &dto.AdminStrategyListRes{List: items}, nil
}
// strategyNames 批量计策名。
func strategyNames(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.Strategy.ListByIds(ctx, ids)
if err != nil {
return nil, err
}
for _, r := range recs {
m[r.Id] = r.Name
}
return m, nil
}
// checkUnlockBefore 解锁前置校验:>0 时目标须存在且非自身。
func checkUnlockBefore(ctx context.Context, id, unlockBefore int64) error {
if unlockBefore <= 0 {
return nil
}
if id > 0 && unlockBefore == id {
return gerror.New("前置计策不能是自身")
}
rec, err := dao.Strategy.GetByPk(ctx, unlockBefore)
if err != nil {
return err
}
if rec == nil {
return gerror.New("前置计策不存在")
}
return nil
}
// Create 新增计策:名称转拼音、文本一次性拼音标注后入库,清内容缓存。
func (s *adminStrategy) Create(ctx context.Context, req *dto.AdminStrategyCreateReq) (*dto.AdminStrategyCreateRes, error) {
if err := checkUnlockBefore(ctx, 0, req.UnlockBefore); err != nil {
return nil, err
}
id, err := dao.Strategy.InsertAndReturnId(ctx, g.Map{
"name": req.Name, "pinyin": common.ToPinyinPlain(req.Name),
"group_no": req.GroupNo, "group_name": req.GroupName,
"meaning": req.Meaning, "meaning_pinyin": common.AnnotatePinyin(req.Meaning),
"teach_content": req.TeachContent, "teach_content_pinyin": common.AnnotatePinyin(req.TeachContent),
"teach_image": req.TeachImage, "teach_audio": req.TeachAudio,
"summary_q": req.SummaryQ, "summary_q_pinyin": common.AnnotatePinyin(req.SummaryQ),
"summary_options": req.SummaryOptions, "summary_options_pinyin": common.AnnotatePinyin(req.SummaryOptions),
"summary_audio": req.SummaryAudio, "icon": req.Icon,
"sort_order": req.SortOrder, "unlock_before": req.UnlockBefore,
"status": consts.StatusEnabled,
})
if err != nil {
return nil, err
}
common.InvalidateContentCache(ctx, consts.TableStrategy)
return &dto.AdminStrategyCreateRes{Id: id}, nil
}
// Update 编辑计策:全字段覆盖,拼音重标,清内容缓存。
func (s *adminStrategy) Update(ctx context.Context, req *dto.AdminStrategyUpdateReq) (*dto.AdminStrategyUpdateRes, error) {
rec, err := dao.Strategy.GetByPk(ctx, req.Id)
if err != nil {
return nil, err
}
if rec == nil {
return nil, gerror.New("计策不存在")
}
if err = checkUnlockBefore(ctx, req.Id, req.UnlockBefore); err != nil {
return nil, err
}
if err = dao.Strategy.UpdateByPk(ctx, req.Id, g.Map{
"name": req.Name, "pinyin": common.ToPinyinPlain(req.Name),
"group_no": req.GroupNo, "group_name": req.GroupName,
"meaning": req.Meaning, "meaning_pinyin": common.AnnotatePinyin(req.Meaning),
"teach_content": req.TeachContent, "teach_content_pinyin": common.AnnotatePinyin(req.TeachContent),
"teach_image": req.TeachImage, "teach_audio": req.TeachAudio,
"summary_q": req.SummaryQ, "summary_q_pinyin": common.AnnotatePinyin(req.SummaryQ),
"summary_options": req.SummaryOptions, "summary_options_pinyin": common.AnnotatePinyin(req.SummaryOptions),
"summary_audio": req.SummaryAudio, "icon": req.Icon,
"sort_order": req.SortOrder, "unlock_before": req.UnlockBefore,
}); err != nil {
return nil, err
}
common.InvalidateContentCache(ctx, consts.TableStrategy)
return &dto.AdminStrategyUpdateRes{}, nil
}
// Disable 下架计策(软删除)。
func (s *adminStrategy) Disable(ctx context.Context, req *dto.AdminStrategyDisableReq) (*dto.AdminStrategyDisableRes, error) {
if err := s.setStatus(ctx, req.Id, consts.StatusDisabled); err != nil {
return nil, err
}
return &dto.AdminStrategyDisableRes{}, nil
}
// Enable 上架计策。
func (s *adminStrategy) Enable(ctx context.Context, req *dto.AdminStrategyEnableReq) (*dto.AdminStrategyEnableRes, error) {
if err := s.setStatus(ctx, req.Id, consts.StatusEnabled); err != nil {
return nil, err
}
return &dto.AdminStrategyEnableRes{}, nil
}
// setStatus 上下架(软删除)。
func (s *adminStrategy) setStatus(ctx context.Context, id int64, status int) error {
rec, err := dao.Strategy.GetByPk(ctx, id)
if err != nil {
return err
}
if rec == nil {
return gerror.New("计策不存在")
}
if err = dao.Strategy.UpdateByPk(ctx, id, g.Map{"status": status}); err != nil {
return err
}
common.InvalidateContentCache(ctx, consts.TableStrategy)
return nil
}