优化结构
This commit is contained in:
parent
d2fb5c11b8
commit
c968f9f24e
|
|
@ -12,39 +12,37 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"kra/pkg/paymentkit"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PaymentAlipay = "alipay"
|
PaymentAlipay = paymentutil.ProviderAlipay
|
||||||
PaymentAlipayV3 = "alipay-v3"
|
PaymentAlipayV3 = paymentutil.ProviderAlipayV3
|
||||||
PaymentWechatV2 = "wechat-v2"
|
PaymentWechatV2 = paymentutil.ProviderWechatV2
|
||||||
PaymentWechatV3 = "wechat-v3"
|
PaymentWechatV3 = paymentutil.ProviderWechatV3
|
||||||
PaymentApple = "apple-iap"
|
PaymentApple = paymentutil.ProviderApple
|
||||||
PaymentChinaums = "chinaums"
|
PaymentChinaums = paymentutil.ProviderChinaums
|
||||||
PaymentSFT = "sft"
|
PaymentSFT = paymentutil.ProviderSFT
|
||||||
PaymentSuperPay = "supper-pay"
|
PaymentSuperPay = paymentutil.ProviderSuperPay
|
||||||
PaymentWechatGame = "wechat-game-pay"
|
PaymentWechatGame = paymentutil.ProviderWechatGame
|
||||||
PaymentDouyinGame = "douyin-game-pay"
|
PaymentDouyinGame = paymentutil.ProviderDouyinGame
|
||||||
// GoPay-backed providers. These are kept separate from the existing
|
// GoPay-backed providers. These are kept separate from the existing
|
||||||
// configuration-driven game/aggregator protocols above.
|
// configuration-driven game/aggregator protocols above.
|
||||||
PaymentDouyin = "douyin"
|
PaymentDouyin = paymentutil.ProviderDouyin
|
||||||
PaymentQQ = "qq"
|
PaymentQQ = paymentutil.ProviderQQ
|
||||||
PaymentAllinPay = "allinpay"
|
PaymentAllinPay = paymentutil.ProviderAllinPay
|
||||||
PaymentLakala = "lakala"
|
PaymentLakala = paymentutil.ProviderLakala
|
||||||
PaymentPayPal = "paypal"
|
PaymentPayPal = paymentutil.ProviderPayPal
|
||||||
PaymentSaobei = "saobei"
|
PaymentSaobei = paymentutil.ProviderSaobei
|
||||||
PaymentInternal = "internal"
|
PaymentInternal = paymentutil.ProviderInternal
|
||||||
|
|
||||||
PaymentModeExternal = "external"
|
PaymentModeExternal = paymentutil.ModeExternal
|
||||||
PaymentModeInternal = "internal"
|
PaymentModeInternal = paymentutil.ModeInternal
|
||||||
)
|
)
|
||||||
|
|
||||||
var SupportedPaymentProviders = []string{
|
var SupportedPaymentProviders = paymentutil.SupportedProviders()
|
||||||
PaymentAlipay, PaymentAlipayV3, PaymentWechatV2, PaymentWechatV3, PaymentApple,
|
|
||||||
PaymentChinaums, PaymentSFT, PaymentSuperPay, PaymentWechatGame, PaymentDouyinGame,
|
|
||||||
PaymentDouyin, PaymentQQ, PaymentAllinPay, PaymentLakala, PaymentPayPal, PaymentSaobei,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrPaymentProviderNotFound = errors.New("支付渠道未配置")
|
ErrPaymentProviderNotFound = errors.New("支付渠道未配置")
|
||||||
|
|
@ -190,74 +188,15 @@ type PaymentResult struct {
|
||||||
FailureAck PaymentCallbackAck `json:"-"`
|
FailureAck PaymentCallbackAck `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentCallbackAck struct {
|
type PaymentCallbackAck = paymentutil.CallbackAck
|
||||||
StatusCode int
|
type PaymentCallbackError = paymentutil.CallbackError
|
||||||
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 {
|
func CallbackFailure(err error, fallback PaymentCallbackAck) PaymentCallbackAck {
|
||||||
var callbackErr *PaymentCallbackError
|
return paymentutil.CallbackFailure(err, fallback)
|
||||||
if errors.As(err, &callbackErr) && callbackErr.Ack.StatusCode != 0 {
|
|
||||||
return callbackErr.Ack
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultPaymentCallbackAck(provider string, success bool) PaymentCallbackAck {
|
func DefaultPaymentCallbackAck(provider string, success bool) PaymentCallbackAck {
|
||||||
status := 200
|
return paymentutil.DefaultCallbackAck(provider, success)
|
||||||
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 {
|
type PaymentCallback struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
# `app/system` 到 `pkg` 复用性审查
|
||||||
|
|
||||||
|
审查范围:`app/system` 下全部 Go 文件,以及现有 `pkg` 目录。判断标准是:
|
||||||
|
公共包只能提供跨模块稳定的机制、协议或纯函数,不能依赖 `app/*/internal`,
|
||||||
|
也不承载 system 的业务表、用例、provider 生命周期或运行时配置。
|
||||||
|
|
||||||
|
## 本轮已抽取
|
||||||
|
|
||||||
|
| 原位置 | 公共位置 | 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| `internal/transport/httpx/response.go` | `pkg/httpx/response.go` | `Response`、`PageResult`、状态码和 Gin 响应助手是跨模块 HTTP 契约。system 原包保留别名/转发,兼容既有调用方。 |
|
||||||
|
| `internal/utils/configutil/json.go` | `pkg/protoutil/json.go` | protobuf JSON 局部合并、snake_case 到 lowerCamel 归一化与 system 业务无关,已由 system wrapper 转发。 |
|
||||||
|
| `internal/biz/payment.go` provider 常量 | `pkg/paymentkit/provider.go` | provider 标识、支付模式和支持列表是跨业务支付边界,system `biz` 保留兼容常量。 |
|
||||||
|
| `internal/biz/payment.go` 回调应答 | `pkg/paymentkit/callback.go` | 回调 ACK、回调错误和默认 provider 应答是跨模块协议,已由 system 类型别名兼容。 |
|
||||||
|
|
||||||
|
## `app/system` 逐目录结论
|
||||||
|
|
||||||
|
| 目录 | 结论 | 代表文件/原因 |
|
||||||
|
|---|---|---|
|
||||||
|
| `cmd` | 保留 system | `main.go`、`wire.go` 是组合根和启动生命周期,不能下沉。 |
|
||||||
|
| `internal/app`、`internal/module` | 保留 system | `catalog.go`、`definition.go` 绑定 system 迁移、菜单、路由和任务贡献。公共协议已在 `pkg/module`。 |
|
||||||
|
| `internal/biz` | 大部分保留;仅抽公共支付边界 | 用户、权限、菜单、审计、任务、支付订单和系统配置都是领域模型/用例;`payment.go` 中 provider 与 ACK 已兼容转发。 |
|
||||||
|
| `internal/conf` | 保留 system | 配置 proto 由 system 运行时和生成代码拥有,不能成为跨模块配置模型。 |
|
||||||
|
| `internal/data` | 保留 system | 数据库连接、迁移、PO、system 表、支付持久化和配置 watcher 都是 system 数据层。通用迁移/分页/GORM 能力已在 `pkg/database`。 |
|
||||||
|
| `internal/dto` | 保留 system | DTO 与 system API 形状绑定,不应跨模块共享。 |
|
||||||
|
| `internal/initialize` | 保留 system | 首次安装、兼容迁移、配置持久化和种子编排具有 system 语义。 |
|
||||||
|
| `internal/integration/cache` | 保留 system | Redis/cache 客户端生命周期和运行时配置属于 integration,不是 stateless utils。 |
|
||||||
|
| `internal/integration/email` | 保留 system | 邮件 provider 配置和发送生命周期属于 system integration。 |
|
||||||
|
| `internal/integration/payment` | 保留 system | `alipay.go`、`wechat_v3.go`、`paypal.go` 等是具体 provider SDK 适配器;不能搬到通用 pkg。纯金额、JSON、签名、状态函数已经在 `pkg/paymentkit`。 |
|
||||||
|
| `internal/integration/storage` | 保留 system | OSS/S3/本地存储客户端、重载和配置绑定属于 integration。 |
|
||||||
|
| `internal/repository` | 保留 system | 仓储实现、PO、Casbin、system 表和事务边界都不能跨模块复用。 |
|
||||||
|
| `internal/routeinfo` | 保留 system | 路由元数据包含 system 初始化与菜单语义。 |
|
||||||
|
| `internal/security/adminauth` | 保留 system | JWT claims、签发和后台认证策略属于 system 安全边界。 |
|
||||||
|
| `internal/service` | 保留 system | DTO 与 system DO 的转换和服务编排,不可放入公共包。 |
|
||||||
|
| `internal/transport/handler` | 保留 system | 每个 handler 都依赖 system service/DTO;只有 HTTP 响应协议抽到 `pkg/httpx`。 |
|
||||||
|
| `internal/transport/httpx` | 兼容层 | `response.go` 已转发到 `pkg/httpx`;`cookie.go` 的 token cookie 名称和认证策略仍是 system 专属。 |
|
||||||
|
| `internal/transport/middleware` | 保留 system | 审计、认证、数据权限、限流、system 错误记录和请求采集均带业务/配置语义。 |
|
||||||
|
| `internal/transport/router` | 保留 system | 路由注册指向 system handlers。 |
|
||||||
|
| `internal/utils/configutil` | 兼容层 | `json.go` 已转发到 `pkg/protoutil`;其余 system 专用工具应留在本目录。 |
|
||||||
|
| `internal/worker` | 保留 system | 任务执行器依赖 system task repo、runtime、日志和业务方法。跨模块任务注册协议已在 `pkg/task`。 |
|
||||||
|
|
||||||
|
## 现有 `pkg` 复核
|
||||||
|
|
||||||
|
- `pkg/database/*`:边界正确,继续保留。
|
||||||
|
- `pkg/logging/*`:边界正确,继续保留。
|
||||||
|
- `pkg/module/*`:跨模块迁移/路由/元数据协议,边界正确。
|
||||||
|
- `pkg/task/*`:跨模块任务注册协议,边界正确。
|
||||||
|
- `pkg/paymentkit/*`:本轮补齐 provider 与 callback 协议;具体渠道 SDK 仍留在 system。
|
||||||
|
- 新增 `pkg/httpx`、`pkg/protoutil`:均不引用 `app/*/internal`。
|
||||||
|
|
||||||
|
## 暂不抽取的后续候选
|
||||||
|
|
||||||
|
1. `internal/transport/httpx/cookie.go`:只有在多个 app 统一 token cookie 名称、SameSite 和反向代理策略后,才适合抽成公共 HTTP cookie 包。
|
||||||
|
2. `internal/transport/middleware/capture.go`:包含 operation-audit 脱敏字段和 system context key,当前不是通用中间件。
|
||||||
|
3. `internal/biz/pagination.go`:依赖 AIP filtering/ordering,且目前只服务 system 的仓储接口;若其他模块形成同样 DO 查询协议,再独立设计 `pkg/query`,不要直接搬 biz 类型。
|
||||||
|
4. `internal/biz/errors.go` 及认证/安全错误:错误 reason 与 system API 语义绑定,不应公共化。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
已运行:
|
||||||
|
|
||||||
|
```text
|
||||||
|
go test ./pkg/... ./app/system/...
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:全部通过。
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
# Internal Architecture
|
|
||||||
|
|
||||||
本项目采用参考 Kratos beer-shop 的模块化单体结构:先按业务边界垂直拆分,再在
|
|
||||||
模块内部保持 Kratos 的 `service -> biz <- data` 依赖方向。
|
|
||||||
|
|
||||||
```text
|
|
||||||
internal/
|
|
||||||
app/ 应用组合根,只决定启用哪些模块
|
|
||||||
conf/ 运行配置
|
|
||||||
logging/ 公共日志基础设施
|
|
||||||
modules/
|
|
||||||
system/ 稳定的后台系统能力
|
|
||||||
biz/ 领域对象、用例、仓储接口
|
|
||||||
data/ PO、仓储实现、系统持久化
|
|
||||||
service/ DTO 与 DO 转换
|
|
||||||
transport/ HTTP handler、router、middleware
|
|
||||||
initialize/ system 首次安装编排
|
|
||||||
integration/ 外部服务适配器
|
|
||||||
worker/ system 后台任务
|
|
||||||
platform/ 不含业务语义的跨模块机制
|
|
||||||
database/
|
|
||||||
module/
|
|
||||||
task/
|
|
||||||
```
|
|
||||||
|
|
||||||
依赖方向:`cmd -> app -> modules -> platform`。`platform` 不得引用业务模块;业务
|
|
||||||
模块之间优先通过 `biz` 接口协作,不直接引用对方的 `data` 或 `transport`。
|
|
||||||
|
|
||||||
新增模块时,在 `internal/modules/<name>` 内完成自己的分层,并在
|
|
||||||
`internal/app/catalog.go` 注册迁移、菜单/API、默认任务、任务实现和路由。组合根
|
|
||||||
是唯一允许同时知道多个模块具体类型的位置。
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
# Internal Modules
|
|
||||||
|
|
||||||
参考 Kratos beer-shop 的垂直业务拆分方式,业务代码按模块组织在
|
|
||||||
`internal/modules/<name>` 下。当前项目是模块化单体,因此共享一个进程和组合根,
|
|
||||||
但每个模块仍独立拥有 `biz`、`data`、`service`、`transport` 和初始化贡献。
|
|
||||||
|
|
||||||
## 当前模块
|
|
||||||
|
|
||||||
`system` 是模板自带的后台管理模块,包含用户、权限、菜单、API、公告、部门、
|
|
||||||
字典、文件、审计、定时任务、支付和系统初始化等现有表与逻辑。
|
|
||||||
|
|
||||||
## 新增业务模块
|
|
||||||
|
|
||||||
新增业务请创建 `internal/modules/<business>`,不要把领域模型、仓储或 HTTP
|
|
||||||
处理器继续添加到 `internal/biz`、`internal/data`、`internal/service` 等根目录。
|
|
||||||
数据库表和迁移由业务模块自己的 `data` 包负责;只有数据库连接生命周期和通用
|
|
||||||
迁移执行器属于 `internal/platform/database`。
|
|
||||||
|
|
||||||
模块通过 `internal/app/catalog.go` 接入组合根:
|
|
||||||
|
|
||||||
- `module.Definition.Migrations`:模块自己的表结构迁移
|
|
||||||
- `module.Definition.Surface`:菜单和 API 元数据,首次初始化时统一写入系统表
|
|
||||||
- `module.Definition.TimedTasks`:模块默认定时任务记录
|
|
||||||
- `module.Definition.Tasks`:无状态的进程内任务方法
|
|
||||||
- `module.RouteRegistrar`:需要依赖 Handler 的路由注册器,在 `app.Runtime` 中组合
|
|
||||||
|
|
||||||
无依赖的任务方法可以放进 `Definition.Tasks`。需要仓储或用例依赖的任务方法由
|
|
||||||
模块实现 `task.Contributor`,再由 `internal/app` 激活。新增模块只修改应用组合根,
|
|
||||||
不修改 `system` 的初始化、worker、路由或数据层。
|
|
||||||
|
|
||||||
`integration` 是外部系统适配器,不是通用工具;只有实现了稳定跨模块协议的能力
|
|
||||||
才上移到 `platform`。`utils` 仅保留无状态、无业务语义且确实被本模块复用的函数。
|
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
- `module`:模块迁移、后台元数据和路由注册协议
|
- `module`:模块迁移、后台元数据和路由注册协议
|
||||||
- `task`:跨模块共享的进程内任务注册表和贡献协议
|
- `task`:跨模块共享的进程内任务注册表和贡献协议
|
||||||
- `logging`:跨 app 复用的结构化日志能力
|
- `logging`:跨 app 复用的结构化日志能力
|
||||||
- `paymentkit`:支付金额、签名、状态和报文处理纯函数
|
- `httpx`:跨 HTTP 模块共享的 JSON 响应结构、分页结构和状态码
|
||||||
|
- `paymentkit`:支付金额、签名、状态、provider 标识和回调应答纯函数
|
||||||
|
- `protoutil`:与业务无关的 protobuf JSON 局部合并工具
|
||||||
|
|
||||||
`pkg` 只能提供机制和稳定协议,不能引用任何 `app/*/internal`。数据库配置加载、
|
`pkg` 只能提供机制和稳定协议,不能引用任何 `app/*/internal`。数据库配置加载、
|
||||||
系统集成配置、系统表和 provider 生命周期仍由 `app/system` 负责。
|
系统集成配置、系统表和 provider 生命周期仍由 `app/system` 负责。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package httpx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResponseHelpers(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
r.GET("/ok", OK)
|
||||||
|
r.GET("/fail", func(c *gin.Context) { Fail(c, "bad") })
|
||||||
|
r.GET("/auth", func(c *gin.Context) { NoAuth(c, "login") })
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
path string
|
||||||
|
statusCode int
|
||||||
|
body string
|
||||||
|
}{
|
||||||
|
{"/ok", 200, `{"code":0,"data":{},"msg":"操作成功"}`},
|
||||||
|
{"/fail", 200, `{"code":7,"data":{},"msg":"bad"}`},
|
||||||
|
{"/auth", 401, `{"code":7,"data":null,"msg":"login"}`},
|
||||||
|
} {
|
||||||
|
req := httptest.NewRequest("GET", tc.path, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != tc.statusCode || w.Body.String() != tc.body {
|
||||||
|
t.Fatalf("%s: status=%d body=%q", tc.path, w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,14 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ProviderAlipay = "alipay"
|
|
||||||
ProviderAlipayV3 = "alipay-v3"
|
|
||||||
ProviderWechatV2 = "wechat-v2"
|
|
||||||
ProviderWechatV3 = "wechat-v3"
|
|
||||||
ProviderApple = "apple-iap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CallbackAck is the provider-facing HTTP acknowledgement returned after a
|
// CallbackAck is the provider-facing HTTP acknowledgement returned after a
|
||||||
// payment notification is processed.
|
// payment notification is processed.
|
||||||
type CallbackAck struct {
|
type CallbackAck struct {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package paymentutil
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderAlipay = "alipay"
|
||||||
|
ProviderAlipayV3 = "alipay-v3"
|
||||||
|
ProviderWechatV2 = "wechat-v2"
|
||||||
|
ProviderWechatV3 = "wechat-v3"
|
||||||
|
ProviderApple = "apple-iap"
|
||||||
|
ProviderChinaums = "chinaums"
|
||||||
|
ProviderSFT = "sft"
|
||||||
|
ProviderSuperPay = "supper-pay"
|
||||||
|
ProviderWechatGame = "wechat-game-pay"
|
||||||
|
ProviderDouyinGame = "douyin-game-pay"
|
||||||
|
ProviderDouyin = "douyin"
|
||||||
|
ProviderQQ = "qq"
|
||||||
|
ProviderAllinPay = "allinpay"
|
||||||
|
ProviderLakala = "lakala"
|
||||||
|
ProviderPayPal = "paypal"
|
||||||
|
ProviderSaobei = "saobei"
|
||||||
|
ProviderInternal = "internal"
|
||||||
|
|
||||||
|
ModeExternal = "external"
|
||||||
|
ModeInternal = "internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var externalProviders = [...]string{
|
||||||
|
ProviderAlipay, ProviderAlipayV3, ProviderWechatV2, ProviderWechatV3, ProviderApple,
|
||||||
|
ProviderChinaums, ProviderSFT, ProviderSuperPay, ProviderWechatGame, ProviderDouyinGame,
|
||||||
|
ProviderDouyin, ProviderQQ, ProviderAllinPay, ProviderLakala, ProviderPayPal, ProviderSaobei,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedProviders returns a copy so callers cannot mutate the process-wide
|
||||||
|
// provider catalog.
|
||||||
|
func SupportedProviders() []string {
|
||||||
|
return append([]string(nil), externalProviders[:]...)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue