feat(supervisor): allow up to 3 supervisors per owner

This commit is contained in:
nepiedg
2026-04-16 12:02:25 +08:00
parent 6cd6424561
commit 50352d67ca
3 changed files with 76 additions and 7 deletions
@@ -62,6 +62,8 @@ func (h *Handler) BindSupervisorInvite(c *gin.Context) {
msg = "不能绑定自己"
case "监督关系已存在":
msg = "已绑定,无需重复操作"
case "监督人已达上限":
msg = "对方监督人已达上限(最多 3 人)"
}
}
c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, msg))
+21 -6
View File
@@ -40,14 +40,17 @@ type SupervisorStatusResult struct {
}
var (
ErrInviteNotFound = errors.New("邀请不存在")
ErrInviteExpired = errors.New("邀请已过期")
ErrInviteUsed = errors.New("邀请已被使用")
ErrCannotBindSelf = errors.New("不能绑定自己为监督人")
ErrBindingExists = errors.New("监督关系已存在")
ErrBindingNotFound = errors.New("监督关系不存在")
ErrInviteNotFound = errors.New("邀请不存在")
ErrInviteExpired = errors.New("邀请已过期")
ErrInviteUsed = errors.New("邀请已被使用")
ErrCannotBindSelf = errors.New("不能绑定自己为监督人")
ErrBindingExists = errors.New("监督关系已存在")
ErrBindingNotFound = errors.New("监督关系不存在")
ErrSupervisorLimitReached = errors.New("监督人已达上限")
)
const maxSupervisorsPerOwner = 3
func (s *Service) CreateSupervisorInvite(ctx context.Context, ownerUID int, now time.Time, days int) (SupervisorInviteResult, error) {
if days <= 0 {
days = 7
@@ -113,6 +116,18 @@ func (s *Service) BindSupervisorInvite(ctx context.Context, supervisorUID int, t
return err
}
// 多人监督:允许多个 supervisor,但限制最多 3 个 active supervisor。
var activeCount int64
if err := tx.WithContext(ctx).
Model(&quitmodel.SupervisorBinding{}).
Where("owner_uid = ? AND status = ?", invite.OwnerUID, "active").
Count(&activeCount).Error; err != nil {
return err
}
if activeCount >= maxSupervisorsPerOwner {
return ErrSupervisorLimitReached
}
binding := quitmodel.SupervisorBinding{
OwnerUID: invite.OwnerUID,
SupervisorUID: supervisorUID,
@@ -2,6 +2,7 @@ package service
import (
"context"
"errors"
"testing"
"time"
@@ -51,7 +52,6 @@ func TestSupervisorInviteBindAndOverview(t *testing.T) {
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)
}
@@ -150,3 +150,55 @@ func TestSupervisorRevokeBindingBySupervisor(t *testing.T) {
t.Fatalf("overview items=%d, want=0 after revoke", len(overview.Items))
}
}
func TestSupervisorBindRespectsMaxSupervisors(t *testing.T) {
t.Parallel()
db := setupSupervisorTestDB(t)
svc := NewService(db)
ctx := context.Background()
ownerUID := 3201
now := time.Date(2026, 4, 16, 10, 0, 0, 0, time.Local)
if err := db.Create(&usermodel.User{ID: uint(ownerUID), NickName: "owner"}).Error; err != nil {
t.Fatalf("seed owner 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)
}
for i := 0; i < 3; i++ {
supervisorUID := 3300 + i
if err := db.Create(&usermodel.User{ID: uint(supervisorUID), NickName: "s"}).Error; err != nil {
t.Fatalf("seed supervisor user: %v", err)
}
invite, err := svc.CreateSupervisorInvite(ctx, ownerUID, now, 7)
if err != nil {
t.Fatalf("create invite: %v", err)
}
if err := svc.BindSupervisorInvite(ctx, supervisorUID, invite.Token, now); err != nil {
t.Fatalf("bind #%d: %v", i+1, err)
}
}
supervisorUID := 3399
if err := db.Create(&usermodel.User{ID: uint(supervisorUID), NickName: "s4"}).Error; err != nil {
t.Fatalf("seed supervisor user: %v", err)
}
invite, err := svc.CreateSupervisorInvite(ctx, ownerUID, now, 7)
if err != nil {
t.Fatalf("create invite: %v", err)
}
if err := svc.BindSupervisorInvite(ctx, supervisorUID, invite.Token, now); err == nil {
t.Fatalf("bind #4 should fail")
} else if !errors.Is(err, ErrSupervisorLimitReached) {
t.Fatalf("bind #4 err=%v, want ErrSupervisorLimitReached", err)
}
}