diff --git a/biz/controller/child.go b/biz/controller/child.go new file mode 100644 index 0000000..600fb44 --- /dev/null +++ b/biz/controller/child.go @@ -0,0 +1,68 @@ +package controller + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" + "36wisdom/common/auth" +) + +type child struct{} + +var Child = &child{} + +func (c *child) Create(ctx context.Context, req *dto.ChildCreateReq) (*dto.ChildCreateRes, error) { + childId, err := service.Child.Create(ctx, auth.GetUid(ctx), req.Nickname, req.AgeGroup) + if err != nil { + return nil, err + } + return &dto.ChildCreateRes{ChildId: childId}, nil +} + +func (c *child) Update(ctx context.Context, req *dto.ChildUpdateReq) (*dto.ChildUpdateRes, error) { + data := g.Map{} + if req.Nickname != "" { + data["nickname"] = req.Nickname + } + if req.Avatar != "" { + data["avatar"] = req.Avatar + } + if req.AgeGroup != "" { + data["age_group"] = req.AgeGroup + } + if req.DailyLimitMinutes > 0 { + data["daily_limit_minutes"] = req.DailyLimitMinutes + } + if len(data) == 0 { + return &dto.ChildUpdateRes{}, nil + } + if err := service.Child.Update(ctx, auth.GetUid(ctx), req.ChildId, data); err != nil { + return nil, err + } + return &dto.ChildUpdateRes{}, nil +} + +func (c *child) List(ctx context.Context, req *dto.ChildListReq) (*dto.ChildListRes, error) { + items, err := service.Child.List(ctx, auth.GetUid(ctx)) + if err != nil { + return nil, err + } + res := &dto.ChildListRes{List: make([]dto.ChildListItem, 0, len(items))} + for _, it := range items { + res.List = append(res.List, dto.ChildListItem{ + ChildId: it.ChildId, + Nickname: it.Nickname, + Avatar: it.Avatar, + AgeGroup: it.AgeGroup, + Points: it.Points, + Level: it.Level, + LevelTitle: it.LevelTitle, + PerfectCount: it.PerfectCount, + DailyLimitMinutes: it.DailyLimitMinutes, + }) + } + return res, nil +} diff --git a/biz/controller/parent.go b/biz/controller/parent.go new file mode 100644 index 0000000..cbd3821 --- /dev/null +++ b/biz/controller/parent.go @@ -0,0 +1,28 @@ +package controller + +import ( + "context" + + "36wisdom/biz/model/dto" + "36wisdom/biz/service" +) + +type parent struct{} + +var Parent = &parent{} + +func (c *parent) Register(ctx context.Context, req *dto.RegisterReq) (*dto.RegisterRes, error) { + parentId, token, err := service.Parent.Register(ctx, req.Phone, req.Password, req.Nickname) + if err != nil { + return nil, err + } + return &dto.RegisterRes{Token: token, ParentId: parentId}, nil +} + +func (c *parent) Login(ctx context.Context, req *dto.LoginReq) (*dto.LoginRes, error) { + parentId, token, err := service.Parent.Login(ctx, req.Phone, req.Password) + if err != nil { + return nil, err + } + return &dto.LoginRes{Token: token, ParentId: parentId}, nil +} diff --git a/biz/controller/router.go b/biz/controller/router.go index c424337..c910b01 100644 --- a/biz/controller/router.go +++ b/biz/controller/router.go @@ -10,14 +10,21 @@ import ( // 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.Middleware(auth.Middleware(secret, "")) + g.Bind(Parent) + + g.Group("/", func(g *ghttp.RouterGroup) { + g.Middleware(auth.Middleware(secret, "")) + g.Bind(Child) + }) }) } diff --git a/biz/model/dto/child.go b/biz/model/dto/child.go new file mode 100644 index 0000000..824d3d1 --- /dev/null +++ b/biz/model/dto/child.go @@ -0,0 +1,44 @@ +package dto + +import "github.com/gogf/gf/v2/frame/g" + +type ChildCreateReq struct { + g.Meta `path:"/parent/child/create" method:"post" summary:"创建孩子档案"` + Nickname string `v:"required|length:1,32" json:"nickname"` + AgeGroup string `v:"required|in:4-6,6-8" json:"age_group"` +} + +type ChildCreateRes struct { + ChildId int64 `json:"child_id"` +} + +type ChildUpdateReq struct { + g.Meta `path:"/parent/child/update" method:"post" summary:"更新孩子资料"` + ChildId int64 `v:"required" json:"child_id"` + Nickname string `v:"length:1,32" json:"nickname"` + Avatar string `v:"max-length:255" json:"avatar"` + AgeGroup string `v:"in:4-6,6-8" json:"age_group"` + DailyLimitMinutes int `v:"min:0|max:600" json:"daily_limit_minutes"` +} + +type ChildUpdateRes struct{} + +type ChildListReq struct { + g.Meta `path:"/parent/child/list" method:"get" summary:"孩子档案列表"` +} + +type ChildListItem struct { + ChildId int64 `json:"child_id"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` + AgeGroup string `json:"age_group"` + Points int `json:"points"` + Level int `json:"level"` + LevelTitle string `json:"level_title"` + PerfectCount int `json:"perfect_count"` + DailyLimitMinutes int `json:"daily_limit_minutes"` +} + +type ChildListRes struct { + List []ChildListItem `json:"list"` +} diff --git a/biz/model/dto/parent.go b/biz/model/dto/parent.go new file mode 100644 index 0000000..4c2cd79 --- /dev/null +++ b/biz/model/dto/parent.go @@ -0,0 +1,26 @@ +package dto + +import "github.com/gogf/gf/v2/frame/g" + +type RegisterReq struct { + g.Meta `path:"/parent/register" method:"post" summary:"家长注册"` + Phone string `v:"required|length:11,11|regex:^1[3-9][0-9]{9}$" json:"phone"` + Password string `v:"required|length:6,32" json:"password"` + Nickname string `v:"required|length:1,32" json:"nickname"` +} + +type RegisterRes struct { + Token string `json:"token"` + ParentId int64 `json:"parent_id"` +} + +type LoginReq struct { + g.Meta `path:"/parent/login" method:"post" summary:"家长登录"` + Phone string `v:"required|length:11,11|regex:^1[3-9][0-9]{9}$" json:"phone"` + Password string `v:"required|length:6,32" json:"password"` +} + +type LoginRes struct { + Token string `json:"token"` + ParentId int64 `json:"parent_id"` +} diff --git a/biz/service/child.go b/biz/service/child.go new file mode 100644 index 0000000..5763cd6 --- /dev/null +++ b/biz/service/child.go @@ -0,0 +1,98 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type child struct{} + +var Child = &child{} + +// ChildItem 孩子列表项(含派生值:完美关卡数)。 +type ChildItem struct { + ChildId int64 + Nickname string + Avatar string + AgeGroup string + Points int + Level int + LevelTitle string + PerfectCount int + DailyLimitMinutes int +} + +// Create 新建孩子档案。 +func (s *child) Create(ctx context.Context, parentId int64, nickname, ageGroup string) (int64, error) { + return dao.Child.InsertAndReturnId(ctx, g.Map{ + "parent_id": parentId, + "nickname": nickname, + "age_group": ageGroup, + "status": consts.StatusEnabled, + }) +} + +// Update 更新孩子档案:仅本人名下档案可改。 +func (s *child) Update(ctx context.Context, parentId, childId int64, data g.Map) error { + rec, err := dao.Child.Model().Ctx(ctx).WherePri(childId).One() + if err != nil { + return err + } + if rec.IsEmpty() || rec["parent_id"].Int64() != parentId { + return gerror.New("孩子档案不存在") + } + return dao.Child.UpdateByPk(ctx, childId, data) +} + +// List 家长名下的孩子列表,perfect 数经 user_progress 批量汇总。 +func (s *child) List(ctx context.Context, parentId int64) ([]*ChildItem, error) { + recs, err := dao.Child.Model().Ctx(ctx).Where("parent_id", parentId).Order("id ASC").All() + if err != nil { + return nil, err + } + + items := make([]*ChildItem, 0, len(recs)) + if len(recs) == 0 { + return items, nil + } + + childIds := make([]int64, 0, len(recs)) + for _, r := range recs { + childIds = append(childIds, r["id"].Int64()) + } + + perfectMap := make(map[int64]int, len(recs)) + rows, err := dao.UserProgress.Model().Ctx(ctx). + Fields("child_id", "COUNT(*) AS cnt"). + Where("perfect", 1). + WhereIn("child_id", childIds). + Group("child_id").All() + if err != nil { + return nil, err + } + for _, r := range rows { + perfectMap[r["child_id"].Int64()] = r["cnt"].Int() + } + + for _, r := range recs { + perfect := perfectMap[r["id"].Int64()] + level, title := LevelOf(perfect) + items = append(items, &ChildItem{ + ChildId: r["id"].Int64(), + Nickname: r["nickname"].String(), + Avatar: r["avatar"].String(), + AgeGroup: r["age_group"].String(), + Points: r["points"].Int(), + Level: level, + LevelTitle: title, + PerfectCount: perfect, + DailyLimitMinutes: r["daily_limit_minutes"].Int(), + }) + } + return items, nil +} diff --git a/biz/service/level.go b/biz/service/level.go new file mode 100644 index 0000000..7e1df0a --- /dev/null +++ b/biz/service/level.go @@ -0,0 +1,26 @@ +package service + +// 成长等级:按完美关卡数映射称号(技术设计.md 4.10,派生值不落库)。 +var levelTitles = []struct { + MinPerfect int + Title string +}{ + {MinPerfect: 0, Title: "小学徒"}, + {MinPerfect: 5, Title: "小书童"}, + {MinPerfect: 10, Title: "小军师"}, + {MinPerfect: 20, Title: "军师"}, + {MinPerfect: 30, Title: "大将军"}, +} + +// LevelOf 按完美关卡数返回等级(1-5)与称号。 +func LevelOf(perfectCount int) (level int, title string) { + level = 1 + title = levelTitles[0].Title + for i, lt := range levelTitles { + if perfectCount >= lt.MinPerfect { + level = i + 1 + title = lt.Title + } + } + return +} diff --git a/biz/service/parent.go b/biz/service/parent.go new file mode 100644 index 0000000..a03604e --- /dev/null +++ b/biz/service/parent.go @@ -0,0 +1,60 @@ +package service + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "golang.org/x/crypto/bcrypt" + + "36wisdom/biz/consts" + "36wisdom/biz/dao" +) + +type parent struct{} + +var Parent = &parent{} + +// Register 家长注册:手机号唯一、bcrypt 落库、签发 token。 +func (s *parent) Register(ctx context.Context, phone, password, nickname string) (parentId int64, token string, err error) { + rec, err := dao.Parent.Model().Ctx(ctx).Where("phone", phone).One() + if err != nil { + return 0, "", err + } + if !rec.IsEmpty() { + return 0, "", gerror.Newf("手机号 %s 已注册", phone) + } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return 0, "", err + } + parentId, err = dao.Parent.InsertAndReturnId(ctx, g.Map{ + "phone": phone, + "password": string(hash), + "nickname": nickname, + "status": consts.StatusEnabled, + }) + if err != nil { + return 0, "", err + } + token, err = Auth.GenerateToken(ctx, parentId, consts.RoleParent) + return parentId, token, err +} + +// Login 家长登录:校验手机号与密码,签发 token。 +func (s *parent) Login(ctx context.Context, phone, password string) (parentId int64, token string, err error) { + rec, err := dao.Parent.Model().Ctx(ctx).Where("phone", phone).One() + if err != nil { + return 0, "", err + } + if rec.IsEmpty() { + return 0, "", gerror.New("手机号或密码错误") + } + if err = bcrypt.CompareHashAndPassword([]byte(rec["password"].String()), []byte(password)); err != nil { + return 0, "", gerror.New("手机号或密码错误") + } + parentId = rec["id"].Int64() + token, err = Auth.GenerateToken(ctx, parentId, consts.RoleParent) + return parentId, token, err +} diff --git a/common/base_dao.go b/common/base_dao.go index 61f8654..5cf33f3 100644 --- a/common/base_dao.go +++ b/common/base_dao.go @@ -14,7 +14,7 @@ type BaseDao struct { func (d *BaseDao) Model() *gdb.Model { return g.DB().Model(d.Table) } -func (d *BaseDao) InsertAndReturnId(ctx context.Context, data gdb.Record) (int64, error) { +func (d *BaseDao) InsertAndReturnId(ctx context.Context, data g.Map) (int64, error) { res, err := d.Model().Ctx(ctx).Data(data).Insert() if err != nil { return 0, err @@ -26,7 +26,7 @@ func (d *BaseDao) GetOneByPk(ctx context.Context, pk int64) (gdb.Record, error) return d.Model().Ctx(ctx).WherePri(pk).One() } -func (d *BaseDao) UpdateByPk(ctx context.Context, pk int64, data gdb.Record) error { +func (d *BaseDao) UpdateByPk(ctx context.Context, pk int64, data g.Map) error { _, err := d.Model().Ctx(ctx).WherePri(pk).Data(data).Update() return err }