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

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
}