feat: 实现注册优惠码功能

- 支持创建/编辑/删除优惠码,设置赠送金额和使用限制
  - 注册页面实时验证优惠码并显示赠送金额
  - 支持 URL 参数自动填充 (?promo=CODE)
  - 添加优惠码验证接口速率限制
  - 使用数据库行锁防止并发超限
  - 新增后台优惠码管理页面,支持复制注册链接
This commit is contained in:
long
2026-01-10 13:14:35 +08:00
parent 7d1fe818be
commit d2fc14fb97
79 changed files with 17045 additions and 54 deletions

View File

@@ -13,6 +13,8 @@ import (
"github.com/Wei-Shaw/sub2api/ent/apikey"
"github.com/Wei-Shaw/sub2api/ent/group"
"github.com/Wei-Shaw/sub2api/ent/predicate"
"github.com/Wei-Shaw/sub2api/ent/promocode"
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
"github.com/Wei-Shaw/sub2api/ent/proxy"
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
"github.com/Wei-Shaw/sub2api/ent/setting"
@@ -188,6 +190,60 @@ func (f TraverseGroup) Traverse(ctx context.Context, q ent.Query) error {
return fmt.Errorf("unexpected query type %T. expect *ent.GroupQuery", q)
}
// The PromoCodeFunc type is an adapter to allow the use of ordinary function as a Querier.
type PromoCodeFunc func(context.Context, *ent.PromoCodeQuery) (ent.Value, error)
// Query calls f(ctx, q).
func (f PromoCodeFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
if q, ok := q.(*ent.PromoCodeQuery); ok {
return f(ctx, q)
}
return nil, fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeQuery", q)
}
// The TraversePromoCode type is an adapter to allow the use of ordinary function as Traverser.
type TraversePromoCode func(context.Context, *ent.PromoCodeQuery) error
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
func (f TraversePromoCode) Intercept(next ent.Querier) ent.Querier {
return next
}
// Traverse calls f(ctx, q).
func (f TraversePromoCode) Traverse(ctx context.Context, q ent.Query) error {
if q, ok := q.(*ent.PromoCodeQuery); ok {
return f(ctx, q)
}
return fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeQuery", q)
}
// The PromoCodeUsageFunc type is an adapter to allow the use of ordinary function as a Querier.
type PromoCodeUsageFunc func(context.Context, *ent.PromoCodeUsageQuery) (ent.Value, error)
// Query calls f(ctx, q).
func (f PromoCodeUsageFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
if q, ok := q.(*ent.PromoCodeUsageQuery); ok {
return f(ctx, q)
}
return nil, fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeUsageQuery", q)
}
// The TraversePromoCodeUsage type is an adapter to allow the use of ordinary function as Traverser.
type TraversePromoCodeUsage func(context.Context, *ent.PromoCodeUsageQuery) error
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
func (f TraversePromoCodeUsage) Intercept(next ent.Querier) ent.Querier {
return next
}
// Traverse calls f(ctx, q).
func (f TraversePromoCodeUsage) Traverse(ctx context.Context, q ent.Query) error {
if q, ok := q.(*ent.PromoCodeUsageQuery); ok {
return f(ctx, q)
}
return fmt.Errorf("unexpected query type %T. expect *ent.PromoCodeUsageQuery", q)
}
// The ProxyFunc type is an adapter to allow the use of ordinary function as a Querier.
type ProxyFunc func(context.Context, *ent.ProxyQuery) (ent.Value, error)
@@ -442,6 +498,10 @@ func NewQuery(q ent.Query) (Query, error) {
return &query[*ent.AccountGroupQuery, predicate.AccountGroup, accountgroup.OrderOption]{typ: ent.TypeAccountGroup, tq: q}, nil
case *ent.GroupQuery:
return &query[*ent.GroupQuery, predicate.Group, group.OrderOption]{typ: ent.TypeGroup, tq: q}, nil
case *ent.PromoCodeQuery:
return &query[*ent.PromoCodeQuery, predicate.PromoCode, promocode.OrderOption]{typ: ent.TypePromoCode, tq: q}, nil
case *ent.PromoCodeUsageQuery:
return &query[*ent.PromoCodeUsageQuery, predicate.PromoCodeUsage, promocodeusage.OrderOption]{typ: ent.TypePromoCodeUsage, tq: q}, nil
case *ent.ProxyQuery:
return &query[*ent.ProxyQuery, predicate.Proxy, proxy.OrderOption]{typ: ent.TypeProxy, tq: q}, nil
case *ent.RedeemCodeQuery: