This commit is contained in:
Yvan
2026-09-23 17:42:55 +08:00
parent 53e8599bdd
commit 0a8ecbe09b
75 changed files with 2299 additions and 276 deletions
@@ -0,0 +1,69 @@
package recharge
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
integrationbiz "kra/internal/biz/integration"
)
type providerConfigRepo struct {
configs []*integrationbiz.IntegrationConfig
err error
}
func (r *providerConfigRepo) ListIntegrationConfigs(context.Context, string) ([]*integrationbiz.IntegrationConfig, error) {
return r.configs, r.err
}
func (*providerConfigRepo) FindIntegrationConfig(context.Context, string, string) (*integrationbiz.IntegrationConfig, error) {
return nil, errors.New("not implemented")
}
func (*providerConfigRepo) SaveIntegrationConfig(context.Context, *integrationbiz.IntegrationConfig) error {
return errors.New("not implemented")
}
func (*providerConfigRepo) DeleteIntegrationConfig(context.Context, string, string) error {
return errors.New("not implemented")
}
func TestProvidersOnlyReturnsEnabledSupportedPaymentConfigs(t *testing.T) {
repo := &providerConfigRepo{configs: []*integrationbiz.IntegrationConfig{
{Kind: integrationbiz.IntegrationKindPayment, Provider: "wechat-v3", Enabled: true, Values: []byte(`{"api_key":"must-not-leak"}`)},
{Kind: integrationbiz.IntegrationKindPayment, Provider: "alipay", Enabled: false},
{Kind: integrationbiz.IntegrationKindPayment, Provider: "unknown-provider", Enabled: true},
{Kind: integrationbiz.IntegrationKindPayment, Provider: "wechat-v3", Enabled: true},
nil,
}}
integrations := integrationbiz.NewIntegrationConfigUsecase(repo, nil)
service := NewService(nil, integrations)
providers, err := service.Providers(context.Background())
if err != nil {
t.Fatal(err)
}
if len(providers) != 1 || providers[0].ID != "wechat-v3" || providers[0].Name == "" {
t.Fatalf("providers = %+v", providers)
}
encoded, err := json.Marshal(providers)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(encoded), "must-not-leak") || strings.Contains(string(encoded), "api_key") {
t.Fatalf("provider response leaked integration config: %s", encoded)
}
}
func TestProvidersPropagatesConfigReadFailure(t *testing.T) {
want := errors.New("load failed")
integrations := integrationbiz.NewIntegrationConfigUsecase(&providerConfigRepo{err: want}, nil)
service := NewService(nil, integrations)
if _, err := service.Providers(context.Background()); !errors.Is(err, want) {
t.Fatalf("Providers() error = %v, want %v", err, want)
}
}
+58 -2
View File
@@ -2,16 +2,48 @@ package recharge
import (
"context"
"sort"
"time"
integrationbiz "kra/internal/biz/integration"
bizpayment "kra/internal/biz/payment"
biz "kra/internal/biz/recharge"
"kra/internal/service/dto"
)
type Service struct{ uc *biz.Usecase }
type Service struct {
uc *biz.Usecase
integrations *integrationbiz.IntegrationConfigUsecase
}
func NewService(uc *biz.Usecase) *Service { return &Service{uc: uc} }
func NewService(uc *biz.Usecase, integrations *integrationbiz.IntegrationConfigUsecase) *Service {
return &Service{uc: uc, integrations: integrations}
}
func (s *Service) Providers(ctx context.Context) ([]*dto.RechargeProvider, error) {
configs, err := s.integrations.List(ctx, integrationbiz.IntegrationKindPayment)
if err != nil {
return nil, err
}
providers := make([]*dto.RechargeProvider, 0, len(configs))
seen := make(map[string]struct{}, len(configs))
for _, config := range configs {
if config == nil || !config.Enabled || !bizpayment.IsSupportedPaymentProvider(config.Provider) {
continue
}
definition, ok := integrationbiz.IntegrationDefinition(integrationbiz.IntegrationKindPayment, config.Provider)
if !ok {
continue
}
if _, exists := seen[definition.Provider]; exists {
continue
}
seen[definition.Provider] = struct{}{}
providers = append(providers, &dto.RechargeProvider{ID: definition.Provider, Name: definition.Name})
}
sort.Slice(providers, func(i, j int) bool { return providers[i].ID < providers[j].ID })
return providers, nil
}
func dates(a, b string) []time.Time {
if a == "" || b == "" {
@@ -76,6 +108,30 @@ func (s *Service) Get(ctx context.Context, no string) (*dto.RechargeOrderRespons
return order(v), nil
}
// GetForUser is the client-facing order lookup. It deliberately maps an
// order owned by another account to not-found so order numbers cannot be used
// to enumerate another user's payment or diamond balance details.
func (s *Service) GetForUser(ctx context.Context, userID uint, no string) (*dto.RechargeOrderResponse, error) {
if userID == 0 {
return nil, biz.ErrNotFound
}
item, err := s.uc.Get(ctx, no)
if err != nil {
return nil, err
}
if item.UserID != userID {
return nil, biz.ErrNotFound
}
return order(item), nil
}
// PaymentStatus is an explicit alias for client polling while a provider
// payment is pending. The response includes both provider and fulfillment
// state through the normal recharge order DTO.
func (s *Service) PaymentStatus(ctx context.Context, userID uint, no string) (*dto.RechargeOrderResponse, error) {
return s.GetForUser(ctx, userID, no)
}
func (s *Service) Create(ctx context.Context, userID uint, in *dto.RechargeOrderRequest) (*dto.RechargeCheckoutResponse, error) {
v, result, err := s.uc.Create(ctx, userID, in.PlanID, biz.Checkout{Provider: in.Provider, NotifyURL: in.NotifyURL, ReturnURL: in.ReturnURL, ClientIP: in.ClientIP, UserAgent: in.UserAgent, DeviceID: in.DeviceID, Extra: in.Extra})
if err != nil {