优化结构

This commit is contained in:
Yvan
2026-08-28 00:29:40 +08:00
parent 04ff85116c
commit 03a4131f6e
118 changed files with 758 additions and 834 deletions
@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
paymentutil "kra/pkg/paymentkit"
bizpayment "kra/internal/biz/payment"
"sort"
"strconv"
"strings"
@@ -316,7 +316,7 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
}
}
switch provider {
case paymentutil.ProviderAlipayV3:
case bizpayment.PaymentAlipayV3:
for _, group := range []struct {
label string
keys []string
@@ -330,21 +330,21 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
return fmt.Errorf("%s 缺少配置字段 %s", provider, group.label)
}
}
case paymentutil.ProviderWechatV2:
case bizpayment.PaymentWechatV2:
if integrationFirst(values, "client_cert", "cert_pem", "apiclient_cert") == "" || integrationFirst(values, "client_key", "key_pem", "apiclient_key") == "" {
return fmt.Errorf("%s 退款要求同时配置 client_cert 和 client_key", provider)
}
case paymentutil.ProviderApple:
case bizpayment.PaymentApple:
if integrationInt64(values, "price_divisor", 0) <= 0 {
if _, exists := values["price_divisors"].(map[string]any); !exists {
return fmt.Errorf("%s 缺少配置字段 price_divisor 或 price_divisors", provider)
}
}
case paymentutil.ProviderDouyin:
case bizpayment.PaymentDouyin:
if integrationFirst(values, "platform_serial_no", "platform_cert_serial") == "" {
return fmt.Errorf("%s 缺少配置字段 platform_serial_no", provider)
}
case paymentutil.ProviderQQ:
case bizpayment.PaymentQQ:
if integrationFirst(values, "mch_id", "merchant_id") == "" {
return fmt.Errorf("%s 缺少配置字段 mch_id", provider)
}
@@ -355,12 +355,12 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
if signType != "" && signType != "MD5" && signType != "HMAC-SHA256" {
return fmt.Errorf("%s sign_type 必须是 MD5 或 HMAC-SHA256", provider)
}
case paymentutil.ProviderAllinPay:
case bizpayment.PaymentAllinPay:
orderType := strings.ToLower(integrationFirst(values, "query_order_type", "order_type"))
if orderType != "" && orderType != "reqsn" && orderType != "trxid" {
return fmt.Errorf("%s query_order_type 必须是 reqsn 或 trxid", provider)
}
case paymentutil.ProviderChinaums, paymentutil.ProviderSFT, paymentutil.ProviderSuperPay, paymentutil.ProviderWechatGame, paymentutil.ProviderDouyinGame:
case bizpayment.PaymentChinaums, bizpayment.PaymentSFT, bizpayment.PaymentSuperPay, bizpayment.PaymentWechatGame, bizpayment.PaymentDouyinGame:
if integrationFirst(values, "app_key", "merchant_key", "signing_secret", "token") == "" {
return fmt.Errorf("%s 缺少签名密钥", provider)
}
@@ -369,7 +369,7 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
}
}
if environment := strings.ToLower(integrationText(values, "environment")); environment != "" {
allowedSandbox := provider != paymentutil.ProviderQQ && provider != paymentutil.ProviderDouyin && provider != paymentutil.ProviderLakala
allowedSandbox := provider != bizpayment.PaymentQQ && provider != bizpayment.PaymentDouyin && provider != bizpayment.PaymentLakala
if environment != "production" && environment != "prod" && (!allowedSandbox || environment != "sandbox") {
return fmt.Errorf("%s environment 配置无效", provider)
}
@@ -1,6 +1,6 @@
package integration
import paymentutil "kra/pkg/paymentkit"
import bizpayment "kra/internal/biz/payment"
func integrationField(key, label string, required, secret bool, fieldType string) IntegrationConfigField {
if fieldType == "" {
@@ -150,35 +150,35 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{
},
},
IntegrationKindPayment: {
paymentDefinition(paymentutil.ProviderAlipay, "支付宝", "支付宝 OpenAPI RSA2 支付", map[string]any{"app_id": "", "private_key": "", "public_key": "", "environment": "production", "sign_type": "RSA2", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"},
paymentDefinition(bizpayment.PaymentAlipay, "支付宝", "支付宝 OpenAPI RSA2 支付", map[string]any{"app_id": "", "private_key": "", "public_key": "", "environment": "production", "sign_type": "RSA2", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", true, true, "textarea"), integrationField("public_key", "支付宝公钥", true, true, "textarea"),
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationSelect("sign_type", "签名算法", false, "RSA2", "RSA"), integrationField("gateway_url", "网关地址", false, false, "url"),
integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
paymentDefinition(paymentutil.ProviderAlipayV3, "支付宝 V3", "支付宝证书模式 V3 接口", map[string]any{"app_id": "", "private_key": "", "app_cert": "", "root_cert": "", "public_cert": "", "environment": "production", "api_base_url": "https://openapi.alipay.com", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"},
paymentDefinition(bizpayment.PaymentAlipayV3, "支付宝 V3", "支付宝证书模式 V3 接口", map[string]any{"app_id": "", "private_key": "", "app_cert": "", "root_cert": "", "public_cert": "", "environment": "production", "api_base_url": "https://openapi.alipay.com", "gateway_url": "https://openapi.alipay.com/gateway.do", "method": "alipay.trade.create"},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "应用私钥", false, true, "textarea"), integrationField("app_cert", "应用公钥证书", false, true, "textarea"), integrationField("root_cert", "支付宝根证书", false, true, "textarea"), integrationField("public_cert", "支付宝公钥证书", false, true, "textarea"),
integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("api_base_url", "API 地址", false, false, "url"), integrationField("gateway_url", "网关地址", false, false, "url"), integrationSelect("method", "默认支付方式", false, "alipay.trade.create", "alipay.trade.pay", "alipay.trade.precreate", "alipay.trade.app.pay", "alipay.trade.page.pay", "alipay.trade.wap.pay")),
paymentDefinition(paymentutil.ProviderWechatV2, "微信支付 V2", "微信支付 V2,含退款双向证书", map[string]any{"app_id": "", "merchant_id": "", "mch_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "client_cert": "", "client_key": ""},
paymentDefinition(bizpayment.PaymentWechatV2, "微信支付 V2", "微信支付 V2,含退款双向证书", map[string]any{"app_id": "", "merchant_id": "", "mch_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "client_cert": "", "client_key": ""},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("mch_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "APP", "NATIVE", "MWEB", "MICROPAY"), integrationField("client_cert", "商户证书", false, true, "textarea"), integrationField("client_key", "证书私钥", false, true, "textarea")),
paymentDefinition(paymentutil.ProviderWechatV3, "微信支付 V3", "微信支付 API v3", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "private_key": "", "api_v3_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi"},
paymentDefinition(bizpayment.PaymentWechatV3, "微信支付 V3", "微信支付 API v3", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "private_key": "", "api_v3_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi"},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("api_v3_key", "API v3 密钥", true, true, "password"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", false, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "jsapi", "app", "native", "h5", "codepay")),
paymentDefinition(paymentutil.ProviderApple, "Apple IAP", "Apple App Store Server API", map[string]any{"issuer_id": "", "key_id": "", "bundle_id": "", "private_key": "", "price_divisor": 10, "environment": "production", "test_product_id": "", "test_transaction_id": ""},
paymentDefinition(bizpayment.PaymentApple, "Apple IAP", "Apple App Store Server API", map[string]any{"issuer_id": "", "key_id": "", "bundle_id": "", "private_key": "", "price_divisor": 10, "environment": "production", "test_product_id": "", "test_transaction_id": ""},
integrationField("issuer_id", "Issuer ID", true, false, "text"), integrationField("key_id", "Key ID", true, false, "text"), integrationField("bundle_id", "Bundle ID", true, false, "text"), integrationField("private_key", "P8 私钥", true, true, "textarea"), integrationField("price_divisor", "价格除数", false, false, "number"), integrationSelect("environment", "环境", false, "production", "sandbox"), integrationField("test_product_id", "测试商品 ID", false, false, "text"), integrationField("test_transaction_id", "沙箱交易 ID", false, false, "text")),
paymentDefinition(paymentutil.ProviderDouyin, "抖音支付", "抖音开放平台支付", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "api_key": "", "private_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi", "environment": "production"},
paymentDefinition(bizpayment.PaymentDouyin, "抖音支付", "抖音开放平台支付", map[string]any{"app_id": "", "merchant_id": "", "serial_no": "", "api_key": "", "private_key": "", "platform_cert": "", "platform_serial_no": "", "trade_type": "jsapi", "environment": "production"},
integrationField("app_id", "应用 ID", true, false, "text"), integrationField("merchant_id", "商户号", true, false, "text"), integrationField("serial_no", "商户证书序列号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("platform_cert", "平台证书", true, true, "textarea"), integrationField("platform_serial_no", "平台证书序列号", false, false, "text"), integrationSelect("trade_type", "默认交易类型", false, "app", "jsapi", "h5", "native"), integrationSelect("environment", "环境", false, "production")),
paymentDefinition(paymentutil.ProviderQQ, "QQ 钱包", "QQ 钱包支付", map[string]any{"mch_id": "", "api_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "cert_file": "", "key_file": "", "environment": "production"},
paymentDefinition(bizpayment.PaymentQQ, "QQ 钱包", "QQ 钱包支付", map[string]any{"mch_id": "", "api_key": "", "sign_type": "MD5", "trade_type": "NATIVE", "cert_file": "", "key_file": "", "environment": "production"},
integrationField("mch_id", "商户号", true, false, "text"), integrationField("api_key", "API 密钥", true, true, "password"), integrationSelect("sign_type", "签名算法", false, "MD5", "HMAC-SHA256"), integrationSelect("trade_type", "默认交易类型", false, "JSAPI", "NATIVE", "APP", "MICROPAY"), integrationField("cert_file", "退款证书路径", false, false, "text"), integrationField("key_file", "退款私钥路径", false, true, "text"), integrationSelect("environment", "环境", false, "production")),
paymentDefinition(paymentutil.ProviderAllinPay, "通联支付", "通联收银宝支付", map[string]any{"cus_id": "", "app_id": "", "private_key": "", "public_key": "", "org_id": "", "pay_type": "W02", "query_order_type": "reqsn", "currency": "CNY", "environment": "production"},
paymentDefinition(bizpayment.PaymentAllinPay, "通联支付", "通联收银宝支付", map[string]any{"cus_id": "", "app_id": "", "private_key": "", "public_key": "", "org_id": "", "pay_type": "W02", "query_order_type": "reqsn", "currency": "CNY", "environment": "production"},
integrationField("cus_id", "商户号", true, false, "text"), integrationField("app_id", "应用 ID", true, false, "text"), integrationField("private_key", "商户私钥", true, true, "textarea"), integrationField("public_key", "平台公钥", true, true, "textarea"), integrationField("org_id", "机构号", false, false, "text"), integrationField("pay_type", "支付类型", false, false, "text"), integrationSelect("query_order_type", "查单标识", false, "reqsn", "trxid"), integrationField("currency", "币种", false, false, "text"), integrationSelect("environment", "环境", false, "production", "sandbox")),
paymentDefinition(paymentutil.ProviderLakala, "拉卡拉", "拉卡拉聚合支付", map[string]any{"partner_code": "", "credential_code": "", "channel": "Wechat", "method": "jsapi", "currency": "CNY", "environment": "production"},
paymentDefinition(bizpayment.PaymentLakala, "拉卡拉", "拉卡拉聚合支付", map[string]any{"partner_code": "", "credential_code": "", "channel": "Wechat", "method": "jsapi", "currency": "CNY", "environment": "production"},
integrationField("partner_code", "合作方编号", true, false, "text"), integrationField("credential_code", "凭证码", true, true, "password"), integrationSelect("channel", "支付渠道", false, "Wechat", "Alipay", "UnionPay"), integrationSelect("method", "默认支付方式", false, "jsapi", "h5", "mini", "native", "qrcode", "native_jsapi", "sdk", "web", "retail", "retail_qrcode"), integrationField("currency", "币种", false, false, "text"), integrationSelect("environment", "环境", false, "production")),
paymentDefinition(paymentutil.ProviderPayPal, "PayPal", "PayPal Checkout", map[string]any{"client_id": "", "client_secret": "", "webhook_id": "", "environment": "sandbox", "return_url": "", "cancel_url": "", "auto_capture": true, "test_currency": "USD"},
paymentDefinition(bizpayment.PaymentPayPal, "PayPal", "PayPal Checkout", map[string]any{"client_id": "", "client_secret": "", "webhook_id": "", "environment": "sandbox", "return_url": "", "cancel_url": "", "auto_capture": true, "test_currency": "USD"},
integrationField("client_id", "Client ID", true, false, "text"), integrationField("client_secret", "Client Secret", true, true, "password"), integrationField("webhook_id", "Webhook ID", true, false, "text"), integrationSelect("environment", "环境", false, "sandbox", "production"), integrationField("cancel_url", "取消跳转地址", false, false, "url"), integrationField("auto_capture", "自动捕获", false, false, "switch")),
paymentDefinition(paymentutil.ProviderSaobei, "扫呗", "扫呗聚合支付", map[string]any{"inst_no": "", "key": "", "merchant_no": "", "terminal_id": "", "access_token": "", "pay_type": "010", "currency": "CNY", "environment": "production"},
paymentDefinition(bizpayment.PaymentSaobei, "扫呗", "扫呗聚合支付", map[string]any{"inst_no": "", "key": "", "merchant_no": "", "terminal_id": "", "access_token": "", "pay_type": "010", "currency": "CNY", "environment": "production"},
integrationField("inst_no", "机构号", true, false, "text"), integrationField("key", "机构密钥", true, true, "password"), integrationField("merchant_no", "商户号", true, false, "text"), integrationField("terminal_id", "终端号", true, false, "text"), integrationField("access_token", "访问令牌", true, true, "password"), integrationField("pay_type", "支付类型", false, false, "text"), integrationField("currency", "币种", false, false, "text"), integrationSelect("environment", "环境", false, "production", "sandbox")),
genericPaymentDefinition(paymentutil.ProviderChinaums, "银联商务", "按商户协议配置的银联商务适配器"),
genericPaymentDefinition(paymentutil.ProviderSFT, "商福通", "按商户协议配置的商福通适配器"),
genericPaymentDefinition(paymentutil.ProviderSuperPay, "Supper Pay", "按商户协议配置的 Supper Pay 适配器"),
genericPaymentDefinition(paymentutil.ProviderWechatGame, "微信小游戏支付", "微信小游戏虚拟支付配置驱动适配器"),
genericPaymentDefinition(paymentutil.ProviderDouyinGame, "抖音小游戏支付", "抖音小游戏支付配置驱动适配器"),
genericPaymentDefinition(bizpayment.PaymentChinaums, "银联商务", "按商户协议配置的银联商务适配器"),
genericPaymentDefinition(bizpayment.PaymentSFT, "商福通", "按商户协议配置的商福通适配器"),
genericPaymentDefinition(bizpayment.PaymentSuperPay, "Supper Pay", "按商户协议配置的 Supper Pay 适配器"),
genericPaymentDefinition(bizpayment.PaymentWechatGame, "微信小游戏支付", "微信小游戏虚拟支付配置驱动适配器"),
genericPaymentDefinition(bizpayment.PaymentDouyinGame, "抖音小游戏支付", "抖音小游戏支付配置驱动适配器"),
},
}
+130 -44
View File
@@ -12,37 +12,39 @@ import (
"sync"
"time"
"kra/pkg/paymentkit"
"github.com/google/uuid"
)
const (
PaymentAlipay = paymentutil.ProviderAlipay
PaymentAlipayV3 = paymentutil.ProviderAlipayV3
PaymentWechatV2 = paymentutil.ProviderWechatV2
PaymentWechatV3 = paymentutil.ProviderWechatV3
PaymentApple = paymentutil.ProviderApple
PaymentChinaums = paymentutil.ProviderChinaums
PaymentSFT = paymentutil.ProviderSFT
PaymentSuperPay = paymentutil.ProviderSuperPay
PaymentWechatGame = paymentutil.ProviderWechatGame
PaymentDouyinGame = paymentutil.ProviderDouyinGame
PaymentAlipay = "alipay"
PaymentAlipayV3 = "alipay-v3"
PaymentWechatV2 = "wechat-v2"
PaymentWechatV3 = "wechat-v3"
PaymentApple = "apple-iap"
PaymentChinaums = "chinaums"
PaymentSFT = "sft"
PaymentSuperPay = "supper-pay"
PaymentWechatGame = "wechat-game-pay"
PaymentDouyinGame = "douyin-game-pay"
// GoPay-backed providers. These are kept separate from the existing
// configuration-driven game/aggregator protocols above.
PaymentDouyin = paymentutil.ProviderDouyin
PaymentQQ = paymentutil.ProviderQQ
PaymentAllinPay = paymentutil.ProviderAllinPay
PaymentLakala = paymentutil.ProviderLakala
PaymentPayPal = paymentutil.ProviderPayPal
PaymentSaobei = paymentutil.ProviderSaobei
PaymentInternal = paymentutil.ProviderInternal
PaymentDouyin = "douyin"
PaymentQQ = "qq"
PaymentAllinPay = "allinpay"
PaymentLakala = "lakala"
PaymentPayPal = "paypal"
PaymentSaobei = "saobei"
PaymentInternal = "internal"
PaymentModeExternal = paymentutil.ModeExternal
PaymentModeInternal = paymentutil.ModeInternal
PaymentModeExternal = "external"
PaymentModeInternal = "internal"
)
var SupportedPaymentProviders = paymentutil.SupportedProviders()
var SupportedPaymentProviders = []string{
PaymentAlipay, PaymentAlipayV3, PaymentWechatV2, PaymentWechatV3, PaymentApple,
PaymentChinaums, PaymentSFT, PaymentSuperPay, PaymentWechatGame, PaymentDouyinGame,
PaymentDouyin, PaymentQQ, PaymentAllinPay, PaymentLakala, PaymentPayPal, PaymentSaobei,
}
var (
ErrPaymentProviderNotFound = errors.New("支付渠道未配置")
@@ -203,15 +205,74 @@ type PaymentTestResult struct {
Result *PaymentResult `json:"result,omitempty"`
}
type PaymentCallbackAck = paymentutil.CallbackAck
type PaymentCallbackError = paymentutil.CallbackError
type PaymentCallbackAck struct {
StatusCode int
ContentType string
Body []byte
}
type PaymentCallbackError struct {
Cause error
Ack PaymentCallbackAck
}
func (e *PaymentCallbackError) Error() string {
if e == nil || e.Cause == nil {
return "支付回调处理失败"
}
return e.Cause.Error()
}
func (e *PaymentCallbackError) Unwrap() error {
if e == nil {
return nil
}
return e.Cause
}
func CallbackFailure(err error, fallback PaymentCallbackAck) PaymentCallbackAck {
return paymentutil.CallbackFailure(err, fallback)
var callbackErr *PaymentCallbackError
if errors.As(err, &callbackErr) && callbackErr.Ack.StatusCode != 0 {
return callbackErr.Ack
}
return fallback
}
func DefaultPaymentCallbackAck(provider string, success bool) PaymentCallbackAck {
return paymentutil.DefaultCallbackAck(provider, success)
status := 200
if !success {
status = 500
}
switch provider {
case PaymentAlipay, PaymentAlipayV3:
body := "success"
if !success {
body = "failure"
}
return PaymentCallbackAck{StatusCode: status, ContentType: "text/plain; charset=utf-8", Body: []byte(body)}
case PaymentWechatV2:
code, message := "SUCCESS", "OK"
if !success {
code, message = "FAIL", "FAIL"
}
body := fmt.Sprintf("<xml><return_code><![CDATA[%s]]></return_code><return_msg><![CDATA[%s]]></return_msg></xml>", code, message)
return PaymentCallbackAck{StatusCode: status, ContentType: "application/xml; charset=utf-8", Body: []byte(body)}
case PaymentWechatV3:
code, message := "SUCCESS", "成功"
if !success {
code, message = "FAIL", "失败"
}
body, _ := json.Marshal(map[string]string{"code": code, "message": message})
return PaymentCallbackAck{StatusCode: status, ContentType: "application/json; charset=utf-8", Body: body}
case PaymentApple:
return PaymentCallbackAck{StatusCode: status}
default:
body := "success"
if !success {
body = "failure"
}
return PaymentCallbackAck{StatusCode: status, ContentType: "text/plain; charset=utf-8", Body: []byte(body)}
}
}
type PaymentCallback struct {
@@ -228,6 +289,19 @@ type PaymentRepo interface {
HandleCallback(context.Context, *PaymentCallback) (*PaymentResult, error)
}
// PaymentAdapter is the integration boundary for provider protocols. SDK
// clients and wire formats stay outside the business and data packages.
type PaymentAdapter interface {
Create(context.Context, *PaymentRequest, map[string]any) (*PaymentResult, error)
Query(context.Context, string, map[string]any) (*PaymentResult, error)
Refund(context.Context, *PaymentRefundRequest, map[string]any) (*PaymentResult, error)
Callback(context.Context, *PaymentCallback, map[string]any) (*PaymentResult, error)
}
type PaymentAdapterFactory interface {
New(string) (PaymentAdapter, error)
}
type PaymentProviderTester interface {
TestProvider(context.Context, string) (*PaymentTestResult, error)
}
@@ -240,6 +314,8 @@ type NoopPaymentHooks struct{}
func (NoopPaymentHooks) BeforeCreate(context.Context, *PaymentRequest) error { return nil }
func NewPaymentHooks() PaymentHooks { return NoopPaymentHooks{} }
type PaymentFulfillmentHandler interface {
Type() string
Fulfill(context.Context, *PaymentConfirmation) error
@@ -273,6 +349,15 @@ func (r *PaymentFulfillmentRegistry) Handler(kind string) PaymentFulfillmentHand
return r.handlers[kind]
}
func (r *PaymentFulfillmentRegistry) Len() int {
if r == nil {
return 0
}
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.handlers)
}
type PaymentUsecase struct {
repo PaymentRepo
orders PaymentOrderRepo
@@ -282,33 +367,34 @@ type PaymentUsecase struct {
logger PaymentLogger
}
func NewPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, appLogger *slog.Logger) *PaymentUsecase {
func NewPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentHooks, sources *PaymentOrderSourceRegistry, fulfillments *PaymentFulfillmentRegistry, appLogger *slog.Logger) *PaymentUsecase {
if hooks == nil {
hooks = NoopPaymentHooks{}
}
if sources == nil {
sources = NewPaymentOrderSourceRegistry()
}
if fulfillments == nil {
fulfillments = NewPaymentFulfillmentRegistry()
}
return &PaymentUsecase{
repo: repo,
orders: orders,
hooks: NoopPaymentHooks{},
sources: NewPaymentOrderSourceRegistry(),
fulfillments: NewPaymentFulfillmentRegistry(),
hooks: hooks,
sources: sources,
fulfillments: fulfillments,
logger: NewPaymentLogger(appLogger),
}
}
func (uc *PaymentUsecase) SetHooks(hooks PaymentHooks) {
if hooks != nil {
uc.hooks = hooks
func NewConfiguredPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, hooks PaymentHooks, sources *PaymentOrderSourceRegistry, fulfillments *PaymentFulfillmentRegistry, appLogger *slog.Logger) (*PaymentUsecase, error) {
if sources == nil || sources.Len() == 0 {
return nil, errors.New("支付业务订单来源未注册,无法启动支付服务")
}
}
func (uc *PaymentUsecase) SetOrderSourceRegistry(registry *PaymentOrderSourceRegistry) {
if registry != nil {
uc.sources = registry
}
}
func (uc *PaymentUsecase) SetFulfillmentRegistry(registry *PaymentFulfillmentRegistry) {
if registry != nil {
uc.fulfillments = registry
if fulfillments == nil || fulfillments.Len() == 0 {
return nil, errors.New("支付发货 handler 未注册,无法启动支付服务")
}
return NewPaymentUsecase(repo, orders, hooks, sources, fulfillments, appLogger), nil
}
func (uc *PaymentUsecase) RegisterBusinessModule(module PaymentBusinessModule) error {
+9
View File
@@ -182,6 +182,15 @@ func (r *PaymentOrderSourceRegistry) Source(kind string) PaymentOrderSource {
return r.sources[kind]
}
func (r *PaymentOrderSourceRegistry) Len() int {
if r == nil {
return 0
}
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.sources)
}
func (r *PaymentOrderSourceRegistry) Unregister(kind string) {
if r == nil {
return
+14 -12
View File
@@ -135,7 +135,12 @@ func (h *paymentFulfillmentStub) Fulfill(_ context.Context, confirmation *Paymen
func newPaymentUsecaseForTest(repo PaymentRepo) *PaymentUsecase {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
return NewPaymentUsecase(repo, &paymentOrderRepoStub{order: testBizPaymentOrder()}, logger)
return NewPaymentUsecase(repo, &paymentOrderRepoStub{order: testBizPaymentOrder()}, nil, nil, nil, logger)
}
func newPaymentUsecaseWithFulfillmentRegistry(repo PaymentRepo, registry *PaymentFulfillmentRegistry) *PaymentUsecase {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
return NewPaymentUsecase(repo, &paymentOrderRepoStub{order: testBizPaymentOrder()}, nil, nil, registry, logger)
}
func testBizPaymentOrder() *PaymentOrder {
@@ -186,7 +191,7 @@ func TestPaymentCallbackPendingDoesNotFulfill(t *testing.T) {
if err := registry.Register(handler); err != nil {
t.Fatal(err)
}
uc.SetFulfillmentRegistry(registry)
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
result, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
if err != nil {
t.Fatal(err)
@@ -200,10 +205,10 @@ func TestPaymentCallbackRejectsAmountMismatch(t *testing.T) {
repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
handler := &paymentFulfillmentStub{}
uc := newPaymentUsecaseForTest(repo)
uc.orders.(*paymentOrderRepoStub).order.Amount = 101
registry := NewPaymentFulfillmentRegistry()
_ = registry.Register(handler)
uc.SetFulfillmentRegistry(registry)
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
uc.orders.(*paymentOrderRepoStub).order.Amount = 101
if _, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}); err == nil {
t.Fatal("amount mismatch was accepted")
}
@@ -226,7 +231,7 @@ func TestPaymentCallbackDuplicateDoesNotFulfillTwice(t *testing.T) {
uc := newPaymentUsecaseForTest(repo)
registry := NewPaymentFulfillmentRegistry()
_ = registry.Register(handler)
uc.SetFulfillmentRegistry(registry)
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
callback := &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}
if _, err := uc.Callback(context.Background(), callback); err != nil {
t.Fatal(err)
@@ -246,7 +251,7 @@ func TestPaymentCallbackFulfillmentFailureCanRetry(t *testing.T) {
uc := newPaymentUsecaseForTest(repo)
registry := NewPaymentFulfillmentRegistry()
_ = registry.Register(handler)
uc.SetFulfillmentRegistry(registry)
uc = newPaymentUsecaseWithFulfillmentRegistry(repo, registry)
callback := &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")}
if _, err := uc.Callback(context.Background(), callback); err == nil {
t.Fatal("first fulfillment failure was accepted")
@@ -290,12 +295,11 @@ func TestPaymentCallbackErrorCarriesSafeAck(t *testing.T) {
func TestPersistentPaymentCreateUsesBusinessCanonicalAmount(t *testing.T) {
repo := &paymentRepoStub{create: &PaymentResult{Provider: PaymentAlipay, Status: "created", TradeNo: "order-1", Payload: []byte(`{"prepay":"x"}`)}}
orders := &paymentOrderRepoStub{}
uc := NewPaymentUsecase(repo, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
sources := NewPaymentOrderSourceRegistry()
if err := sources.Register(&paymentSourceStub{intent: &PaymentIntent{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "canonical item", Amount: 100, Currency: "CNY", Extra: []byte(`{"product_id":"canonical"}`)}}); err != nil {
t.Fatal(err)
}
uc.SetOrderSourceRegistry(sources)
uc := NewPaymentUsecase(repo, orders, nil, sources, nil, slog.New(slog.NewTextHandler(io.Discard, nil)))
result, err := uc.Create(context.Background(), &PaymentRequest{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "forged", Amount: 1, Currency: "USD", Extra: map[string]any{"product_id": "forged"}})
if err != nil {
t.Fatal(err)
@@ -309,12 +313,11 @@ func TestPersistentPaymentCallbackFulfillsAndPersistsDuplicate(t *testing.T) {
repo := &paymentRepoStub{callback: successPaymentResult(), query: successPaymentResult()}
orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPending, FulfillmentStatus: FulfillmentStatusPending, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
handler := &paymentFulfillmentStub{}
uc := NewPaymentUsecase(repo, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
registry := NewPaymentFulfillmentRegistry()
if err := registry.Register(handler); err != nil {
t.Fatal(err)
}
uc.SetFulfillmentRegistry(registry)
uc := NewPaymentUsecase(repo, orders, nil, nil, registry, slog.New(slog.NewTextHandler(io.Discard, nil)))
first, err := uc.Callback(context.Background(), &PaymentCallback{Provider: PaymentAlipay, Body: []byte("callback")})
if err != nil {
t.Fatal(err)
@@ -330,13 +333,12 @@ func TestPersistentPaymentCallbackFulfillsAndPersistsDuplicate(t *testing.T) {
func TestPaymentFulfillRetriesFailedOrder(t *testing.T) {
orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPaid, FulfillmentStatus: FulfillmentStatusFailed, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
uc := NewPaymentUsecase(&paymentRepoStub{}, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
handler := &paymentFulfillmentStub{}
registry := NewPaymentFulfillmentRegistry()
if err := registry.Register(handler); err != nil {
t.Fatal(err)
}
uc.SetFulfillmentRegistry(registry)
uc := NewPaymentUsecase(&paymentRepoStub{}, orders, nil, nil, registry, slog.New(slog.NewTextHandler(io.Discard, nil)))
result, err := uc.Fulfill(context.Background(), PaymentAlipay, "order-1")
if err != nil {
t.Fatal(err)
+6 -1
View File
@@ -5,4 +5,9 @@ import "github.com/google/wire"
// ProviderSet wires the payment domain usecase independently from other
// business domains. Persistence and transport layers consume the payment
// interfaces without depending on the system package.
var ProviderSet = wire.NewSet(NewPaymentUsecase)
var ProviderSet = wire.NewSet(
NewPaymentHooks,
NewPaymentOrderSourceRegistry,
NewPaymentFulfillmentRegistry,
NewConfiguredPaymentUsecase,
)
-1
View File
@@ -23,7 +23,6 @@ type APITokenRepo interface {
UserHasAuthority(context.Context, uint, uint) (*User, bool, error)
CreateAPIToken(context.Context, *APIToken) error
ListAPITokens(context.Context, int, int, uint, *bool) ([]*APIToken, int64, error)
DisableAPIToken(context.Context, uint) (string, error)
DisableAndBlacklistAPIToken(context.Context, uint) error
BlacklistToken(context.Context, string) error
IsTokenDisabled(context.Context, string) (bool, error)
+5 -2
View File
@@ -40,8 +40,11 @@ type LoginLog struct {
}
type LoginLogFilter struct {
Username string
Status *bool
Username string
IP string
Status *bool
StartCreatedAt *time.Time
EndCreatedAt *time.Time
}
type DataAccessLog struct {
-20
View File
@@ -9,7 +9,6 @@ import (
type EmailRepo interface {
Send(context.Context, []string, string, string) error
DefaultRecipients() []string
Enabled() bool
}
type EmailUsecase struct{ repo EmailRepo }
@@ -27,22 +26,3 @@ func (uc *EmailUsecase) Test(ctx context.Context) error {
}
return uc.repo.Send(ctx, recipients, "test", "test")
}
func (uc *EmailUsecase) Alert(ctx context.Context, subject, body string) error {
if !uc.repo.Enabled() {
return nil
}
// ErrorToEmail splits the configured comma-separated recipient
// string, while its explicit test endpoint deliberately sends the whole
// value as one recipient. Keep those two contracts separate.
configured := uc.repo.DefaultRecipients()
recipients := make([]string, 0, len(configured))
for _, value := range configured {
parts := strings.Split(value, ",")
if len(parts) > 1 && parts[len(parts)-1] == "" {
parts = parts[:len(parts)-1]
}
recipients = append(recipients, parts...)
}
return uc.repo.Send(ctx, recipients, subject, body)
}
-1
View File
@@ -41,7 +41,6 @@ type AttachmentCategory struct {
type MediaMetadataRepo interface {
CreateMedia(context.Context, *MediaFile) error
FindMedia(context.Context, uint) (*MediaFile, error)
FindMediaByHash(context.Context, uint, string) (*MediaFile, error)
ListMedia(context.Context, MediaFilter) ([]*MediaFile, int64, error)
UpdateMediaName(context.Context, uint, string) error
DeleteMedia(context.Context, uint) error
-5
View File
@@ -58,7 +58,6 @@ type MenuRepo interface {
Find(context.Context, uint) (*Menu, error)
List(context.Context) ([]*Menu, error)
SetAuthorityMenus(context.Context, uint, []uint) error
AuthorityMenuIDs(context.Context, uint) ([]uint, error)
// ListAuthorityMenus returns the base menus assigned to the target
// authority. Unlike List, it must not apply the current actor's strict
// menu filter: the administration endpoint is explicitly querying another
@@ -73,10 +72,6 @@ type MenuUsecase struct{ repo MenuRepo }
func NewMenuUsecase(repo MenuRepo) *MenuUsecase { return &MenuUsecase{repo: repo} }
func (uc *MenuUsecase) List(ctx context.Context) ([]*Menu, error) {
return uc.repo.List(ctx)
}
func (uc *MenuUsecase) Tree(ctx context.Context) ([]*Menu, error) {
items, err := uc.repo.List(ctx)
if err != nil {
-3
View File
@@ -3,9 +3,6 @@ package system
import "context"
type PermissionRepo interface {
Buttons(context.Context, uint) ([]*MenuButton, error)
SetAuthorityButtons(context.Context, uint, map[uint][]uint) error
AuthorityButtonIDs(context.Context, uint) ([]uint, error)
SelectedButtons(context.Context, uint, uint) ([]uint, error)
SetSelectedButtons(context.Context, uint, uint, []uint) error
CanRemoveButton(context.Context, string) (bool, error)
-8
View File
@@ -233,14 +233,6 @@ func (uc *SecurityUsecase) ActiveToken(ctx context.Context, username string) (st
return value, err
}
func (uc *SecurityUsecase) ActiveTokenMatches(ctx context.Context, username, token string) (bool, error) {
if !uc.settings.UseMultipoint() {
return true, nil
}
active, ok, err := uc.cache.Get(ctx, activeTokenKey(username))
return ok && active == token, err
}
func (uc *SecurityUsecase) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error {
if !uc.settings.UseMultipoint() {
return nil
-2
View File
@@ -42,11 +42,9 @@ type UserRepo interface {
HasAuthorityMenu(context.Context, uint, string) (bool, error)
MenusByAuthority(context.Context, uint) ([]*Menu, error)
ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
CreateUser(context.Context, *User) (*User, error)
CreateUserWithAuthorities(context.Context, *User, []uint) (*User, error)
UpdateUser(context.Context, *User) error
UpdateSelfUser(context.Context, *User) error
UpdateUserWithAuthorities(context.Context, *User, []uint) error
DeleteUser(context.Context, uint) error
UpdatePassword(context.Context, uint, string, bool) error
SetUserAuthorities(context.Context, uint, []uint) error
+1 -1
View File
@@ -50,7 +50,7 @@ type VersionRepo interface {
FindVersion(context.Context, uint) (*Version, error)
ListVersions(context.Context, int, int, string, string, []*time.Time) ([]*Version, int64, error)
BuildVersionBundle(context.Context, []uint, []uint, []uint) (*VersionBundle, error)
ImportVersionBundle(context.Context, *VersionBundle) error
ImportVersionBundle(context.Context, *VersionBundle, *Version) error
}
type VersionUsecase struct{ VersionRepo }