feat(supervisor): add invite, bind, and read-only overview
This commit is contained in:
@@ -89,6 +89,8 @@ func main() {
|
|||||||
&quitcheckinmodel.DailyStatus{},
|
&quitcheckinmodel.DailyStatus{},
|
||||||
&quitcheckinmodel.RelapseEvent{},
|
&quitcheckinmodel.RelapseEvent{},
|
||||||
&quitcheckinmodel.HPChangeLog{},
|
&quitcheckinmodel.HPChangeLog{},
|
||||||
|
&quitcheckinmodel.SupervisorInvite{},
|
||||||
|
&quitcheckinmodel.SupervisorBinding{},
|
||||||
&quitcheckinmodel.RewardGoal{},
|
&quitcheckinmodel.RewardGoal{},
|
||||||
&quitcheckinmodel.DreamPreset{},
|
&quitcheckinmodel.DreamPreset{},
|
||||||
&achievement.Theme{},
|
&achievement.Theme{},
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"wx_service/internal/middleware"
|
||||||
|
"wx_service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createSupervisorInviteRequest struct {
|
||||||
|
Days int `json:"days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSupervisorInvite POST /api/v2/supervisor/invites
|
||||||
|
func (h *Handler) CreateSupervisorInvite(c *gin.Context) {
|
||||||
|
user := middleware.MustCurrentUser(c)
|
||||||
|
|
||||||
|
var req createSupervisorInviteRequest
|
||||||
|
_ = c.ShouldBindJSON(&req)
|
||||||
|
|
||||||
|
res, err := h.service.CreateSupervisorInvite(c.Request.Context(), int(user.ID), time.Now(), req.Days)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "生成邀请失败,请稍后重试"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
type bindSupervisorInviteRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindSupervisorInvite POST /api/v2/supervisor/bind
|
||||||
|
func (h *Handler) BindSupervisorInvite(c *gin.Context) {
|
||||||
|
user := middleware.MustCurrentUser(c)
|
||||||
|
|
||||||
|
var req bindSupervisorInviteRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Token) == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.BindSupervisorInvite(c.Request.Context(), int(user.ID), req.Token, time.Now()); err != nil {
|
||||||
|
msg := "绑定失败,请稍后重试"
|
||||||
|
switch err {
|
||||||
|
case nil:
|
||||||
|
// no-op
|
||||||
|
default:
|
||||||
|
// 针对业务错误给出更友好的提示
|
||||||
|
switch err.Error() {
|
||||||
|
case "邀请不存在":
|
||||||
|
msg = "邀请不存在"
|
||||||
|
case "邀请已过期":
|
||||||
|
msg = "邀请已过期"
|
||||||
|
case "邀请已被使用":
|
||||||
|
msg = "邀请已被使用"
|
||||||
|
case "不能绑定自己为监督人":
|
||||||
|
msg = "不能绑定自己"
|
||||||
|
case "监督关系已存在":
|
||||||
|
msg = "已绑定,无需重复操作"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.Success(gin.H{"ok": true}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupervisorOverview GET /api/v2/supervisor/overview
|
||||||
|
func (h *Handler) GetSupervisorOverview(c *gin.Context) {
|
||||||
|
user := middleware.MustCurrentUser(c)
|
||||||
|
|
||||||
|
res, err := h.service.GetSupervisorOverview(c.Request.Context(), int(user.ID), time.Now())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取监督概览失败,请稍后重试"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupervisorStatus GET /api/v2/supervisor/status
|
||||||
|
func (h *Handler) GetSupervisorStatus(c *gin.Context) {
|
||||||
|
user := middleware.MustCurrentUser(c)
|
||||||
|
|
||||||
|
res, err := h.service.GetSupervisorStatus(c.Request.Context(), int(user.ID))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "获取监督信息失败,请稍后重试"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, model.Success(res))
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SupervisorBinding 表示监督关系(一对:owner -> supervisor)。
|
||||||
|
type SupervisorBinding struct {
|
||||||
|
ID uint `gorm:"primaryKey;comment:主键" json:"id"`
|
||||||
|
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"-"`
|
||||||
|
|
||||||
|
OwnerUID int `gorm:"column:owner_uid;uniqueIndex:uniq_owner_supervisor;index;comment:被监督用户ID" json:"owner_uid"`
|
||||||
|
SupervisorUID int `gorm:"column:supervisor_uid;uniqueIndex:uniq_owner_supervisor;index;comment:监督人用户ID" json:"supervisor_uid"`
|
||||||
|
|
||||||
|
Status string `gorm:"column:status;size:16;index;comment:状态(active|revoked)" json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SupervisorBinding) TableName() string {
|
||||||
|
return "fa_quit_checkin_supervisor_binding"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SupervisorBinding) TableComment() string {
|
||||||
|
return "V2-无烟打卡-监督关系"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SupervisorInvite 表示一次“邀请监督人”的邀请记录。
|
||||||
|
// 邀请通过 token 进行一次性绑定:被接受后 used_at/used_by_uid 会被写入。
|
||||||
|
type SupervisorInvite struct {
|
||||||
|
ID uint `gorm:"primaryKey;comment:主键" json:"id"`
|
||||||
|
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"-"`
|
||||||
|
|
||||||
|
OwnerUID int `gorm:"column:owner_uid;index;comment:被监督用户ID" json:"-"`
|
||||||
|
|
||||||
|
Token string `gorm:"column:token;size:64;uniqueIndex;comment:邀请token" json:"token"`
|
||||||
|
ExpireAt time.Time `gorm:"column:expire_at;comment:过期时间" json:"expire_at"`
|
||||||
|
UsedAt *time.Time `gorm:"column:used_at;comment:被使用时间" json:"used_at,omitempty"`
|
||||||
|
UsedByUID *int `gorm:"column:used_by_uid;comment:使用者(监督人)用户ID" json:"used_by_uid,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SupervisorInvite) TableName() string {
|
||||||
|
return "fa_quit_checkin_supervisor_invite"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SupervisorInvite) TableComment() string {
|
||||||
|
return "V2-无烟打卡-监督人邀请"
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base32"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
usermodel "wx_service/internal/model"
|
||||||
|
quitmodel "wx_service/internal/quitcheckin/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SupervisorInviteResult struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpireAt string `json:"expire_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupervisorOwnerSummary struct {
|
||||||
|
Owner userSummary `json:"owner"`
|
||||||
|
Home HomeResult `json:"home"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userSummary struct {
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Nickname string `json:"nickname,omitempty"`
|
||||||
|
AvatarURL string `json:"avatar_url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupervisorOverviewResult struct {
|
||||||
|
Items []SupervisorOwnerSummary `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupervisorStatusResult struct {
|
||||||
|
Items []userSummary `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInviteNotFound = errors.New("邀请不存在")
|
||||||
|
ErrInviteExpired = errors.New("邀请已过期")
|
||||||
|
ErrInviteUsed = errors.New("邀请已被使用")
|
||||||
|
ErrCannotBindSelf = errors.New("不能绑定自己为监督人")
|
||||||
|
ErrBindingExists = errors.New("监督关系已存在")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Service) CreateSupervisorInvite(ctx context.Context, ownerUID int, now time.Time, days int) (SupervisorInviteResult, error) {
|
||||||
|
if days <= 0 {
|
||||||
|
days = 7
|
||||||
|
}
|
||||||
|
if days > 30 {
|
||||||
|
days = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
token := newInviteToken()
|
||||||
|
expireAt := now.Add(time.Duration(days) * 24 * time.Hour)
|
||||||
|
|
||||||
|
row := quitmodel.SupervisorInvite{
|
||||||
|
OwnerUID: ownerUID,
|
||||||
|
Token: token,
|
||||||
|
ExpireAt: expireAt,
|
||||||
|
UsedAt: nil,
|
||||||
|
UsedByUID: nil,
|
||||||
|
}
|
||||||
|
if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
|
||||||
|
return SupervisorInviteResult{}, fmt.Errorf("create invite: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupervisorInviteResult{
|
||||||
|
Token: token,
|
||||||
|
ExpireAt: expireAt.Format(time.RFC3339),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) BindSupervisorInvite(ctx context.Context, supervisorUID int, token string, now time.Time) error {
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
return ErrInviteNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var invite quitmodel.SupervisorInvite
|
||||||
|
if err := tx.WithContext(ctx).Where("token = ?", token).First(&invite).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return ErrInviteNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if invite.UsedAt != nil || invite.UsedByUID != nil {
|
||||||
|
return ErrInviteUsed
|
||||||
|
}
|
||||||
|
if now.After(invite.ExpireAt) {
|
||||||
|
return ErrInviteExpired
|
||||||
|
}
|
||||||
|
if invite.OwnerUID == supervisorUID {
|
||||||
|
return ErrCannotBindSelf
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在绑定
|
||||||
|
var existing quitmodel.SupervisorBinding
|
||||||
|
err := tx.WithContext(ctx).
|
||||||
|
Where("owner_uid = ? AND supervisor_uid = ? AND status = ?", invite.OwnerUID, supervisorUID, "active").
|
||||||
|
First(&existing).Error
|
||||||
|
if err == nil {
|
||||||
|
return ErrBindingExists
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
binding := quitmodel.SupervisorBinding{
|
||||||
|
OwnerUID: invite.OwnerUID,
|
||||||
|
SupervisorUID: supervisorUID,
|
||||||
|
Status: "active",
|
||||||
|
}
|
||||||
|
if err := tx.WithContext(ctx).Create(&binding).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
usedAt := now
|
||||||
|
usedBy := supervisorUID
|
||||||
|
if err := tx.WithContext(ctx).Model(&quitmodel.SupervisorInvite{}).
|
||||||
|
Where("id = ? AND used_at IS NULL AND used_by_uid IS NULL", invite.ID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"used_at": &usedAt,
|
||||||
|
"used_by_uid": &usedBy,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetSupervisorOverview(ctx context.Context, supervisorUID int, now time.Time) (SupervisorOverviewResult, error) {
|
||||||
|
var bindings []quitmodel.SupervisorBinding
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("supervisor_uid = ? AND status = ?", supervisorUID, "active").
|
||||||
|
Order("id DESC").
|
||||||
|
Find(&bindings).Error; err != nil {
|
||||||
|
return SupervisorOverviewResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bindings) == 0 {
|
||||||
|
return SupervisorOverviewResult{Items: []SupervisorOwnerSummary{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerIDs := make([]int, 0, len(bindings))
|
||||||
|
for _, b := range bindings {
|
||||||
|
ownerIDs = append(ownerIDs, b.OwnerUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉用户昵称/头像(非强依赖:缺失不影响)
|
||||||
|
userMap := make(map[int]userSummary, len(ownerIDs))
|
||||||
|
var users []usermodel.User
|
||||||
|
_ = s.db.WithContext(ctx).Where("id IN ?", ownerIDs).Find(&users).Error
|
||||||
|
for _, u := range users {
|
||||||
|
userMap[int(u.ID)] = userSummary{
|
||||||
|
UserID: int(u.ID),
|
||||||
|
Nickname: u.NickName,
|
||||||
|
AvatarURL: u.AvatarURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]SupervisorOwnerSummary, 0, len(bindings))
|
||||||
|
for _, b := range bindings {
|
||||||
|
owner := userMap[b.OwnerUID]
|
||||||
|
if owner.UserID == 0 {
|
||||||
|
owner = userSummary{UserID: b.OwnerUID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只读概览:复用 Home(内部包含 summary + 今日状态)
|
||||||
|
home, err := s.Home(ctx, b.OwnerUID, now)
|
||||||
|
if err != nil {
|
||||||
|
// 对单个 owner 的失败做降级,不影响其他人的展示
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, SupervisorOwnerSummary{
|
||||||
|
Owner: owner,
|
||||||
|
Home: home,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupervisorOverviewResult{Items: items}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetSupervisorStatus(ctx context.Context, ownerUID int) (SupervisorStatusResult, error) {
|
||||||
|
var bindings []quitmodel.SupervisorBinding
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("owner_uid = ? AND status = ?", ownerUID, "active").
|
||||||
|
Order("id DESC").
|
||||||
|
Find(&bindings).Error; err != nil {
|
||||||
|
return SupervisorStatusResult{}, err
|
||||||
|
}
|
||||||
|
if len(bindings) == 0 {
|
||||||
|
return SupervisorStatusResult{Items: []userSummary{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
supervisorIDs := make([]int, 0, len(bindings))
|
||||||
|
for _, b := range bindings {
|
||||||
|
supervisorIDs = append(supervisorIDs, b.SupervisorUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []usermodel.User
|
||||||
|
if err := s.db.WithContext(ctx).Where("id IN ?", supervisorIDs).Find(&users).Error; err != nil {
|
||||||
|
return SupervisorStatusResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]userSummary, 0, len(users))
|
||||||
|
for _, u := range users {
|
||||||
|
items = append(items, userSummary{
|
||||||
|
UserID: int(u.ID),
|
||||||
|
Nickname: u.NickName,
|
||||||
|
AvatarURL: u.AvatarURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return SupervisorStatusResult{Items: items}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInviteToken() string {
|
||||||
|
// 80-bit random -> base32 -> ~16 chars, URL safe-ish, upper-case; unify to lower for nicer display.
|
||||||
|
buf := make([]byte, 10)
|
||||||
|
_, _ = rand.Read(buf)
|
||||||
|
enc := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(buf)
|
||||||
|
return strings.ToLower(enc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
usermodel "wx_service/internal/model"
|
||||||
|
quitmodel "wx_service/internal/quitcheckin/model"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupSupervisorTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.AutoMigrate(
|
||||||
|
&usermodel.User{},
|
||||||
|
&quitmodel.Profile{},
|
||||||
|
&quitmodel.DailyStatus{},
|
||||||
|
&quitmodel.RelapseEvent{},
|
||||||
|
&quitmodel.HPChangeLog{},
|
||||||
|
&quitmodel.SupervisorInvite{},
|
||||||
|
&quitmodel.SupervisorBinding{},
|
||||||
|
&quitmodel.RewardGoal{},
|
||||||
|
&quitmodel.DreamPreset{},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("auto migrate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupervisorInviteBindAndOverview(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
db := setupSupervisorTestDB(t)
|
||||||
|
svc := NewService(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
ownerUID := 3001
|
||||||
|
supervisorUID := 3002
|
||||||
|
now := time.Date(2026, 4, 16, 10, 0, 0, 0, time.Local)
|
||||||
|
|
||||||
|
// seed users
|
||||||
|
if err := db.Create(&usermodel.User{ID: uint(ownerUID), NickName: "owner"}).Error; err != nil {
|
||||||
|
t.Fatalf("seed owner user: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.Create(&usermodel.User{ID: uint(supervisorUID), NickName: "supervisor"}).Error; err != nil {
|
||||||
|
t.Fatalf("seed supervisor user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
startDate := time.Date(2026, 4, 10, 0, 0, 0, 0, time.Local)
|
||||||
|
if _, err := svc.UpsertProfile(ctx, ownerUID, UpsertProfileRequest{
|
||||||
|
QuitStartDate: &startDate,
|
||||||
|
PackPriceCent: intPtr(2500),
|
||||||
|
BaselineCigsPerDay: intPtr(10),
|
||||||
|
}, "owner", "", now); err != nil {
|
||||||
|
t.Fatalf("upsert profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invite, err := svc.CreateSupervisorInvite(ctx, ownerUID, now, 7)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create invite: %v", err)
|
||||||
|
}
|
||||||
|
if invite.Token == "" {
|
||||||
|
t.Fatalf("invite token empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svc.BindSupervisorInvite(ctx, supervisorUID, invite.Token, now); err != nil {
|
||||||
|
t.Fatalf("bind: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
overview, err := svc.GetSupervisorOverview(ctx, supervisorUID, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("overview: %v", err)
|
||||||
|
}
|
||||||
|
if len(overview.Items) != 1 {
|
||||||
|
t.Fatalf("overview items=%d, want=1", len(overview.Items))
|
||||||
|
}
|
||||||
|
if overview.Items[0].Owner.UserID != ownerUID {
|
||||||
|
t.Fatalf("owner uid=%d, want=%d", overview.Items[0].Owner.UserID, ownerUID)
|
||||||
|
}
|
||||||
|
if overview.Items[0].Home.Summary.HPCurrent <= 0 {
|
||||||
|
t.Fatalf("hp_current=%d, want > 0", overview.Items[0].Home.Summary.HPCurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := svc.GetSupervisorStatus(ctx, ownerUID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("status: %v", err)
|
||||||
|
}
|
||||||
|
if len(status.Items) != 1 || status.Items[0].UserID != supervisorUID {
|
||||||
|
t.Fatalf("status=%v, want one supervisor uid=%d", status.Items, supervisorUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,11 @@ func registerQuitCheckinRoutes(protected *gin.RouterGroup, handler *quitcheckinh
|
|||||||
v2.POST("/checkin/check", handler.Checkin)
|
v2.POST("/checkin/check", handler.Checkin)
|
||||||
v2.POST("/checkin/relapse", handler.Relapse)
|
v2.POST("/checkin/relapse", handler.Relapse)
|
||||||
|
|
||||||
|
v2.POST("/supervisor/invites", handler.CreateSupervisorInvite)
|
||||||
|
v2.POST("/supervisor/bind", handler.BindSupervisorInvite)
|
||||||
|
v2.GET("/supervisor/overview", handler.GetSupervisorOverview)
|
||||||
|
v2.GET("/supervisor/status", handler.GetSupervisorStatus)
|
||||||
|
|
||||||
v2.GET("/stats/overview", handler.StatsOverview)
|
v2.GET("/stats/overview", handler.StatsOverview)
|
||||||
v2.GET("/badges", handler.ListBadges)
|
v2.GET("/badges", handler.ListBadges)
|
||||||
v2.GET("/relapses", handler.ListRelapses)
|
v2.GET("/relapses", handler.ListRelapses)
|
||||||
|
|||||||
Reference in New Issue
Block a user