feat: 家长注册登录与孩子档案接口

注册/登录签发 JWT(bcrypt 校验),孩子档案增改查带归属校验,
列表汇总完美关卡数并派生成长等级。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-08-13 12:21:38 +08:00
co-authored by Claude Opus 4.7
parent 7ce5cccd30
commit 0273a81ca7
9 changed files with 361 additions and 4 deletions
+68
View File
@@ -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
}
+28
View File
@@ -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
}
+9 -2
View File
@@ -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)
})
})
}
+44
View File
@@ -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"`
}
+26
View File
@@ -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"`
}
+98
View File
@@ -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
}
+26
View File
@@ -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
}
+60
View File
@@ -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
View File
@@ -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
}