1
This commit is contained in:
@@ -29,7 +29,8 @@
|
|||||||
|
|
||||||
## 分层文件对齐与代码模式(硬性要求)
|
## 分层文件对齐与代码模式(硬性要求)
|
||||||
|
|
||||||
- 每张业务表对应一组 `entity / dao / service / controller / dto` 文件,数量严格对齐;虚拟表(向量 vec0 / FTS5)不建独立分层文件,由主表 dao 统一管理
|
- 每张业务表对应一组 `entity / dao / service / controller / dto` 文件,数量严格对齐(核验方式:每层目录文件数 = 分层表数,分层表数 = 总表数 − 豁免表数);虚拟表(向量 vec0 / FTS5)与**流水/记录类表(如 point_log)豁免分层对齐**:不建任何独立分层文件(含 entity/dao),建表由主表 dao 统一管理(同虚拟表模式),由使用方 service 事务内直写,禁止为只写不读的审计表造分层门面;无任何读写引用的死表连表带分层整套删除,启动时 DROP 库内残留表与代码保持一致
|
||||||
|
- **非表文件一律不进业务分层目录**:路由注册与中间件装配、表初始化列表(建表 + 死表 DROP)直接写在 `main.go`;鉴权等跨模块通用能力放 `common/`;跨表业务流程归入所属表文件(如闯关 Choose 属 level 表)——分层目录出现非表文件即违反对齐,禁止以非表名开独立分层文件
|
||||||
- **不建 parser/rag 等技术目录**:纯技术能力(文档解析、中文分词、向量序列化)平铺在 `common/`;业务编排(分块、检索、工作流)归入对应 service 文件
|
- **不建 parser/rag 等技术目录**:纯技术能力(文档解析、中文分词、向量序列化)平铺在 `common/`;业务编排(分块、检索、工作流)归入对应 service 文件
|
||||||
- entity:每文件一张表,`orm` 标签与列名一致,时间字段用 `*gtime.Time`,只做表映射
|
- entity:每文件一张表,`orm` 标签与列名一致,时间字段用 `*gtime.Time`,只做表映射
|
||||||
- dao:单例 `var Xxx = &xxxDao{}`,`init()` 内 `CREATE TABLE IF NOT EXISTS` + 索引 + 迁移;通用 CRUD 复用 `common/base_dao.go`(InsertAndReturnId / GetOneByPk / UpdateByPk / DeleteByPk)
|
- dao:单例 `var Xxx = &xxxDao{}`,`init()` 内 `CREATE TABLE IF NOT EXISTS` + 索引 + 迁移;通用 CRUD 复用 `common/base_dao.go`(InsertAndReturnId / GetOneByPk / UpdateByPk / DeleteByPk)
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
|
|
||||||
## 数据访问规范(硬性要求)
|
## 数据访问规范(硬性要求)
|
||||||
|
|
||||||
- **事务**:涉及多张表的增删改操作必须包数据库事务,禁止逐表裸调用。事务放 dao 层方法内,service 层负责编排;`tx.Begin` 后必须用 `defer` 防护已提交后的二次 Rollback
|
- **事务**:涉及多张表的增删改操作必须包数据库事务,禁止逐表裸调用。**事务必须在 service 层**(`g.DB().Transaction` 包裹与事务内写方法如 `XxxInTx`,service 持有 tx 句柄编排多表),dao 层只做单表无状态 CRUD,不持事务;`tx.Begin` 后必须用 `defer` 防护已提交后的二次 Rollback
|
||||||
- **SQL 单表约束**:每个 SQL 只允许访问一张表,禁止 JOIN 与跨表子查询(`IN (SELECT ...)` / `EXISTS`);跨表数据一律拆为多条单表 SQL + 应用层内存组装——先取外键 id 列表,再对目标表 `IN` 查询;`IN` 参数须按 ≤100 分批(SQLite 变量数上限 999)
|
- **SQL 单表约束**:每个 SQL 只允许访问一张表,禁止 JOIN 与跨表子查询(`IN (SELECT ...)` / `EXISTS`);跨表数据一律拆为多条单表 SQL + 应用层内存组装——先取外键 id 列表,再对目标表 `IN` 查询;`IN` 参数须按 ≤100 分批(SQLite 变量数上限 999)
|
||||||
- **禁止 N+1 查询**:禁止在循环中逐条查库。循环场景一律改为批处理——一次 `ListByXxx` 取回后按外键在内存分组
|
- **禁止 N+1 查询**:禁止在循环中逐条查库。循环场景一律改为批处理——一次 `ListByXxx` 取回后按外键在内存分组
|
||||||
- **缓存一致性**:DAO 查询走缓存(TTL 来自 `database.cache.ttl`),写操作后必须清对应缓存
|
- **缓存一致性**:DAO 查询走缓存(TTL 来自 `database.cache.ttl`),写操作后必须清对应缓存
|
||||||
|
|||||||
@@ -103,7 +103,6 @@
|
|||||||
| user_badge | 孩子已获徽章 |
|
| user_badge | 孩子已获徽章 |
|
||||||
| point_log | 积分流水:变动、原因类型、余额快照 |
|
| point_log | 积分流水:变动、原因类型、余额快照 |
|
||||||
| life_task | 生活践行任务:关联计策、任务描述、家长引导语、确认奖励积分 |
|
| life_task | 生活践行任务:关联计策、任务描述、家长引导语、确认奖励积分 |
|
||||||
| life_task_log | 生活任务完成记录:孩子 × 任务、家长确认状态 |
|
|
||||||
| prize | 奖品:积分价格、库存、类型(虚拟/实物) |
|
| prize | 奖品:积分价格、库存、类型(虚拟/实物) |
|
||||||
| redemption | 兑换记录:孩子 × 奖品、状态流转、兑换码 |
|
| redemption | 兑换记录:孩子 × 奖品、状态流转、兑换码 |
|
||||||
| admin_user | 后台管理员账号 |
|
| admin_user | 后台管理员账号 |
|
||||||
|
|||||||
@@ -62,13 +62,6 @@ const (
|
|||||||
RedeemReceived = 5 // 已领取(家长确认)
|
RedeemReceived = 5 // 已领取(家长确认)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 生活任务状态(life_task_log.status)
|
|
||||||
const (
|
|
||||||
TaskIssued = 1 // 已下发
|
|
||||||
TaskConfirming = 2 // 待家长确认
|
|
||||||
TaskConfirmed = 3 // 已确认
|
|
||||||
)
|
|
||||||
|
|
||||||
// 年龄段档位(child.age_group / level.age_group)
|
// 年龄段档位(child.age_group / level.age_group)
|
||||||
const (
|
const (
|
||||||
AgeGroup4_6 = "4-6"
|
AgeGroup4_6 = "4-6"
|
||||||
|
|||||||
@@ -20,5 +20,4 @@ const (
|
|||||||
TableRedemption = "redemption"
|
TableRedemption = "redemption"
|
||||||
TableAdminUser = "admin_user"
|
TableAdminUser = "admin_user"
|
||||||
TableLifeTask = "life_task"
|
TableLifeTask = "life_task"
|
||||||
TableLifeTaskLog = "life_task_log"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type adminUser struct{}
|
||||||
|
|
||||||
|
var AdminUser = &adminUser{}
|
||||||
|
|
||||||
|
func (c *adminUser) Get(ctx context.Context, req *dto.AdminUserGetReq) (*dto.AdminUserGetRes, error) {
|
||||||
|
rec, err := service.AdminUser.GetByUsername(ctx, req.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.AdminUserGetRes{}
|
||||||
|
if !rec.IsEmpty() {
|
||||||
|
res.AdminUser = adminUserItem(rec)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func adminUserItem(r gdb.Record) dto.AdminUserItem {
|
||||||
|
return dto.AdminUserItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
Username: r["username"].String(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type badge struct{}
|
||||||
|
|
||||||
|
var Badge = &badge{}
|
||||||
|
|
||||||
|
func (c *badge) Get(ctx context.Context, req *dto.BadgeGetReq) (*dto.BadgeGetRes, error) {
|
||||||
|
rec, err := service.Badge.GetByPk(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.BadgeGetRes{}
|
||||||
|
if !rec.IsEmpty() {
|
||||||
|
res.Badge = badgeItem(rec)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *badge) List(ctx context.Context, req *dto.BadgeListReq) (*dto.BadgeListRes, error) {
|
||||||
|
recs, err := service.Badge.ListEnabled(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.BadgeListRes{List: make([]dto.BadgeItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, badgeItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func badgeItem(r gdb.Record) dto.BadgeItem {
|
||||||
|
return dto.BadgeItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
Name: r["name"].String(),
|
||||||
|
Icon: r["icon"].String(),
|
||||||
|
CondType: r["cond_type"].Int(),
|
||||||
|
CondValue: r["cond_value"].Int(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chapterReview struct{}
|
||||||
|
|
||||||
|
var ChapterReview = &chapterReview{}
|
||||||
|
|
||||||
|
func (c *chapterReview) ListByChild(ctx context.Context, req *dto.ChapterReviewListReq) (*dto.ChapterReviewListRes, error) {
|
||||||
|
recs, err := service.ChapterReview.ListByChild(ctx, req.ChildId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.ChapterReviewListRes{List: make([]dto.ChapterReviewItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, chapterReviewItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func chapterReviewItem(r gdb.Record) dto.ChapterReviewItem {
|
||||||
|
return dto.ChapterReviewItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
ChildId: r["child_id"].Int64(),
|
||||||
|
StrategyId: r["strategy_id"].Int64(),
|
||||||
|
LevelIds: r["level_ids"].String(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type element struct{}
|
||||||
|
|
||||||
|
var Element = &element{}
|
||||||
|
|
||||||
|
func (c *element) ListByIds(ctx context.Context, req *dto.ElementListReq) (*dto.ElementListRes, error) {
|
||||||
|
recs, err := service.Element.ListEnabledByIds(ctx, req.Ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.ElementListRes{List: make([]dto.ElementItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, elementItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func elementItem(r gdb.Record) dto.ElementItem {
|
||||||
|
return dto.ElementItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
EType: r["e_type"].Int(),
|
||||||
|
Name: r["name"].String(),
|
||||||
|
Image: r["image"].String(),
|
||||||
|
Audio: r["audio"].String(),
|
||||||
|
Description: r["description"].String(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
SortOrder: r["sort_order"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
-1
@@ -33,7 +33,7 @@ func (c *level) Detail(ctx context.Context, req *dto.LevelDetailReq) (*dto.Level
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func elementVO(e *service.Element) *dto.ElementVO {
|
func elementVO(e *service.ElementVO) *dto.ElementVO {
|
||||||
if e == nil {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -77,3 +77,33 @@ func nodeVO(n *service.Node) dto.NodeVO {
|
|||||||
}
|
}
|
||||||
return vo
|
return vo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *level) Choose(ctx context.Context, req *dto.ChooseReq) (*dto.ChooseRes, error) {
|
||||||
|
node, settle, err := service.Level.Choose(ctx, auth.GetUid(ctx), req.ChildId, req.LevelId, req.NodeId, req.OptionId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.ChooseRes{}
|
||||||
|
if node != nil {
|
||||||
|
vo := nodeVO(node)
|
||||||
|
res.Next = &vo
|
||||||
|
}
|
||||||
|
if settle != nil {
|
||||||
|
res.Final = &dto.FinalSettle{
|
||||||
|
ResultType: settle.ResultType,
|
||||||
|
Stars: settle.Stars,
|
||||||
|
ScoreDelta: settle.ScoreDelta,
|
||||||
|
Cleared: settle.Cleared,
|
||||||
|
Perfect: settle.Perfect,
|
||||||
|
UnlockNext: settle.UnlockNext,
|
||||||
|
CollectionUnlocked: settle.CollectionUnlocked,
|
||||||
|
NewLevel: settle.NewLevel,
|
||||||
|
BalanceAfter: settle.BalanceAfter,
|
||||||
|
}
|
||||||
|
if settle.FinalNode != nil {
|
||||||
|
vo := nodeVO(settle.FinalNode)
|
||||||
|
res.Final.FinalNode = &vo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"36wisdom/biz/model/dto"
|
|
||||||
"36wisdom/biz/service"
|
|
||||||
"36wisdom/common/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
type levelPlay struct{}
|
|
||||||
|
|
||||||
var LevelPlay = &levelPlay{}
|
|
||||||
|
|
||||||
func (c *levelPlay) Choose(ctx context.Context, req *dto.ChooseReq) (*dto.ChooseRes, error) {
|
|
||||||
node, settle, err := service.LevelPlay.Choose(ctx, auth.GetUid(ctx), req.ChildId, req.LevelId, req.NodeId, req.OptionId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res := &dto.ChooseRes{}
|
|
||||||
if node != nil {
|
|
||||||
vo := nodeVO(node)
|
|
||||||
res.Next = &vo
|
|
||||||
}
|
|
||||||
if settle != nil {
|
|
||||||
res.Final = &dto.FinalSettle{
|
|
||||||
ResultType: settle.ResultType,
|
|
||||||
Stars: settle.Stars,
|
|
||||||
ScoreDelta: settle.ScoreDelta,
|
|
||||||
Cleared: settle.Cleared,
|
|
||||||
Perfect: settle.Perfect,
|
|
||||||
UnlockNext: settle.UnlockNext,
|
|
||||||
CollectionUnlocked: settle.CollectionUnlocked,
|
|
||||||
NewLevel: settle.NewLevel,
|
|
||||||
BalanceAfter: settle.BalanceAfter,
|
|
||||||
}
|
|
||||||
if settle.FinalNode != nil {
|
|
||||||
vo := nodeVO(settle.FinalNode)
|
|
||||||
res.Final.FinalNode = &vo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type lifeTask struct{}
|
||||||
|
|
||||||
|
var LifeTask = &lifeTask{}
|
||||||
|
|
||||||
|
func (c *lifeTask) Get(ctx context.Context, req *dto.LifeTaskGetReq) (*dto.LifeTaskGetRes, error) {
|
||||||
|
rec, err := service.LifeTask.GetByPk(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.LifeTaskGetRes{}
|
||||||
|
if !rec.IsEmpty() {
|
||||||
|
res.LifeTask = lifeTaskItem(rec)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lifeTask) List(ctx context.Context, req *dto.LifeTaskListReq) (*dto.LifeTaskListRes, error) {
|
||||||
|
recs, err := service.LifeTask.ListEnabled(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.LifeTaskListRes{List: make([]dto.LifeTaskItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, lifeTaskItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lifeTaskItem(r gdb.Record) dto.LifeTaskItem {
|
||||||
|
return dto.LifeTaskItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
StrategyId: r["strategy_id"].Int64(),
|
||||||
|
Title: r["title"].String(),
|
||||||
|
Description: r["description"].String(),
|
||||||
|
Guide: r["guide"].String(),
|
||||||
|
RewardPoints: r["reward_points"].Int(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodeOption struct{}
|
||||||
|
|
||||||
|
var NodeOption = &nodeOption{}
|
||||||
|
|
||||||
|
func (c *nodeOption) Get(ctx context.Context, req *dto.NodeOptionGetReq) (*dto.NodeOptionGetRes, error) {
|
||||||
|
rec, err := service.NodeOption.GetInNode(ctx, req.OptionId, req.NodeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.NodeOptionGetRes{}
|
||||||
|
if !rec.IsEmpty() {
|
||||||
|
res.NodeOption = nodeOptionItem(rec)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *nodeOption) ListByNode(ctx context.Context, req *dto.NodeOptionListReq) (*dto.NodeOptionListRes, error) {
|
||||||
|
recs, err := service.NodeOption.ListEnabledByNode(ctx, req.NodeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.NodeOptionListRes{List: make([]dto.NodeOptionItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, nodeOptionItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeOptionItem(r gdb.Record) dto.NodeOptionItem {
|
||||||
|
return dto.NodeOptionItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
NodeId: r["node_id"].Int64(),
|
||||||
|
Text: r["text"].String(),
|
||||||
|
PropId: r["prop_id"].Int64(),
|
||||||
|
Audio: r["audio"].String(),
|
||||||
|
NextNodeId: r["next_node_id"].Int64(),
|
||||||
|
Feedback: r["feedback"].String(),
|
||||||
|
FeedbackAudio: r["feedback_audio"].String(),
|
||||||
|
SortOrder: r["sort_order"].Int(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prize struct{}
|
||||||
|
|
||||||
|
var Prize = &prize{}
|
||||||
|
|
||||||
|
func (c *prize) Get(ctx context.Context, req *dto.PrizeGetReq) (*dto.PrizeGetRes, error) {
|
||||||
|
rec, err := service.Prize.GetByPk(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.PrizeGetRes{}
|
||||||
|
if !rec.IsEmpty() {
|
||||||
|
res.Prize = prizeItem(rec)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *prize) List(ctx context.Context, req *dto.PrizeListReq) (*dto.PrizeListRes, error) {
|
||||||
|
recs, err := service.Prize.ListEnabled(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.PrizeListRes{List: make([]dto.PrizeItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, prizeItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prizeItem(r gdb.Record) dto.PrizeItem {
|
||||||
|
return dto.PrizeItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
Name: r["name"].String(),
|
||||||
|
Description: r["description"].String(),
|
||||||
|
Icon: r["icon"].String(),
|
||||||
|
PType: r["p_type"].Int(),
|
||||||
|
PointsCost: r["points_cost"].Int(),
|
||||||
|
Stock: r["stock"].Int(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
SortOrder: r["sort_order"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type redemption struct{}
|
||||||
|
|
||||||
|
var Redemption = &redemption{}
|
||||||
|
|
||||||
|
func (c *redemption) ListByUser(ctx context.Context, req *dto.RedemptionListReq) (*dto.RedemptionListRes, error) {
|
||||||
|
recs, err := service.Redemption.ListByUser(ctx, req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.RedemptionListRes{List: make([]dto.RedemptionItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, redemptionItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func redemptionItem(r gdb.Record) dto.RedemptionItem {
|
||||||
|
return dto.RedemptionItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
UserId: r["user_id"].Int64(),
|
||||||
|
PrizeId: r["prize_id"].Int64(),
|
||||||
|
PointsCost: r["points_cost"].Int(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
Code: r["code"].String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
|
||||||
"github.com/gogf/gf/v2/net/ghttp"
|
|
||||||
"github.com/gogf/gf/v2/os/gctx"
|
|
||||||
|
|
||||||
"36wisdom/common/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register 注册全部路由分组;各业务控制器在对应任务中挂载。
|
|
||||||
func Register(s *ghttp.Server) {
|
|
||||||
s.Use(ghttp.MiddlewareHandlerResponse)
|
|
||||||
|
|
||||||
s.BindHandler("GET:/ping", func(r *ghttp.Request) {
|
|
||||||
r.Response.Write("pong")
|
|
||||||
})
|
|
||||||
|
|
||||||
secret := g.Cfg().MustGet(gctx.New(), "auth.secret").String()
|
|
||||||
|
|
||||||
// 前台:公开接口(注册/登录),鉴权接口按模块挂载
|
|
||||||
s.Group("/api", func(g *ghttp.RouterGroup) {
|
|
||||||
g.Bind(Parent)
|
|
||||||
|
|
||||||
g.Group("/", func(g *ghttp.RouterGroup) {
|
|
||||||
g.Middleware(auth.Middleware(secret, ""))
|
|
||||||
g.Bind(Child, Strategy, Level, LevelPlay)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sceneNode struct{}
|
||||||
|
|
||||||
|
var SceneNode = &sceneNode{}
|
||||||
|
|
||||||
|
func (c *sceneNode) Get(ctx context.Context, req *dto.SceneNodeGetReq) (*dto.SceneNodeGetRes, error) {
|
||||||
|
rec, err := service.SceneNode.GetInLevel(ctx, req.NodeId, req.LevelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.SceneNodeGetRes{}
|
||||||
|
if !rec.IsEmpty() {
|
||||||
|
res.SceneNode = sceneNodeItem(rec)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sceneNode) ListByLevel(ctx context.Context, req *dto.SceneNodeListReq) (*dto.SceneNodeListRes, error) {
|
||||||
|
recs, err := service.SceneNode.ListEnabledByLevel(ctx, req.LevelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.SceneNodeListRes{List: make([]dto.SceneNodeItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, sceneNodeItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneNodeItem(r gdb.Record) dto.SceneNodeItem {
|
||||||
|
return dto.SceneNodeItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
LevelId: r["level_id"].Int64(),
|
||||||
|
Title: r["title"].String(),
|
||||||
|
CharacterId: r["character_id"].Int64(),
|
||||||
|
Content: r["content"].String(),
|
||||||
|
Image: r["image"].String(),
|
||||||
|
Audio: r["audio"].String(),
|
||||||
|
NodeType: r["node_type"].Int(),
|
||||||
|
InteractionType: r["interaction_type"].Int(),
|
||||||
|
Config: r["config"].String(),
|
||||||
|
Script: r["script"].String(),
|
||||||
|
ResultType: r["result_type"].Int(),
|
||||||
|
IsEntry: r["is_entry"].Int(),
|
||||||
|
SortOrder: r["sort_order"].Int(),
|
||||||
|
Status: r["status"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userBadge struct{}
|
||||||
|
|
||||||
|
var UserBadge = &userBadge{}
|
||||||
|
|
||||||
|
func (c *userBadge) ListByUser(ctx context.Context, req *dto.UserBadgeListReq) (*dto.UserBadgeListRes, error) {
|
||||||
|
recs, err := service.UserBadge.ListByUser(ctx, req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.UserBadgeListRes{List: make([]dto.UserBadgeItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, userBadgeItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func userBadgeItem(r gdb.Record) dto.UserBadgeItem {
|
||||||
|
return dto.UserBadgeItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
UserId: r["user_id"].Int64(),
|
||||||
|
BadgeId: r["badge_id"].Int64(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userCollection struct{}
|
||||||
|
|
||||||
|
var UserCollection = &userCollection{}
|
||||||
|
|
||||||
|
func (c *userCollection) ListByUser(ctx context.Context, req *dto.UserCollectionListReq) (*dto.UserCollectionListRes, error) {
|
||||||
|
recs, err := service.UserCollection.ListByUser(ctx, req.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.UserCollectionListRes{List: make([]dto.UserCollectionItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, userCollectionItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCollectionItem(r gdb.Record) dto.UserCollectionItem {
|
||||||
|
return dto.UserCollectionItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
UserId: r["user_id"].Int64(),
|
||||||
|
StrategyId: r["strategy_id"].Int64(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userProgress struct{}
|
||||||
|
|
||||||
|
var UserProgress = &userProgress{}
|
||||||
|
|
||||||
|
func (c *userProgress) GetByChildLevel(ctx context.Context, req *dto.UserProgressGetReq) (*dto.UserProgressGetRes, error) {
|
||||||
|
rec, err := service.UserProgress.GetByChildLevel(ctx, req.ChildId, req.LevelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.UserProgressGetRes{}
|
||||||
|
if !rec.IsEmpty() {
|
||||||
|
res.UserProgress = userProgressItem(rec)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *userProgress) ListByChild(ctx context.Context, req *dto.UserProgressListReq) (*dto.UserProgressListRes, error) {
|
||||||
|
recs, err := service.UserProgress.ListByChildLevelIds(ctx, req.ChildId, req.LevelIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.UserProgressListRes{List: make([]dto.UserProgressItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, userProgressItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func userProgressItem(r gdb.Record) dto.UserProgressItem {
|
||||||
|
return dto.UserProgressItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
ChildId: r["child_id"].Int64(),
|
||||||
|
LevelId: r["level_id"].Int64(),
|
||||||
|
Stars: r["stars"].Int(),
|
||||||
|
Score: r["score"].Int(),
|
||||||
|
Perfect: r["perfect"].Int(),
|
||||||
|
ContentVersion: r["content_version"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/model/dto"
|
||||||
|
"36wisdom/biz/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userRouteLog struct{}
|
||||||
|
|
||||||
|
var UserRouteLog = &userRouteLog{}
|
||||||
|
|
||||||
|
func (c *userRouteLog) ListByChildLevel(ctx context.Context, req *dto.UserRouteLogListReq) (*dto.UserRouteLogListRes, error) {
|
||||||
|
recs, err := service.UserRouteLog.ListFinalsByChildLevel(ctx, req.ChildId, req.LevelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := &dto.UserRouteLogListRes{List: make([]dto.UserRouteLogItem, 0, len(recs))}
|
||||||
|
for _, r := range recs {
|
||||||
|
res.List = append(res.List, userRouteLogItem(r))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func userRouteLogItem(r gdb.Record) dto.UserRouteLogItem {
|
||||||
|
return dto.UserRouteLogItem{
|
||||||
|
Id: r["id"].Int64(),
|
||||||
|
ChildId: r["child_id"].Int64(),
|
||||||
|
LevelId: r["level_id"].Int64(),
|
||||||
|
NodeId: r["node_id"].Int64(),
|
||||||
|
OptionId: r["option_id"].Int64(),
|
||||||
|
ResultType: r["result_type"].Int(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS badge (
|
|||||||
cond_type INTEGER NOT NULL,
|
cond_type INTEGER NOT NULL,
|
||||||
cond_value INTEGER NOT NULL,
|
cond_value INTEGER NOT NULL,
|
||||||
status INTEGER NOT NULL DEFAULT 1
|
status INTEGER NOT NULL DEFAULT 1
|
||||||
);`)
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_badge_status ON badge(status);`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -27,6 +27,17 @@ CREATE TABLE IF NOT EXISTS child (
|
|||||||
created_at DATETIME,
|
created_at DATETIME,
|
||||||
updated_at DATETIME
|
updated_at DATETIME
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_child_parent ON child(parent_id);`)
|
CREATE INDEX IF NOT EXISTS idx_child_parent ON child(parent_id);
|
||||||
|
-- point_log 为 child 的附属流水表(纯表,不建独立分层文件),建表由主表 child 托管(同虚拟表模式)
|
||||||
|
CREATE TABLE IF NOT EXISTS point_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
change INTEGER NOT NULL,
|
||||||
|
reason_type INTEGER NOT NULL,
|
||||||
|
ref_id INTEGER,
|
||||||
|
balance_after INTEGER NOT NULL,
|
||||||
|
created_at DATETIME
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_point_log_user ON point_log(user_id, created_at);`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
|
||||||
|
|
||||||
"36wisdom/biz/consts"
|
|
||||||
"36wisdom/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type lifeTaskLogDao struct{ common.BaseDao }
|
|
||||||
|
|
||||||
var LifeTaskLog = &lifeTaskLogDao{BaseDao: common.BaseDao{Table: consts.TableLifeTaskLog}}
|
|
||||||
|
|
||||||
func (d *lifeTaskLogDao) Init(ctx context.Context) error {
|
|
||||||
_, err := g.DB().Exec(ctx, `
|
|
||||||
CREATE TABLE IF NOT EXISTS life_task_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
child_id INTEGER NOT NULL,
|
|
||||||
task_id INTEGER NOT NULL,
|
|
||||||
status INTEGER NOT NULL DEFAULT 1,
|
|
||||||
confirm_photo TEXT,
|
|
||||||
confirmed_at DATETIME,
|
|
||||||
created_at DATETIME,
|
|
||||||
UNIQUE(child_id, task_id)
|
|
||||||
);`)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
|
||||||
|
|
||||||
"36wisdom/biz/consts"
|
|
||||||
"36wisdom/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pointLogDao struct{ common.BaseDao }
|
|
||||||
|
|
||||||
var PointLog = &pointLogDao{BaseDao: common.BaseDao{Table: consts.TablePointLog}}
|
|
||||||
|
|
||||||
func (d *pointLogDao) Init(ctx context.Context) error {
|
|
||||||
_, err := g.DB().Exec(ctx, `
|
|
||||||
CREATE TABLE IF NOT EXISTS point_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
change INTEGER NOT NULL,
|
|
||||||
reason_type INTEGER NOT NULL,
|
|
||||||
ref_id INTEGER,
|
|
||||||
balance_after INTEGER NOT NULL,
|
|
||||||
created_at DATETIME
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_point_log_user ON point_log(user_id, created_at);`)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,7 @@ CREATE TABLE IF NOT EXISTS prize (
|
|||||||
stock INTEGER NOT NULL DEFAULT 0,
|
stock INTEGER NOT NULL DEFAULT 0,
|
||||||
status INTEGER NOT NULL DEFAULT 1,
|
status INTEGER NOT NULL DEFAULT 1,
|
||||||
sort_order INTEGER NOT NULL DEFAULT 1
|
sort_order INTEGER NOT NULL DEFAULT 1
|
||||||
);`)
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_prize_status ON prize(status, sort_order);`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tableInitializer func(ctx context.Context) error
|
|
||||||
|
|
||||||
var tableInitializers = []tableInitializer{
|
|
||||||
Parent.Init,
|
|
||||||
Child.Init,
|
|
||||||
Strategy.Init,
|
|
||||||
Level.Init,
|
|
||||||
SceneNode.Init,
|
|
||||||
NodeOption.Init,
|
|
||||||
Element.Init,
|
|
||||||
UserProgress.Init,
|
|
||||||
ChapterReview.Init,
|
|
||||||
UserRouteLog.Init,
|
|
||||||
UserCollection.Init,
|
|
||||||
Badge.Init,
|
|
||||||
UserBadge.Init,
|
|
||||||
PointLog.Init,
|
|
||||||
Prize.Init,
|
|
||||||
Redemption.Init,
|
|
||||||
AdminUser.Init,
|
|
||||||
LifeTask.Init,
|
|
||||||
LifeTaskLog.Init,
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitTables 创建全部业务表与索引(IF NOT EXISTS 幂等)。
|
|
||||||
func InitTables(ctx context.Context) {
|
|
||||||
for _, init := range tableInitializers {
|
|
||||||
if err := init(ctx); err != nil {
|
|
||||||
g.Log().Fatal(ctx, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type AdminUserItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminUserGetReq struct {
|
||||||
|
Username string `v:"required" json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminUserGetRes struct {
|
||||||
|
AdminUser AdminUserItem `json:"admin_user"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type BadgeItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
CondType int `json:"cond_type"`
|
||||||
|
CondValue int `json:"cond_value"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadgeGetReq struct {
|
||||||
|
Id int64 `v:"required" json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadgeGetRes struct {
|
||||||
|
Badge BadgeItem `json:"badge"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BadgeListReq struct{}
|
||||||
|
|
||||||
|
type BadgeListRes struct {
|
||||||
|
List []BadgeItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type ChapterReviewItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
ChildId int64 `json:"child_id"`
|
||||||
|
StrategyId int64 `json:"strategy_id"`
|
||||||
|
LevelIds string `json:"level_ids"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChapterReviewListReq struct {
|
||||||
|
ChildId int64 `v:"required" json:"child_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChapterReviewListRes struct {
|
||||||
|
List []ChapterReviewItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type ElementItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
EType int `json:"e_type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Audio string `json:"audio"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ElementListReq struct {
|
||||||
|
Ids []int64 `v:"required" json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ElementListRes struct {
|
||||||
|
List []ElementItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -57,3 +57,29 @@ type LevelDetailRes struct {
|
|||||||
Perfect bool `json:"perfect"`
|
Perfect bool `json:"perfect"`
|
||||||
Stars int `json:"stars"`
|
Stars int `json:"stars"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChooseReq struct {
|
||||||
|
g.Meta `path:"/level/choose" method:"post" summary:"分支闯关选择"`
|
||||||
|
ChildId int64 `v:"required" json:"child_id"`
|
||||||
|
LevelId int64 `v:"required" json:"level_id"`
|
||||||
|
NodeId int64 `v:"required" json:"node_id"`
|
||||||
|
OptionId int64 `v:"required" json:"option_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChooseRes struct {
|
||||||
|
Next *NodeVO `json:"next"`
|
||||||
|
Final *FinalSettle `json:"final"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FinalSettle struct {
|
||||||
|
ResultType int `json:"result_type"`
|
||||||
|
Stars int `json:"stars"`
|
||||||
|
ScoreDelta int `json:"score_delta"`
|
||||||
|
Cleared bool `json:"cleared"`
|
||||||
|
Perfect bool `json:"perfect"`
|
||||||
|
UnlockNext bool `json:"unlock_next"`
|
||||||
|
CollectionUnlocked bool `json:"collection_unlocked"`
|
||||||
|
NewLevel int `json:"new_level"`
|
||||||
|
BalanceAfter int `json:"balance_after"`
|
||||||
|
FinalNode *NodeVO `json:"final_node"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package dto
|
|
||||||
|
|
||||||
import "github.com/gogf/gf/v2/frame/g"
|
|
||||||
|
|
||||||
type ChooseReq struct {
|
|
||||||
g.Meta `path:"/level/choose" method:"post" summary:"分支闯关选择"`
|
|
||||||
ChildId int64 `v:"required" json:"child_id"`
|
|
||||||
LevelId int64 `v:"required" json:"level_id"`
|
|
||||||
NodeId int64 `v:"required" json:"node_id"`
|
|
||||||
OptionId int64 `v:"required" json:"option_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChooseRes struct {
|
|
||||||
Next *NodeVO `json:"next"`
|
|
||||||
Final *FinalSettle `json:"final"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FinalSettle struct {
|
|
||||||
ResultType int `json:"result_type"`
|
|
||||||
Stars int `json:"stars"`
|
|
||||||
ScoreDelta int `json:"score_delta"`
|
|
||||||
Cleared bool `json:"cleared"`
|
|
||||||
Perfect bool `json:"perfect"`
|
|
||||||
UnlockNext bool `json:"unlock_next"`
|
|
||||||
CollectionUnlocked bool `json:"collection_unlocked"`
|
|
||||||
NewLevel int `json:"new_level"`
|
|
||||||
BalanceAfter int `json:"balance_after"`
|
|
||||||
FinalNode *NodeVO `json:"final_node"`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type LifeTaskItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
StrategyId int64 `json:"strategy_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Guide string `json:"guide"`
|
||||||
|
RewardPoints int `json:"reward_points"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LifeTaskGetReq struct {
|
||||||
|
Id int64 `v:"required" json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LifeTaskGetRes struct {
|
||||||
|
LifeTask LifeTaskItem `json:"life_task"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LifeTaskListReq struct{}
|
||||||
|
|
||||||
|
type LifeTaskListRes struct {
|
||||||
|
List []LifeTaskItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type NodeOptionItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
NodeId int64 `json:"node_id"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
PropId int64 `json:"prop_id"`
|
||||||
|
Audio string `json:"audio"`
|
||||||
|
NextNodeId int64 `json:"next_node_id"`
|
||||||
|
Feedback string `json:"feedback"`
|
||||||
|
FeedbackAudio string `json:"feedback_audio"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeOptionGetReq struct {
|
||||||
|
OptionId int64 `v:"required" json:"option_id"`
|
||||||
|
NodeId int64 `v:"required" json:"node_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeOptionGetRes struct {
|
||||||
|
NodeOption NodeOptionItem `json:"node_option"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeOptionListReq struct {
|
||||||
|
NodeId int64 `v:"required" json:"node_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeOptionListRes struct {
|
||||||
|
List []NodeOptionItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type PrizeItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
PType int `json:"p_type"`
|
||||||
|
PointsCost int `json:"points_cost"`
|
||||||
|
Stock int `json:"stock"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrizeGetReq struct {
|
||||||
|
Id int64 `v:"required" json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrizeGetRes struct {
|
||||||
|
Prize PrizeItem `json:"prize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrizeListReq struct{}
|
||||||
|
|
||||||
|
type PrizeListRes struct {
|
||||||
|
List []PrizeItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type RedemptionItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
PrizeId int64 `json:"prize_id"`
|
||||||
|
PointsCost int `json:"points_cost"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedemptionListReq struct {
|
||||||
|
UserId int64 `v:"required" json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedemptionListRes struct {
|
||||||
|
List []RedemptionItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type SceneNodeItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
LevelId int64 `json:"level_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
CharacterId int64 `json:"character_id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Audio string `json:"audio"`
|
||||||
|
NodeType int `json:"node_type"`
|
||||||
|
InteractionType int `json:"interaction_type"`
|
||||||
|
Config string `json:"config"`
|
||||||
|
Script string `json:"script"`
|
||||||
|
ResultType int `json:"result_type"`
|
||||||
|
IsEntry int `json:"is_entry"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SceneNodeGetReq struct {
|
||||||
|
NodeId int64 `v:"required" json:"node_id"`
|
||||||
|
LevelId int64 `v:"required" json:"level_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SceneNodeGetRes struct {
|
||||||
|
SceneNode SceneNodeItem `json:"scene_node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SceneNodeListReq struct {
|
||||||
|
LevelId int64 `v:"required" json:"level_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SceneNodeListRes struct {
|
||||||
|
List []SceneNodeItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type UserBadgeItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
BadgeId int64 `json:"badge_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserBadgeListReq struct {
|
||||||
|
UserId int64 `v:"required" json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserBadgeListRes struct {
|
||||||
|
List []UserBadgeItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type UserCollectionItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
StrategyId int64 `json:"strategy_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserCollectionListReq struct {
|
||||||
|
UserId int64 `v:"required" json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserCollectionListRes struct {
|
||||||
|
List []UserCollectionItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type UserProgressItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
ChildId int64 `json:"child_id"`
|
||||||
|
LevelId int64 `json:"level_id"`
|
||||||
|
Stars int `json:"stars"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Perfect int `json:"perfect"`
|
||||||
|
ContentVersion int `json:"content_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProgressGetReq struct {
|
||||||
|
ChildId int64 `v:"required" json:"child_id"`
|
||||||
|
LevelId int64 `v:"required" json:"level_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProgressGetRes struct {
|
||||||
|
UserProgress UserProgressItem `json:"user_progress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProgressListReq struct {
|
||||||
|
ChildId int64 `v:"required" json:"child_id"`
|
||||||
|
LevelIds []int64 `v:"required" json:"level_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProgressListRes struct {
|
||||||
|
List []UserProgressItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type UserRouteLogItem struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
ChildId int64 `json:"child_id"`
|
||||||
|
LevelId int64 `json:"level_id"`
|
||||||
|
NodeId int64 `json:"node_id"`
|
||||||
|
OptionId int64 `json:"option_id"`
|
||||||
|
ResultType int `json:"result_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRouteLogListReq struct {
|
||||||
|
ChildId int64 `v:"required" json:"child_id"`
|
||||||
|
LevelId int64 `v:"required" json:"level_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserRouteLogListRes struct {
|
||||||
|
List []UserRouteLogItem `json:"list"`
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import "github.com/gogf/gf/v2/os/gtime"
|
|
||||||
|
|
||||||
// LifeTaskLog 生活任务完成记录:孩子 × 任务,家长确认状态流转。
|
|
||||||
type LifeTaskLog struct {
|
|
||||||
Id int64 `json:"id" orm:"id"`
|
|
||||||
ChildId int64 `json:"child_id" orm:"child_id"`
|
|
||||||
TaskId int64 `json:"task_id" orm:"task_id"`
|
|
||||||
Status int `json:"status" orm:"status"`
|
|
||||||
ConfirmPhoto string `json:"confirm_photo" orm:"confirm_photo"`
|
|
||||||
ConfirmedAt *gtime.Time `json:"confirmed_at" orm:"confirmed_at"`
|
|
||||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at"`
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package entity
|
|
||||||
|
|
||||||
import "github.com/gogf/gf/v2/os/gtime"
|
|
||||||
|
|
||||||
// PointLog 积分流水:变动、原因类型、余额快照。
|
|
||||||
type PointLog struct {
|
|
||||||
Id int64 `json:"id" orm:"id"`
|
|
||||||
UserId int64 `json:"user_id" orm:"user_id"`
|
|
||||||
Change int `json:"change" orm:"change"`
|
|
||||||
ReasonType int `json:"reason_type" orm:"reason_type"`
|
|
||||||
RefId int64 `json:"ref_id" orm:"ref_id"`
|
|
||||||
BalanceAfter int `json:"balance_after" orm:"balance_after"`
|
|
||||||
CreatedAt *gtime.Time `json:"created_at" orm:"created_at"`
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type adminUser struct{}
|
||||||
|
|
||||||
|
var AdminUser = &adminUser{}
|
||||||
|
|
||||||
|
// GetByUsername 按用户名取管理员账号(含密码列,登录校验用)。
|
||||||
|
func (s *adminUser) GetByUsername(ctx context.Context, username string) (gdb.Record, error) {
|
||||||
|
return dao.AdminUser.Model().Ctx(ctx).Where("username", username).One()
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
|
||||||
|
|
||||||
"36wisdom/biz/consts"
|
|
||||||
"36wisdom/common/auth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// authSvc 鉴权服务:token 签发与解析,密钥统一来自配置 auth.secret。
|
|
||||||
type authSvc struct{}
|
|
||||||
|
|
||||||
var Auth = &authSvc{}
|
|
||||||
|
|
||||||
func (s *authSvc) GenerateToken(ctx context.Context, uid int64, role string) (string, error) {
|
|
||||||
return auth.GenerateToken(secret(ctx), uid, role, consts.AuthExpireSeconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *authSvc) ParseToken(ctx context.Context, tokenString string) (int64, string, error) {
|
|
||||||
return auth.ParseToken(secret(ctx), tokenString)
|
|
||||||
}
|
|
||||||
|
|
||||||
func secret(ctx context.Context) string {
|
|
||||||
return g.Cfg().MustGet(ctx, "auth.secret").String()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type badge struct{}
|
||||||
|
|
||||||
|
var Badge = &badge{}
|
||||||
|
|
||||||
|
// GetByPk 徽章定义。
|
||||||
|
func (s *badge) GetByPk(ctx context.Context, id int64) (gdb.Record, error) {
|
||||||
|
return dao.Badge.GetOneByPk(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnabled 启用徽章定义(内容缓存)。
|
||||||
|
func (s *badge) ListEnabled(ctx context.Context) ([]gdb.Record, error) {
|
||||||
|
return dao.Badge.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("status", consts.StatusEnabled).Order("id ASC").All()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type chapterReview struct{}
|
||||||
|
|
||||||
|
var ChapterReview = &chapterReview{}
|
||||||
|
|
||||||
|
// ListByChild 孩子温故记录。
|
||||||
|
func (s *chapterReview) ListByChild(ctx context.Context, childId int64) ([]gdb.Record, error) {
|
||||||
|
return dao.ChapterReview.Model().Ctx(ctx).Where("child_id", childId).Order("id DESC").All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建温故记录。
|
||||||
|
func (s *chapterReview) Insert(ctx context.Context, childId, strategyId int64, levelIds string) (int64, error) {
|
||||||
|
return dao.ChapterReview.InsertAndReturnId(ctx, g.Map{
|
||||||
|
"child_id": childId,
|
||||||
|
"strategy_id": strategyId,
|
||||||
|
"level_ids": levelIds,
|
||||||
|
"status": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -80,11 +80,7 @@ func (s *child) List(ctx context.Context, parentId int64) ([]*ChildItem, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
perfectMap := make(map[int64]int, len(recs))
|
perfectMap := make(map[int64]int, len(recs))
|
||||||
rows, err := dao.UserProgress.Model().Ctx(ctx).
|
rows, err := UserProgress.CountPerfectByChildIds(ctx, childIds)
|
||||||
Fields("child_id", "COUNT(*) AS cnt").
|
|
||||||
Where("perfect", 1).
|
|
||||||
WhereIn("child_id", childIds).
|
|
||||||
Group("child_id").All()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type element struct{}
|
||||||
|
|
||||||
|
var Element = &element{}
|
||||||
|
|
||||||
|
// ListEnabledByIds 按 id 批量取启用元素(内容缓存)。
|
||||||
|
func (s *element) ListEnabledByIds(ctx context.Context, ids []int64) ([]gdb.Record, error) {
|
||||||
|
return dao.Element.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
WhereIn("id", ids).Where("status", consts.StatusEnabled).All()
|
||||||
|
}
|
||||||
+374
-19
@@ -2,12 +2,16 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
"github.com/gogf/gf/v2/errors/gerror"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
"36wisdom/biz/consts"
|
"36wisdom/biz/consts"
|
||||||
"36wisdom/biz/dao"
|
"36wisdom/biz/dao"
|
||||||
|
"36wisdom/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type level struct{}
|
type level struct{}
|
||||||
@@ -39,8 +43,8 @@ func LevelOf(perfectCount int) (level int, title string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Element 元素 VO(场景/人物/道具)。
|
// ElementVO 元素 VO(场景/人物/道具)。
|
||||||
type Element struct {
|
type ElementVO struct {
|
||||||
Id int64
|
Id int64
|
||||||
EType int
|
EType int
|
||||||
Name string
|
Name string
|
||||||
@@ -58,7 +62,7 @@ type Option struct {
|
|||||||
FeedbackPros string
|
FeedbackPros string
|
||||||
FeedbackCons string
|
FeedbackCons string
|
||||||
Audio string
|
Audio string
|
||||||
Prop *Element
|
Prop *ElementVO
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node 关卡节点 VO(决策/终局)。
|
// Node 关卡节点 VO(决策/终局)。
|
||||||
@@ -69,7 +73,7 @@ type Node struct {
|
|||||||
ContentPinyin string
|
ContentPinyin string
|
||||||
Image string
|
Image string
|
||||||
Audio string
|
Audio string
|
||||||
Character *Element
|
Character *ElementVO
|
||||||
InteractionType int
|
InteractionType int
|
||||||
Config string
|
Config string
|
||||||
Script string
|
Script string
|
||||||
@@ -82,7 +86,7 @@ type Node struct {
|
|||||||
type LevelDetail struct {
|
type LevelDetail struct {
|
||||||
LevelId int64
|
LevelId int64
|
||||||
Title string
|
Title string
|
||||||
Scene *Element
|
Scene *ElementVO
|
||||||
SceneContent string
|
SceneContent string
|
||||||
SceneContentPinyin string
|
SceneContentPinyin string
|
||||||
SceneImage string
|
SceneImage string
|
||||||
@@ -111,9 +115,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) (
|
|||||||
return nil, gerror.New("关卡不属于当前年龄段")
|
return nil, gerror.New("关卡不属于当前年龄段")
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeRecs, err := dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
nodeRecs, err := SceneNode.ListEnabledByLevel(ctx, levelId)
|
||||||
Where("level_id", levelId).Where("status", consts.StatusEnabled).
|
|
||||||
Order("sort_order ASC").All()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -124,9 +126,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) (
|
|||||||
|
|
||||||
optionRecs := []gdb.Record{}
|
optionRecs := []gdb.Record{}
|
||||||
if len(nodeIds) > 0 {
|
if len(nodeIds) > 0 {
|
||||||
optionRecs, err = dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
optionRecs, err = NodeOption.ListEnabledByNodeIds(ctx, nodeIds)
|
||||||
WhereIn("node_id", nodeIds).Where("status", consts.StatusEnabled).
|
|
||||||
Order("sort_order ASC").All()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -137,8 +137,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
progressRec, err := dao.UserProgress.Model().Ctx(ctx).
|
progressRec, err := UserProgress.GetByChildLevel(ctx, childId, levelId)
|
||||||
Where("child_id", childId).Where("level_id", levelId).One()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -173,7 +172,7 @@ func (s *level) Detail(ctx context.Context, parentUid, childId, levelId int64) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// elementsOf 汇总关卡涉及的场景/人物/道具元素,批量查询后按 id 索引。
|
// elementsOf 汇总关卡涉及的场景/人物/道具元素,批量查询后按 id 索引。
|
||||||
func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs []gdb.Record) (map[int64]*Element, error) {
|
func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs []gdb.Record) (map[int64]*ElementVO, error) {
|
||||||
ids := []int64{levelRec["scene_id"].Int64()}
|
ids := []int64{levelRec["scene_id"].Int64()}
|
||||||
for _, n := range nodeRecs {
|
for _, n := range nodeRecs {
|
||||||
if cid := n["character_id"].Int64(); cid > 0 {
|
if cid := n["character_id"].Int64(); cid > 0 {
|
||||||
@@ -187,17 +186,16 @@ func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs [
|
|||||||
}
|
}
|
||||||
ids = uniqueInt64(ids)
|
ids = uniqueInt64(ids)
|
||||||
|
|
||||||
m := make(map[int64]*Element, len(ids))
|
m := make(map[int64]*ElementVO, len(ids))
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
recs, err := dao.Element.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
recs, err := Element.ListEnabledByIds(ctx, ids)
|
||||||
WhereIn("id", ids).Where("status", consts.StatusEnabled).All()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, r := range recs {
|
for _, r := range recs {
|
||||||
m[r["id"].Int64()] = &Element{
|
m[r["id"].Int64()] = &ElementVO{
|
||||||
Id: r["id"].Int64(),
|
Id: r["id"].Int64(),
|
||||||
EType: r["e_type"].Int(),
|
EType: r["e_type"].Int(),
|
||||||
Name: r["name"].String(),
|
Name: r["name"].String(),
|
||||||
@@ -211,7 +209,7 @@ func elementsOf(ctx context.Context, levelRec gdb.Record, nodeRecs, optionRecs [
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildNode 组装单个节点的选项与人物/道具元素。
|
// buildNode 组装单个节点的选项与人物/道具元素。
|
||||||
func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]*Element) *Node {
|
func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]*ElementVO) *Node {
|
||||||
node := &Node{
|
node := &Node{
|
||||||
NodeId: nodeRec["id"].Int64(),
|
NodeId: nodeRec["id"].Int64(),
|
||||||
Title: nodeRec["title"].String(),
|
Title: nodeRec["title"].String(),
|
||||||
@@ -242,3 +240,360 @@ func buildNode(nodeRec gdb.Record, optionRecs []gdb.Record, elements map[int64]*
|
|||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SettleState 结算输入:该关历史状态(由进度 + 路径流水推导,锁内读取)。
|
||||||
|
type SettleState struct {
|
||||||
|
LevelCleared bool // 该关已通关(到过最佳终局)
|
||||||
|
FailStreak int // 连续失败终局次数(非失败终局打断连续)
|
||||||
|
ReachedFinals map[int64]bool // 已到达终局节点 id 集合
|
||||||
|
TotalFinals int // 该关终局节点总数
|
||||||
|
PerfectAwarded bool // 完美奖励是否已发放(进度表为准)
|
||||||
|
BalanceAfter int // 结算前余额
|
||||||
|
}
|
||||||
|
|
||||||
|
// FinalSettle 结算结果(service 层领域值,controller 映射为 dto)。
|
||||||
|
type FinalSettle struct {
|
||||||
|
ResultType int
|
||||||
|
Stars int
|
||||||
|
ScoreDelta int
|
||||||
|
Cleared bool
|
||||||
|
Perfect bool
|
||||||
|
UnlockNext bool
|
||||||
|
CollectionUnlocked bool
|
||||||
|
NewLevel int
|
||||||
|
BalanceAfter int
|
||||||
|
FinalNode *Node // 终局节点(含 script,v2.1 前端演绎结局台词后跳结算)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettleFinal 纯函数判分(技术设计 4.3/4.4,可单测):
|
||||||
|
// - 首次到达某终局才结算,重复到达 delta=0
|
||||||
|
// - 未通关:最佳 +30 / 良好 +10 / 失败 -10(连续失败 ≥2 次后不再扣;余额扣至 0 不为负)
|
||||||
|
// - 已通关后补分支:只记完成度不结算积分
|
||||||
|
// - 终局数集齐 → 完美(+20);最佳终局 → 通关
|
||||||
|
func SettleFinal(s SettleState, finalNodeId int64, resultType int) FinalSettle {
|
||||||
|
f := 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, parentUid, childId, levelId, nodeId, optionId int64) (*Node, *FinalSettle, error) {
|
||||||
|
child, err := getChildOf(ctx, parentUid, childId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
levelRec, err := dao.Level.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(levelId).One()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if levelRec.IsEmpty() || levelRec["status"].Int() != consts.StatusEnabled {
|
||||||
|
return nil, nil, gerror.New("关卡不存在")
|
||||||
|
}
|
||||||
|
if levelRec["age_group"].String() != child["age_group"].String() {
|
||||||
|
return nil, nil, gerror.New("关卡不属于当前年龄段")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeRec, err := SceneNode.GetInLevel(ctx, nodeId, levelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if nodeRec.IsEmpty() {
|
||||||
|
return nil, nil, gerror.New("节点不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
optionRec, err := NodeOption.GetInNode(ctx, optionId, nodeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if optionRec.IsEmpty() {
|
||||||
|
return nil, nil, gerror.New("选项不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
nextRec, err := SceneNode.GetInLevel(ctx, optionRec["next_node_id"].Int64(), levelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if nextRec.IsEmpty() {
|
||||||
|
return nil, nil, gerror.New("目标节点不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
progressRec, err := UserProgress.GetByChildLevel(ctx, childId, levelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !s.levelUnlocked(ctx, childId, child, levelRec, progressRec) {
|
||||||
|
return nil, nil, gerror.New("关卡未解锁")
|
||||||
|
}
|
||||||
|
|
||||||
|
if nextRec["result_type"].Int() == consts.ResultNone {
|
||||||
|
s.logRoute(ctx, childId, levelId, nodeId, optionId, consts.ResultNone)
|
||||||
|
node, err := nodeOf(ctx, levelRec, nextRec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return node, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// v2.1:终局节点剧本先于结算构建(只读查询失败则未结算,无副作用)
|
||||||
|
finalNode, err := nodeOf(ctx, levelRec, nextRec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
// 终局:路径流水在锁内结算后记录,避免状态读到本次到达导致重复结算误判
|
||||||
|
settle, err := common.WithLock(ctx, fmt.Sprintf("child:%d:level:%d", childId, levelId), 10*time.Second, 3, 200*time.Millisecond, func() (*FinalSettle, error) {
|
||||||
|
return s.settle(ctx, child, levelRec, nextRec, nodeId, optionId)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
settle.FinalNode = finalNode
|
||||||
|
return nil, settle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// levelUnlocked 关卡解锁判定:本关已通关可重玩,否则按解锁链校验计策解锁。
|
||||||
|
func (s *level) levelUnlocked(ctx context.Context, childId int64, child gdb.Record, levelRec, progressRec gdb.Record) bool {
|
||||||
|
if progressRec["stars"].Int() >= 3 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
strategyRec, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
WherePri(levelRec["strategy_id"].Int64()).One()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
unlocked, _ := Strategy.unlockState(ctx, childId, child["age_group"].String(), strategyRec)
|
||||||
|
return unlocked
|
||||||
|
}
|
||||||
|
|
||||||
|
// logRoute 路径流水(尽力而为,写失败不阻断主流程)。
|
||||||
|
func (s *level) logRoute(ctx context.Context, childId, levelId, nodeId, optionId int64, resultType int) {
|
||||||
|
_ = UserRouteLog.Append(ctx, childId, levelId, nodeId, optionId, resultType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nodeOf 组装下一决策节点(选项 + 元素)。
|
||||||
|
func nodeOf(ctx context.Context, levelRec, nodeRec gdb.Record) (*Node, error) {
|
||||||
|
optionRecs, err := NodeOption.ListEnabledByNode(ctx, nodeRec["id"].Int64())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
elements, err := elementsOf(ctx, levelRec, []gdb.Record{nodeRec}, optionRecs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buildNode(nodeRec, optionRecs, elements), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// settle 终局结算(调用方持锁):锁内重读状态 → 纯函数判分 → 事务写库 → 记录本次终局路径 → 派生结果。
|
||||||
|
func (s *level) settle(ctx context.Context, child gdb.Record, levelRec, finalRec gdb.Record, nodeId, optionId int64) (*FinalSettle, error) {
|
||||||
|
childId := child["id"].Int64()
|
||||||
|
levelId := levelRec["id"].Int64()
|
||||||
|
|
||||||
|
state, err := s.settleState(ctx, childId, levelId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f := SettleFinal(state, finalRec["id"].Int64(), finalRec["result_type"].Int())
|
||||||
|
|
||||||
|
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["result_type"].Int())
|
||||||
|
s.deriveExtras(ctx, child, levelRec, &f)
|
||||||
|
return &f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// settleState 锁内重读该关历史状态:终局数、已到达终局、连续失败、余额。
|
||||||
|
func (s *level) settleState(ctx context.Context, childId, levelId int64) (SettleState, error) {
|
||||||
|
state := SettleState{ReachedFinals: map[int64]bool{}}
|
||||||
|
|
||||||
|
progressRec, err := UserProgress.GetByChildLevel(ctx, childId, levelId)
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
state.LevelCleared = progressRec["stars"].Int() >= 3
|
||||||
|
state.PerfectAwarded = progressRec["perfect"].Int() == 1
|
||||||
|
|
||||||
|
childRec, err := dao.Child.Model().Ctx(ctx).WherePri(childId).One()
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
state.BalanceAfter = childRec["points"].Int()
|
||||||
|
|
||||||
|
totalFinals, err := SceneNode.CountFinalsByLevel(ctx, levelId)
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
state.TotalFinals = totalFinals
|
||||||
|
|
||||||
|
logs, err := 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["option_id"].Int64())
|
||||||
|
}
|
||||||
|
nextOf := map[int64]int64{}
|
||||||
|
if len(optionIds) > 0 {
|
||||||
|
opts, err := NodeOption.ListByOptionIds(ctx, optionIds)
|
||||||
|
if err != nil {
|
||||||
|
return state, err
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
nextOf[o["id"].Int64()] = o["next_node_id"].Int64()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, l := range logs {
|
||||||
|
if nid, ok := nextOf[l["option_id"].Int64()]; ok {
|
||||||
|
state.ReachedFinals[nid] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := len(logs) - 1; i >= 0; i-- {
|
||||||
|
if logs[i]["result_type"].Int() == consts.ResultFail {
|
||||||
|
state.FailStreak++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitSettle 结算事务:进度(最高星 + 完美标记 + 内容版本)、积分流水与余额、计策卡。
|
||||||
|
func (s *level) commitSettle(ctx context.Context, childId int64, levelRec gdb.Record, f *FinalSettle) error {
|
||||||
|
levelId := levelRec["id"].Int64()
|
||||||
|
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 := UserProgress.UpsertInTx(ctx, tx, childId, levelId, stars, perfect, levelRec["content_version"].Int()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.ScoreDelta != 0 {
|
||||||
|
childRec, err := tx.Model(consts.TableChild).Ctx(ctx).WherePri(childId).One()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
balance := childRec["points"].Int() + 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 gdb.Record, f *FinalSettle) error {
|
||||||
|
if !f.Perfect {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
strategyId := levelRec["strategy_id"].Int64()
|
||||||
|
levels, err := tx.Model(consts.TableLevel).Ctx(ctx).
|
||||||
|
Where("strategy_id", strategyId).Where("status", consts.StatusEnabled).All()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, lv := range levels {
|
||||||
|
rec, err := UserProgress.GetInTx(ctx, tx, childId, lv["id"].Int64())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rec.IsEmpty() || rec["perfect"].Int() != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exists, err := UserCollection.ExistsInTx(ctx, tx, childId, strategyId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err = UserCollection.InsertInTx(ctx, tx, childId, strategyId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.CollectionUnlocked = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deriveExtras 结算后派生:计策卡解锁提示、下一计解锁、成长等级。
|
||||||
|
func (s *level) deriveExtras(ctx context.Context, child gdb.Record, levelRec gdb.Record, f *FinalSettle) {
|
||||||
|
childId := child["id"].Int64()
|
||||||
|
|
||||||
|
if f.CollectionUnlocked {
|
||||||
|
if next, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("unlock_before", levelRec["strategy_id"].Int64()).One(); err == nil && !next.IsEmpty() {
|
||||||
|
f.UnlockNext = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
before, err := 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,428 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/database/gdb"
|
|
||||||
"github.com/gogf/gf/v2/errors/gerror"
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
|
||||||
|
|
||||||
"36wisdom/biz/consts"
|
|
||||||
"36wisdom/biz/dao"
|
|
||||||
"36wisdom/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type levelPlay struct{}
|
|
||||||
|
|
||||||
var LevelPlay = &levelPlay{}
|
|
||||||
|
|
||||||
// SettleState 结算输入:该关历史状态(由进度 + 路径流水推导,锁内读取)。
|
|
||||||
type SettleState struct {
|
|
||||||
LevelCleared bool // 该关已通关(到过最佳终局)
|
|
||||||
FailStreak int // 连续失败终局次数(非失败终局打断连续)
|
|
||||||
ReachedFinals map[int64]bool // 已到达终局节点 id 集合
|
|
||||||
TotalFinals int // 该关终局节点总数
|
|
||||||
PerfectAwarded bool // 完美奖励是否已发放(进度表为准)
|
|
||||||
BalanceAfter int // 结算前余额
|
|
||||||
}
|
|
||||||
|
|
||||||
// FinalSettle 结算结果(service 层领域值,controller 映射为 dto)。
|
|
||||||
type FinalSettle struct {
|
|
||||||
ResultType int
|
|
||||||
Stars int
|
|
||||||
ScoreDelta int
|
|
||||||
Cleared bool
|
|
||||||
Perfect bool
|
|
||||||
UnlockNext bool
|
|
||||||
CollectionUnlocked bool
|
|
||||||
NewLevel int
|
|
||||||
BalanceAfter int
|
|
||||||
FinalNode *Node // 终局节点(含 script,v2.1 前端演绎结局台词后跳结算)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SettleFinal 纯函数判分(技术设计 4.3/4.4,可单测):
|
|
||||||
// - 首次到达某终局才结算,重复到达 delta=0
|
|
||||||
// - 未通关:最佳 +30 / 良好 +10 / 失败 -10(连续失败 ≥2 次后不再扣;余额扣至 0 不为负)
|
|
||||||
// - 已通关后补分支:只记完成度不结算积分
|
|
||||||
// - 终局数集齐 → 完美(+20);最佳终局 → 通关
|
|
||||||
func SettleFinal(s SettleState, finalNodeId int64, resultType int) FinalSettle {
|
|
||||||
f := 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 *levelPlay) Choose(ctx context.Context, parentUid, childId, levelId, nodeId, optionId int64) (*Node, *FinalSettle, error) {
|
|
||||||
child, err := getChildOf(ctx, parentUid, childId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
levelRec, err := dao.Level.Model().Ctx(ctx).Cache(contentCache(ctx)).WherePri(levelId).One()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if levelRec.IsEmpty() || levelRec["status"].Int() != consts.StatusEnabled {
|
|
||||||
return nil, nil, gerror.New("关卡不存在")
|
|
||||||
}
|
|
||||||
if levelRec["age_group"].String() != child["age_group"].String() {
|
|
||||||
return nil, nil, gerror.New("关卡不属于当前年龄段")
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeRec, err := dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
|
||||||
Where("id", nodeId).Where("level_id", levelId).Where("status", consts.StatusEnabled).One()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if nodeRec.IsEmpty() {
|
|
||||||
return nil, nil, gerror.New("节点不存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
optionRec, err := dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
|
||||||
Where("id", optionId).Where("node_id", nodeId).Where("status", consts.StatusEnabled).One()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if optionRec.IsEmpty() {
|
|
||||||
return nil, nil, gerror.New("选项不存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
nextRec, err := dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
|
||||||
Where("id", optionRec["next_node_id"].Int64()).Where("level_id", levelId).Where("status", consts.StatusEnabled).One()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if nextRec.IsEmpty() {
|
|
||||||
return nil, nil, gerror.New("目标节点不存在")
|
|
||||||
}
|
|
||||||
|
|
||||||
progressRec, err := dao.UserProgress.Model().Ctx(ctx).
|
|
||||||
Where("child_id", childId).Where("level_id", levelId).One()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if !s.levelUnlocked(ctx, childId, child, levelRec, progressRec) {
|
|
||||||
return nil, nil, gerror.New("关卡未解锁")
|
|
||||||
}
|
|
||||||
|
|
||||||
if nextRec["result_type"].Int() == consts.ResultNone {
|
|
||||||
s.logRoute(ctx, childId, levelId, nodeId, optionId, consts.ResultNone)
|
|
||||||
node, err := nodeOf(ctx, levelRec, nextRec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return node, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// v2.1:终局节点剧本先于结算构建(只读查询失败则未结算,无副作用)
|
|
||||||
finalNode, err := nodeOf(ctx, levelRec, nextRec)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
// 终局:路径流水在锁内结算后记录,避免状态读到本次到达导致重复结算误判
|
|
||||||
settle, err := common.WithLock(ctx, fmt.Sprintf("child:%d:level:%d", childId, levelId), 10*time.Second, 3, 200*time.Millisecond, func() (*FinalSettle, error) {
|
|
||||||
return s.settle(ctx, child, levelRec, nextRec, nodeId, optionId)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
settle.FinalNode = finalNode
|
|
||||||
return nil, settle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// levelUnlocked 关卡解锁判定:本关已通关可重玩,否则按解锁链校验计策解锁。
|
|
||||||
func (s *levelPlay) levelUnlocked(ctx context.Context, childId int64, child gdb.Record, levelRec, progressRec gdb.Record) bool {
|
|
||||||
if progressRec["stars"].Int() >= 3 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
strategyRec, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
|
||||||
WherePri(levelRec["strategy_id"].Int64()).One()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
unlocked, _ := Strategy.unlockState(ctx, childId, child["age_group"].String(), strategyRec)
|
|
||||||
return unlocked
|
|
||||||
}
|
|
||||||
|
|
||||||
// logRoute 路径流水(尽力而为,写失败不阻断主流程)。
|
|
||||||
func (s *levelPlay) logRoute(ctx context.Context, childId, levelId, nodeId, optionId int64, resultType int) {
|
|
||||||
_, _ = dao.UserRouteLog.InsertAndReturnId(ctx, g.Map{
|
|
||||||
"child_id": childId,
|
|
||||||
"level_id": levelId,
|
|
||||||
"node_id": nodeId,
|
|
||||||
"option_id": optionId,
|
|
||||||
"result_type": resultType,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// nodeOf 组装下一决策节点(选项 + 元素)。
|
|
||||||
func nodeOf(ctx context.Context, levelRec, nodeRec gdb.Record) (*Node, error) {
|
|
||||||
optionRecs, err := dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
|
||||||
Where("node_id", nodeRec["id"].Int64()).Where("status", consts.StatusEnabled).
|
|
||||||
Order("sort_order ASC").All()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
elements, err := elementsOf(ctx, levelRec, []gdb.Record{nodeRec}, optionRecs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buildNode(nodeRec, optionRecs, elements), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// settle 终局结算(调用方持锁):锁内重读状态 → 纯函数判分 → 事务写库 → 记录本次终局路径 → 派生结果。
|
|
||||||
func (s *levelPlay) settle(ctx context.Context, child gdb.Record, levelRec, finalRec gdb.Record, nodeId, optionId int64) (*FinalSettle, error) {
|
|
||||||
childId := child["id"].Int64()
|
|
||||||
levelId := levelRec["id"].Int64()
|
|
||||||
|
|
||||||
state, err := s.settleState(ctx, childId, levelId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
f := SettleFinal(state, finalRec["id"].Int64(), finalRec["result_type"].Int())
|
|
||||||
|
|
||||||
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["result_type"].Int())
|
|
||||||
s.deriveExtras(ctx, child, levelRec, &f)
|
|
||||||
return &f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// settleState 锁内重读该关历史状态:终局数、已到达终局、连续失败、余额。
|
|
||||||
func (s *levelPlay) settleState(ctx context.Context, childId, levelId int64) (SettleState, error) {
|
|
||||||
state := SettleState{ReachedFinals: map[int64]bool{}}
|
|
||||||
|
|
||||||
progressRec, err := dao.UserProgress.Model().Ctx(ctx).
|
|
||||||
Where("child_id", childId).Where("level_id", levelId).One()
|
|
||||||
if err != nil {
|
|
||||||
return state, err
|
|
||||||
}
|
|
||||||
state.LevelCleared = progressRec["stars"].Int() >= 3
|
|
||||||
state.PerfectAwarded = progressRec["perfect"].Int() == 1
|
|
||||||
|
|
||||||
childRec, err := dao.Child.Model().Ctx(ctx).WherePri(childId).One()
|
|
||||||
if err != nil {
|
|
||||||
return state, err
|
|
||||||
}
|
|
||||||
state.BalanceAfter = childRec["points"].Int()
|
|
||||||
|
|
||||||
nodeRecs, err := dao.SceneNode.Model().Ctx(ctx).
|
|
||||||
Where("level_id", levelId).Where("status", consts.StatusEnabled).All()
|
|
||||||
if err != nil {
|
|
||||||
return state, err
|
|
||||||
}
|
|
||||||
for _, n := range nodeRecs {
|
|
||||||
if n["result_type"].Int() > 0 {
|
|
||||||
state.TotalFinals++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logs, err := dao.UserRouteLog.Model().Ctx(ctx).
|
|
||||||
Where("child_id", childId).Where("level_id", levelId).
|
|
||||||
WhereGT("result_type", 0).Order("id ASC").All()
|
|
||||||
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["option_id"].Int64())
|
|
||||||
}
|
|
||||||
nextOf := map[int64]int64{}
|
|
||||||
if len(optionIds) > 0 {
|
|
||||||
opts, err := dao.NodeOption.Model().Ctx(ctx).WhereIn("id", optionIds).All()
|
|
||||||
if err != nil {
|
|
||||||
return state, err
|
|
||||||
}
|
|
||||||
for _, o := range opts {
|
|
||||||
nextOf[o["id"].Int64()] = o["next_node_id"].Int64()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, l := range logs {
|
|
||||||
if nid, ok := nextOf[l["option_id"].Int64()]; ok {
|
|
||||||
state.ReachedFinals[nid] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := len(logs) - 1; i >= 0; i-- {
|
|
||||||
if logs[i]["result_type"].Int() == consts.ResultFail {
|
|
||||||
state.FailStreak++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// commitSettle 结算事务:进度(最高星 + 完美标记 + 内容版本)、积分流水与余额、计策卡。
|
|
||||||
func (s *levelPlay) commitSettle(ctx context.Context, childId int64, levelRec gdb.Record, f *FinalSettle) error {
|
|
||||||
levelId := levelRec["id"].Int64()
|
|
||||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
||||||
progressRec, err := tx.Model(consts.TableUserProgress).Ctx(ctx).
|
|
||||||
Where("child_id", childId).Where("level_id", levelId).One()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stars := f.Stars
|
|
||||||
perfect := 0
|
|
||||||
if f.Perfect {
|
|
||||||
perfect = 1
|
|
||||||
}
|
|
||||||
if !progressRec.IsEmpty() {
|
|
||||||
if stars < progressRec["stars"].Int() {
|
|
||||||
stars = progressRec["stars"].Int()
|
|
||||||
}
|
|
||||||
perfect = progressRec["perfect"].Int() | perfect
|
|
||||||
}
|
|
||||||
progressData := g.Map{
|
|
||||||
"stars": stars,
|
|
||||||
"perfect": perfect,
|
|
||||||
"content_version": levelRec["content_version"].Int(),
|
|
||||||
"completed_at": gtime.Now(),
|
|
||||||
}
|
|
||||||
if progressRec.IsEmpty() {
|
|
||||||
progressData["child_id"] = childId
|
|
||||||
progressData["level_id"] = levelId
|
|
||||||
_, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(progressData).Insert()
|
|
||||||
} else {
|
|
||||||
_, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(progressData).
|
|
||||||
Where("child_id", childId).Where("level_id", levelId).Update()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.ScoreDelta != 0 {
|
|
||||||
childRec, err := tx.Model(consts.TableChild).Ctx(ctx).WherePri(childId).One()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
balance := childRec["points"].Int() + 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 *levelPlay) collectIfAllPerfect(ctx context.Context, tx gdb.TX, childId int64, levelRec gdb.Record, f *FinalSettle) error {
|
|
||||||
if !f.Perfect {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
strategyId := levelRec["strategy_id"].Int64()
|
|
||||||
levels, err := tx.Model(consts.TableLevel).Ctx(ctx).
|
|
||||||
Where("strategy_id", strategyId).Where("status", consts.StatusEnabled).All()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, lv := range levels {
|
|
||||||
rec, err := tx.Model(consts.TableUserProgress).Ctx(ctx).
|
|
||||||
Where("child_id", childId).Where("level_id", lv["id"].Int64()).One()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if rec.IsEmpty() || rec["perfect"].Int() != 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exists, err := tx.Model(consts.TableUserCollection).Ctx(ctx).
|
|
||||||
Where("user_id", childId).Where("strategy_id", strategyId).Count()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exists > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err = tx.Model(consts.TableUserCollection).Ctx(ctx).Data(g.Map{
|
|
||||||
"user_id": childId,
|
|
||||||
"strategy_id": strategyId,
|
|
||||||
}).Insert(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.CollectionUnlocked = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deriveExtras 结算后派生:计策卡解锁提示、下一计解锁、成长等级。
|
|
||||||
func (s *levelPlay) deriveExtras(ctx context.Context, child gdb.Record, levelRec gdb.Record, f *FinalSettle) {
|
|
||||||
childId := child["id"].Int64()
|
|
||||||
|
|
||||||
if f.CollectionUnlocked {
|
|
||||||
if next, err := dao.Strategy.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
|
||||||
Where("unlock_before", levelRec["strategy_id"].Int64()).One(); err == nil && !next.IsEmpty() {
|
|
||||||
f.UnlockNext = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
before, err := dao.UserProgress.Model().Ctx(ctx).
|
|
||||||
Where("child_id", childId).Where("perfect", 1).Count()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
after := before
|
|
||||||
if f.Perfect {
|
|
||||||
after++
|
|
||||||
}
|
|
||||||
oldLevel, _ := LevelOf(before)
|
|
||||||
newLevel, _ := LevelOf(after)
|
|
||||||
if oldLevel != newLevel {
|
|
||||||
f.NewLevel = newLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type lifeTask struct{}
|
||||||
|
|
||||||
|
var LifeTask = &lifeTask{}
|
||||||
|
|
||||||
|
// GetByPk 生活践行任务。
|
||||||
|
func (s *lifeTask) GetByPk(ctx context.Context, id int64) (gdb.Record, error) {
|
||||||
|
return dao.LifeTask.GetOneByPk(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnabled 启用任务(内容缓存)。
|
||||||
|
func (s *lifeTask) ListEnabled(ctx context.Context) ([]gdb.Record, error) {
|
||||||
|
return dao.LifeTask.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("status", consts.StatusEnabled).Order("id ASC").All()
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodeOption struct{}
|
||||||
|
|
||||||
|
var NodeOption = &nodeOption{}
|
||||||
|
|
||||||
|
// GetInNode 节点下启用选项(按选项 id + 节点 id 精确查询)。
|
||||||
|
func (s *nodeOption) GetInNode(ctx context.Context, optionId, nodeId int64) (gdb.Record, error) {
|
||||||
|
return dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("id", optionId).Where("node_id", nodeId).Where("status", consts.StatusEnabled).One()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnabledByNode 节点下启用选项,按 sort_order 升序(内容缓存)。
|
||||||
|
func (s *nodeOption) ListEnabledByNode(ctx context.Context, nodeId int64) ([]gdb.Record, error) {
|
||||||
|
return dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("node_id", nodeId).Where("status", consts.StatusEnabled).
|
||||||
|
Order("sort_order ASC").All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnabledByNodeIds 批量节点下启用选项,按 sort_order 升序(内容缓存)。
|
||||||
|
func (s *nodeOption) ListEnabledByNodeIds(ctx context.Context, nodeIds []int64) ([]gdb.Record, error) {
|
||||||
|
return dao.NodeOption.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
WhereIn("node_id", nodeIds).Where("status", consts.StatusEnabled).
|
||||||
|
Order("sort_order ASC").All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByOptionIds 按选项 id 批量取(终局判定用,不缓存)。
|
||||||
|
func (s *nodeOption) ListByOptionIds(ctx context.Context, optionIds []int64) ([]gdb.Record, error) {
|
||||||
|
return dao.NodeOption.Model().Ctx(ctx).WhereIn("id", optionIds).All()
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"36wisdom/biz/consts"
|
"36wisdom/biz/consts"
|
||||||
"36wisdom/biz/dao"
|
"36wisdom/biz/dao"
|
||||||
|
"36wisdom/common/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type parent struct{}
|
type parent struct{}
|
||||||
@@ -38,7 +39,7 @@ func (s *parent) Register(ctx context.Context, phone, password, nickname string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
token, err = Auth.GenerateToken(ctx, parentId, consts.RoleParent)
|
token, err = auth.GenerateToken(auth.Secret(ctx), parentId, consts.RoleParent, consts.AuthExpireSeconds)
|
||||||
return parentId, token, err
|
return parentId, token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +56,6 @@ func (s *parent) Login(ctx context.Context, phone, password string) (parentId in
|
|||||||
return 0, "", gerror.New("手机号或密码错误")
|
return 0, "", gerror.New("手机号或密码错误")
|
||||||
}
|
}
|
||||||
parentId = rec["id"].Int64()
|
parentId = rec["id"].Int64()
|
||||||
token, err = Auth.GenerateToken(ctx, parentId, consts.RoleParent)
|
token, err = auth.GenerateToken(auth.Secret(ctx), parentId, consts.RoleParent, consts.AuthExpireSeconds)
|
||||||
return parentId, token, err
|
return parentId, token, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type prize struct{}
|
||||||
|
|
||||||
|
var Prize = &prize{}
|
||||||
|
|
||||||
|
// GetByPk 奖品定义。
|
||||||
|
func (s *prize) GetByPk(ctx context.Context, id int64) (gdb.Record, error) {
|
||||||
|
return dao.Prize.GetOneByPk(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnabled 可兑换奖品(启用态,按 sort_order 升序,内容缓存)。
|
||||||
|
func (s *prize) ListEnabled(ctx context.Context) ([]gdb.Record, error) {
|
||||||
|
return dao.Prize.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("status", consts.StatusEnabled).Order("sort_order ASC").All()
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type redemption struct{}
|
||||||
|
|
||||||
|
var Redemption = &redemption{}
|
||||||
|
|
||||||
|
// ListByUser 孩子兑换记录(按创建时间倒序)。
|
||||||
|
func (s *redemption) ListByUser(ctx context.Context, userId int64) ([]gdb.Record, error) {
|
||||||
|
return dao.Redemption.Model().Ctx(ctx).Where("user_id", userId).Order("id DESC").All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建兑换记录。
|
||||||
|
func (s *redemption) Insert(ctx context.Context, userId, prizeId int64, pointsCost int) (int64, error) {
|
||||||
|
return dao.Redemption.InsertAndReturnId(ctx, g.Map{
|
||||||
|
"user_id": userId,
|
||||||
|
"prize_id": prizeId,
|
||||||
|
"points_cost": pointsCost,
|
||||||
|
"status": 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sceneNode struct{}
|
||||||
|
|
||||||
|
var SceneNode = &sceneNode{}
|
||||||
|
|
||||||
|
// GetInLevel 关卡内启用节点(按节点 id + 关卡 id 精确查询)。
|
||||||
|
func (s *sceneNode) GetInLevel(ctx context.Context, nodeId, levelId int64) (gdb.Record, error) {
|
||||||
|
return dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("id", nodeId).Where("level_id", levelId).Where("status", consts.StatusEnabled).One()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEnabledByLevel 关卡内启用节点,按 sort_order 升序(内容缓存)。
|
||||||
|
func (s *sceneNode) ListEnabledByLevel(ctx context.Context, levelId int64) ([]gdb.Record, error) {
|
||||||
|
return dao.SceneNode.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
||||||
|
Where("level_id", levelId).Where("status", consts.StatusEnabled).
|
||||||
|
Order("sort_order ASC").All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountFinalsByLevel 关卡内终局节点数(result_type > 0,不缓存)。
|
||||||
|
func (s *sceneNode) CountFinalsByLevel(ctx context.Context, levelId int64) (int, error) {
|
||||||
|
return dao.SceneNode.Model().Ctx(ctx).
|
||||||
|
Where("level_id", levelId).Where("status", consts.StatusEnabled).WhereGT("result_type", 0).Count()
|
||||||
|
}
|
||||||
@@ -265,9 +265,7 @@ func (s *strategy) progressOfLevels(ctx context.Context, childId int64, levelIds
|
|||||||
if len(levelIds) == 0 {
|
if len(levelIds) == 0 {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
recs, err := dao.UserProgress.Model().Ctx(ctx).
|
recs, err := UserProgress.ListByChildLevelIds(ctx, childId, levelIds)
|
||||||
Where("child_id", childId).
|
|
||||||
WhereIn("level_id", levelIds).All()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -284,8 +282,7 @@ func (s *strategy) elementNames(ctx context.Context, ids []int64) (map[int64]str
|
|||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
recs, err := dao.Element.Model().Ctx(ctx).Cache(contentCache(ctx)).
|
recs, err := Element.ListEnabledByIds(ctx, ids)
|
||||||
WhereIn("id", ids).Where("status", consts.StatusEnabled).All()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userBadge struct{}
|
||||||
|
|
||||||
|
var UserBadge = &userBadge{}
|
||||||
|
|
||||||
|
// ListByUser 孩子已获徽章。
|
||||||
|
func (s *userBadge) ListByUser(ctx context.Context, userId int64) ([]gdb.Record, error) {
|
||||||
|
return dao.UserBadge.Model().Ctx(ctx).Where("user_id", userId).Order("id ASC").All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 记录孩子获得徽章。
|
||||||
|
func (s *userBadge) Insert(ctx context.Context, userId, badgeId int64) (int64, error) {
|
||||||
|
return dao.UserBadge.InsertAndReturnId(ctx, g.Map{
|
||||||
|
"user_id": userId,
|
||||||
|
"badge_id": badgeId,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userCollection struct{}
|
||||||
|
|
||||||
|
var UserCollection = &userCollection{}
|
||||||
|
|
||||||
|
// ExistsInTx 事务内判断孩子是否已收集该计策。
|
||||||
|
func (s *userCollection) ExistsInTx(ctx context.Context, tx gdb.TX, userId, strategyId int64) (bool, error) {
|
||||||
|
n, err := tx.Model(consts.TableUserCollection).Ctx(ctx).
|
||||||
|
Where("user_id", userId).Where("strategy_id", strategyId).Count()
|
||||||
|
return n > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertInTx 事务内写入计策卡收集。
|
||||||
|
func (s *userCollection) InsertInTx(ctx context.Context, tx gdb.TX, userId, strategyId int64) error {
|
||||||
|
_, err := tx.Model(consts.TableUserCollection).Ctx(ctx).Data(g.Map{
|
||||||
|
"user_id": userId,
|
||||||
|
"strategy_id": strategyId,
|
||||||
|
}).Insert()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByUser 孩子已收集计策列表。
|
||||||
|
func (s *userCollection) ListByUser(ctx context.Context, userId int64) ([]gdb.Record, error) {
|
||||||
|
return dao.UserCollection.Model().Ctx(ctx).Where("user_id", userId).All()
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
|
|
||||||
|
"36wisdom/biz/consts"
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userProgress struct{}
|
||||||
|
|
||||||
|
var UserProgress = &userProgress{}
|
||||||
|
|
||||||
|
// GetByChildLevel 孩子某关进度(不缓存,随闯关更新)。
|
||||||
|
func (s *userProgress) GetByChildLevel(ctx context.Context, childId, levelId int64) (gdb.Record, error) {
|
||||||
|
return dao.UserProgress.Model().Ctx(ctx).
|
||||||
|
Where("child_id", childId).Where("level_id", levelId).One()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByChildLevelIds 孩子多关进度(不缓存)。
|
||||||
|
func (s *userProgress) ListByChildLevelIds(ctx context.Context, childId int64, levelIds []int64) ([]gdb.Record, error) {
|
||||||
|
return dao.UserProgress.Model().Ctx(ctx).
|
||||||
|
Where("child_id", childId).
|
||||||
|
WhereIn("level_id", levelIds).All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountPerfectByChild 孩子完美通关关卡数。
|
||||||
|
func (s *userProgress) CountPerfectByChild(ctx context.Context, childId int64) (int, error) {
|
||||||
|
return dao.UserProgress.Model().Ctx(ctx).
|
||||||
|
Where("child_id", childId).Where("perfect", 1).Count()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountPerfectByChildIds 批量孩子的完美通关数(按 child_id 分组)。
|
||||||
|
func (s *userProgress) CountPerfectByChildIds(ctx context.Context, childIds []int64) ([]gdb.Record, error) {
|
||||||
|
return dao.UserProgress.Model().Ctx(ctx).
|
||||||
|
Fields("child_id", "COUNT(*) AS cnt").
|
||||||
|
Where("perfect", 1).
|
||||||
|
WhereIn("child_id", childIds).
|
||||||
|
Group("child_id").All()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInTx 事务内读孩子某关进度。
|
||||||
|
func (s *userProgress) GetInTx(ctx context.Context, tx gdb.TX, childId, levelId int64) (gdb.Record, error) {
|
||||||
|
return tx.Model(consts.TableUserProgress).Ctx(ctx).
|
||||||
|
Where("child_id", childId).Where("level_id", levelId).One()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertInTx 事务内合并写入进度:星星取历史最高、完美取并集;不存在则插入。
|
||||||
|
func (s *userProgress) UpsertInTx(ctx context.Context, tx gdb.TX, childId, levelId int64, stars, perfect, contentVersion int) error {
|
||||||
|
rec, err := tx.Model(consts.TableUserProgress).Ctx(ctx).
|
||||||
|
Where("child_id", childId).Where("level_id", levelId).One()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stars < rec["stars"].Int() {
|
||||||
|
stars = rec["stars"].Int()
|
||||||
|
}
|
||||||
|
perfect = rec["perfect"].Int() | perfect
|
||||||
|
data := g.Map{
|
||||||
|
"stars": stars,
|
||||||
|
"perfect": perfect,
|
||||||
|
"content_version": contentVersion,
|
||||||
|
"completed_at": gtime.Now(),
|
||||||
|
}
|
||||||
|
if rec.IsEmpty() {
|
||||||
|
data["child_id"] = childId
|
||||||
|
data["level_id"] = levelId
|
||||||
|
_, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(data).Insert()
|
||||||
|
} else {
|
||||||
|
_, err = tx.Model(consts.TableUserProgress).Ctx(ctx).Data(data).
|
||||||
|
Where("child_id", childId).Where("level_id", levelId).Update()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/database/gdb"
|
||||||
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
|
||||||
|
"36wisdom/biz/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type userRouteLog struct{}
|
||||||
|
|
||||||
|
var UserRouteLog = &userRouteLog{}
|
||||||
|
|
||||||
|
// Append 追加一条决策路径流水。
|
||||||
|
func (s *userRouteLog) Append(ctx context.Context, childId, levelId, nodeId, optionId int64, resultType int) error {
|
||||||
|
_, err := dao.UserRouteLog.InsertAndReturnId(ctx, g.Map{
|
||||||
|
"child_id": childId,
|
||||||
|
"level_id": levelId,
|
||||||
|
"node_id": nodeId,
|
||||||
|
"option_id": optionId,
|
||||||
|
"result_type": resultType,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFinalsByChildLevel 孩子某关的终局路径(result_type > 0,按 id 升序,不缓存)。
|
||||||
|
func (s *userRouteLog) ListFinalsByChildLevel(ctx context.Context, childId, levelId int64) ([]gdb.Record, error) {
|
||||||
|
return dao.UserRouteLog.Model().Ctx(ctx).
|
||||||
|
Where("child_id", childId).Where("level_id", levelId).
|
||||||
|
WhereGT("result_type", 0).Order("id ASC").All()
|
||||||
|
}
|
||||||
@@ -31,6 +31,11 @@ func GenerateToken(secret string, uid int64, role string, expireSeconds int) (st
|
|||||||
return claims.SignedString([]byte(secret))
|
return claims.SignedString([]byte(secret))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Secret 鉴权密钥(config.yml auth.secret),token 签发与解析共用入口。
|
||||||
|
func Secret(ctx context.Context) string {
|
||||||
|
return g.Cfg().MustGet(ctx, "auth.secret").String()
|
||||||
|
}
|
||||||
|
|
||||||
// ParseToken 校验并解析 token,返回 uid 与 role。
|
// ParseToken 校验并解析 token,返回 uid 与 role。
|
||||||
func ParseToken(secret, tokenString string) (uid int64, role string, err error) {
|
func ParseToken(secret, tokenString string) (uid int64, role string, err error) {
|
||||||
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/net/ghttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MiddlewareNoiseSilence 静默浏览器/DevTools 噪音探测路径(favicon、well-known),避免访问日志刷 404
|
||||||
|
func MiddlewareNoiseSilence(r *ghttp.Request) {
|
||||||
|
p := r.URL.Path
|
||||||
|
if p == "/favicon.ico" || strings.HasPrefix(p, "/.well-known/") {
|
||||||
|
r.Response.WriteHeader(http.StatusNoContent)
|
||||||
|
r.Exit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Middleware.Next()
|
||||||
|
}
|
||||||
@@ -7,11 +7,14 @@ import (
|
|||||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
|
"github.com/gogf/gf/v2/net/ghttp"
|
||||||
"github.com/gogf/gf/v2/os/gctx"
|
"github.com/gogf/gf/v2/os/gctx"
|
||||||
|
|
||||||
"36wisdom/biz/controller"
|
"36wisdom/biz/controller"
|
||||||
"36wisdom/biz/dao"
|
"36wisdom/biz/dao"
|
||||||
"36wisdom/biz/service/seed"
|
"36wisdom/biz/service/seed"
|
||||||
|
"36wisdom/common"
|
||||||
|
"36wisdom/common/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -21,11 +24,28 @@ func main() {
|
|||||||
g.Log().Fatal(ctx, err)
|
g.Log().Fatal(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dao.InitTables(ctx)
|
initTables(ctx)
|
||||||
seed.EnsureSeeded(ctx)
|
seed.EnsureSeeded(ctx)
|
||||||
|
|
||||||
s := g.Server()
|
s := g.Server()
|
||||||
controller.Register(s)
|
// 路由注册与中间件装配(静态,不属业务表分层)
|
||||||
|
s.Use(common.MiddlewareNoiseSilence)
|
||||||
|
s.Use(ghttp.MiddlewareHandlerResponse)
|
||||||
|
s.BindHandler("GET:/ping", func(r *ghttp.Request) {
|
||||||
|
r.Response.Write("pong")
|
||||||
|
})
|
||||||
|
|
||||||
|
secret := g.Cfg().MustGet(ctx, "auth.secret").String()
|
||||||
|
|
||||||
|
// 前台:公开接口(注册/登录),鉴权接口按模块挂载
|
||||||
|
s.Group("/api", func(g *ghttp.RouterGroup) {
|
||||||
|
g.Bind(controller.Parent)
|
||||||
|
|
||||||
|
g.Group("/", func(g *ghttp.RouterGroup) {
|
||||||
|
g.Middleware(auth.Middleware(secret, ""))
|
||||||
|
g.Bind(controller.Child, controller.Strategy, controller.Level)
|
||||||
|
})
|
||||||
|
})
|
||||||
// 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口)
|
// 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口)
|
||||||
// 注:根路径须用 SetServerRoot——AddStaticPath("/", ...) 因前缀防误匹配守卫只服务根路径,
|
// 注:根路径须用 SetServerRoot——AddStaticPath("/", ...) 因前缀防误匹配守卫只服务根路径,
|
||||||
// /assets 等子路径全部 404;素材目录 ui-src/static 以 /static 前缀单独挂载
|
// /assets 等子路径全部 404;素材目录 ui-src/static 以 /static 前缀单独挂载
|
||||||
@@ -37,3 +57,22 @@ func main() {
|
|||||||
}
|
}
|
||||||
s.Run()
|
s.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initTables 建表与死表清理(各 dao 的 Init 幂等;life_task_log 无读写引用整套移除,
|
||||||
|
// 库内残留表启动时 DROP 与代码保持一致)。
|
||||||
|
func initTables(ctx context.Context) {
|
||||||
|
inits := []func(ctx context.Context) error{
|
||||||
|
dao.Parent.Init, dao.Child.Init, dao.Strategy.Init, dao.Level.Init, dao.SceneNode.Init,
|
||||||
|
dao.NodeOption.Init, dao.Element.Init, dao.UserProgress.Init, dao.ChapterReview.Init,
|
||||||
|
dao.UserRouteLog.Init, dao.UserCollection.Init, dao.Badge.Init, dao.UserBadge.Init,
|
||||||
|
dao.Prize.Init, dao.Redemption.Init, dao.AdminUser.Init, dao.LifeTask.Init,
|
||||||
|
}
|
||||||
|
for _, init := range inits {
|
||||||
|
if err := init(ctx); err != nil {
|
||||||
|
g.Log().Fatal(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := g.DB().Exec(ctx, "DROP TABLE IF EXISTS life_task_log"); err != nil {
|
||||||
|
g.Log().Fatal(ctx, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -246,9 +246,9 @@ CREATE TABLE user_badge (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### life_task / life_task_log(生活践行任务)
|
### life_task(生活践行任务)
|
||||||
|
|
||||||
游戏内践行(情境闯关)之外的真实生活践行:每章完美后任务下发,家长引导孩子实践,家长确认为权威入口(天然防刷)。
|
游戏内践行(情境闯关)之外的真实生活践行:每章完美后任务下发,家长引导孩子实践,家长确认为权威入口(天然防刷)。孩子 × 任务的执行状态记录(原 life_task_log)无读写引用,已连同建表与库内数据整套移除(启动时 DROP),M2 实现时按孩子 × 任务唯一约束重建。
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE life_task (
|
CREATE TABLE life_task (
|
||||||
@@ -261,21 +261,12 @@ CREATE TABLE life_task (
|
|||||||
status INTEGER NOT NULL DEFAULT 1
|
status INTEGER NOT NULL DEFAULT 1
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_life_task_strategy ON life_task(strategy_id);
|
CREATE INDEX idx_life_task_strategy ON life_task(strategy_id);
|
||||||
|
|
||||||
CREATE TABLE life_task_log (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
child_id INTEGER NOT NULL,
|
|
||||||
task_id INTEGER NOT NULL,
|
|
||||||
status INTEGER NOT NULL DEFAULT 1, -- 1 已下发 2 待家长确认 3 已确认
|
|
||||||
confirm_photo TEXT, -- 家长确认时上传的照片(可选)
|
|
||||||
confirmed_at DATETIME,
|
|
||||||
created_at DATETIME,
|
|
||||||
UNIQUE(child_id, task_id)
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 积分与奖品
|
### 积分与奖品
|
||||||
|
|
||||||
|
point_log 为 child 的附属流水表,建表由主表 child 的 dao 托管(豁免规则见 CLAUDE.md「分层文件对齐与代码模式」)。
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE point_log (
|
CREATE TABLE point_log (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -330,7 +321,7 @@ CREATE TABLE admin_user (
|
|||||||
### 4.1 种子数据初始化
|
### 4.1 种子数据初始化
|
||||||
|
|
||||||
- 种子 JSON 用 `go:embed` 打进二进制(`biz/service/seed/`),含:元素库(场景约 15 / 人物约 10 / 道具约 20)、36 计全部内容、每计 1-3 个现代情境关卡(决策树:节点 + 分支选项 + 元素关联)、初始奖品、初始徽章、默认管理员账号
|
- 种子 JSON 用 `go:embed` 打进二进制(`biz/service/seed/`),含:元素库(场景约 15 / 人物约 10 / 道具约 20)、36 计全部内容、每计 1-3 个现代情境关卡(决策树:节点 + 分支选项 + 元素关联)、初始奖品、初始徽章、默认管理员账号
|
||||||
- 启动时在 dao 层(各表 `init()` 内已有 `CREATE TABLE IF NOT EXISTS`)之后,service 层执行 `seed.EnsureSeeded()`:查 `strategy` 空表则按序插入,插入包事务;随后对全部内容调用标注服务生成拼音(见 4.6),TTS 音频异步生成,均只执行一次
|
- 启动时先建表(main.go 依次调各 dao 的 `Init`,`CREATE TABLE IF NOT EXISTS` 幂等),随后执行 `seed.EnsureSeeded()`:查 `strategy` 空表则按序插入,插入包事务;随后对全部内容调用标注服务生成拼音(见 4.6),TTS 音频异步生成,均只执行一次
|
||||||
- 后台运营迭代 = 后台改内容 + 可选导出种子;不覆盖用户数据表
|
- 后台运营迭代 = 后台改内容 + 可选导出种子;不覆盖用户数据表
|
||||||
|
|
||||||
### 4.2 年龄段分级
|
### 4.2 年龄段分级
|
||||||
@@ -491,7 +482,7 @@ config JSON 结构(`scene_node.config`,前端解析渲染):`{"kind":"pro
|
|||||||
|
|
||||||
### 4.12 践行与成长体系
|
### 4.12 践行与成长体系
|
||||||
|
|
||||||
**生活践行任务**(知行合一):章完美后任务下发(life_task 按 strategy 关联)→ 孩子端显示"和爸爸妈妈一起做"卡片 → 家长查看引导语、带孩子实践 → 家长确认(POST /api/task/confirm,可选照片)→ 积分到账 + 状态流转(1 已下发 → 2 待家长确认 → 3 已确认)。防刷:UNIQUE(child_id, task_id) + 家长确认为唯一权威入口。
|
**生活践行任务**(知行合一):章完美后任务下发(life_task 按 strategy 关联)→ 孩子端显示"和爸爸妈妈一起做"卡片 → 家长查看引导语、带孩子实践 → 家长确认(POST /api/task/confirm,可选照片)→ 积分到账 + 状态流转(1 已下发 → 2 待家长确认 → 3 已确认)。防刷:家长确认为唯一权威入口,M2 实现时按 child × task 唯一约束建执行记录表(原 life_task_log 无读写引用,已整套移除)。
|
||||||
|
|
||||||
**成长等级**:按完美关卡数映射称号(规则表在 biz/consts,如小学徒 → 小书童 → 小军师 → 军师 → 大将军),**派生值不落库**——结算接口计算并返回新等级,前端播升级动画;家长报告展示当前称号。
|
**成长等级**:按完美关卡数映射称号(规则表在 biz/consts,如小学徒 → 小书童 → 小军师 → 军师 → 大将军),**派生值不落库**——结算接口计算并返回新等级,前端播升级动画;家长报告展示当前称号。
|
||||||
|
|
||||||
@@ -547,6 +538,13 @@ config JSON 结构(`scene_node.config`,前端解析渲染):`{"kind":"pro
|
|||||||
- 幂等:仅处理 `script` 为空的节点;草稿已存在跳过(防重跑覆盖人工精修,`--force` 覆盖);`--strategy` 限定计策分批执行
|
- 幂等:仅处理 `script` 为空的节点;草稿已存在跳过(防重跑覆盖人工精修,`--force` 覆盖);`--strategy` 限定计策分批执行
|
||||||
- 试点:第 1 计 8 节点已导入精修,全量 36 计剧本由主会话按计分批生成
|
- 试点:第 1 计 8 节点已导入精修,全量 36 计剧本由主会话按计分批生成
|
||||||
|
|
||||||
|
### 4.15 分层文件对齐(沿革)
|
||||||
|
|
||||||
|
对齐规则(每表一组文件、豁免与归属、非表文件处置)见 CLAUDE.md「分层文件对齐与代码模式」,本节只记沿革:
|
||||||
|
|
||||||
|
- **13 张表补全**:M1 时仅 5 个模块(parent/child/strategy/level/level_play)有完整分层,其余 15 张只有 entity/dao;一次补齐为每表一组文件,6 张在用表的跨表 DAO 直调收拢到各自 service;补齐的表先只建门面结构,不挂 g.Meta 不注册路由(路由静态,按需暴露)
|
||||||
|
- **收口(2026-08-14)**:level_play(闯关流程)并入 level 表文件、表初始化列表并入 main.go、service/auth.go 并入 common/auth——各层严格 = 17 表文件(18 表 − point_log 豁免)
|
||||||
|
|
||||||
## 5. 开发计划
|
## 5. 开发计划
|
||||||
|
|
||||||
| 里程碑 | 内容 | 验收标准 |
|
| 里程碑 | 内容 | 验收标准 |
|
||||||
|
|||||||
Reference in New Issue
Block a user