Commit 794e8172 authored by erio's avatar erio
Browse files

refactor: remove PaymentChannel, reuse upstream Channel with features field

- Delete payment_channels table and PaymentChannel Ent schema
- Add `features` column to upstream channels table (migration 095)
- Add Features field to Channel struct, input types, handler request/response
- Payment user/admin handlers now use ChannelService directly
- Remove Channel CRUD from PaymentConfigService and admin payment routes
- Remove "渠道管理" tab from admin orders page (use /admin/channels)
parent 37c23ecc
...@@ -333,10 +333,10 @@ func (c *Client) Use(hooks ...Hook) { ...@@ -333,10 +333,10 @@ func (c *Client) Use(hooks ...Hook) {
for _, n := range []interface{ Use(...Hook) }{ for _, n := range []interface{ Use(...Hook) }{
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead, c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PaymentAuditLog, c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PaymentAuditLog,
c.PaymentOrder, c.PaymentProviderInstance, c.PromoCode, c.PromoCodeUsage, c.PaymentOrder, c.PaymentProviderInstance, c.PromoCode,
c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting, c.SubscriptionPlan, c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User, c.SubscriptionPlan, c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog,
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue, c.User, c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
c.UserSubscription, c.UserSubscription,
} { } {
n.Use(hooks...) n.Use(hooks...)
...@@ -349,10 +349,10 @@ func (c *Client) Intercept(interceptors ...Interceptor) { ...@@ -349,10 +349,10 @@ func (c *Client) Intercept(interceptors ...Interceptor) {
for _, n := range []interface{ Intercept(...Interceptor) }{ for _, n := range []interface{ Intercept(...Interceptor) }{
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead, c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PaymentAuditLog, c.ErrorPassthroughRule, c.Group, c.IdempotencyRecord, c.PaymentAuditLog,
c.PaymentOrder, c.PaymentProviderInstance, c.PromoCode, c.PromoCodeUsage, c.PaymentOrder, c.PaymentProviderInstance, c.PromoCode,
c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting, c.SubscriptionPlan, c.PromoCodeUsage, c.Proxy, c.RedeemCode, c.SecuritySecret, c.Setting,
c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog, c.User, c.SubscriptionPlan, c.TLSFingerprintProfile, c.UsageCleanupTask, c.UsageLog,
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue, c.User, c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
c.UserSubscription, c.UserSubscription,
} { } {
n.Intercept(interceptors...) n.Intercept(interceptors...)
...@@ -4629,19 +4629,19 @@ func (c *UserSubscriptionClient) mutate(ctx context.Context, m *UserSubscription ...@@ -4629,19 +4629,19 @@ func (c *UserSubscriptionClient) mutate(ctx context.Context, m *UserSubscription
type ( type (
hooks struct { hooks struct {
APIKey, Account, AccountGroup, Announcement, AnnouncementRead, APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
ErrorPassthroughRule, Group, IdempotencyRecord, PaymentAuditLog, PaymentOrder, ErrorPassthroughRule, Group, IdempotencyRecord, PaymentAuditLog,
PaymentProviderInstance, PromoCode, PromoCodeUsage, Proxy, RedeemCode, PaymentOrder, PaymentProviderInstance, PromoCode,
SecuritySecret, Setting, SubscriptionPlan, TLSFingerprintProfile, PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting, SubscriptionPlan,
UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition, TLSFingerprintProfile, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
UserAttributeValue, UserSubscription []ent.Hook UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Hook
} }
inters struct { inters struct {
APIKey, Account, AccountGroup, Announcement, AnnouncementRead, APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
ErrorPassthroughRule, Group, IdempotencyRecord, PaymentAuditLog, PaymentOrder, ErrorPassthroughRule, Group, IdempotencyRecord, PaymentAuditLog,
PaymentProviderInstance, PromoCode, PromoCodeUsage, Proxy, RedeemCode, PaymentOrder, PaymentProviderInstance, PromoCode,
SecuritySecret, Setting, SubscriptionPlan, TLSFingerprintProfile, PromoCodeUsage, Proxy, RedeemCode, SecuritySecret, Setting, SubscriptionPlan,
UsageCleanupTask, UsageLog, User, UserAllowedGroup, UserAttributeDefinition, TLSFingerprintProfile, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
UserAttributeValue, UserSubscription []ent.Interceptor UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Interceptor
} }
) )
......
...@@ -336,6 +336,7 @@ func (f TraversePaymentAuditLog) Traverse(ctx context.Context, q ent.Query) erro ...@@ -336,6 +336,7 @@ func (f TraversePaymentAuditLog) Traverse(ctx context.Context, q ent.Query) erro
return fmt.Errorf("unexpected query type %T. expect *ent.PaymentAuditLogQuery", q) return fmt.Errorf("unexpected query type %T. expect *ent.PaymentAuditLogQuery", q)
} }
// The PaymentOrderFunc type is an adapter to allow the use of ordinary function as a Querier. // The PaymentOrderFunc type is an adapter to allow the use of ordinary function as a Querier.
type PaymentOrderFunc func(context.Context, *ent.PaymentOrderQuery) (ent.Value, error) type PaymentOrderFunc func(context.Context, *ent.PaymentOrderQuery) (ent.Value, error)
......
...@@ -33,6 +33,7 @@ type IdempotencyRecord func(*sql.Selector) ...@@ -33,6 +33,7 @@ type IdempotencyRecord func(*sql.Selector)
// PaymentAuditLog is the predicate function for paymentauditlog builders. // PaymentAuditLog is the predicate function for paymentauditlog builders.
type PaymentAuditLog func(*sql.Selector) type PaymentAuditLog func(*sql.Selector)
// PaymentOrder is the predicate function for paymentorder builders. // PaymentOrder is the predicate function for paymentorder builders.
type PaymentOrder func(*sql.Selector) type PaymentOrder func(*sql.Selector)
......
...@@ -33,6 +33,7 @@ type createChannelRequest struct { ...@@ -33,6 +33,7 @@ type createChannelRequest struct {
ModelMapping map[string]map[string]string `json:"model_mapping"` ModelMapping map[string]map[string]string `json:"model_mapping"`
BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"` BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"`
RestrictModels bool `json:"restrict_models"` RestrictModels bool `json:"restrict_models"`
Features string `json:"features"`
} }
type updateChannelRequest struct { type updateChannelRequest struct {
...@@ -44,6 +45,7 @@ type updateChannelRequest struct { ...@@ -44,6 +45,7 @@ type updateChannelRequest struct {
ModelMapping map[string]map[string]string `json:"model_mapping"` ModelMapping map[string]map[string]string `json:"model_mapping"`
BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"` BillingModelSource string `json:"billing_model_source" binding:"omitempty,oneof=requested upstream channel_mapped"`
RestrictModels *bool `json:"restrict_models"` RestrictModels *bool `json:"restrict_models"`
Features *string `json:"features"`
} }
type channelModelPricingRequest struct { type channelModelPricingRequest struct {
...@@ -78,6 +80,7 @@ type channelResponse struct { ...@@ -78,6 +80,7 @@ type channelResponse struct {
Status string `json:"status"` Status string `json:"status"`
BillingModelSource string `json:"billing_model_source"` BillingModelSource string `json:"billing_model_source"`
RestrictModels bool `json:"restrict_models"` RestrictModels bool `json:"restrict_models"`
Features string `json:"features"`
GroupIDs []int64 `json:"group_ids"` GroupIDs []int64 `json:"group_ids"`
ModelPricing []channelModelPricingResponse `json:"model_pricing"` ModelPricing []channelModelPricingResponse `json:"model_pricing"`
ModelMapping map[string]map[string]string `json:"model_mapping"` ModelMapping map[string]map[string]string `json:"model_mapping"`
...@@ -122,6 +125,7 @@ func channelToResponse(ch *service.Channel) *channelResponse { ...@@ -122,6 +125,7 @@ func channelToResponse(ch *service.Channel) *channelResponse {
Description: ch.Description, Description: ch.Description,
Status: ch.Status, Status: ch.Status,
RestrictModels: ch.RestrictModels, RestrictModels: ch.RestrictModels,
Features: ch.Features,
GroupIDs: ch.GroupIDs, GroupIDs: ch.GroupIDs,
ModelMapping: ch.ModelMapping, ModelMapping: ch.ModelMapping,
CreatedAt: ch.CreatedAt.Format("2006-01-02T15:04:05Z"), CreatedAt: ch.CreatedAt.Format("2006-01-02T15:04:05Z"),
...@@ -300,6 +304,7 @@ func (h *ChannelHandler) Create(c *gin.Context) { ...@@ -300,6 +304,7 @@ func (h *ChannelHandler) Create(c *gin.Context) {
ModelMapping: req.ModelMapping, ModelMapping: req.ModelMapping,
BillingModelSource: req.BillingModelSource, BillingModelSource: req.BillingModelSource,
RestrictModels: req.RestrictModels, RestrictModels: req.RestrictModels,
Features: req.Features,
}) })
if err != nil { if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
...@@ -332,6 +337,7 @@ func (h *ChannelHandler) Update(c *gin.Context) { ...@@ -332,6 +337,7 @@ func (h *ChannelHandler) Update(c *gin.Context) {
ModelMapping: req.ModelMapping, ModelMapping: req.ModelMapping,
BillingModelSource: req.BillingModelSource, BillingModelSource: req.BillingModelSource,
RestrictModels: req.RestrictModels, RestrictModels: req.RestrictModels,
Features: req.Features,
} }
if req.ModelPricing != nil { if req.ModelPricing != nil {
pricing := pricingRequestToService(*req.ModelPricing) pricing := pricingRequestToService(*req.ModelPricing)
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/pkg/response"
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
......
...@@ -42,9 +42,9 @@ func (r *channelRepository) Create(ctx context.Context, channel *service.Channel ...@@ -42,9 +42,9 @@ func (r *channelRepository) Create(ctx context.Context, channel *service.Channel
return err return err
} }
err = tx.QueryRowContext(ctx, err = tx.QueryRowContext(ctx,
`INSERT INTO channels (name, description, status, model_mapping, billing_model_source, restrict_models) VALUES ($1, $2, $3, $4, $5, $6) `INSERT INTO channels (name, description, status, model_mapping, billing_model_source, restrict_models, features) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, created_at, updated_at`, RETURNING id, created_at, updated_at`,
channel.Name, channel.Description, channel.Status, modelMappingJSON, channel.BillingModelSource, channel.RestrictModels, channel.Name, channel.Description, channel.Status, modelMappingJSON, channel.BillingModelSource, channel.RestrictModels, channel.Features,
).Scan(&channel.ID, &channel.CreatedAt, &channel.UpdatedAt) ).Scan(&channel.ID, &channel.CreatedAt, &channel.UpdatedAt)
if err != nil { if err != nil {
if isUniqueViolation(err) { if isUniqueViolation(err) {
...@@ -75,9 +75,9 @@ func (r *channelRepository) GetByID(ctx context.Context, id int64) (*service.Cha ...@@ -75,9 +75,9 @@ func (r *channelRepository) GetByID(ctx context.Context, id int64) (*service.Cha
ch := &service.Channel{} ch := &service.Channel{}
var modelMappingJSON []byte var modelMappingJSON []byte
err := r.db.QueryRowContext(ctx, err := r.db.QueryRowContext(ctx,
`SELECT id, name, description, status, model_mapping, billing_model_source, restrict_models, created_at, updated_at `SELECT id, name, description, status, model_mapping, billing_model_source, restrict_models, features, created_at, updated_at
FROM channels WHERE id = $1`, id, FROM channels WHERE id = $1`, id,
).Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.CreatedAt, &ch.UpdatedAt) ).Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.Features, &ch.CreatedAt, &ch.UpdatedAt)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, service.ErrChannelNotFound return nil, service.ErrChannelNotFound
} }
...@@ -108,9 +108,9 @@ func (r *channelRepository) Update(ctx context.Context, channel *service.Channel ...@@ -108,9 +108,9 @@ func (r *channelRepository) Update(ctx context.Context, channel *service.Channel
return err return err
} }
result, err := tx.ExecContext(ctx, result, err := tx.ExecContext(ctx,
`UPDATE channels SET name = $1, description = $2, status = $3, model_mapping = $4, billing_model_source = $5, restrict_models = $6, updated_at = NOW() `UPDATE channels SET name = $1, description = $2, status = $3, model_mapping = $4, billing_model_source = $5, restrict_models = $6, features = $7, updated_at = NOW()
WHERE id = $7`, WHERE id = $8`,
channel.Name, channel.Description, channel.Status, modelMappingJSON, channel.BillingModelSource, channel.RestrictModels, channel.ID, channel.Name, channel.Description, channel.Status, modelMappingJSON, channel.BillingModelSource, channel.RestrictModels, channel.Features, channel.ID,
) )
if err != nil { if err != nil {
if isUniqueViolation(err) { if isUniqueViolation(err) {
...@@ -204,7 +204,7 @@ func (r *channelRepository) List(ctx context.Context, params pagination.Paginati ...@@ -204,7 +204,7 @@ func (r *channelRepository) List(ctx context.Context, params pagination.Paginati
for rows.Next() { for rows.Next() {
var ch service.Channel var ch service.Channel
var modelMappingJSON []byte var modelMappingJSON []byte
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.CreatedAt, &ch.UpdatedAt); err != nil { if err := rows.Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.Features, &ch.CreatedAt, &ch.UpdatedAt); err != nil {
return nil, nil, fmt.Errorf("scan channel: %w", err) return nil, nil, fmt.Errorf("scan channel: %w", err)
} }
ch.ModelMapping = unmarshalModelMapping(modelMappingJSON) ch.ModelMapping = unmarshalModelMapping(modelMappingJSON)
...@@ -273,7 +273,7 @@ func channelListOrderBy(params pagination.PaginationParams) string { ...@@ -273,7 +273,7 @@ func channelListOrderBy(params pagination.PaginationParams) string {
func (r *channelRepository) ListAll(ctx context.Context) ([]service.Channel, error) { func (r *channelRepository) ListAll(ctx context.Context) ([]service.Channel, error) {
rows, err := r.db.QueryContext(ctx, rows, err := r.db.QueryContext(ctx,
`SELECT id, name, description, status, model_mapping, billing_model_source, restrict_models, created_at, updated_at FROM channels ORDER BY id`, `SELECT id, name, description, status, model_mapping, billing_model_source, restrict_models, features, created_at, updated_at FROM channels ORDER BY id`,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("query all channels: %w", err) return nil, fmt.Errorf("query all channels: %w", err)
...@@ -285,7 +285,7 @@ func (r *channelRepository) ListAll(ctx context.Context) ([]service.Channel, err ...@@ -285,7 +285,7 @@ func (r *channelRepository) ListAll(ctx context.Context) ([]service.Channel, err
for rows.Next() { for rows.Next() {
var ch service.Channel var ch service.Channel
var modelMappingJSON []byte var modelMappingJSON []byte
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.CreatedAt, &ch.UpdatedAt); err != nil { if err := rows.Scan(&ch.ID, &ch.Name, &ch.Description, &ch.Status, &modelMappingJSON, &ch.BillingModelSource, &ch.RestrictModels, &ch.Features, &ch.CreatedAt, &ch.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan channel: %w", err) return nil, fmt.Errorf("scan channel: %w", err)
} }
ch.ModelMapping = unmarshalModelMapping(modelMappingJSON) ch.ModelMapping = unmarshalModelMapping(modelMappingJSON)
......
...@@ -26,7 +26,6 @@ func RegisterPaymentRoutes( ...@@ -26,7 +26,6 @@ func RegisterPaymentRoutes(
authenticated.Use(middleware.BackendModeUserGuard(settingService)) authenticated.Use(middleware.BackendModeUserGuard(settingService))
{ {
authenticated.GET("/config", paymentHandler.GetPaymentConfig) authenticated.GET("/config", paymentHandler.GetPaymentConfig)
authenticated.GET("/checkout-info", paymentHandler.GetCheckoutInfo)
authenticated.GET("/plans", paymentHandler.GetPlans) authenticated.GET("/plans", paymentHandler.GetPlans)
authenticated.GET("/channels", paymentHandler.GetChannels) authenticated.GET("/channels", paymentHandler.GetChannels)
authenticated.GET("/limits", paymentHandler.GetLimits) authenticated.GET("/limits", paymentHandler.GetLimits)
...@@ -34,7 +33,6 @@ func RegisterPaymentRoutes( ...@@ -34,7 +33,6 @@ func RegisterPaymentRoutes(
orders := authenticated.Group("/orders") orders := authenticated.Group("/orders")
{ {
orders.POST("", paymentHandler.CreateOrder) orders.POST("", paymentHandler.CreateOrder)
orders.POST("/verify", paymentHandler.VerifyOrder)
orders.GET("/my", paymentHandler.GetMyOrders) orders.GET("/my", paymentHandler.GetMyOrders)
orders.GET("/:id", paymentHandler.GetOrder) orders.GET("/:id", paymentHandler.GetOrder)
orders.POST("/:id/cancel", paymentHandler.CancelOrder) orders.POST("/:id/cancel", paymentHandler.CancelOrder)
...@@ -42,19 +40,9 @@ func RegisterPaymentRoutes( ...@@ -42,19 +40,9 @@ func RegisterPaymentRoutes(
} }
} }
// --- Public payment endpoints (no auth) ---
// Payment result page needs to verify order status without login
// (user session may have expired during provider redirect).
public := v1.Group("/payment/public")
{
public.POST("/orders/verify", paymentHandler.VerifyOrderPublic)
}
// --- Webhook endpoints (no auth) --- // --- Webhook endpoints (no auth) ---
webhook := v1.Group("/payment/webhook") webhook := v1.Group("/payment/webhook")
{ {
// EasyPay sends GET callbacks with query params
webhook.GET("/easypay", webhookHandler.EasyPayNotify)
webhook.POST("/easypay", webhookHandler.EasyPayNotify) webhook.POST("/easypay", webhookHandler.EasyPayNotify)
webhook.POST("/alipay", webhookHandler.AlipayNotify) webhook.POST("/alipay", webhookHandler.AlipayNotify)
webhook.POST("/wxpay", webhookHandler.WxpayNotify) webhook.POST("/wxpay", webhookHandler.WxpayNotify)
...@@ -82,6 +70,7 @@ func RegisterPaymentRoutes( ...@@ -82,6 +70,7 @@ func RegisterPaymentRoutes(
adminOrders.POST("/:id/refund", adminPaymentHandler.ProcessRefund) adminOrders.POST("/:id/refund", adminPaymentHandler.ProcessRefund)
} }
// Subscription Plans // Subscription Plans
plans := adminGroup.Group("/plans") plans := adminGroup.Group("/plans")
{ {
......
...@@ -39,6 +39,7 @@ type Channel struct { ...@@ -39,6 +39,7 @@ type Channel struct {
Status string Status string
BillingModelSource string // "requested", "upstream", or "channel_mapped" BillingModelSource string // "requested", "upstream", or "channel_mapped"
RestrictModels bool // 是否限制模型(仅允许定价列表中的模型) RestrictModels bool // 是否限制模型(仅允许定价列表中的模型)
Features string // 渠道特性描述(JSON 数组),用于支付页面展示
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
......
...@@ -584,6 +584,7 @@ func (s *ChannelService) Create(ctx context.Context, input *CreateChannelInput) ...@@ -584,6 +584,7 @@ func (s *ChannelService) Create(ctx context.Context, input *CreateChannelInput)
GroupIDs: input.GroupIDs, GroupIDs: input.GroupIDs,
ModelPricing: input.ModelPricing, ModelPricing: input.ModelPricing,
ModelMapping: input.ModelMapping, ModelMapping: input.ModelMapping,
Features: input.Features,
} }
if channel.BillingModelSource == "" { if channel.BillingModelSource == "" {
channel.BillingModelSource = BillingModelSourceChannelMapped channel.BillingModelSource = BillingModelSourceChannelMapped
...@@ -641,6 +642,9 @@ func (s *ChannelService) Update(ctx context.Context, id int64, input *UpdateChan ...@@ -641,6 +642,9 @@ func (s *ChannelService) Update(ctx context.Context, id int64, input *UpdateChan
if input.RestrictModels != nil { if input.RestrictModels != nil {
channel.RestrictModels = *input.RestrictModels channel.RestrictModels = *input.RestrictModels
} }
if input.Features != nil {
channel.Features = *input.Features
}
// 检查分组冲突 // 检查分组冲突
if input.GroupIDs != nil { if input.GroupIDs != nil {
...@@ -842,6 +846,7 @@ type CreateChannelInput struct { ...@@ -842,6 +846,7 @@ type CreateChannelInput struct {
ModelMapping map[string]map[string]string // platform → {src→dst} ModelMapping map[string]map[string]string // platform → {src→dst}
BillingModelSource string BillingModelSource string
RestrictModels bool RestrictModels bool
Features string
} }
// UpdateChannelInput 更新渠道输入 // UpdateChannelInput 更新渠道输入
...@@ -854,4 +859,5 @@ type UpdateChannelInput struct { ...@@ -854,4 +859,5 @@ type UpdateChannelInput struct {
ModelMapping map[string]map[string]string // platform → {src→dst} ModelMapping map[string]map[string]string // platform → {src→dst}
BillingModelSource string BillingModelSource string
RestrictModels *bool RestrictModels *bool
Features *string
} }
...@@ -2,13 +2,16 @@ package service ...@@ -2,13 +2,16 @@ package service
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
dbent "github.com/Wei-Shaw/sub2api/ent" dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance" "github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
"github.com/Wei-Shaw/sub2api/ent/subscriptionplan"
"github.com/Wei-Shaw/sub2api/internal/payment" "github.com/Wei-Shaw/sub2api/internal/payment"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
) )
const ( const (
...@@ -23,8 +26,6 @@ const ( ...@@ -23,8 +26,6 @@ const (
SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED" SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED"
SettingProductNamePrefix = "PRODUCT_NAME_PREFIX" SettingProductNamePrefix = "PRODUCT_NAME_PREFIX"
SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX" SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX"
SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL"
SettingHelpText = "PAYMENT_HELP_TEXT"
SettingCancelRateLimitOn = "CANCEL_RATE_LIMIT_ENABLED" SettingCancelRateLimitOn = "CANCEL_RATE_LIMIT_ENABLED"
SettingCancelRateLimitMax = "CANCEL_RATE_LIMIT_MAX" SettingCancelRateLimitMax = "CANCEL_RATE_LIMIT_MAX"
SettingCancelWindowSize = "CANCEL_RATE_LIMIT_WINDOW" SettingCancelWindowSize = "CANCEL_RATE_LIMIT_WINDOW"
...@@ -32,126 +33,91 @@ const ( ...@@ -32,126 +33,91 @@ const (
SettingCancelWindowMode = "CANCEL_RATE_LIMIT_WINDOW_MODE" SettingCancelWindowMode = "CANCEL_RATE_LIMIT_WINDOW_MODE"
) )
// Default values for payment configuration settings.
const (
defaultOrderTimeoutMin = 30
defaultMaxPendingOrders = 3
)
// PaymentConfig holds the payment system configuration. // PaymentConfig holds the payment system configuration.
type PaymentConfig struct { type PaymentConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
MinAmount float64 `json:"min_amount"` MinAmount float64 `json:"minAmount"`
MaxAmount float64 `json:"max_amount"` MaxAmount float64 `json:"maxAmount"`
DailyLimit float64 `json:"daily_limit"` DailyLimit float64 `json:"dailyLimit"`
OrderTimeoutMin int `json:"order_timeout_minutes"` OrderTimeoutMin int `json:"orderTimeoutMinutes"`
MaxPendingOrders int `json:"max_pending_orders"` MaxPendingOrders int `json:"maxPendingOrders"`
EnabledTypes []string `json:"enabled_payment_types"` EnabledTypes []string `json:"enabledTypes"`
BalanceDisabled bool `json:"balance_disabled"` BalanceDisabled bool `json:"balanceDisabled"`
LoadBalanceStrategy string `json:"load_balance_strategy"` LoadBalanceStrategy string `json:"loadBalanceStrategy"`
ProductNamePrefix string `json:"product_name_prefix"` ProductNamePrefix string `json:"productNamePrefix"`
ProductNameSuffix string `json:"product_name_suffix"` ProductNameSuffix string `json:"productNameSuffix"`
HelpImageURL string `json:"help_image_url"`
HelpText string `json:"help_text"`
StripePublishableKey string `json:"stripe_publishable_key,omitempty"`
// Cancel rate limit settings
CancelRateLimitEnabled bool `json:"cancel_rate_limit_enabled"`
CancelRateLimitMax int `json:"cancel_rate_limit_max"`
CancelRateLimitWindow int `json:"cancel_rate_limit_window"`
CancelRateLimitUnit string `json:"cancel_rate_limit_unit"`
CancelRateLimitMode string `json:"cancel_rate_limit_window_mode"`
} }
// UpdatePaymentConfigRequest contains fields to update payment configuration. // UpdatePaymentConfigRequest contains fields to update payment configuration.
type UpdatePaymentConfigRequest struct { type UpdatePaymentConfigRequest struct {
Enabled *bool `json:"enabled"` Enabled *bool `json:"enabled"`
MinAmount *float64 `json:"min_amount"` MinAmount *float64 `json:"minAmount"`
MaxAmount *float64 `json:"max_amount"` MaxAmount *float64 `json:"maxAmount"`
DailyLimit *float64 `json:"daily_limit"` DailyLimit *float64 `json:"dailyLimit"`
OrderTimeoutMin *int `json:"order_timeout_minutes"` OrderTimeoutMin *int `json:"orderTimeoutMinutes"`
MaxPendingOrders *int `json:"max_pending_orders"` MaxPendingOrders *int `json:"maxPendingOrders"`
EnabledTypes []string `json:"enabled_payment_types"` EnabledTypes []string `json:"enabledTypes"`
BalanceDisabled *bool `json:"balance_disabled"` BalanceDisabled *bool `json:"balanceDisabled"`
LoadBalanceStrategy *string `json:"load_balance_strategy"` LoadBalanceStrategy *string `json:"loadBalanceStrategy"`
ProductNamePrefix *string `json:"product_name_prefix"` ProductNamePrefix *string `json:"productNamePrefix"`
ProductNameSuffix *string `json:"product_name_suffix"` ProductNameSuffix *string `json:"productNameSuffix"`
HelpImageURL *string `json:"help_image_url"`
HelpText *string `json:"help_text"`
// Cancel rate limit settings
CancelRateLimitEnabled *bool `json:"cancel_rate_limit_enabled"`
CancelRateLimitMax *int `json:"cancel_rate_limit_max"`
CancelRateLimitWindow *int `json:"cancel_rate_limit_window"`
CancelRateLimitUnit *string `json:"cancel_rate_limit_unit"`
CancelRateLimitMode *string `json:"cancel_rate_limit_window_mode"`
} }
// MethodLimits holds per-payment-type limits. // MethodLimits holds per-payment-type limits.
type MethodLimits struct { type MethodLimits struct {
PaymentType string `json:"payment_type"` PaymentType string `json:"paymentType"`
FeeRate float64 `json:"fee_rate"` FeeRate float64 `json:"feeRate"`
DailyLimit float64 `json:"daily_limit"` DailyLimit float64 `json:"dailyLimit"`
SingleMin float64 `json:"single_min"` SingleMin float64 `json:"singleMin"`
SingleMax float64 `json:"single_max"` SingleMax float64 `json:"singleMax"`
}
// MethodLimitsResponse is the full response for the user-facing /limits API.
// It includes per-method limits and the global widest range (union of all methods).
type MethodLimitsResponse struct {
Methods map[string]MethodLimits `json:"methods"`
GlobalMin float64 `json:"global_min"` // 0 = no minimum
GlobalMax float64 `json:"global_max"` // 0 = no maximum
} }
type CreateProviderInstanceRequest struct { type CreateProviderInstanceRequest struct {
ProviderKey string `json:"provider_key"` ProviderKey string `json:"providerKey"`
Name string `json:"name"` Name string `json:"name"`
Config map[string]string `json:"config"` Config map[string]string `json:"config"`
SupportedTypes []string `json:"supported_types"` SupportedTypes string `json:"supportedTypes"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
PaymentMode string `json:"payment_mode"` SortOrder int `json:"sortOrder"`
SortOrder int `json:"sort_order"`
Limits string `json:"limits"` Limits string `json:"limits"`
RefundEnabled bool `json:"refund_enabled"` RefundEnabled bool `json:"refundEnabled"`
} }
type UpdateProviderInstanceRequest struct { type UpdateProviderInstanceRequest struct {
Name *string `json:"name"` Name *string `json:"name"`
Config map[string]string `json:"config"` Config map[string]string `json:"config"`
SupportedTypes []string `json:"supported_types"` SupportedTypes *string `json:"supportedTypes"`
Enabled *bool `json:"enabled"` Enabled *bool `json:"enabled"`
PaymentMode *string `json:"payment_mode"` SortOrder *int `json:"sortOrder"`
SortOrder *int `json:"sort_order"`
Limits *string `json:"limits"` Limits *string `json:"limits"`
RefundEnabled *bool `json:"refund_enabled"` RefundEnabled *bool `json:"refundEnabled"`
} }
type CreatePlanRequest struct { type CreatePlanRequest struct {
GroupID int64 `json:"group_id"` GroupID int64 `json:"groupId"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Price float64 `json:"price"` Price float64 `json:"price"`
OriginalPrice *float64 `json:"original_price"` OriginalPrice *float64 `json:"originalPrice"`
ValidityDays int `json:"validity_days"` ValidityDays int `json:"validityDays"`
ValidityUnit string `json:"validity_unit"` ValidityUnit string `json:"validityUnit"`
Features string `json:"features"` Features string `json:"features"`
ProductName string `json:"product_name"` ProductName string `json:"productName"`
ForSale bool `json:"for_sale"` ForSale bool `json:"forSale"`
SortOrder int `json:"sort_order"` SortOrder int `json:"sortOrder"`
} }
type UpdatePlanRequest struct { type UpdatePlanRequest struct {
GroupID *int64 `json:"group_id"` GroupID *int64 `json:"groupId"`
Name *string `json:"name"` Name *string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
Price *float64 `json:"price"` Price *float64 `json:"price"`
OriginalPrice *float64 `json:"original_price"` OriginalPrice *float64 `json:"originalPrice"`
ValidityDays *int `json:"validity_days"` ValidityDays *int `json:"validityDays"`
ValidityUnit *string `json:"validity_unit"` ValidityUnit *string `json:"validityUnit"`
Features *string `json:"features"` Features *string `json:"features"`
ProductName *string `json:"product_name"` ProductName *string `json:"productName"`
ForSale *bool `json:"for_sale"` ForSale *bool `json:"forSale"`
SortOrder *int `json:"sort_order"` SortOrder *int `json:"sortOrder"`
} }
// PaymentConfigService manages payment configuration and CRUD for // PaymentConfigService manages payment configuration and CRUD for
...@@ -183,43 +149,29 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo ...@@ -183,43 +149,29 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo
SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders, SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders,
SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingLoadBalanceStrategy, SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingLoadBalanceStrategy,
SettingProductNamePrefix, SettingProductNameSuffix, SettingProductNamePrefix, SettingProductNameSuffix,
SettingHelpImageURL, SettingHelpText,
SettingCancelRateLimitOn, SettingCancelRateLimitMax,
SettingCancelWindowSize, SettingCancelWindowUnit, SettingCancelWindowMode,
} }
vals, err := s.settingRepo.GetMultiple(ctx, keys) vals, err := s.settingRepo.GetMultiple(ctx, keys)
if err != nil { if err != nil {
return nil, fmt.Errorf("get payment config settings: %w", err) return nil, fmt.Errorf("get payment config settings: %w", err)
} }
cfg := s.parsePaymentConfig(vals) return s.parsePaymentConfig(vals), nil
// Load Stripe publishable key from the first enabled Stripe provider instance
cfg.StripePublishableKey = s.getStripePublishableKey(ctx)
return cfg, nil
} }
func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *PaymentConfig { func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *PaymentConfig {
cfg := &PaymentConfig{ cfg := &PaymentConfig{
Enabled: vals[SettingPaymentEnabled] == "true", Enabled: vals[SettingPaymentEnabled] == "true",
MinAmount: pcParseFloat(vals[SettingMinRechargeAmount], 1), MinAmount: pcParseFloat(vals[SettingMinRechargeAmount], 1),
MaxAmount: pcParseFloat(vals[SettingMaxRechargeAmount], 0), MaxAmount: pcParseFloat(vals[SettingMaxRechargeAmount], 99999999.99),
DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0), DailyLimit: pcParseFloat(vals[SettingDailyRechargeLimit], 0),
OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], defaultOrderTimeoutMin), OrderTimeoutMin: pcParseInt(vals[SettingOrderTimeoutMinutes], 30),
MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], defaultMaxPendingOrders), MaxPendingOrders: pcParseInt(vals[SettingMaxPendingOrders], 3),
BalanceDisabled: vals[SettingBalancePayDisabled] == "true", BalanceDisabled: vals[SettingBalancePayDisabled] == "true",
LoadBalanceStrategy: vals[SettingLoadBalanceStrategy], LoadBalanceStrategy: vals[SettingLoadBalanceStrategy],
ProductNamePrefix: vals[SettingProductNamePrefix], ProductNamePrefix: vals[SettingProductNamePrefix],
ProductNameSuffix: vals[SettingProductNameSuffix], ProductNameSuffix: vals[SettingProductNameSuffix],
HelpImageURL: vals[SettingHelpImageURL],
HelpText: vals[SettingHelpText],
CancelRateLimitEnabled: vals[SettingCancelRateLimitOn] == "true",
CancelRateLimitMax: pcParseInt(vals[SettingCancelRateLimitMax], 10),
CancelRateLimitWindow: pcParseInt(vals[SettingCancelWindowSize], 1),
CancelRateLimitUnit: vals[SettingCancelWindowUnit],
CancelRateLimitMode: vals[SettingCancelWindowMode],
} }
if cfg.LoadBalanceStrategy == "" { if cfg.LoadBalanceStrategy == "" {
cfg.LoadBalanceStrategy = payment.DefaultLoadBalanceStrategy cfg.LoadBalanceStrategy = "round-robin"
} }
if raw := vals[SettingEnabledPaymentTypes]; raw != "" { if raw := vals[SettingEnabledPaymentTypes]; raw != "" {
for _, t := range strings.Split(raw, ",") { for _, t := range strings.Split(raw, ",") {
...@@ -232,100 +184,242 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme ...@@ -232,100 +184,242 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme
return cfg return cfg
} }
// getStripePublishableKey finds the publishable key from the first enabled Stripe provider instance.
func (s *PaymentConfigService) getStripePublishableKey(ctx context.Context) string {
instances, err := s.entClient.PaymentProviderInstance.Query().
Where(
paymentproviderinstance.EnabledEQ(true),
paymentproviderinstance.ProviderKeyEQ(payment.TypeStripe),
).Limit(1).All(ctx)
if err != nil || len(instances) == 0 {
return ""
}
cfg, err := s.decryptConfig(instances[0].Config)
if err != nil || cfg == nil {
return ""
}
return cfg[payment.ConfigKeyPublishableKey]
}
// UpdatePaymentConfig updates the payment configuration settings. // UpdatePaymentConfig updates the payment configuration settings.
// NOTE: This function exceeds 30 lines because each field requires an independent
// nil-check before serialisation — this is inherent to patch-style update patterns
// and cannot be meaningfully decomposed without introducing unnecessary abstraction.
func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error { func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error {
m := map[string]string{ m := make(map[string]string)
SettingPaymentEnabled: formatBoolOrEmpty(req.Enabled), if req.Enabled != nil {
SettingMinRechargeAmount: formatPositiveFloat(req.MinAmount), m[SettingPaymentEnabled] = strconv.FormatBool(*req.Enabled)
SettingMaxRechargeAmount: formatPositiveFloat(req.MaxAmount), }
SettingDailyRechargeLimit: formatPositiveFloat(req.DailyLimit), if req.MinAmount != nil {
SettingOrderTimeoutMinutes: formatPositiveInt(req.OrderTimeoutMin), m[SettingMinRechargeAmount] = strconv.FormatFloat(*req.MinAmount, 'f', 2, 64)
SettingMaxPendingOrders: formatPositiveInt(req.MaxPendingOrders), }
SettingBalancePayDisabled: formatBoolOrEmpty(req.BalanceDisabled), if req.MaxAmount != nil {
SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy), m[SettingMaxRechargeAmount] = strconv.FormatFloat(*req.MaxAmount, 'f', 2, 64)
SettingProductNamePrefix: derefStr(req.ProductNamePrefix), }
SettingProductNameSuffix: derefStr(req.ProductNameSuffix), if req.DailyLimit != nil {
SettingHelpImageURL: derefStr(req.HelpImageURL), m[SettingDailyRechargeLimit] = strconv.FormatFloat(*req.DailyLimit, 'f', 2, 64)
SettingHelpText: derefStr(req.HelpText), }
SettingCancelRateLimitOn: formatBoolOrEmpty(req.CancelRateLimitEnabled), if req.OrderTimeoutMin != nil {
SettingCancelRateLimitMax: formatPositiveInt(req.CancelRateLimitMax), m[SettingOrderTimeoutMinutes] = strconv.Itoa(*req.OrderTimeoutMin)
SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow), }
SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit), if req.MaxPendingOrders != nil {
SettingCancelWindowMode: derefStr(req.CancelRateLimitMode), m[SettingMaxPendingOrders] = strconv.Itoa(*req.MaxPendingOrders)
} }
if req.EnabledTypes != nil { if req.EnabledTypes != nil {
m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",") m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",")
} else { }
m[SettingEnabledPaymentTypes] = "" if req.BalanceDisabled != nil {
m[SettingBalancePayDisabled] = strconv.FormatBool(*req.BalanceDisabled)
}
if req.LoadBalanceStrategy != nil {
m[SettingLoadBalanceStrategy] = *req.LoadBalanceStrategy
}
if req.ProductNamePrefix != nil {
m[SettingProductNamePrefix] = *req.ProductNamePrefix
}
if req.ProductNameSuffix != nil {
m[SettingProductNameSuffix] = *req.ProductNameSuffix
}
if len(m) == 0 {
return nil
} }
return s.settingRepo.SetMultiple(ctx, m) return s.settingRepo.SetMultiple(ctx, m)
} }
func formatBoolOrEmpty(v *bool) string { // --- Provider Instance CRUD ---
if v == nil {
return "" func (s *PaymentConfigService) ListProviderInstances(ctx context.Context) ([]*dbent.PaymentProviderInstance, error) {
return s.entClient.PaymentProviderInstance.Query().Order(paymentproviderinstance.BySortOrder()).All(ctx)
}
func (s *PaymentConfigService) CreateProviderInstance(ctx context.Context, req CreateProviderInstanceRequest) (*dbent.PaymentProviderInstance, error) {
enc, err := s.encryptConfig(req.Config)
if err != nil {
return nil, err
}
return s.entClient.PaymentProviderInstance.Create().
SetProviderKey(req.ProviderKey).SetName(req.Name).SetConfig(enc).
SetSupportedTypes(req.SupportedTypes).SetEnabled(req.Enabled).
SetSortOrder(req.SortOrder).SetLimits(req.Limits).SetRefundEnabled(req.RefundEnabled).
Save(ctx)
}
func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id int64, req UpdateProviderInstanceRequest) (*dbent.PaymentProviderInstance, error) {
u := s.entClient.PaymentProviderInstance.UpdateOneID(id)
if req.Name != nil {
u.SetName(*req.Name)
}
if req.Config != nil {
enc, err := s.encryptConfig(req.Config)
if err != nil {
return nil, err
}
u.SetConfig(enc)
}
if req.SupportedTypes != nil {
u.SetSupportedTypes(*req.SupportedTypes)
}
if req.Enabled != nil {
u.SetEnabled(*req.Enabled)
}
if req.SortOrder != nil {
u.SetSortOrder(*req.SortOrder)
}
if req.Limits != nil {
u.SetLimits(*req.Limits)
}
if req.RefundEnabled != nil {
u.SetRefundEnabled(*req.RefundEnabled)
}
return u.Save(ctx)
}
func (s *PaymentConfigService) DeleteProviderInstance(ctx context.Context, id int64) error {
return s.entClient.PaymentProviderInstance.DeleteOneID(id).Exec(ctx)
}
func (s *PaymentConfigService) encryptConfig(cfg map[string]string) (string, error) {
data, err := json.Marshal(cfg)
if err != nil {
return "", fmt.Errorf("marshal config: %w", err)
}
enc, err := payment.Encrypt(string(data), s.encryptionKey)
if err != nil {
return "", fmt.Errorf("encrypt config: %w", err)
} }
return strconv.FormatBool(*v) return enc, nil
}
// --- Channel CRUD ---
// --- Plan CRUD ---
func (s *PaymentConfigService) ListPlans(ctx context.Context) ([]*dbent.SubscriptionPlan, error) {
return s.entClient.SubscriptionPlan.Query().Order(subscriptionplan.BySortOrder()).All(ctx)
}
func (s *PaymentConfigService) ListPlansForSale(ctx context.Context) ([]*dbent.SubscriptionPlan, error) {
return s.entClient.SubscriptionPlan.Query().Where(subscriptionplan.ForSaleEQ(true)).Order(subscriptionplan.BySortOrder()).All(ctx)
} }
func formatPositiveFloat(v *float64) string { func (s *PaymentConfigService) CreatePlan(ctx context.Context, req CreatePlanRequest) (*dbent.SubscriptionPlan, error) {
if v == nil || *v <= 0 { b := s.entClient.SubscriptionPlan.Create().
return "" // empty → parsePaymentConfig uses default SetGroupID(req.GroupID).SetName(req.Name).SetDescription(req.Description).
SetPrice(req.Price).SetValidityDays(req.ValidityDays).SetValidityUnit(req.ValidityUnit).
SetFeatures(req.Features).SetProductName(req.ProductName).
SetForSale(req.ForSale).SetSortOrder(req.SortOrder)
if req.OriginalPrice != nil {
b.SetOriginalPrice(*req.OriginalPrice)
} }
return strconv.FormatFloat(*v, 'f', 2, 64) return b.Save(ctx)
} }
func formatPositiveInt(v *int) string { func (s *PaymentConfigService) UpdatePlan(ctx context.Context, id int64, req UpdatePlanRequest) (*dbent.SubscriptionPlan, error) {
if v == nil || *v <= 0 { u := s.entClient.SubscriptionPlan.UpdateOneID(id)
return "" if req.GroupID != nil {
u.SetGroupID(*req.GroupID)
}
if req.Name != nil {
u.SetName(*req.Name)
}
if req.Description != nil {
u.SetDescription(*req.Description)
}
if req.Price != nil {
u.SetPrice(*req.Price)
}
if req.OriginalPrice != nil {
u.SetOriginalPrice(*req.OriginalPrice)
}
if req.ValidityDays != nil {
u.SetValidityDays(*req.ValidityDays)
}
if req.ValidityUnit != nil {
u.SetValidityUnit(*req.ValidityUnit)
}
if req.Features != nil {
u.SetFeatures(*req.Features)
}
if req.ProductName != nil {
u.SetProductName(*req.ProductName)
}
if req.ForSale != nil {
u.SetForSale(*req.ForSale)
}
if req.SortOrder != nil {
u.SetSortOrder(*req.SortOrder)
} }
return strconv.Itoa(*v) return u.Save(ctx)
} }
func derefStr(v *string) string { func (s *PaymentConfigService) DeletePlan(ctx context.Context, id int64) error {
if v == nil { return s.entClient.SubscriptionPlan.DeleteOneID(id).Exec(ctx)
return "" }
// GetPlan returns a subscription plan by ID.
func (s *PaymentConfigService) GetPlan(ctx context.Context, id int64) (*dbent.SubscriptionPlan, error) {
plan, err := s.entClient.SubscriptionPlan.Get(ctx, id)
if err != nil {
return nil, infraerrors.NotFound("PLAN_NOT_FOUND", "subscription plan not found")
} }
return *v return plan, nil
} }
func splitTypes(s string) []string { // GetMethodLimits returns per-payment-type limits from enabled provider instances.
if s == "" { func (s *PaymentConfigService) GetMethodLimits(ctx context.Context, types []string) ([]MethodLimits, error) {
return nil instances, err := s.entClient.PaymentProviderInstance.Query().
Where(paymentproviderinstance.EnabledEQ(true)).All(ctx)
if err != nil {
return nil, fmt.Errorf("query provider instances: %w", err)
} }
parts := strings.Split(s, ",") result := make([]MethodLimits, 0, len(types))
result := make([]string, 0, len(parts)) for _, pt := range types {
for _, p := range parts { ml := MethodLimits{PaymentType: pt}
p = strings.TrimSpace(p) for _, inst := range instances {
if p != "" { if !pcInstanceSupportsType(inst, pt) {
result = append(result, p) continue
}
pcApplyInstanceLimits(inst, pt, &ml)
} }
result = append(result, ml)
} }
return result return result, nil
} }
func joinTypes(types []string) string { func pcInstanceSupportsType(inst *dbent.PaymentProviderInstance, pt string) bool {
return strings.Join(types, ",") if inst.SupportedTypes == "" {
return true
}
for _, t := range strings.Split(inst.SupportedTypes, ",") {
if strings.TrimSpace(t) == pt {
return true
}
}
return false
}
func pcApplyInstanceLimits(inst *dbent.PaymentProviderInstance, pt string, ml *MethodLimits) {
if inst.Limits == "" {
return
}
var limits payment.InstanceLimits
if err := json.Unmarshal([]byte(inst.Limits), &limits); err != nil {
return
}
cl, ok := limits[pt]
if !ok {
return
}
if cl.DailyLimit > 0 && (ml.DailyLimit == 0 || cl.DailyLimit < ml.DailyLimit) {
ml.DailyLimit = cl.DailyLimit
}
if cl.SingleMin > 0 && (ml.SingleMin == 0 || cl.SingleMin > ml.SingleMin) {
ml.SingleMin = cl.SingleMin
}
if cl.SingleMax > 0 && (ml.SingleMax == 0 || cl.SingleMax < ml.SingleMax) {
ml.SingleMax = cl.SingleMax
}
} }
func pcParseFloat(s string, defaultVal float64) float64 { func pcParseFloat(s string, defaultVal float64) float64 {
......
ALTER TABLE channels ADD COLUMN IF NOT EXISTS features TEXT NOT NULL DEFAULT '';
COMMENT ON COLUMN channels.features IS '渠道特性描述,JSON 数组格式,用于支付页面展示';
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment