fix(subscriptions): 用户订阅不返回分配信息
- 用户侧 UserSubscription DTO 移除 assigned_by/assigned_at/notes/assigned_by_user 等管理员字段\n- 新增 AdminUserSubscription,并调整管理员订阅接口与批量分配结果使用\n- 增加 /api/v1/subscriptions 契约测试,确保用户侧响应不包含上述字段
This commit is contained in:
@@ -83,9 +83,9 @@ func (h *SubscriptionHandler) List(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
||||||
for i := range subscriptions {
|
for i := range subscriptions {
|
||||||
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]))
|
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
||||||
}
|
}
|
||||||
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ func (h *SubscriptionHandler) GetByID(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.UserSubscriptionFromService(subscription))
|
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProgress handles getting subscription usage progress
|
// GetProgress handles getting subscription usage progress
|
||||||
@@ -150,7 +150,7 @@ func (h *SubscriptionHandler) Assign(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.UserSubscriptionFromService(subscription))
|
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
||||||
}
|
}
|
||||||
|
|
||||||
// BulkAssign handles bulk assigning subscriptions to multiple users
|
// BulkAssign handles bulk assigning subscriptions to multiple users
|
||||||
@@ -201,7 +201,7 @@ func (h *SubscriptionHandler) Extend(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.UserSubscriptionFromService(subscription))
|
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke handles revoking a subscription
|
// Revoke handles revoking a subscription
|
||||||
@@ -239,9 +239,9 @@ func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
||||||
for i := range subscriptions {
|
for i := range subscriptions {
|
||||||
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]))
|
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
||||||
}
|
}
|
||||||
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
response.PaginatedWithResult(c, out, toResponsePagination(pagination))
|
||||||
}
|
}
|
||||||
@@ -261,9 +261,9 @@ func (h *SubscriptionHandler) ListByUser(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.UserSubscription, 0, len(subscriptions))
|
out := make([]dto.AdminUserSubscription, 0, len(subscriptions))
|
||||||
for i := range subscriptions {
|
for i := range subscriptions {
|
||||||
out = append(out, *dto.UserSubscriptionFromService(&subscriptions[i]))
|
out = append(out, *dto.UserSubscriptionFromServiceAdmin(&subscriptions[i]))
|
||||||
}
|
}
|
||||||
response.Success(c, out)
|
response.Success(c, out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,7 +442,27 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
|
|||||||
if sub == nil {
|
if sub == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &UserSubscription{
|
out := userSubscriptionFromServiceBase(sub)
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSubscriptionFromServiceAdmin converts a service UserSubscription to DTO for admin users.
|
||||||
|
// It includes assignment metadata and notes.
|
||||||
|
func UserSubscriptionFromServiceAdmin(sub *service.UserSubscription) *AdminUserSubscription {
|
||||||
|
if sub == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &AdminUserSubscription{
|
||||||
|
UserSubscription: userSubscriptionFromServiceBase(sub),
|
||||||
|
AssignedBy: sub.AssignedBy,
|
||||||
|
AssignedAt: sub.AssignedAt,
|
||||||
|
Notes: sub.Notes,
|
||||||
|
AssignedByUser: UserFromServiceShallow(sub.AssignedByUser),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userSubscriptionFromServiceBase(sub *service.UserSubscription) UserSubscription {
|
||||||
|
return UserSubscription{
|
||||||
ID: sub.ID,
|
ID: sub.ID,
|
||||||
UserID: sub.UserID,
|
UserID: sub.UserID,
|
||||||
GroupID: sub.GroupID,
|
GroupID: sub.GroupID,
|
||||||
@@ -455,14 +475,10 @@ func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscriptio
|
|||||||
DailyUsageUSD: sub.DailyUsageUSD,
|
DailyUsageUSD: sub.DailyUsageUSD,
|
||||||
WeeklyUsageUSD: sub.WeeklyUsageUSD,
|
WeeklyUsageUSD: sub.WeeklyUsageUSD,
|
||||||
MonthlyUsageUSD: sub.MonthlyUsageUSD,
|
MonthlyUsageUSD: sub.MonthlyUsageUSD,
|
||||||
AssignedBy: sub.AssignedBy,
|
|
||||||
AssignedAt: sub.AssignedAt,
|
|
||||||
Notes: sub.Notes,
|
|
||||||
CreatedAt: sub.CreatedAt,
|
CreatedAt: sub.CreatedAt,
|
||||||
UpdatedAt: sub.UpdatedAt,
|
UpdatedAt: sub.UpdatedAt,
|
||||||
User: UserFromServiceShallow(sub.User),
|
User: UserFromServiceShallow(sub.User),
|
||||||
Group: GroupFromServiceShallow(sub.Group),
|
Group: GroupFromServiceShallow(sub.Group),
|
||||||
AssignedByUser: UserFromServiceShallow(sub.AssignedByUser),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,9 +486,9 @@ func BulkAssignResultFromService(r *service.BulkAssignResult) *BulkAssignResult
|
|||||||
if r == nil {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
subs := make([]UserSubscription, 0, len(r.Subscriptions))
|
subs := make([]AdminUserSubscription, 0, len(r.Subscriptions))
|
||||||
for i := range r.Subscriptions {
|
for i := range r.Subscriptions {
|
||||||
subs = append(subs, *UserSubscriptionFromService(&r.Subscriptions[i]))
|
subs = append(subs, *UserSubscriptionFromServiceAdmin(&r.Subscriptions[i]))
|
||||||
}
|
}
|
||||||
return &BulkAssignResult{
|
return &BulkAssignResult{
|
||||||
SuccessCount: r.SuccessCount,
|
SuccessCount: r.SuccessCount,
|
||||||
|
|||||||
@@ -323,22 +323,29 @@ type UserSubscription struct {
|
|||||||
WeeklyUsageUSD float64 `json:"weekly_usage_usd"`
|
WeeklyUsageUSD float64 `json:"weekly_usage_usd"`
|
||||||
MonthlyUsageUSD float64 `json:"monthly_usage_usd"`
|
MonthlyUsageUSD float64 `json:"monthly_usage_usd"`
|
||||||
|
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
User *User `json:"user,omitempty"`
|
||||||
|
Group *Group `json:"group,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUserSubscription 是管理员接口使用的订阅 DTO(包含分配信息/备注等字段)。
|
||||||
|
// 注意:普通用户接口不得返回 assigned_by/assigned_at/notes/assigned_by_user 等管理员字段。
|
||||||
|
type AdminUserSubscription struct {
|
||||||
|
UserSubscription
|
||||||
|
|
||||||
AssignedBy *int64 `json:"assigned_by"`
|
AssignedBy *int64 `json:"assigned_by"`
|
||||||
AssignedAt time.Time `json:"assigned_at"`
|
AssignedAt time.Time `json:"assigned_at"`
|
||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
AssignedByUser *User `json:"assigned_by_user,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
|
|
||||||
User *User `json:"user,omitempty"`
|
|
||||||
Group *Group `json:"group,omitempty"`
|
|
||||||
AssignedByUser *User `json:"assigned_by_user,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BulkAssignResult struct {
|
type BulkAssignResult struct {
|
||||||
SuccessCount int `json:"success_count"`
|
SuccessCount int `json:"success_count"`
|
||||||
FailedCount int `json:"failed_count"`
|
FailedCount int `json:"failed_count"`
|
||||||
Subscriptions []UserSubscription `json:"subscriptions"`
|
Subscriptions []AdminUserSubscription `json:"subscriptions"`
|
||||||
Errors []string `json:"errors"`
|
Errors []string `json:"errors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,56 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "GET /api/v1/subscriptions",
|
||||||
|
setup: func(t *testing.T, deps *contractDeps) {
|
||||||
|
t.Helper()
|
||||||
|
// 普通用户订阅接口不应包含 assigned_* / notes 等管理员字段。
|
||||||
|
deps.userSubRepo.SetByUserID(1, []service.UserSubscription{
|
||||||
|
{
|
||||||
|
ID: 501,
|
||||||
|
UserID: 1,
|
||||||
|
GroupID: 10,
|
||||||
|
StartsAt: deps.now,
|
||||||
|
ExpiresAt: deps.now.Add(24 * time.Hour),
|
||||||
|
Status: service.SubscriptionStatusActive,
|
||||||
|
DailyUsageUSD: 1.23,
|
||||||
|
WeeklyUsageUSD: 2.34,
|
||||||
|
MonthlyUsageUSD: 3.45,
|
||||||
|
AssignedBy: ptr(int64(999)),
|
||||||
|
AssignedAt: deps.now,
|
||||||
|
Notes: "admin-note",
|
||||||
|
CreatedAt: deps.now,
|
||||||
|
UpdatedAt: deps.now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: "/api/v1/subscriptions",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantJSON: `{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 501,
|
||||||
|
"user_id": 1,
|
||||||
|
"group_id": 10,
|
||||||
|
"starts_at": "2025-01-02T03:04:05Z",
|
||||||
|
"expires_at": "2025-01-03T03:04:05Z",
|
||||||
|
"status": "active",
|
||||||
|
"daily_window_start": null,
|
||||||
|
"weekly_window_start": null,
|
||||||
|
"monthly_window_start": null,
|
||||||
|
"daily_usage_usd": 1.23,
|
||||||
|
"weekly_usage_usd": 2.34,
|
||||||
|
"monthly_usage_usd": 3.45,
|
||||||
|
"created_at": "2025-01-02T03:04:05Z",
|
||||||
|
"updated_at": "2025-01-02T03:04:05Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "GET /api/v1/usage/stats",
|
name: "GET /api/v1/usage/stats",
|
||||||
setup: func(t *testing.T, deps *contractDeps) {
|
setup: func(t *testing.T, deps *contractDeps) {
|
||||||
@@ -490,6 +540,9 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
usageRepo := newStubUsageLogRepo()
|
usageRepo := newStubUsageLogRepo()
|
||||||
usageService := service.NewUsageService(usageRepo, userRepo, nil, nil)
|
usageService := service.NewUsageService(usageRepo, userRepo, nil, nil)
|
||||||
|
|
||||||
|
subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil)
|
||||||
|
subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
|
||||||
|
|
||||||
settingRepo := newStubSettingRepo()
|
settingRepo := newStubSettingRepo()
|
||||||
settingService := service.NewSettingService(settingRepo, cfg)
|
settingService := service.NewSettingService(settingRepo, cfg)
|
||||||
|
|
||||||
@@ -536,6 +589,10 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
v1Usage.GET("/usage", usageHandler.List)
|
v1Usage.GET("/usage", usageHandler.List)
|
||||||
v1Usage.GET("/usage/stats", usageHandler.Stats)
|
v1Usage.GET("/usage/stats", usageHandler.Stats)
|
||||||
|
|
||||||
|
v1Subs := v1.Group("")
|
||||||
|
v1Subs.Use(jwtAuth)
|
||||||
|
v1Subs.GET("/subscriptions", subscriptionHandler.List)
|
||||||
|
|
||||||
v1Admin := v1.Group("/admin")
|
v1Admin := v1.Group("/admin")
|
||||||
v1Admin.Use(adminAuth)
|
v1Admin.Use(adminAuth)
|
||||||
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
|
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
|
||||||
|
|||||||
Reference in New Issue
Block a user