d88c1ae647
手动合并 tsk_x7wrlA87orsY(设备管理 + 订阅校验中间件)到 main:
冲突解决:
- server/internal/apierr/apierr.go:保留 tsk_GXDoc3Cs07Rn 版本(New/StatusFor/
Middleware/ErrConflict/改善文档),并入 tsk_x7wrlA87orsY 新增的 ErrAccountBanned
及对应 StatusFor case(→ 403)。
新增文件(来自 tsk_x7wrlA87orsY):
- server/internal/devices/doc.go package 文档(替换占位 stub)
- server/internal/devices/context.go CtxKeyUserID / Plan / WithPlan / PlanFromCtx
- server/internal/devices/handler.go GET /v1/me/devices · DELETE /v1/me/devices/{id}
- server/internal/devices/middleware.go SubscriptionMiddleware · CheckDeviceQuota · RequirePaidTier
- server/internal/devices/service.go RegisterIfAbsent / DeleteDevice / ResolvePlan + 纯函数 resolveEffectivePlan
- server/internal/devices/store.go MySQL 数据访问层
- server/internal/devices/service_test.go 15 个单测(全通过)
- server/internal/devices/devices_integration_test.go testcontainers 集成测试
OpenAPI 更新(来自 tsk_x7wrlA87orsY):
- server/api/openapi.yaml:SubscriptionInfo.source 枚举补 free
- design/server/openapi.yaml:SubscriptionInfo.source 枚举补 admin, free
测试:go build ./... ✓;go test ./internal/apierr/... ✓(8 tests);
go test ./internal/devices/... ✓(15 tests)。
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
81 lines
2.6 KiB
Go
81 lines
2.6 KiB
Go
package devices
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// ctxKey is a private type for context keys to avoid collisions.
|
|
type ctxKey string
|
|
|
|
// CtxKeyUserID is the context key under which the authenticated user's
|
|
// internal int64 ID is stored by the JWT auth middleware (module #2).
|
|
//
|
|
// It mirrors the key used by the codes module so that, once the auth
|
|
// middleware lands, a single canonical key can be reconciled across modules.
|
|
const CtxKeyUserID ctxKey = "user_id"
|
|
|
|
// ctxKeyPlan is the context key under which the resolved subscription Plan is
|
|
// stored by SubscriptionMiddleware.
|
|
const ctxKeyPlan ctxKey = "plan"
|
|
|
|
// WithUserID returns a copy of ctx carrying the authenticated user ID.
|
|
// Primarily used in tests and by the auth middleware.
|
|
func WithUserID(ctx context.Context, userID int64) context.Context {
|
|
return context.WithValue(ctx, CtxKeyUserID, userID)
|
|
}
|
|
|
|
// UserIDFromContext extracts the authenticated user ID from ctx.
|
|
// ok is false when no (valid) user ID is present.
|
|
func UserIDFromContext(ctx context.Context) (int64, bool) {
|
|
v, ok := ctx.Value(CtxKeyUserID).(int64)
|
|
if !ok || v == 0 {
|
|
return 0, false
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
// Plan is the resolved effective subscription for a user, injected into the
|
|
// request context by SubscriptionMiddleware and consumed by nodes (catalogue
|
|
// filtering, connect), usage (daily caps), and the /v1/me summary.
|
|
type Plan struct {
|
|
// PlanCode is one of "free", "pro", "team".
|
|
PlanCode string
|
|
// ExpiresAt is the effective subscription expiry (UTC); nil for the free
|
|
// fallback when the user has no active paid/trial subscription.
|
|
ExpiresAt *time.Time
|
|
// MaxDevices is the per-plan device cap (free 1 / pro 5 / team 10).
|
|
MaxDevices int
|
|
// DailyMinutes is the free-plan daily time cap in minutes; nil = unlimited.
|
|
DailyMinutes *int
|
|
// AdGate is true when the user must watch a rewarded ad to unlock daily use.
|
|
AdGate bool
|
|
// Source is the subscription source: "trial", "code", "admin", or "free"
|
|
// for the no-subscription fallback.
|
|
Source string
|
|
}
|
|
|
|
// WithPlan returns a copy of ctx carrying the resolved Plan.
|
|
func WithPlan(ctx context.Context, p Plan) context.Context {
|
|
return context.WithValue(ctx, ctxKeyPlan, p)
|
|
}
|
|
|
|
// PlanFromCtx extracts the resolved Plan from ctx. ok is false when
|
|
// SubscriptionMiddleware has not run.
|
|
func PlanFromCtx(ctx context.Context) (Plan, bool) {
|
|
p, ok := ctx.Value(ctxKeyPlan).(Plan)
|
|
return p, ok
|
|
}
|
|
|
|
// planTier maps a plan code to a numeric tier (higher = better) for comparison.
|
|
func planTier(code string) int {
|
|
switch code {
|
|
case "team":
|
|
return 3
|
|
case "pro":
|
|
return 2
|
|
default: // "free" or unknown
|
|
return 1
|
|
}
|
|
}
|