feat: 家长注册登录与孩子档案接口
注册/登录签发 JWT(bcrypt 校验),孩子档案增改查带归属校验, 列表汇总完美关卡数并派生成长等级。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user