fix(redeem): 用户兑换历史不返回备注
- 用户侧 RedeemCode DTO 移除 notes 字段,避免泄露内部备注\n- 新增 AdminRedeemCode,并调整管理员兑换码接口继续返回 notes\n- 增加 /api/v1/redeem/history 契约测试,确保用户侧响应不包含 notes
This commit is contained in:
@@ -54,9 +54,9 @@ func (h *RedeemHandler) List(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.RedeemCode, 0, len(codes))
|
out := make([]dto.AdminRedeemCode, 0, len(codes))
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
out = append(out, *dto.RedeemCodeFromService(&codes[i]))
|
out = append(out, *dto.RedeemCodeFromServiceAdmin(&codes[i]))
|
||||||
}
|
}
|
||||||
response.Paginated(c, out, total, page, pageSize)
|
response.Paginated(c, out, total, page, pageSize)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ func (h *RedeemHandler) GetByID(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.RedeemCodeFromService(code))
|
response.Success(c, dto.RedeemCodeFromServiceAdmin(code))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate handles generating new redeem codes
|
// Generate handles generating new redeem codes
|
||||||
@@ -100,9 +100,9 @@ func (h *RedeemHandler) Generate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.RedeemCode, 0, len(codes))
|
out := make([]dto.AdminRedeemCode, 0, len(codes))
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
out = append(out, *dto.RedeemCodeFromService(&codes[i]))
|
out = append(out, *dto.RedeemCodeFromServiceAdmin(&codes[i]))
|
||||||
}
|
}
|
||||||
response.Success(c, out)
|
response.Success(c, out)
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ func (h *RedeemHandler) Expire(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.RedeemCodeFromService(code))
|
response.Success(c, dto.RedeemCodeFromServiceAdmin(code))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats handles getting redeem code statistics
|
// GetStats handles getting redeem code statistics
|
||||||
|
|||||||
@@ -304,7 +304,24 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
|
|||||||
if rc == nil {
|
if rc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &RedeemCode{
|
out := redeemCodeFromServiceBase(rc)
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedeemCodeFromServiceAdmin converts a service RedeemCode to DTO for admin users.
|
||||||
|
// It includes notes - user-facing endpoints must not use this.
|
||||||
|
func RedeemCodeFromServiceAdmin(rc *service.RedeemCode) *AdminRedeemCode {
|
||||||
|
if rc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &AdminRedeemCode{
|
||||||
|
RedeemCode: redeemCodeFromServiceBase(rc),
|
||||||
|
Notes: rc.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redeemCodeFromServiceBase(rc *service.RedeemCode) RedeemCode {
|
||||||
|
return RedeemCode{
|
||||||
ID: rc.ID,
|
ID: rc.ID,
|
||||||
Code: rc.Code,
|
Code: rc.Code,
|
||||||
Type: rc.Type,
|
Type: rc.Type,
|
||||||
@@ -312,7 +329,6 @@ func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
|
|||||||
Status: rc.Status,
|
Status: rc.Status,
|
||||||
UsedBy: rc.UsedBy,
|
UsedBy: rc.UsedBy,
|
||||||
UsedAt: rc.UsedAt,
|
UsedAt: rc.UsedAt,
|
||||||
Notes: rc.Notes,
|
|
||||||
CreatedAt: rc.CreatedAt,
|
CreatedAt: rc.CreatedAt,
|
||||||
GroupID: rc.GroupID,
|
GroupID: rc.GroupID,
|
||||||
ValidityDays: rc.ValidityDays,
|
ValidityDays: rc.ValidityDays,
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ type RedeemCode struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
UsedBy *int64 `json:"used_by"`
|
UsedBy *int64 `json:"used_by"`
|
||||||
UsedAt *time.Time `json:"used_at"`
|
UsedAt *time.Time `json:"used_at"`
|
||||||
Notes string `json:"notes"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|
||||||
GroupID *int64 `json:"group_id"`
|
GroupID *int64 `json:"group_id"`
|
||||||
@@ -203,6 +202,14 @@ type RedeemCode struct {
|
|||||||
Group *Group `json:"group,omitempty"`
|
Group *Group `json:"group,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminRedeemCode 是管理员接口使用的 redeem code DTO(包含 notes 等字段)。
|
||||||
|
// 注意:普通用户接口不得返回 notes 等内部信息。
|
||||||
|
type AdminRedeemCode struct {
|
||||||
|
RedeemCode
|
||||||
|
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
// UsageLog 是普通用户接口使用的 usage log DTO(不包含管理员字段)。
|
// UsageLog 是普通用户接口使用的 usage log DTO(不包含管理员字段)。
|
||||||
type UsageLog struct {
|
type UsageLog struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
|
|||||||
@@ -236,6 +236,47 @@ func TestAPIContracts(t *testing.T) {
|
|||||||
]
|
]
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "GET /api/v1/redeem/history",
|
||||||
|
setup: func(t *testing.T, deps *contractDeps) {
|
||||||
|
t.Helper()
|
||||||
|
// 普通用户兑换历史不应包含 notes 等内部字段。
|
||||||
|
deps.redeemRepo.SetByUser(1, []service.RedeemCode{
|
||||||
|
{
|
||||||
|
ID: 900,
|
||||||
|
Code: "CODE-123",
|
||||||
|
Type: service.RedeemTypeBalance,
|
||||||
|
Value: 1.25,
|
||||||
|
Status: service.StatusUsed,
|
||||||
|
UsedBy: ptr(int64(1)),
|
||||||
|
UsedAt: ptr(deps.now),
|
||||||
|
Notes: "internal-note",
|
||||||
|
CreatedAt: deps.now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: "/api/v1/redeem/history",
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
wantJSON: `{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 900,
|
||||||
|
"code": "CODE-123",
|
||||||
|
"type": "balance",
|
||||||
|
"value": 1.25,
|
||||||
|
"status": "used",
|
||||||
|
"used_by": 1,
|
||||||
|
"used_at": "2025-01-02T03:04:05Z",
|
||||||
|
"created_at": "2025-01-02T03:04:05Z",
|
||||||
|
"group_id": null,
|
||||||
|
"validity_days": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
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) {
|
||||||
@@ -494,6 +535,7 @@ type contractDeps struct {
|
|||||||
userSubRepo *stubUserSubscriptionRepo
|
userSubRepo *stubUserSubscriptionRepo
|
||||||
usageRepo *stubUsageLogRepo
|
usageRepo *stubUsageLogRepo
|
||||||
settingRepo *stubSettingRepo
|
settingRepo *stubSettingRepo
|
||||||
|
redeemRepo *stubRedeemCodeRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContractDeps(t *testing.T) *contractDeps {
|
func newContractDeps(t *testing.T) *contractDeps {
|
||||||
@@ -543,6 +585,9 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil)
|
subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil)
|
||||||
subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
|
subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
|
||||||
|
|
||||||
|
redeemService := service.NewRedeemService(redeemRepo, userRepo, subscriptionService, nil, nil, nil, nil)
|
||||||
|
redeemHandler := handler.NewRedeemHandler(redeemService)
|
||||||
|
|
||||||
settingRepo := newStubSettingRepo()
|
settingRepo := newStubSettingRepo()
|
||||||
settingService := service.NewSettingService(settingRepo, cfg)
|
settingService := service.NewSettingService(settingRepo, cfg)
|
||||||
|
|
||||||
@@ -593,6 +638,10 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
v1Subs.Use(jwtAuth)
|
v1Subs.Use(jwtAuth)
|
||||||
v1Subs.GET("/subscriptions", subscriptionHandler.List)
|
v1Subs.GET("/subscriptions", subscriptionHandler.List)
|
||||||
|
|
||||||
|
v1Redeem := v1.Group("")
|
||||||
|
v1Redeem.Use(jwtAuth)
|
||||||
|
v1Redeem.GET("/redeem/history", redeemHandler.GetHistory)
|
||||||
|
|
||||||
v1Admin := v1.Group("/admin")
|
v1Admin := v1.Group("/admin")
|
||||||
v1Admin.Use(adminAuth)
|
v1Admin.Use(adminAuth)
|
||||||
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
|
v1Admin.GET("/settings", adminSettingHandler.GetSettings)
|
||||||
@@ -606,6 +655,7 @@ func newContractDeps(t *testing.T) *contractDeps {
|
|||||||
userSubRepo: userSubRepo,
|
userSubRepo: userSubRepo,
|
||||||
usageRepo: usageRepo,
|
usageRepo: usageRepo,
|
||||||
settingRepo: settingRepo,
|
settingRepo: settingRepo,
|
||||||
|
redeemRepo: redeemRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,7 +1063,16 @@ func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID
|
|||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
type stubRedeemCodeRepo struct{}
|
type stubRedeemCodeRepo struct {
|
||||||
|
byUser map[int64][]service.RedeemCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *stubRedeemCodeRepo) SetByUser(userID int64, codes []service.RedeemCode) {
|
||||||
|
if r.byUser == nil {
|
||||||
|
r.byUser = make(map[int64][]service.RedeemCode)
|
||||||
|
}
|
||||||
|
r.byUser[userID] = append([]service.RedeemCode(nil), codes...)
|
||||||
|
}
|
||||||
|
|
||||||
func (stubRedeemCodeRepo) Create(ctx context.Context, code *service.RedeemCode) error {
|
func (stubRedeemCodeRepo) Create(ctx context.Context, code *service.RedeemCode) error {
|
||||||
return errors.New("not implemented")
|
return errors.New("not implemented")
|
||||||
@@ -1051,8 +1110,15 @@ func (stubRedeemCodeRepo) ListWithFilters(ctx context.Context, params pagination
|
|||||||
return nil, nil, errors.New("not implemented")
|
return nil, nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
|
func (r *stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
|
||||||
return nil, errors.New("not implemented")
|
if r.byUser == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
codes := r.byUser[userID]
|
||||||
|
if limit > 0 && len(codes) > limit {
|
||||||
|
codes = codes[:limit]
|
||||||
|
}
|
||||||
|
return append([]service.RedeemCode(nil), codes...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type stubUserSubscriptionRepo struct {
|
type stubUserSubscriptionRepo struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user