269 lines
8.0 KiB
Go
269 lines
8.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
|
|
"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/dto"
|
|
"36wisdom/biz/model/entity"
|
|
"36wisdom/common"
|
|
)
|
|
|
|
// ---------- 后台管理 ----------
|
|
|
|
type adminNodeOption struct{}
|
|
|
|
var AdminNodeOption = &adminNodeOption{}
|
|
|
|
// List 指定节点下全部选项(含下架),按序号排序。
|
|
func (s *adminNodeOption) List(ctx context.Context, req *dto.AdminNodeOptionListReq) (*dto.AdminNodeOptionListRes, error) {
|
|
recs, err := dao.NodeOption.ListByNode(ctx, req.NodeId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nodeIds := make([]int64, 0, len(recs)*2)
|
|
propIds := make([]int64, 0, len(recs))
|
|
for _, r := range recs {
|
|
nodeIds = append(nodeIds, r.NodeId, r.NextNodeId)
|
|
propIds = append(propIds, r.PropId)
|
|
}
|
|
titles, err := nodeTitles(ctx, nodeIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names, err := elementNamesAll(ctx, propIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]*dto.AdminNodeOptionItem, 0, len(recs))
|
|
for _, r := range recs {
|
|
items = append(items, &dto.AdminNodeOptionItem{
|
|
Id: r.Id, NodeId: r.NodeId, NodeTitle: titles[r.NodeId],
|
|
Text: r.Text, PropId: r.PropId, PropName: names[r.PropId],
|
|
Audio: r.Audio, NextNodeId: r.NextNodeId, NextNodeTitle: titles[r.NextNodeId],
|
|
Feedback: r.Feedback, FeedbackAudio: r.FeedbackAudio,
|
|
SortOrder: r.SortOrder, Status: r.Status,
|
|
})
|
|
}
|
|
return &dto.AdminNodeOptionListRes{List: items}, nil
|
|
}
|
|
|
|
// nodeTitles 批量节点标题(无标题回退内容前 20 字)。
|
|
func nodeTitles(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.SceneNode.ListByIds(ctx, ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range recs {
|
|
t := r.Title
|
|
if t == "" {
|
|
t = r.Content
|
|
if len([]rune(t)) > 20 {
|
|
t = string([]rune(t)[:20]) + "…"
|
|
}
|
|
}
|
|
m[r.Id] = t
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// checkNext 下一节点校验:同关存在 + 沿 next 链无环(步数 ≤ 本关节点数,超限即有环)。
|
|
func checkNext(ctx context.Context, nodeId int64, nodeRec *entity.SceneNode, nextNodeId int64) error {
|
|
levelId := nodeRec.LevelId
|
|
rec, err := dao.SceneNode.GetByPk(ctx, nextNodeId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rec == nil || rec.LevelId != levelId {
|
|
return gerror.New("下一节点不存在或不属于同一关卡")
|
|
}
|
|
nodeRecs, err := dao.SceneNode.ListByLevel(ctx, levelId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nodeIds := make([]int64, 0, len(nodeRecs))
|
|
for _, r := range nodeRecs {
|
|
nodeIds = append(nodeIds, r.Id)
|
|
}
|
|
optRecs, err := dao.NodeOption.ListByNodeIds(ctx, nodeIds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nextByNode := make(map[int64]int64, len(optRecs))
|
|
for _, r := range optRecs {
|
|
nextByNode[r.NodeId] = r.NextNodeId
|
|
}
|
|
cur := nextNodeId
|
|
for step := 0; step <= len(nodeIds)+1; step++ {
|
|
if cur == nodeId {
|
|
return gerror.New("选项指向形成循环,无法到达终局")
|
|
}
|
|
nxt, ok := nextByNode[cur]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cur = nxt
|
|
}
|
|
return gerror.New("选项指向形成循环,无法到达终局")
|
|
}
|
|
|
|
// checkProp 道具元素存在性校验(prop_id>0 时)。
|
|
func checkProp(ctx context.Context, propId int64) error {
|
|
if propId <= 0 {
|
|
return nil
|
|
}
|
|
rec, err := dao.Element.GetByPk(ctx, propId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rec == nil {
|
|
return gerror.New("道具元素不存在")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bumpLevelVersion 事务内 bump 选项所属关卡的内容版本。
|
|
func bumpLevelVersion(ctx context.Context, tx gdb.TX, nodeId int64) error {
|
|
nodeRec, err := dao.SceneNode.GetByPk(ctx, nodeId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if nodeRec == nil {
|
|
return gerror.New("节点不存在")
|
|
}
|
|
_, err = tx.Model(consts.TableLevel).Ctx(ctx).WherePri(nodeRec.LevelId).
|
|
Data(g.Map{"content_version": gdb.Raw("content_version+1")}).Update()
|
|
return err
|
|
}
|
|
|
|
// Create 新增选项:同关 + 环校验,拼音标注,bump 关卡版本,清内容缓存。
|
|
func (s *adminNodeOption) Create(ctx context.Context, req *dto.AdminNodeOptionCreateReq) (*dto.AdminNodeOptionCreateRes, error) {
|
|
nodeRec, err := dao.SceneNode.GetByPk(ctx, req.NodeId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if nodeRec == nil {
|
|
return nil, gerror.New("节点不存在")
|
|
}
|
|
if err = checkProp(ctx, req.PropId); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = checkNext(ctx, req.NodeId, nodeRec, req.NextNodeId); err != nil {
|
|
return nil, err
|
|
}
|
|
var id int64
|
|
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
res, err := tx.Model(consts.TableNodeOption).Ctx(ctx).Data(g.Map{
|
|
"node_id": req.NodeId, "text": req.Text, "text_pinyin": common.AnnotatePinyin(req.Text),
|
|
"prop_id": req.PropId, "audio": req.Audio,
|
|
"next_node_id": req.NextNodeId,
|
|
"feedback": req.Feedback, "feedback_pinyin": common.AnnotatePinyin(req.Feedback),
|
|
"feedback_audio": req.FeedbackAudio, "sort_order": req.SortOrder,
|
|
"status": consts.StatusEnabled,
|
|
}).Insert()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, err = res.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return bumpLevelVersion(ctx, tx, req.NodeId)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
common.InvalidateContentCache(ctx, consts.TableNodeOption, consts.TableLevel)
|
|
return &dto.AdminNodeOptionCreateRes{Id: id}, nil
|
|
}
|
|
|
|
// Update 编辑选项:全字段覆盖,拼音重标,bump 关卡版本,清内容缓存。
|
|
func (s *adminNodeOption) Update(ctx context.Context, req *dto.AdminNodeOptionUpdateReq) (*dto.AdminNodeOptionUpdateRes, error) {
|
|
rec, err := dao.NodeOption.GetByPk(ctx, req.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rec == nil {
|
|
return nil, gerror.New("选项不存在")
|
|
}
|
|
nodeRec, err := dao.SceneNode.GetByPk(ctx, req.NodeId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if nodeRec == nil {
|
|
return nil, gerror.New("节点不存在")
|
|
}
|
|
if err = checkProp(ctx, req.PropId); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = checkNext(ctx, req.NodeId, nodeRec, req.NextNodeId); err != nil {
|
|
return nil, err
|
|
}
|
|
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
if _, err := tx.Model(consts.TableNodeOption).Ctx(ctx).WherePri(req.Id).Data(g.Map{
|
|
"node_id": req.NodeId, "text": req.Text, "text_pinyin": common.AnnotatePinyin(req.Text),
|
|
"prop_id": req.PropId, "audio": req.Audio,
|
|
"next_node_id": req.NextNodeId,
|
|
"feedback": req.Feedback, "feedback_pinyin": common.AnnotatePinyin(req.Feedback),
|
|
"feedback_audio": req.FeedbackAudio, "sort_order": req.SortOrder,
|
|
}).Update(); err != nil {
|
|
return err
|
|
}
|
|
return bumpLevelVersion(ctx, tx, req.NodeId)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
common.InvalidateContentCache(ctx, consts.TableNodeOption, consts.TableLevel)
|
|
return &dto.AdminNodeOptionUpdateRes{}, nil
|
|
}
|
|
|
|
// setStatus 上下架(软删除):bump 关卡版本。
|
|
func (s *adminNodeOption) setStatus(ctx context.Context, id int64, status int) error {
|
|
rec, err := dao.NodeOption.GetByPk(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rec == nil {
|
|
return gerror.New("选项不存在")
|
|
}
|
|
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
if _, err := tx.Model(consts.TableNodeOption).Ctx(ctx).WherePri(id).
|
|
Data(g.Map{"status": status}).Update(); err != nil {
|
|
return err
|
|
}
|
|
return bumpLevelVersion(ctx, tx, rec.NodeId)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
common.InvalidateContentCache(ctx, consts.TableNodeOption, consts.TableLevel)
|
|
return nil
|
|
}
|
|
|
|
// Disable 下架选项。
|
|
func (s *adminNodeOption) Disable(ctx context.Context, req *dto.AdminNodeOptionDisableReq) (*dto.AdminNodeOptionDisableRes, error) {
|
|
if err := s.setStatus(ctx, req.Id, consts.StatusDisabled); err != nil {
|
|
return nil, err
|
|
}
|
|
return &dto.AdminNodeOptionDisableRes{}, nil
|
|
}
|
|
|
|
// Enable 上架选项。
|
|
func (s *adminNodeOption) Enable(ctx context.Context, req *dto.AdminNodeOptionEnableReq) (*dto.AdminNodeOptionEnableRes, error) {
|
|
if err := s.setStatus(ctx, req.Id, consts.StatusEnabled); err != nil {
|
|
return nil, err
|
|
}
|
|
return &dto.AdminNodeOptionEnableRes{}, nil
|
|
}
|