399 lines
12 KiB
Go
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
|
|
}
|