Compare commits
No commits in common. "b4d843290df99135bb8523075541d42fd86b3690" and "ca52de5db0b43221f4da832eb35f726e62f82462" have entirely different histories.
b4d843290d
...
ca52de5db0
|
|
@ -7,7 +7,7 @@ Kra 是基于 Kratos 生命周期与 Wire 依赖注入、使用 Gin 提供管理
|
||||||
```text
|
```text
|
||||||
cmd/ 服务入口与 Wire
|
cmd/ 服务入口与 Wire
|
||||||
configs/ 运行配置
|
configs/ 运行配置
|
||||||
internal/biz/ 领域对象、用例和仓储接口(按 system/payment/integration/task 拆分)
|
internal/biz/ 领域对象、用例和仓储接口
|
||||||
internal/data/ 数据库、缓存、对象存储及仓储实现
|
internal/data/ 数据库、缓存、对象存储及仓储实现
|
||||||
internal/server/ Gin 服务、路由、中间件和 Handler
|
internal/server/ Gin 服务、路由、中间件和 Handler
|
||||||
internal/service/ HTTP 输入输出与领域对象转换
|
internal/service/ HTTP 输入输出与领域对象转换
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,12 @@ import (
|
||||||
|
|
||||||
"kra/internal/app"
|
"kra/internal/app"
|
||||||
"kra/internal/biz"
|
"kra/internal/biz"
|
||||||
taskbiz "kra/internal/biz/task"
|
systembiz "kra/internal/biz/system"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
"kra/internal/data"
|
"kra/internal/data"
|
||||||
"kra/internal/initialize"
|
"kra/internal/initialize"
|
||||||
"kra/internal/integration"
|
"kra/internal/integration"
|
||||||
"kra/internal/integration/cache"
|
"kra/internal/integration/cache"
|
||||||
"kra/internal/modules"
|
|
||||||
"kra/internal/server"
|
"kra/internal/server"
|
||||||
"kra/internal/server/handler"
|
"kra/internal/server/handler"
|
||||||
"kra/internal/server/middleware"
|
"kra/internal/server/middleware"
|
||||||
|
|
@ -37,7 +36,7 @@ func wireApp(*conf.Server, *conf.Runtime, *slog.Logger, *logging.ReloadableLogge
|
||||||
handler.ProviderSet,
|
handler.ProviderSet,
|
||||||
router.ProviderSet,
|
router.ProviderSet,
|
||||||
worker.ProviderSet,
|
worker.ProviderSet,
|
||||||
modules.Catalog,
|
app.Catalog,
|
||||||
app.TaskRegistry,
|
app.TaskRegistry,
|
||||||
runtimeContributions,
|
runtimeContributions,
|
||||||
app.Runtime,
|
app.Runtime,
|
||||||
|
|
@ -47,7 +46,7 @@ func wireApp(*conf.Server, *conf.Runtime, *slog.Logger, *logging.ReloadableLogge
|
||||||
wire.Bind(new(initialize.Backend), new(*data.Data)),
|
wire.Bind(new(initialize.Backend), new(*data.Data)),
|
||||||
wire.Bind(new(cache.RedisProvider), new(*data.Data)),
|
wire.Bind(new(cache.RedisProvider), new(*data.Data)),
|
||||||
wire.Bind(new(middleware.TokenAuthenticator), new(*service.AuthService)),
|
wire.Bind(new(middleware.TokenAuthenticator), new(*service.AuthService)),
|
||||||
wire.Bind(new(taskbiz.TaskMethodRegistry), new(*platformtask.Registry)),
|
wire.Bind(new(systembiz.TaskMethodRegistry), new(*platformtask.Registry)),
|
||||||
biz.ProviderSet,
|
biz.ProviderSet,
|
||||||
service.ProviderSet,
|
service.ProviderSet,
|
||||||
newApp,
|
newApp,
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,18 @@ package main
|
||||||
import (
|
import (
|
||||||
"github.com/go-kratos/kratos/v3"
|
"github.com/go-kratos/kratos/v3"
|
||||||
"kra/internal/app"
|
"kra/internal/app"
|
||||||
integration3 "kra/internal/biz/integration"
|
|
||||||
payment2 "kra/internal/biz/payment"
|
|
||||||
system2 "kra/internal/biz/system"
|
system2 "kra/internal/biz/system"
|
||||||
task2 "kra/internal/biz/task"
|
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
"kra/internal/data"
|
"kra/internal/data"
|
||||||
"kra/internal/data/integration"
|
|
||||||
"kra/internal/data/payment"
|
"kra/internal/data/payment"
|
||||||
"kra/internal/data/system"
|
"kra/internal/data/repository"
|
||||||
"kra/internal/data/task"
|
|
||||||
"kra/internal/initialize"
|
"kra/internal/initialize"
|
||||||
integration2 "kra/internal/integration"
|
"kra/internal/integration"
|
||||||
"kra/internal/integration/cache"
|
"kra/internal/integration/cache"
|
||||||
"kra/internal/integration/email"
|
"kra/internal/integration/email"
|
||||||
"kra/internal/integration/mq"
|
"kra/internal/integration/mq"
|
||||||
"kra/internal/integration/storage"
|
"kra/internal/integration/storage"
|
||||||
"kra/internal/integration/websocket"
|
"kra/internal/integration/websocket"
|
||||||
"kra/internal/modules"
|
|
||||||
"kra/internal/server"
|
"kra/internal/server"
|
||||||
"kra/internal/server/handler"
|
"kra/internal/server/handler"
|
||||||
"kra/internal/server/router"
|
"kra/internal/server/router"
|
||||||
|
|
@ -48,7 +42,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
catalog := modules.Catalog()
|
catalog := app.Catalog()
|
||||||
dataData, cleanup, err := data.NewData(runtime, logger, reloadable, catalog)
|
dataData, cleanup, err := data.NewData(runtime, logger, reloadable, catalog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
@ -101,23 +95,22 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
emailUsecase := system2.NewEmailUsecase(emailRepo)
|
emailUsecase := system2.NewEmailUsecase(emailRepo)
|
||||||
emailService := service.NewEmailService(emailUsecase)
|
emailService := service.NewEmailService(emailUsecase)
|
||||||
handlerEmail := handler.NewEmail(emailService)
|
handlerEmail := handler.NewEmail(emailService)
|
||||||
paymentConfigReader := integration.NewPaymentConfigReader(dataData)
|
paymentRepo := payment.NewPaymentRepo(dataData)
|
||||||
paymentRepo := payment.NewPaymentRepo(dataData, paymentConfigReader)
|
|
||||||
paymentOrderRepo := payment.NewPaymentOrderRepo(dataData)
|
paymentOrderRepo := payment.NewPaymentOrderRepo(dataData)
|
||||||
paymentUsecase := payment2.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
|
paymentUsecase := system2.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
|
||||||
paymentService := service.NewPaymentService(paymentUsecase)
|
paymentService := service.NewPaymentService(paymentUsecase)
|
||||||
handlerPayment := handler.NewPayment(paymentService)
|
handlerPayment := handler.NewPayment(paymentService)
|
||||||
taskRepo := task.NewTaskRepo(dataData)
|
taskRepo := system.NewTaskRepo(dataData)
|
||||||
registry := app.TaskRegistry(catalog)
|
registry := app.TaskRegistry(catalog)
|
||||||
taskUsecase := task2.NewTaskUsecaseWithRegistry(taskRepo, registry)
|
taskUsecase := system2.NewTaskUsecaseWithRegistry(taskRepo, registry)
|
||||||
mediaRepo := system.NewMediaRepo(dataData)
|
mediaRepo := system.NewMediaRepo(dataData)
|
||||||
mediaUsecase := system2.NewMediaUsecase(mediaRepo, reloadable, runtimeSettings)
|
mediaUsecase := system2.NewMediaUsecase(mediaRepo, reloadable, runtimeSettings)
|
||||||
taskExecutor := worker.NewTaskExecutorWithRegistry(taskUsecase, mediaUsecase, runtime, registry)
|
taskExecutor := worker.NewTaskExecutorWithRegistry(taskUsecase, mediaUsecase, runtime, registry)
|
||||||
taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger)
|
taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger)
|
||||||
taskRuntime := worker.NewTaskRuntime(taskScheduler)
|
taskRuntime := worker.NewTaskRuntime(taskScheduler)
|
||||||
taskApplicationUsecase := task2.NewTaskApplicationUsecase(taskUsecase, taskRuntime)
|
taskApplicationUsecase := system2.NewTaskApplicationUsecase(taskUsecase, taskRuntime)
|
||||||
taskService := service.NewTaskService(taskApplicationUsecase)
|
taskService := service.NewTaskService(taskApplicationUsecase)
|
||||||
handlerTask := handler.NewTask(taskService)
|
task := handler.NewTask(taskService)
|
||||||
mediaService := service.NewMediaService(mediaUsecase, runtimeSettings)
|
mediaService := service.NewMediaService(mediaUsecase, runtimeSettings)
|
||||||
media := handler.NewMedia(mediaService)
|
media := handler.NewMedia(mediaService)
|
||||||
auditQueryRepo := system.NewAuditRepo(dataData)
|
auditQueryRepo := system.NewAuditRepo(dataData)
|
||||||
|
|
@ -155,17 +148,15 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
user := handler.NewUser(userService, authService)
|
user := handler.NewUser(userService, authService)
|
||||||
navigation := handler.NewNavigation(userService)
|
navigation := handler.NewNavigation(userService)
|
||||||
session := handler.NewSession(tokenService)
|
session := handler.NewSession(tokenService)
|
||||||
integrationConfigRepo := integration.NewIntegrationConfigRepo(dataData)
|
integrationConfigRepo := system.NewIntegrationConfigRepo(dataData)
|
||||||
store := data.NewIntegrationRuntime(dataData)
|
store := data.NewIntegrationRuntime(dataData)
|
||||||
connectivityTester := integration2.NewConnectivityTester(store)
|
connectivityTester := integration.NewConnectivityTester(store)
|
||||||
integrationConfigUsecase := integration3.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester)
|
integrationConfigUsecase := system2.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester)
|
||||||
integrationConfigService := service.NewIntegrationConfigService(integrationConfigUsecase)
|
integrationConfigService := service.NewIntegrationConfigService(integrationConfigUsecase)
|
||||||
integrationConfig := handler.NewIntegrationConfig(integrationConfigService)
|
integrationConfig := handler.NewIntegrationConfig(integrationConfigService)
|
||||||
v := handler.NewSet(authority, menu, api, permission, organization, announcement, handlerEmail, handlerPayment, handlerTask, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, integrationConfig)
|
v := handler.NewSet(authority, menu, api, permission, organization, announcement, handlerEmail, handlerPayment, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, integrationConfig)
|
||||||
routes := router.NewRoutes(v)
|
routes := router.NewRoutes(v)
|
||||||
maintenanceRepo := system.NewMaintenanceRepo(dataData)
|
taskMethods := worker.NewTaskMethods(taskUsecase, mediaUsecase, runtime)
|
||||||
maintenanceUsecase := system2.NewMaintenanceUsecase(maintenanceRepo)
|
|
||||||
taskMethods := worker.NewTaskMethods(taskUsecase, maintenanceUsecase, mediaUsecase, runtime)
|
|
||||||
appRuntimeContributions := runtimeContributions(routes, taskMethods)
|
appRuntimeContributions := runtimeContributions(routes, taskMethods)
|
||||||
moduleRuntime := app.Runtime(appRuntimeContributions, registry)
|
moduleRuntime := app.Runtime(appRuntimeContributions, registry)
|
||||||
websocketServer, cleanup2, err := websocket.New(store)
|
websocketServer, cleanup2, err := websocket.New(store)
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ web/src/modules/<module>/
|
||||||
components/
|
components/
|
||||||
```
|
```
|
||||||
|
|
||||||
修改 KRA 既有系统表和系统行为的内容仍放在现有 `internal/biz/system`、`internal/data/system`、`internal/service`、`internal/server` 中,避免创建第二套用户、角色、部门和权限系统。
|
修改 KRA 既有系统表和系统行为的内容仍放在现有 `internal/biz`、`internal/data/repository`、`internal/service`、`internal/server` 中,避免创建第二套用户、角色、部门和权限系统。
|
||||||
|
|
||||||
每个业务模块通过 KRA 现有的 `pkg/module` 机制贡献:
|
每个业务模块通过 KRA 现有的 `pkg/module` 机制贡献:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ PayPal `AUTHORIZE` 意图后的授权捕获,以及账单、分账、转账等
|
||||||
|
|
||||||
| Provider | 实现方式 |
|
| Provider | 实现方式 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `alipay`、`alipay-v3`、`wechat-v2`、`wechat-v3`、`apple-iap`、`paypal`、`douyin`、`qq`、`allinpay`、`lakala`、`saobei` | 统一走上面的 GoPay v1.5.122 adapter;业务层只接收 `paymentbiz.PaymentResult` |
|
| `alipay`、`alipay-v3`、`wechat-v2`、`wechat-v3`、`apple-iap`、`paypal`、`douyin`、`qq`、`allinpay`、`lakala`、`saobei` | 统一走上面的 GoPay v1.5.122 adapter;业务层只接收 `biz.PaymentResult` |
|
||||||
| `chinaums` | 配置驱动的银联商务 JSON 签名适配器 |
|
| `chinaums` | 配置驱动的银联商务 JSON 签名适配器 |
|
||||||
| `sft` | 配置驱动的商福通 JSON/MD5 适配器 |
|
| `sft` | 配置驱动的商福通 JSON/MD5 适配器 |
|
||||||
| `supper-pay` | 配置驱动的 Supper Pay HMAC 适配器 |
|
| `supper-pay` | 配置驱动的 Supper Pay HMAC 适配器 |
|
||||||
|
|
@ -131,20 +131,18 @@ PayPal `AUTHORIZE` 意图后的授权捕获,以及账单、分账、转账等
|
||||||
每个业务模块先实现可信订单来源:
|
每个业务模块先实现可信订单来源:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import paymentbiz "kra/internal/biz/payment"
|
|
||||||
|
|
||||||
type GameItemPayment struct {
|
type GameItemPayment struct {
|
||||||
orders GameItemOrderRepo
|
orders GameItemOrderRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (GameItemPayment) Type() string { return "game_item" }
|
func (GameItemPayment) Type() string { return "game_item" }
|
||||||
|
|
||||||
func (p GameItemPayment) PreparePayment(ctx context.Context, provider, tradeNo, businessID string) (*paymentbiz.PaymentIntent, error) {
|
func (p GameItemPayment) PreparePayment(ctx context.Context, provider, tradeNo, businessID string) (*biz.PaymentIntent, error) {
|
||||||
order, err := p.orders.FindPayable(ctx, businessID)
|
order, err := p.orders.FindPayable(ctx, businessID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &paymentbiz.PaymentIntent{
|
return &biz.PaymentIntent{
|
||||||
Provider: provider, TradeNo: tradeNo,
|
Provider: provider, TradeNo: tradeNo,
|
||||||
BusinessType: "game_item", BusinessID: businessID,
|
BusinessType: "game_item", BusinessID: businessID,
|
||||||
Subject: order.Title, Amount: order.PayableAmount, Currency: order.Currency,
|
Subject: order.Title, Amount: order.PayableAmount, Currency: order.Currency,
|
||||||
|
|
@ -155,7 +153,7 @@ func (p GameItemPayment) PreparePayment(ctx context.Context, provider, tradeNo,
|
||||||
然后注册按业务类型分发的发货处理器:
|
然后注册按业务类型分发的发货处理器:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (p GameItemPayment) Fulfill(ctx context.Context, c *paymentbiz.PaymentConfirmation) error {
|
func (p GameItemPayment) Fulfill(ctx context.Context, c *biz.PaymentConfirmation) error {
|
||||||
return p.orders.Transaction(ctx, func(tx GameItemOrderTx) error {
|
return p.orders.Transaction(ctx, func(tx GameItemOrderTx) error {
|
||||||
// confirmation_id 必须有唯一约束。已处理时直接返回 nil。
|
// confirmation_id 必须有唯一约束。已处理时直接返回 nil。
|
||||||
if tx.HasPaymentConfirmation(c.ID) {
|
if tx.HasPaymentConfirmation(c.ID) {
|
||||||
|
|
@ -168,7 +166,7 @@ func (p GameItemPayment) Fulfill(ctx context.Context, c *paymentbiz.PaymentConfi
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p GameItemPayment) AuthorizeRefund(ctx context.Context, order *paymentbiz.PaymentOrder, amount int64) error {
|
func (p GameItemPayment) AuthorizeRefund(ctx context.Context, order *biz.PaymentOrder, amount int64) error {
|
||||||
return p.orders.CheckRefundable(ctx, order.BusinessID, amount)
|
return p.orders.CheckRefundable(ctx, order.BusinessID, amount)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -407,4 +405,4 @@ Chinaums、SFT、Supper Pay、微信小游戏和抖音小游戏没有在代码
|
||||||
|
|
||||||
## 日志
|
## 日志
|
||||||
|
|
||||||
支付日志在 `internal/biz/payment/payment_log.go` 通过 `PaymentLogger` 独立抽象,包含下单、查单失败、回调查单、金额校验、重复回调和发货结果等结构化事件。日志只记录渠道、商户订单号、业务类型、业务 ID、确认 ID 等审计字段,不记录私钥、密钥、证书内容或完整敏感回调原文。
|
支付日志在 `internal/biz/payment_log.go` 通过 `PaymentLogger` 独立抽象,包含下单、查单失败、回调查单、金额校验、重复回调和发货结果等结构化事件。日志只记录渠道、商户订单号、业务类型、业务 ID、确认 ID 等审计字段,不记录私钥、密钥、证书内容或完整敏感回调原文。
|
||||||
|
|
|
||||||
|
|
@ -10,31 +10,24 @@
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| HTTP JSON 响应契约 | `pkg/httpx` | `Response`、`PageResult`、状态码和 Gin 响应助手;`server/httpx/response.go` 仅保留 system 适配。 |
|
| HTTP JSON 响应契约 | `pkg/httpx` | `Response`、`PageResult`、状态码和 Gin 响应助手;`server/httpx/response.go` 仅保留 system 适配。 |
|
||||||
| protobuf JSON 局部合并 | `pkg/protoutil` | 与业务无关的字段归一化和局部反序列化;初始化直接使用公共包。 |
|
| protobuf JSON 局部合并 | `pkg/protoutil` | 与业务无关的字段归一化和局部反序列化;初始化直接使用公共包。 |
|
||||||
| 支付 provider/mode 标识 | `pkg/paymentkit` | provider 常量、支持列表、金额/签名/JSON 等跨模块协议;`biz/payment` 与 `biz/integration` 直接复用。 |
|
| 支付 provider/mode 标识 | `pkg/paymentkit` | provider 常量、支持列表、金额/签名/JSON 等跨模块协议;system `biz` 只保留兼容别名。 |
|
||||||
| 支付回调 ACK | `pkg/paymentkit` | 回调应答、失败包装和默认 provider 应答;具体渠道 SDK 留在 `internal/integration/payment`。 |
|
| 支付回调 ACK | `pkg/paymentkit` | 回调应答、失败包装和默认 provider 应答;具体渠道 SDK 仍留在 system integration。 |
|
||||||
| WebSocket 通用收发 | `pkg/websocket` | Melody 的连接、事件、点对点发送、广播和会话查询封装;`internal/integration` 管理配置与生命周期。 |
|
| WebSocket 通用收发 | `pkg/websocket` | Melody 的连接、事件、点对点发送、广播和会话查询封装;system integration 管理配置与生命周期。 |
|
||||||
| 消息队列 | `pkg/mq` | Broker 无关的发布、订阅、JSON 和 QoS 接口;EMQX/Paho 与 RabbitMQ/AMQP 客户端由 `internal/integration` 管理。 |
|
| 消息队列 | `pkg/mq` | Broker 无关的发布、订阅、JSON 和 QoS 接口;EMQX/Paho 与 RabbitMQ/AMQP 客户端由 system integration 管理。 |
|
||||||
| 模块、任务和迁移协议 | `pkg/module`、`pkg/task`、`pkg/database/migration` | 供不同业务模块注册贡献,不带 system 业务语义。 |
|
| 模块、任务和迁移协议 | `pkg/module`、`pkg/task`、`pkg/database/migration` | 供不同业务模块注册贡献,不带 system 业务语义。 |
|
||||||
|
|
||||||
## system 内部保留边界
|
## system 内部保留边界
|
||||||
|
|
||||||
- `app`:运行时组合根,汇总依赖注入后的路由和任务贡献。
|
- `app`:组合根,汇总各模块的迁移、菜单、路由和任务贡献。
|
||||||
- `modules`:静态模块 catalog,汇总各模块迁移、菜单、API 和默认任务。
|
- `modules/system`:system 模块的 Definition,声明迁移、管理面和默认任务。
|
||||||
- `modules/system`:system 模块的 Definition,声明系统表迁移。
|
|
||||||
- `modules/integration`:integration 配置迁移和管理面贡献。
|
|
||||||
- `modules/task`:定时任务迁移和默认任务贡献。
|
|
||||||
- `modules/payment`:payment 模块的 Definition,声明支付迁移和支付管理面。
|
- `modules/payment`:payment 模块的 Definition,声明支付迁移和支付管理面。
|
||||||
- `biz/system`:用户、权限、菜单、审计、媒体和系统配置等系统领域模型与用例。
|
- `biz`:用户、权限、菜单、审计、任务、支付订单和系统配置等领域模型与用例。
|
||||||
- `biz/payment`:支付订单、支付流程、支付接口和支付日志。
|
|
||||||
- `biz/integration`:支付/消息队列/WebSocket 集成配置定义与校验。
|
|
||||||
- `biz/task`:定时任务模型、任务用例和任务注册协议。
|
|
||||||
- `conf`:system 配置 proto、运行时快照和生成代码。
|
- `conf`:system 配置 proto、运行时快照和生成代码。
|
||||||
- `data`:共享数据库生命周期与配置 watcher;PO/仓储按 `system`、`integration`、`task`、`payment` 子包隔离。
|
- `data`:数据库连接、PO、仓储、system 表、支付持久化和配置 watcher。
|
||||||
- `initialize`:首次安装、配置迁移、种子编排和运行时重载。
|
- `initialize`:首次安装、配置迁移、种子编排和运行时重载。
|
||||||
- `integration`:Redis、邮件、存储、支付、WebSocket、EMQX 和 RabbitMQ 的 provider 生命周期。
|
- `integration`:Redis、邮件、存储、支付、WebSocket、EMQX 和 RabbitMQ 的 provider 生命周期。
|
||||||
- `security`:JWT claims、签发/解析和后台安全实现。
|
- `security`:JWT claims、签发/解析和后台安全实现。
|
||||||
- `routecatalog`:HTTP 公开性、操作审计、请求体策略和 API 分组/说明的统一目录。
|
- `service`:HTTP DTO(`service/dto`)、DTO 与 DO 转换、应用服务和路由元数据。
|
||||||
- `service`:HTTP DTO(`service/dto`)、DTO 与 DO 转换和应用服务。
|
|
||||||
- `server`:Gin 生命周期;handler、middleware、router、HTTP 适配按子包维护。
|
- `server`:Gin 生命周期;handler、middleware、router、HTTP 适配按子包维护。
|
||||||
- `worker`:任务调度、执行器、SSE 订阅及其并发状态。
|
- `worker`:任务调度、执行器、SSE 订阅及其并发状态。
|
||||||
|
|
||||||
|
|
@ -45,19 +38,11 @@
|
||||||
|
|
||||||
```text
|
```text
|
||||||
internal/
|
internal/
|
||||||
app/ # 运行时组合根
|
app/ # 组合根和 catalog
|
||||||
modules/ # 静态 catalog、业务模块定义及其模块级贡献
|
modules/ # 业务模块定义及其模块级贡献
|
||||||
biz/
|
biz/ # DO、usecase、repo interface
|
||||||
system/ # 系统领域
|
|
||||||
payment/ # 支付领域
|
|
||||||
integration/# 集成配置领域
|
|
||||||
task/ # 定时任务领域
|
|
||||||
conf/ # 配置 proto/runtime
|
conf/ # 配置 proto/runtime
|
||||||
data/
|
data/ # PO、repo、数据库和迁移
|
||||||
system/ # 系统表与系统仓储
|
|
||||||
integration/# 集成配置表与仓储
|
|
||||||
task/ # 定时任务表与仓储
|
|
||||||
payment/ # 支付表与仓储
|
|
||||||
initialize/ # 首次安装和配置编排
|
initialize/ # 首次安装和配置编排
|
||||||
integration/ # 外部 I/O provider
|
integration/ # 外部 I/O provider
|
||||||
security/ # JWT 和安全实现
|
security/ # JWT 和安全实现
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
|
|
||||||
```text
|
```text
|
||||||
internal/
|
internal/
|
||||||
app/ # 运行时组合根
|
app/ # 组合根、模块 catalog 和运行时组合
|
||||||
modules/ # 静态 catalog 和模块定义
|
modules/system/ # system 模块定义
|
||||||
modules/payment/ # payment 模块定义
|
modules/payment/ # payment 模块定义
|
||||||
biz/ # DO、usecase、repo interface
|
biz/ # DO、usecase、repo interface
|
||||||
conf/ # 配置 proto/runtime
|
conf/ # 配置 proto/runtime
|
||||||
|
|
@ -40,12 +40,12 @@ internal/
|
||||||
独立边界时不继续拆分。
|
独立边界时不继续拆分。
|
||||||
- 删除只转发 `pkg/protoutil` 的 `utils/configutil`。
|
- 删除只转发 `pkg/protoutil` 的 `utils/configutil`。
|
||||||
|
|
||||||
## `internal/app` 为什么只保留运行时组合
|
## `internal/app` 为什么只保留组合代码
|
||||||
|
|
||||||
`modules/catalog.go` 是静态模块注册点,负责按依赖顺序汇总各模块
|
`app/catalog.go` 是有意保留的组合根,负责组装模块、任务注册与运行时。
|
||||||
`Definition()`;`app/runtime.go` 只负责任务注册和依赖注入后的运行时路由组合。
|
system 自身的迁移、管理面和默认定时任务位于
|
||||||
这样模块定义不再和应用组合逻辑混在一起,也不能误并入 `biz`、`service` 或
|
`modules/system/definition.go`,由模块包声明后再被 catalog 汇总。这样模块
|
||||||
`data`。
|
定义不再和应用组合逻辑混在一起,也不能误并入 `biz`、`service` 或 `data`。
|
||||||
|
|
||||||
Catalog 只能自动汇总静态模块贡献;新增模块若提供运行时路由或依赖型任务,仍需
|
Catalog 只能自动汇总静态模块贡献;新增模块若提供运行时路由或依赖型任务,仍需
|
||||||
在 cmd/Wire 中显式注册,直到统一的 runtime contribution 协议落地。
|
在 cmd/Wire 中显式注册,直到统一的 runtime contribution 协议落地。
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,17 @@
|
||||||
系统模块承载当前管理后台的完整业务边界。`internal` 顶层只保留有明确
|
系统模块承载当前管理后台的完整业务边界。`internal` 顶层只保留有明确
|
||||||
生命周期或分层职责的包:
|
生命周期或分层职责的包:
|
||||||
|
|
||||||
- `app`:运行时组合根,负责依赖注入后的任务/路由组合
|
- `app`:组合根、模块 catalog 和任务/路由运行时组合
|
||||||
- `modules`:静态模块 catalog,按 system/integration/task/payment 维护 Definition
|
- `modules`:按业务模块维护 Definition 等模块贡献
|
||||||
- `biz/system`:用户、权限、菜单、审计、媒体和系统配置领域
|
- `biz`:系统领域对象、用例和仓储接口
|
||||||
- `biz/payment`:支付订单、支付流程、支付接口和支付日志
|
|
||||||
- `biz/integration`:集成配置定义、校验和连接测试边界
|
|
||||||
- `biz/task`:定时任务模型、用例和任务注册协议
|
|
||||||
- `conf`:基础配置 proto 与运行时配置解析
|
- `conf`:基础配置 proto 与运行时配置解析
|
||||||
- `data`:共享数据库生命周期;仓储按 `data/system`、`data/integration`、`data/task`、`data/payment` 隔离
|
- `data`:数据库生命周期、系统仓储、系统表和支付持久化
|
||||||
- `initialize`:数据库首次初始化和系统种子数据编排
|
- `initialize`:数据库首次初始化和系统种子数据编排
|
||||||
- `integration`:Redis、邮件、对象存储、支付、WebSocket、EMQX 和 RabbitMQ 适配器
|
- `integration`:Redis、邮件、对象存储、支付、WebSocket、EMQX 和 RabbitMQ 适配器
|
||||||
- `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据
|
|
||||||
- `security`:后台 JWT 等安全实现
|
- `security`:后台 JWT 等安全实现
|
||||||
- `server`:Gin server 组合与生命周期;横切 HTTP 代码按子包维护:
|
- `server`:Gin server 组合与生命周期;横切 HTTP 代码按子包维护:
|
||||||
`server/handler`、`server/middleware`、`server/router`、`server/httpx`
|
`server/handler`、`server/middleware`、`server/router`、`server/httpx`
|
||||||
- `service`:应用服务、DTO 与领域对象转换;DTO 集中在
|
- `service`:应用服务、DTO 与领域对象转换和路由元数据;DTO 集中在
|
||||||
`service/dto`,根包中的 `dto_aliases.go` 只负责兼容旧调用方
|
`service/dto`,根包中的 `dto_aliases.go` 只负责兼容旧调用方
|
||||||
- `worker`:定时任务执行与调度
|
- `worker`:定时任务执行与调度
|
||||||
|
|
||||||
|
|
@ -27,17 +23,14 @@
|
||||||
定义位于 `modules/system`,JWT 实现集中在 `security`,protobuf JSON 统一使用
|
定义位于 `modules/system`,JWT 实现集中在 `security`,protobuf JSON 统一使用
|
||||||
`pkg/protoutil`。
|
`pkg/protoutil`。
|
||||||
|
|
||||||
`internal/modules/catalog.go` 是静态模块 catalog 的唯一注册点,负责按依赖顺序
|
`internal/app` 只保留 `catalog.go` 作为组合根:它负责组装模块 catalog、任务
|
||||||
汇总各模块 Definition。`internal/app/runtime.go` 只负责依赖注入后的任务注册
|
注册和运行时。system 的迁移、管理面和定时任务由 `internal/modules/system`
|
||||||
和路由运行时组合。这样新增模块只需在 modules catalog 注册一次,app 不再重复
|
自己的 `Definition()` 声明,便于后续业务模块独立接入。
|
||||||
维护模块声明。
|
|
||||||
|
|
||||||
系统表统一使用 `sys_` 前缀;业务表应由新业务模块自行命名和迁移,不要混入
|
系统表统一使用 `sys_` 前缀;业务表应由新业务模块自行命名和迁移,不要混入
|
||||||
本目录。
|
本目录。
|
||||||
|
|
||||||
system 通过 `modules/system.Definition()` 提供系统迁移;integration 通过
|
system 通过 `modules/system.Definition()` 提供系统迁移、通信集成菜单/API 和默认任务;
|
||||||
`modules/integration.Definition()` 提供通信集成菜单/API;task 通过
|
|
||||||
`modules/task.Definition()` 提供定时任务迁移和默认任务;
|
|
||||||
payment 通过 `modules/payment.Definition()` 提供支付迁移及支付菜单/API。两者通过
|
payment 通过 `modules/payment.Definition()` 提供支付迁移及支付菜单/API。两者通过
|
||||||
`worker.TaskMethods` 提供依赖系统用例的任务实现,通过 `server/router.Routes` 提供
|
`worker.TaskMethods` 提供依赖系统用例的任务实现,通过 `server/router.Routes` 提供
|
||||||
路由。静态模块贡献可由 catalog 汇总;带运行时依赖的路由和任务仍需在 cmd/Wire
|
路由。静态模块贡献可由 catalog 汇总;带运行时依赖的路由和任务仍需在 cmd/Wire
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,26 @@
|
||||||
// Package app is the composition root for the running administration service.
|
// Package app is the composition root for the running administration service.
|
||||||
// It wires runtime objects that need constructed dependencies. Static module
|
// It knows which business modules are enabled and wires their contributions
|
||||||
// declarations live in the sibling internal/modules package.
|
// into the shared platform. Individual modules do not import this package.
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
paymentmodule "kra/internal/modules/payment"
|
||||||
|
systemmodule "kra/internal/modules/system"
|
||||||
"kra/pkg/module"
|
"kra/pkg/module"
|
||||||
platformtask "kra/pkg/task"
|
platformtask "kra/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Catalog lists the business modules enabled in this binary. Static module
|
||||||
|
// contributions (migrations, admin metadata, default tasks) enter through a
|
||||||
|
// Definition; runtime routes and dependency-bearing task contributors still
|
||||||
|
// need explicit wiring below.
|
||||||
|
func Catalog() module.Catalog {
|
||||||
|
return module.Catalog{Definitions: []module.Definition{
|
||||||
|
systemmodule.Definition(),
|
||||||
|
paymentmodule.Definition(),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
// TaskRegistry builds the process-wide registry from dependency-free module
|
// TaskRegistry builds the process-wide registry from dependency-free module
|
||||||
// contributions. Dependency-bearing methods are added by their module runtime
|
// contributions. Dependency-bearing methods are added by their module runtime
|
||||||
// constructors after the usecases have been created.
|
// constructors after the usecases have been created.
|
||||||
|
|
@ -10,6 +10,25 @@ import (
|
||||||
platformtask "kra/pkg/task"
|
platformtask "kra/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCatalogIncludesSystemDefinition(t *testing.T) {
|
||||||
|
catalog := Catalog()
|
||||||
|
if len(catalog.Definitions) != 2 {
|
||||||
|
t.Fatalf("definitions = %d, want 2", len(catalog.Definitions))
|
||||||
|
}
|
||||||
|
if catalog.Definitions[0].Name != "system" || catalog.Definitions[1].Name != "payment" {
|
||||||
|
t.Fatalf("definition order = [%q, %q], want [system, payment]", catalog.Definitions[0].Name, catalog.Definitions[1].Name)
|
||||||
|
}
|
||||||
|
if got := catalog.MigrationSteps(); len(got) != 5 {
|
||||||
|
t.Fatalf("module migrations = %d, want 5", len(got))
|
||||||
|
}
|
||||||
|
if surface := catalog.Surface(); len(surface.Menus) != 3 || len(surface.APIs) != 15 {
|
||||||
|
t.Fatalf("admin surface = %d menus/%d APIs, want 3/15", len(surface.Menus), len(surface.APIs))
|
||||||
|
}
|
||||||
|
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||||
|
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTaskRegistryRegistersStaticModuleMethods(t *testing.T) {
|
func TestTaskRegistryRegistersStaticModuleMethods(t *testing.T) {
|
||||||
method := platformtask.Method{
|
method := platformtask.Method{
|
||||||
Name: "test.static",
|
Name: "test.static",
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
package biz
|
package biz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"kra/internal/biz/integration"
|
|
||||||
"kra/internal/biz/payment"
|
|
||||||
"kra/internal/biz/system"
|
"kra/internal/biz/system"
|
||||||
"kra/internal/biz/task"
|
|
||||||
|
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProviderSet is biz providers.
|
// ProviderSet is biz providers.
|
||||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet)
|
var ProviderSet = wire.NewSet(system.ProviderSet)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
import "github.com/google/wire"
|
|
||||||
|
|
||||||
// ProviderSet wires communication and integration-configuration usecases.
|
|
||||||
var ProviderSet = wire.NewSet(NewIntegrationConfigUsecase)
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package payment
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package integration
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
paymentutil "kra/pkg/paymentkit"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -57,23 +56,6 @@ type IntegrationConfigRepo interface {
|
||||||
DeleteIntegrationConfig(context.Context, string, string) error
|
DeleteIntegrationConfig(context.Context, string, string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrPaymentConfigNotFound marks an absent payment integration row without
|
|
||||||
// exposing the storage driver's not-found error to the payment data module.
|
|
||||||
var ErrPaymentConfigNotFound = errors.New("支付渠道配置不存在")
|
|
||||||
|
|
||||||
// PaymentConfig is the storage-neutral, unmasked snapshot used by payment
|
|
||||||
// persistence. It deliberately contains no ORM or table metadata.
|
|
||||||
type PaymentConfig struct {
|
|
||||||
Enabled bool
|
|
||||||
Values json.RawMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// PaymentConfigReader is the narrow inversion seam between the payment and
|
|
||||||
// integration data modules. The integration module owns its ConfigPO.
|
|
||||||
type PaymentConfigReader interface {
|
|
||||||
ReadPaymentConfig(context.Context, string) (*PaymentConfig, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type IntegrationConnectionTester interface {
|
type IntegrationConnectionTester interface {
|
||||||
TestIntegration(context.Context, *IntegrationConfig) error
|
TestIntegration(context.Context, *IntegrationConfig) error
|
||||||
}
|
}
|
||||||
|
|
@ -115,9 +97,9 @@ func (uc *IntegrationConfigUsecase) Save(ctx context.Context, config *Integratio
|
||||||
if !json.Valid(config.Values) {
|
if !json.Valid(config.Values) {
|
||||||
return errors.New("集成配置必须是合法 JSON")
|
return errors.New("集成配置必须是合法 JSON")
|
||||||
}
|
}
|
||||||
values, err := decodeIntegrationObject(config.Values)
|
values := map[string]any{}
|
||||||
if err != nil {
|
if err := json.Unmarshal(config.Values, &values); err != nil {
|
||||||
return err
|
return errors.New("集成配置必须是 JSON 对象")
|
||||||
}
|
}
|
||||||
if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok {
|
if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok {
|
||||||
values = mergeIntegrationDefaults(definition.Defaults, values)
|
values = mergeIntegrationDefaults(definition.Defaults, values)
|
||||||
|
|
@ -148,9 +130,9 @@ func (uc *IntegrationConfigUsecase) Test(ctx context.Context, config *Integratio
|
||||||
if !json.Valid(config.Values) {
|
if !json.Valid(config.Values) {
|
||||||
return errors.New("集成配置必须是合法 JSON")
|
return errors.New("集成配置必须是合法 JSON")
|
||||||
}
|
}
|
||||||
values, err := decodeIntegrationObject(config.Values)
|
values := map[string]any{}
|
||||||
if err != nil {
|
if err := json.Unmarshal(config.Values, &values); err != nil {
|
||||||
return err
|
return errors.New("集成配置必须是 JSON 对象")
|
||||||
}
|
}
|
||||||
if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok {
|
if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok {
|
||||||
values = mergeIntegrationDefaults(definition.Defaults, values)
|
values = mergeIntegrationDefaults(definition.Defaults, values)
|
||||||
|
|
@ -179,14 +161,6 @@ func normalizeIntegrationPart(value string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(value))
|
return strings.ToLower(strings.TrimSpace(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeIntegrationObject(raw json.RawMessage) (map[string]any, error) {
|
|
||||||
values := map[string]any{}
|
|
||||||
if err := json.Unmarshal(raw, &values); err != nil || values == nil {
|
|
||||||
return nil, errors.New("集成配置必须是 JSON 对象")
|
|
||||||
}
|
|
||||||
return values, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func IntegrationDefinitions(kind string) []IntegrationConfigDefinition {
|
func IntegrationDefinitions(kind string) []IntegrationConfigDefinition {
|
||||||
kind = normalizeIntegrationPart(kind)
|
kind = normalizeIntegrationPart(kind)
|
||||||
definitions := integrationDefinitions[kind]
|
definitions := integrationDefinitions[kind]
|
||||||
|
|
@ -316,7 +290,7 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch provider {
|
switch provider {
|
||||||
case paymentutil.ProviderAlipayV3:
|
case PaymentAlipayV3:
|
||||||
for _, group := range []struct {
|
for _, group := range []struct {
|
||||||
label string
|
label string
|
||||||
keys []string
|
keys []string
|
||||||
|
|
@ -330,21 +304,21 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
|
||||||
return fmt.Errorf("%s 缺少配置字段 %s", provider, group.label)
|
return fmt.Errorf("%s 缺少配置字段 %s", provider, group.label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case paymentutil.ProviderWechatV2:
|
case PaymentWechatV2:
|
||||||
if integrationFirst(values, "client_cert", "cert_pem", "apiclient_cert") == "" || integrationFirst(values, "client_key", "key_pem", "apiclient_key") == "" {
|
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)
|
return fmt.Errorf("%s 退款要求同时配置 client_cert 和 client_key", provider)
|
||||||
}
|
}
|
||||||
case paymentutil.ProviderApple:
|
case PaymentApple:
|
||||||
if integrationInt64(values, "price_divisor", 0) <= 0 {
|
if integrationInt64(values, "price_divisor", 0) <= 0 {
|
||||||
if _, exists := values["price_divisors"].(map[string]any); !exists {
|
if _, exists := values["price_divisors"].(map[string]any); !exists {
|
||||||
return fmt.Errorf("%s 缺少配置字段 price_divisor 或 price_divisors", provider)
|
return fmt.Errorf("%s 缺少配置字段 price_divisor 或 price_divisors", provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case paymentutil.ProviderDouyin:
|
case PaymentDouyin:
|
||||||
if integrationFirst(values, "platform_serial_no", "platform_cert_serial") == "" {
|
if integrationFirst(values, "platform_serial_no", "platform_cert_serial") == "" {
|
||||||
return fmt.Errorf("%s 缺少配置字段 platform_serial_no", provider)
|
return fmt.Errorf("%s 缺少配置字段 platform_serial_no", provider)
|
||||||
}
|
}
|
||||||
case paymentutil.ProviderQQ:
|
case PaymentQQ:
|
||||||
if integrationFirst(values, "mch_id", "merchant_id") == "" {
|
if integrationFirst(values, "mch_id", "merchant_id") == "" {
|
||||||
return fmt.Errorf("%s 缺少配置字段 mch_id", provider)
|
return fmt.Errorf("%s 缺少配置字段 mch_id", provider)
|
||||||
}
|
}
|
||||||
|
|
@ -355,12 +329,12 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
|
||||||
if signType != "" && signType != "MD5" && signType != "HMAC-SHA256" {
|
if signType != "" && signType != "MD5" && signType != "HMAC-SHA256" {
|
||||||
return fmt.Errorf("%s sign_type 必须是 MD5 或 HMAC-SHA256", provider)
|
return fmt.Errorf("%s sign_type 必须是 MD5 或 HMAC-SHA256", provider)
|
||||||
}
|
}
|
||||||
case paymentutil.ProviderAllinPay:
|
case PaymentAllinPay:
|
||||||
orderType := strings.ToLower(integrationFirst(values, "query_order_type", "order_type"))
|
orderType := strings.ToLower(integrationFirst(values, "query_order_type", "order_type"))
|
||||||
if orderType != "" && orderType != "reqsn" && orderType != "trxid" {
|
if orderType != "" && orderType != "reqsn" && orderType != "trxid" {
|
||||||
return fmt.Errorf("%s query_order_type 必须是 reqsn 或 trxid", provider)
|
return fmt.Errorf("%s query_order_type 必须是 reqsn 或 trxid", provider)
|
||||||
}
|
}
|
||||||
case paymentutil.ProviderChinaums, paymentutil.ProviderSFT, paymentutil.ProviderSuperPay, paymentutil.ProviderWechatGame, paymentutil.ProviderDouyinGame:
|
case PaymentChinaums, PaymentSFT, PaymentSuperPay, PaymentWechatGame, PaymentDouyinGame:
|
||||||
if integrationFirst(values, "app_key", "merchant_key", "signing_secret", "token") == "" {
|
if integrationFirst(values, "app_key", "merchant_key", "signing_secret", "token") == "" {
|
||||||
return fmt.Errorf("%s 缺少签名密钥", provider)
|
return fmt.Errorf("%s 缺少签名密钥", provider)
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +343,7 @@ func validatePaymentIntegrationConfig(provider string, values map[string]any) er
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if environment := strings.ToLower(integrationText(values, "environment")); environment != "" {
|
if environment := strings.ToLower(integrationText(values, "environment")); environment != "" {
|
||||||
allowedSandbox := provider != paymentutil.ProviderQQ && provider != paymentutil.ProviderDouyin && provider != paymentutil.ProviderLakala
|
allowedSandbox := provider != PaymentQQ && provider != PaymentDouyin && provider != PaymentLakala
|
||||||
if environment != "production" && environment != "prod" && (!allowedSandbox || environment != "sandbox") {
|
if environment != "production" && environment != "prod" && (!allowedSandbox || environment != "sandbox") {
|
||||||
return fmt.Errorf("%s environment 配置无效", provider)
|
return fmt.Errorf("%s environment 配置无效", provider)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -96,14 +96,3 @@ func TestIntegrationConfigTestDoesNotPersistCandidate(t *testing.T) {
|
||||||
t.Fatalf("tested values = %#v", values)
|
t.Fatalf("tested values = %#v", values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationConfigRejectsJSONNull(t *testing.T) {
|
|
||||||
repo := &integrationConfigRepoTestDouble{}
|
|
||||||
usecase := NewIntegrationConfigUsecase(repo, &integrationConnectionTesterDouble{})
|
|
||||||
if err := usecase.Save(context.Background(), &IntegrationConfig{Kind: IntegrationKindMQ, Provider: "emqx", Values: json.RawMessage("null")}); err == nil {
|
|
||||||
t.Fatal("Save() accepted JSON null as an object")
|
|
||||||
}
|
|
||||||
if err := usecase.Test(context.Background(), &IntegrationConfig{Kind: IntegrationKindMQ, Provider: "emqx", Values: json.RawMessage("null")}); err == nil {
|
|
||||||
t.Fatal("Test() accepted JSON null as an object")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
package integration
|
package system
|
||||||
|
|
||||||
import paymentutil "kra/pkg/paymentkit"
|
|
||||||
|
|
||||||
func integrationField(key, label string, required, secret bool, fieldType string) IntegrationConfigField {
|
func integrationField(key, label string, required, secret bool, fieldType string) IntegrationConfigField {
|
||||||
if fieldType == "" {
|
if fieldType == "" {
|
||||||
|
|
@ -146,35 +144,35 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
IntegrationKindPayment: {
|
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(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"),
|
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("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")),
|
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(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"),
|
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")),
|
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(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")),
|
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(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")),
|
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(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")),
|
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(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")),
|
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(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")),
|
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(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")),
|
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(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")),
|
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(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")),
|
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(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")),
|
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(PaymentChinaums, "银联商务", "按商户协议配置的银联商务适配器"),
|
||||||
genericPaymentDefinition(paymentutil.ProviderSFT, "商福通", "按商户协议配置的商福通适配器"),
|
genericPaymentDefinition(PaymentSFT, "商福通", "按商户协议配置的商福通适配器"),
|
||||||
genericPaymentDefinition(paymentutil.ProviderSuperPay, "Supper Pay", "按商户协议配置的 Supper Pay 适配器"),
|
genericPaymentDefinition(PaymentSuperPay, "Supper Pay", "按商户协议配置的 Supper Pay 适配器"),
|
||||||
genericPaymentDefinition(paymentutil.ProviderWechatGame, "微信小游戏支付", "微信小游戏虚拟支付配置驱动适配器"),
|
genericPaymentDefinition(PaymentWechatGame, "微信小游戏支付", "微信小游戏虚拟支付配置驱动适配器"),
|
||||||
genericPaymentDefinition(paymentutil.ProviderDouyinGame, "抖音小游戏支付", "抖音小游戏支付配置驱动适配器"),
|
genericPaymentDefinition(PaymentDouyinGame, "抖音小游戏支付", "抖音小游戏支付配置驱动适配器"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package system
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
// MaintenanceRepo owns retention work for system-owned tables. Other data
|
|
||||||
// modules clean their own tables and worker composes the operations.
|
|
||||||
type MaintenanceRepo interface {
|
|
||||||
CleanupExpired(context.Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type MaintenanceUsecase struct{ repo MaintenanceRepo }
|
|
||||||
|
|
||||||
func NewMaintenanceUsecase(repo MaintenanceRepo) *MaintenanceUsecase {
|
|
||||||
return &MaintenanceUsecase{repo: repo}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *MaintenanceUsecase) CleanupExpired(ctx context.Context) error {
|
|
||||||
return uc.repo.CleanupExpired(ctx)
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package payment
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package payment
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package payment
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package payment
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -24,8 +24,11 @@ var ProviderSet = wire.NewSet(
|
||||||
NewAuditUsecase,
|
NewAuditUsecase,
|
||||||
NewAuditRecorderUsecase,
|
NewAuditRecorderUsecase,
|
||||||
NewLogViewerUsecase,
|
NewLogViewerUsecase,
|
||||||
|
NewTaskUsecaseWithRegistry,
|
||||||
|
NewTaskApplicationUsecase,
|
||||||
NewMediaUsecase,
|
NewMediaUsecase,
|
||||||
NewAnnouncementUsecase,
|
NewAnnouncementUsecase,
|
||||||
NewEmailUsecase,
|
NewEmailUsecase,
|
||||||
NewMaintenanceUsecase,
|
NewPaymentUsecase,
|
||||||
|
NewIntegrationConfigUsecase,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ type InitializationRepo interface {
|
||||||
DiskMountPoints() []string
|
DiskMountPoints() []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskReloader is the narrow scheduler boundary needed after configuration
|
|
||||||
// changes. The consumer owns this interface; worker supplies the implementation.
|
|
||||||
type TaskReloader interface {
|
type TaskReloader interface {
|
||||||
Reload(context.Context) error
|
Reload(context.Context) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package task
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -58,7 +58,7 @@ type TaskRepo interface {
|
||||||
ToggleTask(context.Context, uint, bool) error
|
ToggleTask(context.Context, uint, bool) error
|
||||||
RecordTaskLog(context.Context, *TimedTaskLog) error
|
RecordTaskLog(context.Context, *TimedTaskLog) error
|
||||||
ListTaskLogs(context.Context, int, int, uint, string) ([]*TimedTaskLog, int64, error)
|
ListTaskLogs(context.Context, int, int, uint, string) ([]*TimedTaskLog, int64, error)
|
||||||
CleanupTaskLogs(context.Context) error
|
CleanupLogs(context.Context) error
|
||||||
TaskNameExists(context.Context, string, uint) (bool, error)
|
TaskNameExists(context.Context, string, uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package task
|
package system
|
||||||
|
|
||||||
import platformtask "kra/pkg/task"
|
import platformtask "kra/pkg/task"
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package task
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -65,7 +65,7 @@ func (r *applicationTaskRepo) RecordTaskLog(context.Context, *TimedTaskLog) erro
|
||||||
func (r *applicationTaskRepo) ListTaskLogs(context.Context, int, int, uint, string) ([]*TimedTaskLog, int64, error) {
|
func (r *applicationTaskRepo) ListTaskLogs(context.Context, int, int, uint, string) ([]*TimedTaskLog, int64, error) {
|
||||||
return nil, 0, nil
|
return nil, 0, nil
|
||||||
}
|
}
|
||||||
func (r *applicationTaskRepo) CleanupTaskLogs(context.Context) error { return nil }
|
func (r *applicationTaskRepo) CleanupLogs(context.Context) error { return nil }
|
||||||
func (r *applicationTaskRepo) TaskNameExists(context.Context, string, uint) (bool, error) {
|
func (r *applicationTaskRepo) TaskNameExists(context.Context, string, uint) (bool, error) {
|
||||||
return r.nameExists, r.nameExistsErr
|
return r.nameExists, r.nameExistsErr
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package task
|
|
||||||
|
|
||||||
import "github.com/google/wire"
|
|
||||||
|
|
||||||
// ProviderSet wires timed-task usecases independently from the system domain.
|
|
||||||
var ProviderSet = wire.NewSet(
|
|
||||||
NewTaskUsecaseWithRegistry,
|
|
||||||
NewTaskApplicationUsecase,
|
|
||||||
)
|
|
||||||
|
|
@ -3,30 +3,12 @@
|
||||||
`data` owns database clients, persistence models, migrations, configuration
|
`data` owns database clients, persistence models, migrations, configuration
|
||||||
watching, and repository implementations.
|
watching, and repository implementations.
|
||||||
|
|
||||||
- `system/`: system repositories and system table persistence
|
- `repository/`: system repositories and table persistence
|
||||||
- `task/`: timed-task tables and task persistence
|
- `payment/`: payment configuration and payment-order persistence
|
||||||
- `integration/`: `sys_integration_configs` and integration configuration persistence
|
|
||||||
- `payment/`: payment-order persistence
|
|
||||||
- each subpackage owns its Wire `ProviderSet`; the root package only binds the
|
- each subpackage owns its Wire `ProviderSet`; the root package only binds the
|
||||||
shared `Data` infrastructure and aggregates those sets, mirroring `biz`
|
shared `Data` infrastructure and aggregates those sets
|
||||||
- root files: shared database lifecycle, runtime clients, configuration
|
- root files: shared database lifecycle, runtime clients, integration-config
|
||||||
orchestration, data-scope auditing, and migration orchestration
|
storage, data-scope auditing, and migration orchestration
|
||||||
|
|
||||||
新增数据模块时创建 `internal/data/<module>`,提供 `ProviderSet` 和
|
|
||||||
`Migrations()`,再在 `internal/modules/<module>/definition.go` 注册迁移与
|
|
||||||
管理面,最后在根 `data.ProviderSet` 中注册该模块。根 `data` 不应直接
|
|
||||||
拥有业务表 PO,也不应让一个模块引用另一个模块的私有 PO。
|
|
||||||
|
|
||||||
Root files intentionally stay in one package because they share `Data` state and
|
Root files intentionally stay in one package because they share `Data` state and
|
||||||
reload locks. Do not split them into packages only to reduce file count.
|
reload locks. Do not split them into packages only to reduce file count.
|
||||||
|
|
||||||
当前内置数据模块为 `system`、`integration`、`task` 和 `payment`。其中:
|
|
||||||
|
|
||||||
- `system` 只拥有 `sys_*` 系统表、系统仓储、种子和系统维护清理;
|
|
||||||
- `integration` 唯一拥有 `sys_integration_configs` 表模型、配置仓储和通信默认值;
|
|
||||||
- `task` 只拥有定时任务与任务日志表;
|
|
||||||
- `payment` 只拥有 `pay_orders` 等支付持久化,通过 `biz/integration.PaymentConfigReader`
|
|
||||||
读取支付配置,不感知 integration 的 PO 或表结构。
|
|
||||||
|
|
||||||
配置文件迁移、数据库切换、Redis/Mongo/对象存储重载仍属于根 data 的生命周期编排,
|
|
||||||
不等同于某个业务模块的表仓储。
|
|
||||||
|
|
|
||||||
|
|
@ -382,12 +382,6 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
|
useRedis := next.Admin.System != nil && next.Admin.System.UseRedis
|
||||||
candidateRedis := openRedis(next.Data.Redis, useRedis, d.logger())
|
candidateRedis := openRedis(next.Data.Redis, useRedis, d.logger())
|
||||||
candidateRedisAccepted := false
|
|
||||||
defer func() {
|
|
||||||
if !candidateRedisAccepted && candidateRedis != nil {
|
|
||||||
_ = candidateRedis.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
useMongo := next.Admin.System != nil && next.Admin.System.UseMongo
|
useMongo := next.Admin.System != nil && next.Admin.System.UseMongo
|
||||||
candidateMongo, mongoErr := openMongo(next.Data.Mongo, useMongo)
|
candidateMongo, mongoErr := openMongo(next.Data.Mongo, useMongo)
|
||||||
if mongoErr != nil {
|
if mongoErr != nil {
|
||||||
|
|
@ -403,16 +397,6 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
candidateDBListAccepted := false
|
|
||||||
defer func() {
|
|
||||||
if !candidateDBListAccepted {
|
|
||||||
closeDatabaseList(candidateDBList)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
integrationConfigs, err := readIntegrationRuntime(candidateDB)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reload integration runtime: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
d.gormDB.replace(candidateDB, d.enqueueDataScopeAudit)
|
d.gormDB.replace(candidateDB, d.enqueueDataScopeAudit)
|
||||||
d.databaseReady.Store(databaseReady)
|
d.databaseReady.Store(databaseReady)
|
||||||
|
|
@ -425,16 +409,14 @@ func (d *Data) reloadConfig(ctx context.Context) error {
|
||||||
d.mongo.replace(candidateMongo)
|
d.mongo.replace(candidateMongo)
|
||||||
mongoAccepted = true
|
mongoAccepted = true
|
||||||
}
|
}
|
||||||
closeCandidate = false
|
|
||||||
candidateDBListAccepted = true
|
|
||||||
candidateRedisAccepted = true
|
|
||||||
d.runtime.Replace(next.Data, next.Admin)
|
d.runtime.Replace(next.Data, next.Admin)
|
||||||
if d.integrations != nil {
|
if err = d.loadIntegrationRuntime(candidateDB); err != nil {
|
||||||
d.integrations.Replace(integrationConfigs)
|
return fmt.Errorf("reload integration runtime: %w", err)
|
||||||
}
|
}
|
||||||
if d.storage != nil {
|
if d.storage != nil {
|
||||||
d.storage.Replace(candidateStorage)
|
d.storage.Replace(candidateStorage)
|
||||||
}
|
}
|
||||||
|
closeCandidate = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,8 @@ import (
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
dataintegration "kra/internal/data/integration"
|
|
||||||
datapayment "kra/internal/data/payment"
|
datapayment "kra/internal/data/payment"
|
||||||
datasystem "kra/internal/data/system"
|
datasystem "kra/internal/data/repository"
|
||||||
datatask "kra/internal/data/task"
|
|
||||||
"kra/internal/integration/runtimeconfig"
|
"kra/internal/integration/runtimeconfig"
|
||||||
"kra/internal/integration/storage"
|
"kra/internal/integration/storage"
|
||||||
"kra/pkg/module"
|
"kra/pkg/module"
|
||||||
|
|
@ -26,12 +24,8 @@ var ProviderSet = wire.NewSet(
|
||||||
NewIntegrationRuntime,
|
NewIntegrationRuntime,
|
||||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||||
wire.Bind(new(dataintegration.Provider), new(*Data)),
|
|
||||||
wire.Bind(new(datatask.Provider), new(*Data)),
|
|
||||||
wire.Bind(new(datapayment.Provider), new(*Data)),
|
wire.Bind(new(datapayment.Provider), new(*Data)),
|
||||||
datasystem.ProviderSet,
|
datasystem.ProviderSet,
|
||||||
dataintegration.ProviderSet,
|
|
||||||
datatask.ProviderSet,
|
|
||||||
datapayment.ProviderSet,
|
datapayment.ProviderSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -176,34 +170,6 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger, storageManager *stor
|
||||||
c.Database = &conf.Data_Database{}
|
c.Database = &conf.Data_Database{}
|
||||||
}
|
}
|
||||||
d := &Data{runtime: runtime, integrations: runtimeconfig.NewStore(), appLogger: appLogger, storage: storageManager, catalog: catalog}
|
d := &Data{runtime: runtime, integrations: runtimeconfig.NewStore(), appLogger: appLogger, storage: storageManager, catalog: catalog}
|
||||||
var stopConfigWatcher func()
|
|
||||||
var cleanupOnce sync.Once
|
|
||||||
cleanup := func() {
|
|
||||||
cleanupOnce.Do(func() {
|
|
||||||
if stopConfigWatcher != nil {
|
|
||||||
stopConfigWatcher()
|
|
||||||
}
|
|
||||||
if d.auditLog != nil {
|
|
||||||
d.auditLog.Close()
|
|
||||||
}
|
|
||||||
if d.gormDB != nil {
|
|
||||||
d.gormDB.close()
|
|
||||||
}
|
|
||||||
closeDatabaseList(d.dbList)
|
|
||||||
if d.redis != nil {
|
|
||||||
d.redis.close()
|
|
||||||
}
|
|
||||||
if d.mongo != nil {
|
|
||||||
d.mongo.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
initialized := false
|
|
||||||
defer func() {
|
|
||||||
if !initialized {
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
usingFallback := !databaseConnectionConfigured(c.Database)
|
usingFallback := !databaseConnectionConfigured(c.Database)
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -227,6 +193,8 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger, storageManager *stor
|
||||||
d.auditLog = newDataScopeAuditWriter(d, appLogger)
|
d.auditLog = newDataScopeAuditWriter(d, appLogger)
|
||||||
d.dbList, err = openDatabaseList(c.DatabaseList, appLogger)
|
d.dbList, err = openDatabaseList(c.DatabaseList, appLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
d.auditLog.Close()
|
||||||
|
d.gormDB.close()
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
for _, item := range d.dbList {
|
for _, item := range d.dbList {
|
||||||
|
|
@ -264,7 +232,7 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger, storageManager *stor
|
||||||
if storageManager != nil {
|
if storageManager != nil {
|
||||||
storageManager.Replace(activeStorage)
|
storageManager.Replace(activeStorage)
|
||||||
}
|
}
|
||||||
if db.Migrator().HasTable("sys_integration_configs") {
|
if db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
if removeErr := d.removeIntegrationConfigFromFile(); removeErr != nil {
|
if removeErr := d.removeIntegrationConfigFromFile(); removeErr != nil {
|
||||||
appLogger.Warn("remove legacy integration configuration from file", "mod", "integration", "error", removeErr)
|
appLogger.Warn("remove legacy integration configuration from file", "mod", "integration", "error", removeErr)
|
||||||
}
|
}
|
||||||
|
|
@ -282,8 +250,15 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger, storageManager *stor
|
||||||
mongoClient = nil
|
mongoClient = nil
|
||||||
}
|
}
|
||||||
d.mongo = newReloadableMongo(mongoClient)
|
d.mongo = newReloadableMongo(mongoClient)
|
||||||
stopConfigWatcher = d.watchConfig()
|
stopConfigWatcher := d.watchConfig()
|
||||||
initialized = true
|
cleanup := func() {
|
||||||
|
stopConfigWatcher()
|
||||||
|
d.auditLog.Close()
|
||||||
|
d.gormDB.close()
|
||||||
|
closeDatabaseList(d.dbList)
|
||||||
|
d.redis.close()
|
||||||
|
d.mongo.close()
|
||||||
|
}
|
||||||
return d, cleanup, nil
|
return d, cleanup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
datasystem "kra/internal/data/system"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dataAccessLogPO is the infrastructure-side write model used by GORM
|
// dataAccessLogPO is the infrastructure-side write model used by GORM
|
||||||
// callbacks. The system module owns the query repository for the same table.
|
// callbacks. The system module owns the query repository for the same table.
|
||||||
type dataAccessLogPO = datasystem.DataAccessLogPO
|
type dataAccessLogPO struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
EventType, TargetTable, Operation string
|
||||||
|
UserID, AuthorityID uint
|
||||||
|
Scope int
|
||||||
|
RequestID, Method, Path, Detail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
|
||||||
|
|
|
||||||
|
|
@ -209,12 +209,7 @@ func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseCon
|
||||||
if err := d.persistDatabaseConfig(config, signingKey); err != nil {
|
if err := d.persistDatabaseConfig(config, signingKey); err != nil {
|
||||||
return fmt.Errorf("persist database configuration: %w", err)
|
return fmt.Errorf("persist database configuration: %w", err)
|
||||||
}
|
}
|
||||||
integrationConfigs, err := readIntegrationRuntime(candidate)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialize integration runtime: %w", err)
|
|
||||||
}
|
|
||||||
d.activateDatabase(candidate, config)
|
d.activateDatabase(candidate, config)
|
||||||
activated = true
|
|
||||||
currentData, currentAdmin := d.runtime.Values()
|
currentData, currentAdmin := d.runtime.Values()
|
||||||
if currentAdmin == nil {
|
if currentAdmin == nil {
|
||||||
currentAdmin = &conf.AdminBackend{}
|
currentAdmin = &conf.AdminBackend{}
|
||||||
|
|
@ -226,8 +221,9 @@ func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseCon
|
||||||
currentAdmin.Storage = storageConfig
|
currentAdmin.Storage = storageConfig
|
||||||
currentAdmin.Email = emailConfig
|
currentAdmin.Email = emailConfig
|
||||||
d.runtime.Replace(currentData, currentAdmin)
|
d.runtime.Replace(currentData, currentAdmin)
|
||||||
if d.integrations != nil {
|
if err = d.loadIntegrationRuntime(candidate); err != nil {
|
||||||
d.integrations.Replace(integrationConfigs)
|
return fmt.Errorf("initialize integration runtime: %w", err)
|
||||||
}
|
}
|
||||||
|
activated = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
integrationbiz "kra/internal/biz/integration"
|
|
||||||
"kra/pkg/database/migration"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Migrations() []migration.Step {
|
|
||||||
return []migration.Step{
|
|
||||||
{ID: "202608200001_data_infrastructure", Migrate: func(db *gorm.DB) error {
|
|
||||||
return migration.CreateMissingTables(db, &ConfigPO{})
|
|
||||||
}},
|
|
||||||
{ID: "202608210001_communication_integration_defaults", Migrate: ensureCommunicationIntegrationConfigs},
|
|
||||||
{ID: "202608200004_payment_defaults", Migrate: ensurePaymentIntegrationConfigs},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureCommunicationIntegrationConfigs(db *gorm.DB) error {
|
|
||||||
defaults := []struct{ kind, provider string }{
|
|
||||||
{integrationbiz.IntegrationKindMQ, "emqx"},
|
|
||||||
{integrationbiz.IntegrationKindMQ, "rabbitmq"},
|
|
||||||
{integrationbiz.IntegrationKindWebSocket, "melody"},
|
|
||||||
}
|
|
||||||
for _, item := range defaults {
|
|
||||||
var row ConfigPO
|
|
||||||
err := db.Where("kind = ? AND provider = ?", item.kind, item.provider).First(&row).Error
|
|
||||||
if gorm.ErrRecordNotFound == err {
|
|
||||||
values, marshalErr := json.Marshal(integrationbiz.DefaultIntegrationConfig(item.kind, item.provider))
|
|
||||||
if marshalErr != nil {
|
|
||||||
return marshalErr
|
|
||||||
}
|
|
||||||
if err = db.Create(&ConfigPO{Kind: item.kind, Provider: item.provider, Enabled: false, Config: string(values)}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
|
|
||||||
for _, definition := range integrationbiz.IntegrationDefinitions(integrationbiz.IntegrationKindPayment) {
|
|
||||||
provider := definition.Provider
|
|
||||||
var row ConfigPO
|
|
||||||
err := db.Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindPayment, provider).First(&row).Error
|
|
||||||
defaults := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindPayment, provider)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
encoded, marshalErr := json.Marshal(defaults)
|
|
||||||
if marshalErr != nil {
|
|
||||||
return marshalErr
|
|
||||||
}
|
|
||||||
if err := db.Create(&ConfigPO{Kind: integrationbiz.IntegrationKindPayment, Provider: provider, Config: string(encoded)}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
values := integrationObject(json.RawMessage(row.Config))
|
|
||||||
changed := false
|
|
||||||
for key, value := range defaults {
|
|
||||||
if _, exists := values[key]; !exists {
|
|
||||||
values[key] = value
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if changed {
|
|
||||||
encoded, marshalErr := json.Marshal(values)
|
|
||||||
if marshalErr != nil {
|
|
||||||
return marshalErr
|
|
||||||
}
|
|
||||||
if err := db.Model(&row).Update("config", string(encoded)).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"kra/internal/integration/runtimeconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Provider is the minimal database/runtime seam for integration configuration.
|
|
||||||
type Provider interface {
|
|
||||||
DB() *gorm.DB
|
|
||||||
IntegrationRuntime() *runtimeconfig.Store
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
import "github.com/google/wire"
|
|
||||||
|
|
||||||
var ProviderSet = wire.NewSet(NewIntegrationConfigRepo, NewPaymentConfigReader)
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"kra/internal/integration/runtimeconfig"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ReadRuntime returns the communication integration snapshot used by the
|
|
||||||
// long-lived MQ/WebSocket adapters.
|
|
||||||
func ReadRuntime(db *gorm.DB) ([]runtimeconfig.Config, error) {
|
|
||||||
if db == nil || !db.Migrator().HasTable(&ConfigPO{}) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var rows []ConfigPO
|
|
||||||
if err := db.Session(&gorm.Session{NewDB: true}).
|
|
||||||
Where("kind IN ?", []string{"mq", "websocket"}).
|
|
||||||
Order("kind ASC, provider ASC").
|
|
||||||
Find(&rows).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
configs := make([]runtimeconfig.Config, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
configs = append(configs, runtimeconfig.Config{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: []byte(row.Config)})
|
|
||||||
}
|
|
||||||
return configs, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"kra/internal/integration/runtimeconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Data struct {
|
|
||||||
gormDB *reloadableDB
|
|
||||||
store *runtimeconfig.Store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Data) DB() *gorm.DB {
|
|
||||||
if d == nil || d.gormDB == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return d.gormDB.DB()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Data) IntegrationRuntime() *runtimeconfig.Store { return d.store }
|
|
||||||
|
|
||||||
type reloadableDB struct{ db *gorm.DB }
|
|
||||||
|
|
||||||
func newReloadableDB(db *gorm.DB, _ ...any) *reloadableDB { return &reloadableDB{db: db} }
|
|
||||||
func (r *reloadableDB) DB() *gorm.DB {
|
|
||||||
if r == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return r.db
|
|
||||||
}
|
|
||||||
func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB { return r.db.WithContext(ctx) }
|
|
||||||
|
|
||||||
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
|
||||||
if driver != "sqlite" {
|
|
||||||
return nil, fmt.Errorf("unsupported test database driver %q", driver)
|
|
||||||
}
|
|
||||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
|
||||||
}
|
|
||||||
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
dataintegration "kra/internal/data/integration"
|
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
@ -18,8 +18,24 @@ import (
|
||||||
const (
|
const (
|
||||||
integrationKindStorage = "storage"
|
integrationKindStorage = "storage"
|
||||||
integrationKindEmail = "email"
|
integrationKindEmail = "email"
|
||||||
|
integrationKindPayment = "payment"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// integrationConfigPO stores credentials and provider-specific options for
|
||||||
|
// external services. Payment integrations use the same table with kind
|
||||||
|
// "payment", keeping secrets out of the bootstrap configuration file.
|
||||||
|
type integrationConfigPO struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Kind string `gorm:"size:32;not null;uniqueIndex:idx_integration_kind_provider"`
|
||||||
|
Provider string `gorm:"size:64;not null;uniqueIndex:idx_integration_kind_provider"`
|
||||||
|
Enabled bool `gorm:"not null;default:false;index"`
|
||||||
|
Config string `gorm:"type:text;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (integrationConfigPO) TableName() string { return "sys_integration_configs" }
|
||||||
|
|
||||||
var storageProviderNames = []string{
|
var storageProviderNames = []string{
|
||||||
"local",
|
"local",
|
||||||
"qiniu",
|
"qiniu",
|
||||||
|
|
@ -124,11 +140,11 @@ func saveStorageIntegrationConfig(db *gorm.DB, storage *conf.AdminBackend_Storag
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encode %s integration configuration: %w", provider, err)
|
return fmt.Errorf("encode %s integration configuration: %w", provider, err)
|
||||||
}
|
}
|
||||||
var current dataintegration.ConfigPO
|
var current integrationConfigPO
|
||||||
err = tx.Where("kind = ? AND provider = ?", integrationKindStorage, provider).First(¤t).Error
|
err = tx.Where("kind = ? AND provider = ?", integrationKindStorage, provider).First(¤t).Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
current = dataintegration.ConfigPO{Kind: integrationKindStorage, Provider: provider}
|
current = integrationConfigPO{Kind: integrationKindStorage, Provider: provider}
|
||||||
current.Enabled, current.Config = provider == active, value
|
current.Enabled, current.Config = provider == active, value
|
||||||
if err = tx.Create(¤t).Error; err != nil {
|
if err = tx.Create(¤t).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -146,7 +162,7 @@ func saveStorageIntegrationConfig(db *gorm.DB, storage *conf.AdminBackend_Storag
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadStorageIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_Storage, bool, error) {
|
func loadStorageIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_Storage, bool, error) {
|
||||||
var rows []dataintegration.ConfigPO
|
var rows []integrationConfigPO
|
||||||
err := db.Session(&gorm.Session{NewDB: true}).
|
err := db.Session(&gorm.Session{NewDB: true}).
|
||||||
Where("kind = ?", integrationKindStorage).
|
Where("kind = ?", integrationKindStorage).
|
||||||
Order("id ASC").
|
Order("id ASC").
|
||||||
|
|
@ -175,7 +191,7 @@ func loadStorageIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_Storage, bool
|
||||||
// sole source of truth.
|
// sole source of truth.
|
||||||
func resolveStorageIntegrationConfig(db *gorm.DB, legacy *conf.AdminBackend_Storage) (*conf.AdminBackend_Storage, error) {
|
func resolveStorageIntegrationConfig(db *gorm.DB, legacy *conf.AdminBackend_Storage) (*conf.AdminBackend_Storage, error) {
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
clean := db.Session(&gorm.Session{NewDB: true})
|
||||||
if !clean.Migrator().HasTable(&dataintegration.ConfigPO{}) {
|
if !clean.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
if legacy == nil {
|
if legacy == nil {
|
||||||
return &conf.AdminBackend_Storage{Type: "local"}, nil
|
return &conf.AdminBackend_Storage{Type: "local"}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +219,7 @@ func (d *Data) persistStorageIntegrationConfig(ctx context.Context, storage *con
|
||||||
return errors.New("database is not initialized")
|
return errors.New("database is not initialized")
|
||||||
}
|
}
|
||||||
db := d.gormDB.WithContext(ctx)
|
db := d.gormDB.WithContext(ctx)
|
||||||
if !db.Migrator().HasTable(&dataintegration.ConfigPO{}) {
|
if !db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
return errors.New("integration configuration table does not exist")
|
return errors.New("integration configuration table does not exist")
|
||||||
}
|
}
|
||||||
return saveStorageIntegrationConfig(db, storage)
|
return saveStorageIntegrationConfig(db, storage)
|
||||||
|
|
@ -223,11 +239,11 @@ func saveEmailIntegrationConfig(db *gorm.DB, email *conf.AdminBackend_Email) err
|
||||||
}
|
}
|
||||||
enabled := email.Host != "" && email.From != "" && email.Secret != "" && email.Port > 0
|
enabled := email.Host != "" && email.From != "" && email.Secret != "" && email.Port > 0
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
clean := db.Session(&gorm.Session{NewDB: true})
|
||||||
var current dataintegration.ConfigPO
|
var current integrationConfigPO
|
||||||
err = clean.Where("kind = ? AND provider = ?", integrationKindEmail, "smtp").First(¤t).Error
|
err = clean.Where("kind = ? AND provider = ?", integrationKindEmail, "smtp").First(¤t).Error
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||||
return clean.Create(&dataintegration.ConfigPO{
|
return clean.Create(&integrationConfigPO{
|
||||||
Kind: integrationKindEmail, Provider: "smtp", Enabled: enabled, Config: string(raw),
|
Kind: integrationKindEmail, Provider: "smtp", Enabled: enabled, Config: string(raw),
|
||||||
}).Error
|
}).Error
|
||||||
case err != nil:
|
case err != nil:
|
||||||
|
|
@ -238,7 +254,7 @@ func saveEmailIntegrationConfig(db *gorm.DB, email *conf.AdminBackend_Email) err
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadEmailIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_Email, bool, error) {
|
func loadEmailIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_Email, bool, error) {
|
||||||
var row dataintegration.ConfigPO
|
var row integrationConfigPO
|
||||||
err := db.Session(&gorm.Session{NewDB: true}).
|
err := db.Session(&gorm.Session{NewDB: true}).
|
||||||
Where("kind = ? AND provider = ?", integrationKindEmail, "smtp").
|
Where("kind = ? AND provider = ?", integrationKindEmail, "smtp").
|
||||||
First(&row).Error
|
First(&row).Error
|
||||||
|
|
@ -260,7 +276,7 @@ func loadEmailIntegrationConfig(db *gorm.DB) (*conf.AdminBackend_Email, bool, er
|
||||||
|
|
||||||
func resolveEmailIntegrationConfig(db *gorm.DB, legacy *conf.AdminBackend_Email) (*conf.AdminBackend_Email, error) {
|
func resolveEmailIntegrationConfig(db *gorm.DB, legacy *conf.AdminBackend_Email) (*conf.AdminBackend_Email, error) {
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
clean := db.Session(&gorm.Session{NewDB: true})
|
||||||
if !clean.Migrator().HasTable(&dataintegration.ConfigPO{}) {
|
if !clean.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
if legacy == nil {
|
if legacy == nil {
|
||||||
return defaultEmailIntegrationConfig(), nil
|
return defaultEmailIntegrationConfig(), nil
|
||||||
}
|
}
|
||||||
|
|
@ -288,7 +304,7 @@ func (d *Data) persistEmailIntegrationConfig(ctx context.Context, email *conf.Ad
|
||||||
return errors.New("database is not initialized")
|
return errors.New("database is not initialized")
|
||||||
}
|
}
|
||||||
db := d.gormDB.WithContext(ctx)
|
db := d.gormDB.WithContext(ctx)
|
||||||
if !db.Migrator().HasTable(&dataintegration.ConfigPO{}) {
|
if !db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
return errors.New("integration configuration table does not exist")
|
return errors.New("integration configuration table does not exist")
|
||||||
}
|
}
|
||||||
return saveEmailIntegrationConfig(db, email)
|
return saveEmailIntegrationConfig(db, email)
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
integrationbiz "kra/internal/biz/integration"
|
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
dataintegration "kra/internal/data/integration"
|
|
||||||
"kra/internal/integration/storage"
|
"kra/internal/integration/storage"
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
@ -28,7 +26,7 @@ func openIntegrationConfigTestDB(t *testing.T) *gorm.DB {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
if err = db.AutoMigrate(&dataintegration.ConfigPO{}); err != nil {
|
if err = db.AutoMigrate(&integrationConfigPO{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return db
|
return db
|
||||||
|
|
@ -44,10 +42,10 @@ func TestMigrateAllCreatesIntegrationConfigTable(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||||
if err = migrateAll(db, testCatalog()); err != nil {
|
if err = migrateAll(db); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !db.Migrator().HasTable(&dataintegration.ConfigPO{}) {
|
if !db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
t.Fatal("migrateAll did not create sys_integration_configs")
|
t.Fatal("migrateAll did not create sys_integration_configs")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +73,7 @@ func TestStorageIntegrationConfigRoundTrip(t *testing.T) {
|
||||||
if err := saveEmailIntegrationConfig(db, email); err != nil {
|
if err := saveEmailIntegrationConfig(db, email); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := db.Create(&dataintegration.ConfigPO{Kind: integrationbiz.IntegrationKindPayment, Provider: "wechat-pay", Config: `{"merchant_id":"123"}`}).Error; err != nil {
|
if err := db.Create(&integrationConfigPO{Kind: integrationKindPayment, Provider: "wechat-pay", Config: `{"merchant_id":"123"}`}).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,13 +103,13 @@ func TestStorageIntegrationConfigRoundTrip(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var storageCount, emailCount, paymentCount int64
|
var storageCount, emailCount, paymentCount int64
|
||||||
if err = db.Model(&dataintegration.ConfigPO{}).Where("kind = ?", integrationKindStorage).Count(&storageCount).Error; err != nil {
|
if err = db.Model(&integrationConfigPO{}).Where("kind = ?", integrationKindStorage).Count(&storageCount).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = db.Model(&dataintegration.ConfigPO{}).Where("kind = ?", integrationbiz.IntegrationKindPayment).Count(&paymentCount).Error; err != nil {
|
if err = db.Model(&integrationConfigPO{}).Where("kind = ?", integrationKindPayment).Count(&paymentCount).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = db.Model(&dataintegration.ConfigPO{}).Where("kind = ?", integrationKindEmail).Count(&emailCount).Error; err != nil {
|
if err = db.Model(&integrationConfigPO{}).Where("kind = ?", integrationKindEmail).Count(&emailCount).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if storageCount != int64(len(storageProviderNames)) {
|
if storageCount != int64(len(storageProviderNames)) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"kra/internal/biz/system"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureCommunicationIntegrationConfigs(db *gorm.DB) error {
|
||||||
|
defaults := []struct {
|
||||||
|
kind string
|
||||||
|
provider string
|
||||||
|
}{
|
||||||
|
{kind: system.IntegrationKindMQ, provider: "emqx"},
|
||||||
|
{kind: system.IntegrationKindMQ, provider: "rabbitmq"},
|
||||||
|
{kind: system.IntegrationKindWebSocket, provider: "melody"},
|
||||||
|
}
|
||||||
|
for _, item := range defaults {
|
||||||
|
var row integrationConfigPO
|
||||||
|
err := db.Where("kind = ? AND provider = ?", item.kind, item.provider).First(&row).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
values, marshalErr := json.Marshal(system.DefaultIntegrationConfig(item.kind, item.provider))
|
||||||
|
if marshalErr != nil {
|
||||||
|
return marshalErr
|
||||||
|
}
|
||||||
|
if err = db.Create(&integrationConfigPO{Kind: item.kind, Provider: item.provider, Enabled: false, Config: string(values)}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gorm.io/gorm"
|
"errors"
|
||||||
dataintegration "kra/internal/data/integration"
|
|
||||||
"kra/internal/integration/runtimeconfig"
|
"kra/internal/integration/runtimeconfig"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readIntegrationRuntime(db *gorm.DB) ([]runtimeconfig.Config, error) {
|
func readIntegrationRuntime(db *gorm.DB) ([]runtimeconfig.Config, error) {
|
||||||
return dataintegration.ReadRuntime(db)
|
if db == nil || !db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var rows []integrationConfigPO
|
||||||
|
if err := db.Session(&gorm.Session{NewDB: true}).
|
||||||
|
Where("kind IN ?", []string{"mq", "websocket"}).
|
||||||
|
Order("kind ASC, provider ASC").
|
||||||
|
Find(&rows).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
configs := make([]runtimeconfig.Config, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
configs = append(configs, runtimeconfig.Config{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: []byte(row.Config)})
|
||||||
|
}
|
||||||
|
return configs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Data) loadIntegrationRuntime(db *gorm.DB) error {
|
func (d *Data) loadIntegrationRuntime(db *gorm.DB) error {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,35 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
datapayment "kra/internal/data/payment"
|
||||||
|
datasystem "kra/internal/data/repository"
|
||||||
"kra/pkg/database/migration"
|
"kra/pkg/database/migration"
|
||||||
"kra/pkg/module"
|
"kra/pkg/module"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// migrateAll is the single data-layer migration entry point. Every module
|
func InfrastructureMigrations() []migration.Step {
|
||||||
// must register its migrations through the application catalog; there is no
|
return []migration.Step{
|
||||||
// hidden system/payment fallback that could silently omit a new module.
|
{
|
||||||
func migrateAll(db *gorm.DB, catalog module.Catalog) error {
|
ID: "202608200001_data_infrastructure",
|
||||||
return migration.Run(db, catalog.MigrationSteps())
|
Migrate: func(db *gorm.DB) error {
|
||||||
|
return migration.CreateMissingTables(db, &integrationConfigPO{})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ID: "202608210001_communication_integration_defaults", Migrate: ensureCommunicationIntegrationConfigs},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateAll is the single data-layer migration entry point. Module-specific
|
||||||
|
// schema work stays with the module that owns its persistent objects.
|
||||||
|
func migrateAll(db *gorm.DB, catalogs ...module.Catalog) error {
|
||||||
|
steps := InfrastructureMigrations()
|
||||||
|
if len(catalogs) > 0 && len(catalogs[0].MigrationSteps()) > 0 {
|
||||||
|
steps = append(steps, catalogs[0].MigrationSteps()...)
|
||||||
|
} else {
|
||||||
|
steps = append(steps, datasystem.Migrations()...)
|
||||||
|
steps = append(steps, datapayment.Migrations()...)
|
||||||
|
}
|
||||||
|
return migration.Run(db, steps)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,18 @@ package data
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
bizpayment "kra/internal/biz/payment"
|
|
||||||
dataintegration "kra/internal/data/integration"
|
|
||||||
"kra/internal/modules"
|
|
||||||
"kra/pkg/database/migration"
|
"kra/pkg/database/migration"
|
||||||
platformmodule "kra/pkg/module"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func testCatalog() platformmodule.Catalog {
|
|
||||||
return modules.Catalog()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||||
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = migrateAll(db, testCatalog()); err != nil {
|
if err = migrateAll(db); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = migrateAll(db, testCatalog()); err != nil {
|
if err = migrateAll(db); err != nil {
|
||||||
t.Fatalf("second migration run: %v", err)
|
t.Fatalf("second migration run: %v", err)
|
||||||
}
|
}
|
||||||
for _, table := range []string{"sys_integration_configs", "sys_users", "sys_base_menus", "pay_orders"} {
|
for _, table := range []string{"sys_integration_configs", "sys_users", "sys_base_menus", "pay_orders"} {
|
||||||
|
|
@ -34,23 +26,16 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||||
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
|
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if versions != 8 {
|
if versions != 7 {
|
||||||
t.Fatalf("migration versions = %d, want 8", versions)
|
t.Fatalf("migration versions = %d, want 7", versions)
|
||||||
}
|
}
|
||||||
var communicationRows []dataintegration.ConfigPO
|
var communicationRows []integrationConfigPO
|
||||||
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
|
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(communicationRows) != 3 {
|
if len(communicationRows) != 3 {
|
||||||
t.Fatalf("communication integration rows = %d, want 3", len(communicationRows))
|
t.Fatalf("communication integration rows = %d, want 3", len(communicationRows))
|
||||||
}
|
}
|
||||||
var paymentRows int64
|
|
||||||
if err = db.Model(&dataintegration.ConfigPO{}).Where("kind = ?", "payment").Count(&paymentRows).Error; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if paymentRows != int64(len(bizpayment.SupportedPaymentProviders)) {
|
|
||||||
t.Fatalf("payment integration rows = %d, want %d", paymentRows, len(bizpayment.SupportedPaymentProviders))
|
|
||||||
}
|
|
||||||
for _, row := range communicationRows {
|
for _, row := range communicationRows {
|
||||||
if row.Enabled || row.Config == "" {
|
if row.Enabled || row.Config == "" {
|
||||||
t.Fatalf("default communication integration = %#v", row)
|
t.Fatalf("default communication integration = %#v", row)
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@ func Migrations() []migration.Step {
|
||||||
{ID: "202608200003_payment_schema", Migrate: func(db *gorm.DB) error {
|
{ID: "202608200003_payment_schema", Migrate: func(db *gorm.DB) error {
|
||||||
return migration.CreateMissingTables(db, &paymentOrderPO{})
|
return migration.CreateMissingTables(db, &paymentOrderPO{})
|
||||||
}},
|
}},
|
||||||
|
{ID: "202608200004_payment_defaults", Migrate: ensurePaymentIntegrationConfigs},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package payment
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const integrationKindPayment = "payment"
|
||||||
|
|
||||||
|
type integrationConfigPO struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
Kind string `gorm:"size:32;not null;uniqueIndex:idx_integration_kind_provider"`
|
||||||
|
Provider string `gorm:"size:64;not null;uniqueIndex:idx_integration_kind_provider"`
|
||||||
|
Enabled bool `gorm:"not null;default:false;index"`
|
||||||
|
Config string `gorm:"type:text;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (integrationConfigPO) TableName() string { return "sys_integration_configs" }
|
||||||
|
|
@ -7,8 +7,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
integrationbiz "kra/internal/biz/integration"
|
"kra/internal/biz/system"
|
||||||
bizpayment "kra/internal/biz/payment"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -17,40 +16,67 @@ import (
|
||||||
datapayment "kra/internal/integration/payment"
|
datapayment "kra/internal/integration/payment"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type paymentRepo struct {
|
type paymentRepo struct{ data Provider }
|
||||||
data Provider
|
|
||||||
config integrationbiz.PaymentConfigReader
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPaymentRepo(data Provider, config integrationbiz.PaymentConfigReader) bizpayment.PaymentRepo {
|
func NewPaymentRepo(data Provider) system.PaymentRepo { return &paymentRepo{data: data} }
|
||||||
return &paymentRepo{data: data, config: config}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *paymentRepo) values(ctx context.Context, provider string) (map[string]any, error) {
|
func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
|
||||||
if r == nil || r.config == nil {
|
for _, provider := range system.SupportedPaymentProviders {
|
||||||
return nil, errors.New("支付配置仓储未接入")
|
var row integrationConfigPO
|
||||||
|
err := db.Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error
|
||||||
|
defaults := system.DefaultIntegrationConfig(integrationKindPayment, provider)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
encoded, _ := json.Marshal(defaults)
|
||||||
|
if err := db.Create(&integrationConfigPO{Kind: integrationKindPayment, Provider: provider, Enabled: false, Config: string(encoded)}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
config, err := r.config.ReadPaymentConfig(ctx, provider)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, integrationbiz.ErrPaymentConfigNotFound) {
|
return err
|
||||||
return nil, bizpayment.ErrPaymentProviderNotFound
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if config == nil || !config.Enabled {
|
|
||||||
return nil, fmt.Errorf("支付渠道 %s 未启用", provider)
|
|
||||||
}
|
}
|
||||||
values := map[string]any{}
|
values := map[string]any{}
|
||||||
if err := json.Unmarshal(config.Values, &values); err != nil {
|
_ = json.Unmarshal([]byte(row.Config), &values)
|
||||||
return nil, fmt.Errorf("支付配置格式错误: %w", err)
|
changed := false
|
||||||
|
for key, value := range defaults {
|
||||||
|
if _, exists := values[key]; !exists {
|
||||||
|
values[key] = value
|
||||||
|
changed = true
|
||||||
}
|
}
|
||||||
return values, nil
|
}
|
||||||
|
if changed {
|
||||||
|
encoded, _ := json.Marshal(values)
|
||||||
|
if err := db.Model(&row).Update("config", string(encoded)).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *paymentRepo) row(ctx context.Context, provider string) (*integrationConfigPO, map[string]any, error) {
|
||||||
|
var row integrationConfigPO
|
||||||
|
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil, system.ErrPaymentProviderNotFound
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if !row.Enabled {
|
||||||
|
return nil, nil, fmt.Errorf("支付渠道 %s 未启用", provider)
|
||||||
|
}
|
||||||
|
values := map[string]any{}
|
||||||
|
if err := json.Unmarshal([]byte(row.Config), &values); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("支付配置格式错误: %w", err)
|
||||||
|
}
|
||||||
|
return &row, values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentRepo) adapter(ctx context.Context, provider string) (datapayment.Adapter, map[string]any, error) {
|
func (r *paymentRepo) adapter(ctx context.Context, provider string) (datapayment.Adapter, map[string]any, error) {
|
||||||
values, err := r.values(ctx, provider)
|
_, values, err := r.row(ctx, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -58,11 +84,11 @@ func (r *paymentRepo) adapter(ctx context.Context, provider string) (datapayment
|
||||||
return adapter, values, err
|
return adapter, values, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpayment.PaymentTestResult, error) {
|
func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*system.PaymentTestResult, error) {
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
test := &bizpayment.PaymentTestResult{Provider: provider, TradeNo: "", Passed: false, Stages: []bizpayment.PaymentTestStage{}}
|
test := &system.PaymentTestResult{Provider: provider, TradeNo: "", Passed: false, Stages: []system.PaymentTestStage{}}
|
||||||
add := func(name, status, message, tradeNo string, since time.Time) {
|
add := func(name, status, message, tradeNo string, since time.Time) {
|
||||||
test.Stages = append(test.Stages, bizpayment.PaymentTestStage{Name: name, Status: status, Message: message, TradeNo: tradeNo, Duration: time.Since(since).Milliseconds()})
|
test.Stages = append(test.Stages, system.PaymentTestStage{Name: name, Status: status, Message: message, TradeNo: tradeNo, Duration: time.Since(since).Milliseconds()})
|
||||||
}
|
}
|
||||||
values, err := r.testRow(ctx, provider)
|
values, err := r.testRow(ctx, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -71,7 +97,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
}
|
}
|
||||||
test.Mode = strings.ToLower(strings.TrimSpace(text(values, "environment")))
|
test.Mode = strings.ToLower(strings.TrimSpace(text(values, "environment")))
|
||||||
configStart := time.Now()
|
configStart := time.Now()
|
||||||
if err = integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, provider, values); err != nil {
|
if err = system.ValidateIntegrationConfig(system.IntegrationKindPayment, provider, values); err != nil {
|
||||||
add("config", "failed", err.Error(), "", configStart)
|
add("config", "failed", err.Error(), "", configStart)
|
||||||
return test, err
|
return test, err
|
||||||
}
|
}
|
||||||
|
|
@ -91,11 +117,11 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
orders := &paymentOrderRepo{data: r.data}
|
orders := &paymentOrderRepo{data: r.data}
|
||||||
extra, _ := json.Marshal(req.Extra)
|
extra, _ := json.Marshal(req.Extra)
|
||||||
localStart := time.Now()
|
localStart := time.Now()
|
||||||
order, _, err := orders.CreatePaymentOrder(ctx, &bizpayment.PaymentOrder{
|
order, _, err := orders.CreatePaymentOrder(ctx, &system.PaymentOrder{
|
||||||
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
|
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
|
||||||
Subject: req.Subject, PaymentMode: bizpayment.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
|
Subject: req.Subject, PaymentMode: system.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
|
||||||
Currency: req.Currency, PaymentStatus: bizpayment.PaymentStatusInitialized, FulfillmentStatus: bizpayment.FulfillmentStatusPending,
|
Currency: req.Currency, PaymentStatus: system.PaymentStatusInitialized, FulfillmentStatus: system.FulfillmentStatusPending,
|
||||||
RefundStatus: bizpayment.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
|
RefundStatus: system.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
|
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
|
||||||
|
|
@ -133,7 +159,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
if queryID == "" {
|
if queryID == "" {
|
||||||
queryID = req.TradeNo
|
queryID = req.TradeNo
|
||||||
}
|
}
|
||||||
if provider == bizpayment.PaymentApple {
|
if provider == system.PaymentApple {
|
||||||
queryID = strings.TrimSpace(text(values, "test_transaction_id"))
|
queryID = strings.TrimSpace(text(values, "test_transaction_id"))
|
||||||
if queryID == "" {
|
if queryID == "" {
|
||||||
err = errors.New("Apple 连通性测试需要配置 test_transaction_id(沙箱交易 ID)")
|
err = errors.New("Apple 连通性测试需要配置 test_transaction_id(沙箱交易 ID)")
|
||||||
|
|
@ -153,7 +179,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
add("query", "failed", err.Error(), req.TradeNo, queryStart)
|
add("query", "failed", err.Error(), req.TradeNo, queryStart)
|
||||||
return test, err
|
return test, err
|
||||||
}
|
}
|
||||||
if provider != bizpayment.PaymentApple {
|
if provider != system.PaymentApple {
|
||||||
if order, err = orders.ApplyPaymentResult(ctx, provider, req.TradeNo, paymentTestProviderUpdate(queried)); err != nil {
|
if order, err = orders.ApplyPaymentResult(ctx, provider, req.TradeNo, paymentTestProviderUpdate(queried)); err != nil {
|
||||||
add("local_order", "failed", "回写测试查单结果失败: "+err.Error(), req.TradeNo, queryStart)
|
add("local_order", "failed", "回写测试查单结果失败: "+err.Error(), req.TradeNo, queryStart)
|
||||||
return test, err
|
return test, err
|
||||||
|
|
@ -167,9 +193,9 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
test.Result = queried
|
test.Result = queried
|
||||||
add("query", "passed", "测试订单查询成功,状态: "+queried.Status, req.TradeNo, queryStart)
|
add("query", "passed", "测试订单查询成功,状态: "+queried.Status, req.TradeNo, queryStart)
|
||||||
|
|
||||||
if queried.Status != "success" || provider == bizpayment.PaymentApple {
|
if queried.Status != "success" || provider == system.PaymentApple {
|
||||||
message := "订单尚未支付成功,已完成配置、下单和查单连通性测试;请在沙箱完成付款后重试"
|
message := "订单尚未支付成功,已完成配置、下单和查单连通性测试;请在沙箱完成付款后重试"
|
||||||
if provider == bizpayment.PaymentApple {
|
if provider == system.PaymentApple {
|
||||||
message = "Apple 退款由 App Store 管理,已完成配置、下单和交易查询测试"
|
message = "Apple 退款由 App Store 管理,已完成配置、下单和交易查询测试"
|
||||||
}
|
}
|
||||||
add("refund", "skipped", message, req.TradeNo, time.Now())
|
add("refund", "skipped", message, req.TradeNo, time.Now())
|
||||||
|
|
@ -182,7 +208,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
add("refund", "failed", beginErr.Error(), req.TradeNo, refundStart)
|
add("refund", "failed", beginErr.Error(), req.TradeNo, refundStart)
|
||||||
return test, beginErr
|
return test, beginErr
|
||||||
}
|
}
|
||||||
refund, refundErr := adapter.Refund(ctx, &bizpayment.PaymentRefundRequest{Provider: provider, TradeNo: req.TradeNo, ProviderTradeNo: order.ProviderTradeNo, QueryID: order.QueryID, RefundNo: order.RefundNo, Amount: req.Amount, TotalAmount: req.Amount, Currency: req.Currency}, values)
|
refund, refundErr := adapter.Refund(ctx, &system.PaymentRefundRequest{Provider: provider, TradeNo: req.TradeNo, ProviderTradeNo: order.ProviderTradeNo, QueryID: order.QueryID, RefundNo: order.RefundNo, Amount: req.Amount, TotalAmount: req.Amount, Currency: req.Currency}, values)
|
||||||
if refundErr != nil {
|
if refundErr != nil {
|
||||||
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, refundErr)
|
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, refundErr)
|
||||||
add("refund", "failed", refundErr.Error(), req.TradeNo, refundStart)
|
add("refund", "failed", refundErr.Error(), req.TradeNo, refundStart)
|
||||||
|
|
@ -204,17 +230,17 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*bizpa
|
||||||
return test, nil
|
return test, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentTestFingerprint(req *bizpayment.PaymentRequest) string {
|
func paymentTestFingerprint(req *system.PaymentRequest) string {
|
||||||
raw, _ := json.Marshal(req)
|
raw, _ := json.Marshal(req)
|
||||||
hash := sha256.Sum256(raw)
|
hash := sha256.Sum256(raw)
|
||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentTestProviderUpdate(result *bizpayment.PaymentResult) *bizpayment.PaymentProviderUpdate {
|
func paymentTestProviderUpdate(result *system.PaymentResult) *system.PaymentProviderUpdate {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &bizpayment.PaymentProviderUpdate{
|
return &system.PaymentProviderUpdate{
|
||||||
Status: result.Status, ProviderStatus: result.Status, ProviderTradeNo: result.ProviderTradeNo, QueryID: result.QueryID,
|
Status: result.Status, ProviderStatus: result.Status, ProviderTradeNo: result.ProviderTradeNo, QueryID: result.QueryID,
|
||||||
Amount: result.Amount, PayerPaidAmount: result.PayerPaidAmount, CashPaidAmount: result.CashPaidAmount,
|
Amount: result.Amount, PayerPaidAmount: result.PayerPaidAmount, CashPaidAmount: result.CashPaidAmount,
|
||||||
PointPaidAmount: result.PointPaidAmount, DiscountAmount: result.DiscountAmount,
|
PointPaidAmount: result.PointPaidAmount, DiscountAmount: result.DiscountAmount,
|
||||||
|
|
@ -224,21 +250,21 @@ func paymentTestProviderUpdate(result *bizpayment.PaymentResult) *bizpayment.Pay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePaymentTestResult(provider, tradeNo string, result *bizpayment.PaymentResult) error {
|
func validatePaymentTestResult(provider, tradeNo string, result *system.PaymentResult) error {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
return errors.New("支付渠道响应为空")
|
return errors.New("支付渠道响应为空")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(result.Provider) != provider {
|
if strings.TrimSpace(result.Provider) != provider {
|
||||||
return errors.New("支付渠道响应的 provider 不匹配")
|
return errors.New("支付渠道响应的 provider 不匹配")
|
||||||
}
|
}
|
||||||
if value := strings.TrimSpace(result.TradeNo); provider != bizpayment.PaymentApple && value != "" && value != tradeNo {
|
if value := strings.TrimSpace(result.TradeNo); provider != system.PaymentApple && value != "" && value != tradeNo {
|
||||||
return errors.New("支付渠道响应的商户订单号不匹配")
|
return errors.New("支付渠道响应的商户订单号不匹配")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryPaymentTest(ctx context.Context, adapter datapayment.Adapter, queryID string, values map[string]any) (*bizpayment.PaymentResult, error) {
|
func queryPaymentTest(ctx context.Context, adapter datapayment.Adapter, queryID string, values map[string]any) (*system.PaymentResult, error) {
|
||||||
var result *bizpayment.PaymentResult
|
var result *system.PaymentResult
|
||||||
var err error
|
var err error
|
||||||
for attempt := 0; attempt < 3; attempt++ {
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
result, err = adapter.Query(ctx, queryID, values)
|
result, err = adapter.Query(ctx, queryID, values)
|
||||||
|
|
@ -271,37 +297,31 @@ func recordPaymentTestError(ctx context.Context, data Provider, provider, tradeN
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]any, error) {
|
func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]any, error) {
|
||||||
if r == nil || r.config == nil {
|
var row integrationConfigPO
|
||||||
return nil, errors.New("支付配置仓储未接入")
|
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
|
||||||
}
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
config, err := r.config.ReadPaymentConfig(ctx, provider)
|
return nil, system.ErrPaymentProviderNotFound
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, integrationbiz.ErrPaymentConfigNotFound) {
|
|
||||||
return nil, bizpayment.ErrPaymentProviderNotFound
|
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if config == nil {
|
|
||||||
return nil, errors.New("支付配置为空")
|
|
||||||
}
|
|
||||||
values := map[string]any{}
|
values := map[string]any{}
|
||||||
if err := json.Unmarshal(config.Values, &values); err != nil || values == nil {
|
if err := json.Unmarshal([]byte(row.Config), &values); err != nil {
|
||||||
return nil, errors.New("支付配置格式错误")
|
return nil, fmt.Errorf("支付配置格式错误: %w", err)
|
||||||
}
|
}
|
||||||
return values, nil
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentTestRequest(provider string, values map[string]any) *bizpayment.PaymentRequest {
|
func paymentTestRequest(provider string, values map[string]any) *system.PaymentRequest {
|
||||||
tradeNo := "kra-test-" + time.Now().UTC().Format("20060102150405.000000000")
|
tradeNo := "kra-test-" + time.Now().UTC().Format("20060102150405.000000000")
|
||||||
amount := configuredInt64(values, "test_amount", 1)
|
amount := configuredInt64(values, "test_amount", 1)
|
||||||
if amount <= 0 {
|
if amount <= 0 {
|
||||||
amount = 1
|
amount = 1
|
||||||
}
|
}
|
||||||
req := &bizpayment.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
|
req := &system.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
|
||||||
if req.Currency == "" {
|
if req.Currency == "" {
|
||||||
req.Currency = "CNY"
|
req.Currency = "CNY"
|
||||||
}
|
}
|
||||||
if provider == bizpayment.PaymentApple {
|
if provider == system.PaymentApple {
|
||||||
req.TradeNo = uuid.NewString()
|
req.TradeNo = uuid.NewString()
|
||||||
req.Extra["product_id"] = firstAny(values, "product_id", "test_product_id")
|
req.Extra["product_id"] = firstAny(values, "product_id", "test_product_id")
|
||||||
}
|
}
|
||||||
|
|
@ -331,10 +351,10 @@ func validatePaymentTestSettings(provider string, values map[string]any) error {
|
||||||
return fmt.Errorf("test_extra 必须是 JSON 对象: %w", err)
|
return fmt.Errorf("test_extra 必须是 JSON 对象: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if provider == bizpayment.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
|
if provider == system.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
|
||||||
return errors.New("Apple 测试需要 test_transaction_id(沙箱交易 ID)")
|
return errors.New("Apple 测试需要 test_transaction_id(沙箱交易 ID)")
|
||||||
}
|
}
|
||||||
if provider == bizpayment.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
|
if provider == system.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
|
||||||
return errors.New("Apple 测试需要 test_product_id(沙箱商品 ID)")
|
return errors.New("Apple 测试需要 test_product_id(沙箱商品 ID)")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -357,7 +377,7 @@ func testModeEnabled(values map[string]any) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentRepo) Create(ctx context.Context, req *bizpayment.PaymentRequest) (*bizpayment.PaymentResult, error) {
|
func (r *paymentRepo) Create(ctx context.Context, req *system.PaymentRequest) (*system.PaymentResult, error) {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return nil, errors.New("支付下单请求为空")
|
return nil, errors.New("支付下单请求为空")
|
||||||
}
|
}
|
||||||
|
|
@ -376,7 +396,7 @@ func (r *paymentRepo) Create(ctx context.Context, req *bizpayment.PaymentRequest
|
||||||
|
|
||||||
func paymentProviderRequiresNotifyURL(provider string) bool {
|
func paymentProviderRequiresNotifyURL(provider string) bool {
|
||||||
switch provider {
|
switch provider {
|
||||||
case bizpayment.PaymentApple, bizpayment.PaymentAllinPay, bizpayment.PaymentSaobei, bizpayment.PaymentPayPal:
|
case system.PaymentApple, system.PaymentAllinPay, system.PaymentSaobei, system.PaymentPayPal:
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
|
|
@ -393,15 +413,15 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
|
||||||
}
|
}
|
||||||
keys := []string{"method", "pay_method", "trade_type", "pay_type", "channel"}
|
keys := []string{"method", "pay_method", "trade_type", "pay_type", "channel"}
|
||||||
switch provider {
|
switch provider {
|
||||||
case bizpayment.PaymentAlipay, bizpayment.PaymentAlipayV3:
|
case system.PaymentAlipay, system.PaymentAlipayV3:
|
||||||
keys = []string{"method", "pay_method", "trade_type", "channel"}
|
keys = []string{"method", "pay_method", "trade_type", "channel"}
|
||||||
case bizpayment.PaymentWechatV2:
|
case system.PaymentWechatV2:
|
||||||
keys = []string{"trade_type", "pay_type", "method", "pay_method", "channel"}
|
keys = []string{"trade_type", "pay_type", "method", "pay_method", "channel"}
|
||||||
case bizpayment.PaymentWechatV3:
|
case system.PaymentWechatV3:
|
||||||
keys = []string{"trade_type", "pay_type", "method"}
|
keys = []string{"trade_type", "pay_type", "method"}
|
||||||
case bizpayment.PaymentQQ:
|
case system.PaymentQQ:
|
||||||
keys = []string{"trade_type", "pay_type", "method", "pay_method"}
|
keys = []string{"trade_type", "pay_type", "method", "pay_method"}
|
||||||
case bizpayment.PaymentLakala:
|
case system.PaymentLakala:
|
||||||
keys = []string{"method", "pay_method", "trade_type"}
|
keys = []string{"method", "pay_method", "trade_type"}
|
||||||
}
|
}
|
||||||
value := firstAny(extra, keys...)
|
value := firstAny(extra, keys...)
|
||||||
|
|
@ -410,28 +430,28 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
|
||||||
}
|
}
|
||||||
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
|
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
|
||||||
switch provider {
|
switch provider {
|
||||||
case bizpayment.PaymentAlipay, bizpayment.PaymentAlipayV3:
|
case system.PaymentAlipay, system.PaymentAlipayV3:
|
||||||
return !contains([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized)
|
return !contains([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized)
|
||||||
case bizpayment.PaymentWechatV2:
|
case system.PaymentWechatV2:
|
||||||
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay", "pay_code", "payment_code"}, normalized)
|
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay", "pay_code", "payment_code"}, normalized)
|
||||||
case bizpayment.PaymentWechatV3:
|
case system.PaymentWechatV3:
|
||||||
return !contains([]string{"micropay", "micro_pay", "codepay", "code_pay", "barcode", "barcode_pay", "facepay", "face_pay"}, normalized)
|
return !contains([]string{"micropay", "micro_pay", "codepay", "code_pay", "barcode", "barcode_pay", "facepay", "face_pay"}, normalized)
|
||||||
case bizpayment.PaymentQQ:
|
case system.PaymentQQ:
|
||||||
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay"}, normalized)
|
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay"}, normalized)
|
||||||
case bizpayment.PaymentLakala:
|
case system.PaymentLakala:
|
||||||
return !contains([]string{"retail", "retail_pay", "micropay", "barcode"}, normalized)
|
return !contains([]string{"retail", "retail_pay", "micropay", "barcode"}, normalized)
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (r *paymentRepo) Query(ctx context.Context, provider, tradeNo string) (*bizpayment.PaymentResult, error) {
|
func (r *paymentRepo) Query(ctx context.Context, provider, tradeNo string) (*system.PaymentResult, error) {
|
||||||
a, c, err := r.adapter(ctx, provider)
|
a, c, err := r.adapter(ctx, provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return a.Query(ctx, tradeNo, c)
|
return a.Query(ctx, tradeNo, c)
|
||||||
}
|
}
|
||||||
func (r *paymentRepo) Refund(ctx context.Context, req *bizpayment.PaymentRefundRequest) (*bizpayment.PaymentResult, error) {
|
func (r *paymentRepo) Refund(ctx context.Context, req *system.PaymentRefundRequest) (*system.PaymentResult, error) {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return nil, errors.New("支付退款请求为空")
|
return nil, errors.New("支付退款请求为空")
|
||||||
}
|
}
|
||||||
|
|
@ -441,7 +461,7 @@ func (r *paymentRepo) Refund(ctx context.Context, req *bizpayment.PaymentRefundR
|
||||||
}
|
}
|
||||||
return a.Refund(ctx, req, c)
|
return a.Refund(ctx, req, c)
|
||||||
}
|
}
|
||||||
func (r *paymentRepo) HandleCallback(ctx context.Context, callback *bizpayment.PaymentCallback) (*bizpayment.PaymentResult, error) {
|
func (r *paymentRepo) HandleCallback(ctx context.Context, callback *system.PaymentCallback) (*system.PaymentResult, error) {
|
||||||
if callback == nil {
|
if callback == nil {
|
||||||
return nil, errors.New("支付回调为空")
|
return nil, errors.New("支付回调为空")
|
||||||
}
|
}
|
||||||
|
|
@ -451,23 +471,23 @@ func (r *paymentRepo) HandleCallback(ctx context.Context, callback *bizpayment.P
|
||||||
}
|
}
|
||||||
result, err := a.Callback(ctx, callback, c)
|
result, err := a.Callback(ctx, callback, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &bizpayment.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
|
return nil, &system.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
|
||||||
}
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
err = errors.New("支付回调解析结果为空")
|
err = errors.New("支付回调解析结果为空")
|
||||||
return nil, &bizpayment.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
|
return nil, &system.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
|
||||||
}
|
}
|
||||||
result.SuccessAck = paymentCallbackAck(callback.Provider, c, true)
|
result.SuccessAck = paymentCallbackAck(callback.Provider, c, true)
|
||||||
result.FailureAck = paymentCallbackAck(callback.Provider, c, false)
|
result.FailureAck = paymentCallbackAck(callback.Provider, c, false)
|
||||||
if result.Provider != callback.Provider {
|
if result.Provider != callback.Provider {
|
||||||
err = errors.New("支付回调渠道不匹配")
|
err = errors.New("支付回调渠道不匹配")
|
||||||
return nil, &bizpayment.PaymentCallbackError{Cause: err, Ack: result.FailureAck}
|
return nil, &system.PaymentCallbackError{Cause: err, Ack: result.FailureAck}
|
||||||
}
|
}
|
||||||
result.EventID = paymentCallbackEventID(callback, result)
|
result.EventID = paymentCallbackEventID(callback, result)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentCallbackEventID(callback *bizpayment.PaymentCallback, result *bizpayment.PaymentResult) string {
|
func paymentCallbackEventID(callback *system.PaymentCallback, result *system.PaymentResult) string {
|
||||||
if result != nil {
|
if result != nil {
|
||||||
if eventID := strings.TrimSpace(result.EventID); eventID != "" {
|
if eventID := strings.TrimSpace(result.EventID); eventID != "" {
|
||||||
return eventID
|
return eventID
|
||||||
|
|
@ -484,8 +504,8 @@ func paymentCallbackEventID(callback *bizpayment.PaymentCallback, result *bizpay
|
||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func paymentCallbackAck(provider string, values map[string]any, success bool) bizpayment.PaymentCallbackAck {
|
func paymentCallbackAck(provider string, values map[string]any, success bool) system.PaymentCallbackAck {
|
||||||
ack := bizpayment.DefaultPaymentCallbackAck(provider, success)
|
ack := system.DefaultPaymentCallbackAck(provider, success)
|
||||||
prefix := "callback_success_"
|
prefix := "callback_success_"
|
||||||
if !success {
|
if !success {
|
||||||
prefix = "callback_failure_"
|
prefix = "callback_failure_"
|
||||||
|
|
@ -504,7 +524,7 @@ func paymentCallbackAck(provider string, values map[string]any, success bool) bi
|
||||||
return ack
|
return ack
|
||||||
}
|
}
|
||||||
|
|
||||||
func callbackFields(callback *bizpayment.PaymentCallback) map[string]string {
|
func callbackFields(callback *system.PaymentCallback) map[string]string {
|
||||||
fields := map[string]string{}
|
fields := map[string]string{}
|
||||||
for key, value := range callback.Query {
|
for key, value := range callback.Query {
|
||||||
fields[key] = value
|
fields[key] = value
|
||||||
|
|
@ -549,5 +569,5 @@ func contains(values []string, value string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePaymentConfig(provider string, values map[string]any) error {
|
func validatePaymentConfig(provider string, values map[string]any) error {
|
||||||
return integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, provider, values)
|
return system.ValidateIntegrationConfig(system.IntegrationKindPayment, provider, values)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package payment
|
package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
integrationbiz "kra/internal/biz/integration"
|
"kra/internal/biz/system"
|
||||||
bizpayment "kra/internal/biz/payment"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
@ -12,18 +11,18 @@ func TestValidatePaymentConfigRequiresDouyinAppIDWhenEnabled(t *testing.T) {
|
||||||
"merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
|
"merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
|
||||||
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_serial_no": "platform-serial",
|
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_serial_no": "platform-serial",
|
||||||
}
|
}
|
||||||
err := integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, bizpayment.PaymentDouyin, values)
|
err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentDouyin, values)
|
||||||
if err == nil || !strings.Contains(err.Error(), "app_id") {
|
if err == nil || !strings.Contains(err.Error(), "app_id") {
|
||||||
t.Fatalf("missing app_id error = %v", err)
|
t.Fatalf("missing app_id error = %v", err)
|
||||||
}
|
}
|
||||||
values["app_id"] = "douyin-app"
|
values["app_id"] = "douyin-app"
|
||||||
if err = integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, bizpayment.PaymentDouyin, values); err != nil {
|
if err = system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentDouyin, values); err != nil {
|
||||||
t.Fatalf("valid Douyin configuration rejected: %v", err)
|
t.Fatalf("valid Douyin configuration rejected: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatePaymentConfigAcceptsProviderAliases(t *testing.T) {
|
func TestValidatePaymentConfigAcceptsProviderAliases(t *testing.T) {
|
||||||
err := validatePaymentConfig(bizpayment.PaymentDouyin, map[string]any{
|
err := validatePaymentConfig(system.PaymentDouyin, map[string]any{
|
||||||
"app_id": "douyin-app", "merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
|
"app_id": "douyin-app", "merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
|
||||||
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_cert_serial": "platform-serial",
|
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_cert_serial": "platform-serial",
|
||||||
})
|
})
|
||||||
|
|
@ -37,13 +36,13 @@ func TestValidatePaymentConfigProviderRules(t *testing.T) {
|
||||||
name, provider, want string
|
name, provider, want string
|
||||||
values map[string]any
|
values map[string]any
|
||||||
}{
|
}{
|
||||||
{"allinpay order type", bizpayment.PaymentAllinPay, "reqsn", map[string]any{"cus_id": "customer", "app_id": "app", "private_key": "private-key", "public_key": "public-key", "query_order_type": "payinfo"}},
|
{"allinpay order type", system.PaymentAllinPay, "reqsn", map[string]any{"cus_id": "customer", "app_id": "app", "private_key": "private-key", "public_key": "public-key", "query_order_type": "payinfo"}},
|
||||||
{"paypal webhook", bizpayment.PaymentPayPal, "webhook_id", map[string]any{"client_id": "client-id", "client_secret": "client-secret"}},
|
{"paypal webhook", system.PaymentPayPal, "webhook_id", map[string]any{"client_id": "client-id", "client_secret": "client-secret"}},
|
||||||
{"wechat v2 refund cert", bizpayment.PaymentWechatV2, "client_cert", map[string]any{"app_id": "app", "merchant_id": "merchant", "mch_key": "key"}},
|
{"wechat v2 refund cert", system.PaymentWechatV2, "client_cert", map[string]any{"app_id": "app", "merchant_id": "merchant", "mch_key": "key"}},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
err := integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, test.provider, test.values)
|
err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, test.provider, test.values)
|
||||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||||
t.Fatalf("error = %v, want %q", err, test.want)
|
t.Fatalf("error = %v, want %q", err, test.want)
|
||||||
}
|
}
|
||||||
|
|
@ -52,13 +51,13 @@ func TestValidatePaymentConfigProviderRules(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatePaymentConfigGenericRequiresRuntimeFields(t *testing.T) {
|
func TestValidatePaymentConfigGenericRequiresRuntimeFields(t *testing.T) {
|
||||||
values := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindPayment, bizpayment.PaymentChinaums)
|
values := system.DefaultIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums)
|
||||||
if err := integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, bizpayment.PaymentChinaums, values); err == nil {
|
if err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums, values); err == nil {
|
||||||
t.Fatal("empty generic payment config unexpectedly accepted")
|
t.Fatal("empty generic payment config unexpectedly accepted")
|
||||||
}
|
}
|
||||||
values["app_id"], values["merchant_id"] = "app", "merchant"
|
values["app_id"], values["merchant_id"] = "app", "merchant"
|
||||||
values["create_url"], values["query_url"], values["refund_url"], values["app_key"] = "https://pay.test/create", "https://pay.test/query", "https://pay.test/refund", "secret"
|
values["create_url"], values["query_url"], values["refund_url"], values["app_key"] = "https://pay.test/create", "https://pay.test/query", "https://pay.test/refund", "secret"
|
||||||
if err := integrationbiz.ValidateIntegrationConfig(integrationbiz.IntegrationKindPayment, bizpayment.PaymentChinaums, values); err == nil {
|
if err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums, values); err == nil {
|
||||||
t.Fatal("generic config with only identity/endpoints unexpectedly accepted")
|
t.Fatal("generic config with only identity/endpoints unexpectedly accepted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
package payment
|
package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
integrationbiz "kra/internal/biz/integration"
|
"kra/internal/biz/system"
|
||||||
bizpayment "kra/internal/biz/payment"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPaymentDefinitionsProvideNonEmptyDefaults(t *testing.T) {
|
func TestPaymentDefinitionsProvideNonEmptyDefaults(t *testing.T) {
|
||||||
definitions := integrationbiz.IntegrationDefinitions(integrationbiz.IntegrationKindPayment)
|
definitions := system.IntegrationDefinitions(system.IntegrationKindPayment)
|
||||||
if len(definitions) != len(bizpayment.SupportedPaymentProviders) {
|
if len(definitions) != len(system.SupportedPaymentProviders) {
|
||||||
t.Fatalf("payment definitions = %d, want %d", len(definitions), len(bizpayment.SupportedPaymentProviders))
|
t.Fatalf("payment definitions = %d, want %d", len(definitions), len(system.SupportedPaymentProviders))
|
||||||
}
|
}
|
||||||
for _, definition := range definitions {
|
for _, definition := range definitions {
|
||||||
if definition.Provider == "" || definition.Name == "" || len(definition.Fields) == 0 {
|
if definition.Provider == "" || definition.Name == "" || len(definition.Fields) == 0 {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
bizpayment "kra/internal/biz/payment"
|
"kra/internal/biz/system"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -68,11 +68,11 @@ func (paymentOrderPO) TableName() string { return "pay_orders" }
|
||||||
|
|
||||||
type paymentOrderRepo struct{ data Provider }
|
type paymentOrderRepo struct{ data Provider }
|
||||||
|
|
||||||
func NewPaymentOrderRepo(data Provider) bizpayment.PaymentOrderRepo {
|
func NewPaymentOrderRepo(data Provider) system.PaymentOrderRepo {
|
||||||
return &paymentOrderRepo{data: data}
|
return &paymentOrderRepo{data: data}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPaymentOrderPO(order *bizpayment.PaymentOrder) (*paymentOrderPO, error) {
|
func newPaymentOrderPO(order *system.PaymentOrder) (*paymentOrderPO, error) {
|
||||||
if order == nil {
|
if order == nil {
|
||||||
return nil, errors.New("支付订单为空")
|
return nil, errors.New("支付订单为空")
|
||||||
}
|
}
|
||||||
|
|
@ -85,16 +85,16 @@ func newPaymentOrderPO(order *bizpayment.PaymentOrder) (*paymentOrderPO, error)
|
||||||
ID: order.ID, TradeNo: order.TradeNo, Provider: order.Provider,
|
ID: order.ID, TradeNo: order.TradeNo, Provider: order.Provider,
|
||||||
ProviderTradeNo: optionalString(order.ProviderTradeNo), QueryID: order.QueryID,
|
ProviderTradeNo: optionalString(order.ProviderTradeNo), QueryID: order.QueryID,
|
||||||
BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
|
BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
|
||||||
PaymentMode: defaultString(order.PaymentMode, bizpayment.PaymentModeExternal), OriginalAmount: order.OriginalAmount,
|
PaymentMode: defaultString(order.PaymentMode, system.PaymentModeExternal), OriginalAmount: order.OriginalAmount,
|
||||||
Amount: order.Amount, PaidAmount: order.PaidAmount, PayerPaidAmount: order.PayerPaidAmount,
|
Amount: order.Amount, PaidAmount: order.PaidAmount, PayerPaidAmount: order.PayerPaidAmount,
|
||||||
CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount, DiscountAmount: order.DiscountAmount,
|
CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount, DiscountAmount: order.DiscountAmount,
|
||||||
ProviderDiscountAmount: order.ProviderDiscountAmount, MerchantDiscountAmount: order.MerchantDiscountAmount,
|
ProviderDiscountAmount: order.ProviderDiscountAmount, MerchantDiscountAmount: order.MerchantDiscountAmount,
|
||||||
SettlementAmount: order.SettlementAmount, Currency: order.Currency, PayerCurrency: order.PayerCurrency,
|
SettlementAmount: order.SettlementAmount, Currency: order.Currency, PayerCurrency: order.PayerCurrency,
|
||||||
AmountBreakdownKnown: order.AmountBreakdownKnown,
|
AmountBreakdownKnown: order.AmountBreakdownKnown,
|
||||||
PaymentStatus: defaultString(order.PaymentStatus, bizpayment.PaymentStatusInitialized),
|
PaymentStatus: defaultString(order.PaymentStatus, system.PaymentStatusInitialized),
|
||||||
ProviderStatus: order.ProviderStatus,
|
ProviderStatus: order.ProviderStatus,
|
||||||
FulfillmentStatus: defaultString(order.FulfillmentStatus, bizpayment.FulfillmentStatusPending),
|
FulfillmentStatus: defaultString(order.FulfillmentStatus, system.FulfillmentStatusPending),
|
||||||
RefundStatus: defaultString(order.RefundStatus, bizpayment.RefundStatusNone),
|
RefundStatus: defaultString(order.RefundStatus, system.RefundStatusNone),
|
||||||
RefundedAmount: order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount, RefundNo: order.RefundNo,
|
RefundedAmount: order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount, RefundNo: order.RefundNo,
|
||||||
ConfirmationID: order.ConfirmationID, RequestFingerprint: order.RequestFingerprint,
|
ConfirmationID: order.ConfirmationID, RequestFingerprint: order.RequestFingerprint,
|
||||||
CreatePayload: createPayload, Extra: extra, LastEventID: order.LastEventID,
|
CreatePayload: createPayload, Extra: extra, LastEventID: order.LastEventID,
|
||||||
|
|
@ -105,15 +105,15 @@ func newPaymentOrderPO(order *bizpayment.PaymentOrder) (*paymentOrderPO, error)
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toBizPaymentOrder(po *paymentOrderPO) *bizpayment.PaymentOrder {
|
func toBizPaymentOrder(po *paymentOrderPO) *system.PaymentOrder {
|
||||||
if po == nil {
|
if po == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &bizpayment.PaymentOrder{
|
return &system.PaymentOrder{
|
||||||
ID: po.ID, TradeNo: po.TradeNo, Provider: po.Provider,
|
ID: po.ID, TradeNo: po.TradeNo, Provider: po.Provider,
|
||||||
ProviderTradeNo: dereferenceString(po.ProviderTradeNo), QueryID: po.QueryID,
|
ProviderTradeNo: dereferenceString(po.ProviderTradeNo), QueryID: po.QueryID,
|
||||||
BusinessType: po.BusinessType, BusinessID: po.BusinessID, Subject: po.Subject,
|
BusinessType: po.BusinessType, BusinessID: po.BusinessID, Subject: po.Subject,
|
||||||
PaymentMode: defaultString(po.PaymentMode, bizpayment.PaymentModeExternal), OriginalAmount: po.OriginalAmount,
|
PaymentMode: defaultString(po.PaymentMode, system.PaymentModeExternal), OriginalAmount: po.OriginalAmount,
|
||||||
Amount: po.Amount, PaidAmount: po.PaidAmount, PayerPaidAmount: po.PayerPaidAmount,
|
Amount: po.Amount, PaidAmount: po.PaidAmount, PayerPaidAmount: po.PayerPaidAmount,
|
||||||
CashPaidAmount: po.CashPaidAmount, PointPaidAmount: po.PointPaidAmount, DiscountAmount: po.DiscountAmount,
|
CashPaidAmount: po.CashPaidAmount, PointPaidAmount: po.PointPaidAmount, DiscountAmount: po.DiscountAmount,
|
||||||
ProviderDiscountAmount: po.ProviderDiscountAmount, MerchantDiscountAmount: po.MerchantDiscountAmount,
|
ProviderDiscountAmount: po.ProviderDiscountAmount, MerchantDiscountAmount: po.MerchantDiscountAmount,
|
||||||
|
|
@ -131,7 +131,7 @@ func toBizPaymentOrder(po *paymentOrderPO) *bizpayment.PaymentOrder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *bizpayment.PaymentOrder) (*bizpayment.PaymentOrder, bool, error) {
|
func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *system.PaymentOrder) (*system.PaymentOrder, bool, error) {
|
||||||
po, err := newPaymentOrderPO(order)
|
po, err := newPaymentOrderPO(order)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
|
|
@ -154,18 +154,18 @@ func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *bizpay
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*bizpayment.PaymentOrder, error) {
|
func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*system.PaymentOrder, error) {
|
||||||
var po paymentOrderPO
|
var po paymentOrderPO
|
||||||
if err := r.data.DB().WithContext(ctx).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, bizpayment.ErrPaymentOrderNotFound
|
return nil, system.ErrPaymentOrderNotFound
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return toBizPaymentOrder(&po), nil
|
return toBizPaymentOrder(&po), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter bizpayment.PaymentOrderFilter) ([]*bizpayment.PaymentOrder, int64, error) {
|
func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter system.PaymentOrderFilter) ([]*system.PaymentOrder, int64, error) {
|
||||||
db := r.data.DB().WithContext(ctx).Model(&paymentOrderPO{})
|
db := r.data.DB().WithContext(ctx).Model(&paymentOrderPO{})
|
||||||
if value := strings.TrimSpace(filter.Provider); value != "" {
|
if value := strings.TrimSpace(filter.Provider); value != "" {
|
||||||
db = db.Where("provider = ?", value)
|
db = db.Where("provider = ?", value)
|
||||||
|
|
@ -193,14 +193,14 @@ func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize
|
||||||
if err := pagination.ApplyRequired(db.Order("id desc"), page, pageSize, 100).Find(&rows).Error; err != nil {
|
if err := pagination.ApplyRequired(db.Order("id desc"), page, pageSize, 100).Find(&rows).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
items := make([]*bizpayment.PaymentOrder, 0, len(rows))
|
items := make([]*system.PaymentOrder, 0, len(rows))
|
||||||
for i := range rows {
|
for i := range rows {
|
||||||
items = append(items, toBizPaymentOrder(&rows[i]))
|
items = append(items, toBizPaymentOrder(&rows[i]))
|
||||||
}
|
}
|
||||||
return items, total, nil
|
return items, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *bizpayment.PaymentProviderUpdate) (*bizpayment.PaymentOrder, error) {
|
func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *system.PaymentProviderUpdate) (*system.PaymentOrder, error) {
|
||||||
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
||||||
if update == nil {
|
if update == nil {
|
||||||
return errors.New("支付下单结果为空")
|
return errors.New("支付下单结果为空")
|
||||||
|
|
@ -215,11 +215,11 @@ func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tr
|
||||||
po.LastEventID = trimTo(update.EventID, 128)
|
po.LastEventID = trimTo(update.EventID, 128)
|
||||||
po.LastPayloadHash = trimTo(update.PayloadHash, 64)
|
po.LastPayloadHash = trimTo(update.PayloadHash, 64)
|
||||||
status := normalizeOrderPaymentStatus(update.Status)
|
status := normalizeOrderPaymentStatus(update.Status)
|
||||||
if status == bizpayment.PaymentStatusPaid {
|
if status == system.PaymentStatusPaid {
|
||||||
// Provider create responses are never sufficient proof of payment.
|
// Provider create responses are never sufficient proof of payment.
|
||||||
status = bizpayment.PaymentStatusPending
|
status = system.PaymentStatusPending
|
||||||
}
|
}
|
||||||
if po.PaymentStatus != bizpayment.PaymentStatusPaid && po.PaymentStatus != bizpayment.PaymentStatusPartiallyRefunded && po.PaymentStatus != bizpayment.PaymentStatusRefunded && status != "" {
|
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded && status != "" {
|
||||||
po.PaymentStatus = status
|
po.PaymentStatus = status
|
||||||
}
|
}
|
||||||
po.Version++
|
po.Version++
|
||||||
|
|
@ -227,7 +227,7 @@ func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tr
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tradeNo string, update *bizpayment.PaymentProviderUpdate) (*bizpayment.PaymentOrder, error) {
|
func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tradeNo string, update *system.PaymentProviderUpdate) (*system.PaymentOrder, error) {
|
||||||
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
||||||
if update == nil {
|
if update == nil {
|
||||||
return errors.New("支付查单结果为空")
|
return errors.New("支付查单结果为空")
|
||||||
|
|
@ -236,13 +236,13 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if update.Amount > 0 && update.Amount != po.Amount {
|
if update.Amount > 0 && update.Amount != po.Amount {
|
||||||
return bizpayment.ErrPaymentOrderConflict
|
return system.ErrPaymentOrderConflict
|
||||||
}
|
}
|
||||||
if update.PayerPaidAmount < 0 || update.CashPaidAmount < 0 || update.PointPaidAmount < 0 || update.DiscountAmount < 0 || update.ProviderDiscountAmount < 0 || update.MerchantDiscountAmount < 0 || update.SettlementAmount < 0 {
|
if update.PayerPaidAmount < 0 || update.CashPaidAmount < 0 || update.PointPaidAmount < 0 || update.DiscountAmount < 0 || update.ProviderDiscountAmount < 0 || update.MerchantDiscountAmount < 0 || update.SettlementAmount < 0 {
|
||||||
return bizpayment.ErrPaymentOrderConflict
|
return system.ErrPaymentOrderConflict
|
||||||
}
|
}
|
||||||
if update.Currency != "" && !strings.EqualFold(update.Currency, po.Currency) {
|
if update.Currency != "" && !strings.EqualFold(update.Currency, po.Currency) {
|
||||||
return bizpayment.ErrPaymentOrderConflict
|
return system.ErrPaymentOrderConflict
|
||||||
}
|
}
|
||||||
po.ProviderStatus = trimTo(update.ProviderStatus, 64)
|
po.ProviderStatus = trimTo(update.ProviderStatus, 64)
|
||||||
po.LastEventID = trimTo(update.EventID, 128)
|
po.LastEventID = trimTo(update.EventID, 128)
|
||||||
|
|
@ -259,22 +259,22 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
|
||||||
po.AmountBreakdownKnown = true
|
po.AmountBreakdownKnown = true
|
||||||
}
|
}
|
||||||
switch normalizeOrderPaymentStatus(update.Status) {
|
switch normalizeOrderPaymentStatus(update.Status) {
|
||||||
case bizpayment.PaymentStatusPaid:
|
case system.PaymentStatusPaid:
|
||||||
if po.PaymentStatus != bizpayment.PaymentStatusPartiallyRefunded && po.PaymentStatus != bizpayment.PaymentStatusRefunded {
|
if po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
|
||||||
po.PaymentStatus = bizpayment.PaymentStatusPaid
|
po.PaymentStatus = system.PaymentStatusPaid
|
||||||
}
|
}
|
||||||
po.PaidAmount = po.Amount
|
po.PaidAmount = po.Amount
|
||||||
if po.PaidAt == nil {
|
if po.PaidAt == nil {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
po.PaidAt = &now
|
po.PaidAt = &now
|
||||||
}
|
}
|
||||||
case bizpayment.PaymentStatusPending:
|
case system.PaymentStatusPending:
|
||||||
if po.PaymentStatus != bizpayment.PaymentStatusPaid && po.PaymentStatus != bizpayment.PaymentStatusPartiallyRefunded && po.PaymentStatus != bizpayment.PaymentStatusRefunded {
|
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
|
||||||
po.PaymentStatus = bizpayment.PaymentStatusPending
|
po.PaymentStatus = system.PaymentStatusPending
|
||||||
}
|
}
|
||||||
case bizpayment.PaymentStatusFailed:
|
case system.PaymentStatusFailed:
|
||||||
if po.PaymentStatus != bizpayment.PaymentStatusPaid && po.PaymentStatus != bizpayment.PaymentStatusPartiallyRefunded && po.PaymentStatus != bizpayment.PaymentStatusRefunded {
|
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
|
||||||
po.PaymentStatus = bizpayment.PaymentStatusFailed
|
po.PaymentStatus = system.PaymentStatusFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
po.Version++
|
po.Version++
|
||||||
|
|
@ -282,27 +282,27 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider, tradeNo string, lease time.Duration) (*bizpayment.PaymentOrder, string, bool, error) {
|
func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider, tradeNo string, lease time.Duration) (*system.PaymentOrder, string, bool, error) {
|
||||||
var token string
|
var token string
|
||||||
var duplicate bool
|
var duplicate bool
|
||||||
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
||||||
if po.FulfillmentStatus == bizpayment.FulfillmentStatusSucceeded {
|
if po.FulfillmentStatus == system.FulfillmentStatusSucceeded {
|
||||||
duplicate = true
|
duplicate = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if po.PaymentStatus != bizpayment.PaymentStatusPaid {
|
if po.PaymentStatus != system.PaymentStatusPaid {
|
||||||
return bizpayment.ErrPaymentOrderState
|
return system.ErrPaymentOrderState
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
if po.FulfillmentStatus == bizpayment.FulfillmentStatusProcessing && po.FulfillmentLeaseUntil != nil && po.FulfillmentLeaseUntil.After(now) {
|
if po.FulfillmentStatus == system.FulfillmentStatusProcessing && po.FulfillmentLeaseUntil != nil && po.FulfillmentLeaseUntil.After(now) {
|
||||||
return bizpayment.ErrPaymentOrderBusy
|
return system.ErrPaymentOrderBusy
|
||||||
}
|
}
|
||||||
if lease <= 0 {
|
if lease <= 0 {
|
||||||
lease = 10 * time.Minute
|
lease = 10 * time.Minute
|
||||||
}
|
}
|
||||||
token = uuid.NewString()
|
token = uuid.NewString()
|
||||||
until := now.Add(lease)
|
until := now.Add(lease)
|
||||||
po.FulfillmentStatus = bizpayment.FulfillmentStatusProcessing
|
po.FulfillmentStatus = system.FulfillmentStatusProcessing
|
||||||
po.FulfillmentToken = token
|
po.FulfillmentToken = token
|
||||||
po.FulfillmentLeaseUntil = &until
|
po.FulfillmentLeaseUntil = &until
|
||||||
po.LastError = ""
|
po.LastError = ""
|
||||||
|
|
@ -312,56 +312,56 @@ func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider
|
||||||
return order, token, duplicate, err
|
return order, token, duplicate, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) CompletePaymentFulfillment(ctx context.Context, provider, tradeNo, token string, success bool, message string) (*bizpayment.PaymentOrder, error) {
|
func (r *paymentOrderRepo) CompletePaymentFulfillment(ctx context.Context, provider, tradeNo, token string, success bool, message string) (*system.PaymentOrder, error) {
|
||||||
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
||||||
if po.FulfillmentStatus != bizpayment.FulfillmentStatusProcessing || po.FulfillmentToken != token {
|
if po.FulfillmentStatus != system.FulfillmentStatusProcessing || po.FulfillmentToken != token {
|
||||||
return bizpayment.ErrPaymentOrderBusy
|
return system.ErrPaymentOrderBusy
|
||||||
}
|
}
|
||||||
po.FulfillmentToken = ""
|
po.FulfillmentToken = ""
|
||||||
po.FulfillmentLeaseUntil = nil
|
po.FulfillmentLeaseUntil = nil
|
||||||
po.LastError = trimTo(message, 512)
|
po.LastError = trimTo(message, 512)
|
||||||
if success {
|
if success {
|
||||||
po.FulfillmentStatus = bizpayment.FulfillmentStatusSucceeded
|
po.FulfillmentStatus = system.FulfillmentStatusSucceeded
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
po.FulfilledAt = &now
|
po.FulfilledAt = &now
|
||||||
} else {
|
} else {
|
||||||
po.FulfillmentStatus = bizpayment.FulfillmentStatusFailed
|
po.FulfillmentStatus = system.FulfillmentStatusFailed
|
||||||
}
|
}
|
||||||
po.Version++
|
po.Version++
|
||||||
return tx.Save(po).Error
|
return tx.Save(po).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tradeNo string, amount int64, lease time.Duration) (*bizpayment.PaymentOrder, string, error) {
|
func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tradeNo string, amount int64, lease time.Duration) (*system.PaymentOrder, string, error) {
|
||||||
var token string
|
var token string
|
||||||
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
||||||
if po.PaymentStatus != bizpayment.PaymentStatusPaid && po.PaymentStatus != bizpayment.PaymentStatusPartiallyRefunded {
|
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded {
|
||||||
return bizpayment.ErrPaymentOrderState
|
return system.ErrPaymentOrderState
|
||||||
}
|
}
|
||||||
if amount <= 0 {
|
if amount <= 0 {
|
||||||
return bizpayment.ErrPaymentOrderConflict
|
return system.ErrPaymentOrderConflict
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
if po.RefundStatus == bizpayment.RefundStatusProcessing && po.RefundLeaseUntil != nil && po.RefundLeaseUntil.After(now) {
|
if po.RefundStatus == system.RefundStatusProcessing && po.RefundLeaseUntil != nil && po.RefundLeaseUntil.After(now) {
|
||||||
return bizpayment.ErrPaymentOrderBusy
|
return system.ErrPaymentOrderBusy
|
||||||
}
|
}
|
||||||
if po.RefundStatus == bizpayment.RefundStatusProcessing && po.RefundRequestedAmount != amount {
|
if po.RefundStatus == system.RefundStatusProcessing && po.RefundRequestedAmount != amount {
|
||||||
return bizpayment.ErrPaymentOrderConflict
|
return system.ErrPaymentOrderConflict
|
||||||
}
|
}
|
||||||
if po.RefundStatus == bizpayment.RefundStatusPending {
|
if po.RefundStatus == system.RefundStatusPending {
|
||||||
return bizpayment.ErrPaymentOrderBusy
|
return system.ErrPaymentOrderBusy
|
||||||
}
|
}
|
||||||
// An expired processing lease means the provider outcome is unknown.
|
// An expired processing lease means the provider outcome is unknown.
|
||||||
// Retry the same refund amount with the same durable refund number. A
|
// Retry the same refund amount with the same durable refund number. A
|
||||||
// different amount must never reuse that operation identity.
|
// different amount must never reuse that operation identity.
|
||||||
reserved := int64(0)
|
reserved := int64(0)
|
||||||
if po.RefundStatus == bizpayment.RefundStatusProcessing && po.RefundLeaseUntil != nil && !po.RefundLeaseUntil.After(now) {
|
if po.RefundStatus == system.RefundStatusProcessing && po.RefundLeaseUntil != nil && !po.RefundLeaseUntil.After(now) {
|
||||||
reserved = 0
|
reserved = 0
|
||||||
} else {
|
} else {
|
||||||
reserved = po.RefundRequestedAmount
|
reserved = po.RefundRequestedAmount
|
||||||
}
|
}
|
||||||
if amount > po.Amount-po.RefundedAmount-reserved {
|
if amount > po.Amount-po.RefundedAmount-reserved {
|
||||||
return bizpayment.ErrPaymentOrderConflict
|
return system.ErrPaymentOrderConflict
|
||||||
}
|
}
|
||||||
if lease <= 0 {
|
if lease <= 0 {
|
||||||
lease = 10 * time.Minute
|
lease = 10 * time.Minute
|
||||||
|
|
@ -371,7 +371,7 @@ func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tra
|
||||||
po.RefundNo = uuid.NewString()
|
po.RefundNo = uuid.NewString()
|
||||||
}
|
}
|
||||||
until := now.Add(lease)
|
until := now.Add(lease)
|
||||||
po.RefundStatus = bizpayment.RefundStatusProcessing
|
po.RefundStatus = system.RefundStatusProcessing
|
||||||
po.RefundRequestedAmount = amount
|
po.RefundRequestedAmount = amount
|
||||||
po.RefundToken = token
|
po.RefundToken = token
|
||||||
po.RefundLeaseUntil = &until
|
po.RefundLeaseUntil = &until
|
||||||
|
|
@ -382,18 +382,18 @@ func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tra
|
||||||
return order, token, err
|
return order, token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, provider, tradeNo, token string, accepted bool, message string) (*bizpayment.PaymentOrder, error) {
|
func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, provider, tradeNo, token string, accepted bool, message string) (*system.PaymentOrder, error) {
|
||||||
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
||||||
if po.RefundStatus != bizpayment.RefundStatusProcessing || po.RefundToken != token {
|
if po.RefundStatus != system.RefundStatusProcessing || po.RefundToken != token {
|
||||||
return bizpayment.ErrPaymentOrderBusy
|
return system.ErrPaymentOrderBusy
|
||||||
}
|
}
|
||||||
po.RefundToken = ""
|
po.RefundToken = ""
|
||||||
po.RefundLeaseUntil = nil
|
po.RefundLeaseUntil = nil
|
||||||
po.LastError = trimTo(message, 512)
|
po.LastError = trimTo(message, 512)
|
||||||
if accepted {
|
if accepted {
|
||||||
po.RefundStatus = bizpayment.RefundStatusPending
|
po.RefundStatus = system.RefundStatusPending
|
||||||
} else {
|
} else {
|
||||||
po.RefundStatus = bizpayment.RefundStatusFailed
|
po.RefundStatus = system.RefundStatusFailed
|
||||||
po.RefundRequestedAmount = 0
|
po.RefundRequestedAmount = 0
|
||||||
po.RefundNo = ""
|
po.RefundNo = ""
|
||||||
}
|
}
|
||||||
|
|
@ -402,26 +402,26 @@ func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, pro
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*bizpayment.PaymentOrder, error) {
|
func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*system.PaymentOrder, error) {
|
||||||
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
|
||||||
if po.RefundStatus != bizpayment.RefundStatusPending || po.RefundNo == "" || po.RefundNo != refundNo || po.RefundRequestedAmount != amount {
|
if po.RefundStatus != system.RefundStatusPending || po.RefundNo == "" || po.RefundNo != refundNo || po.RefundRequestedAmount != amount {
|
||||||
return bizpayment.ErrPaymentOrderState
|
return system.ErrPaymentOrderState
|
||||||
}
|
}
|
||||||
po.LastError = trimTo(message, 512)
|
po.LastError = trimTo(message, 512)
|
||||||
po.RefundRequestedAmount = 0
|
po.RefundRequestedAmount = 0
|
||||||
if !success {
|
if !success {
|
||||||
po.RefundStatus = bizpayment.RefundStatusFailed
|
po.RefundStatus = system.RefundStatusFailed
|
||||||
po.RefundNo = ""
|
po.RefundNo = ""
|
||||||
po.Version++
|
po.Version++
|
||||||
return tx.Save(po).Error
|
return tx.Save(po).Error
|
||||||
}
|
}
|
||||||
po.RefundedAmount += amount
|
po.RefundedAmount += amount
|
||||||
if po.RefundedAmount >= po.Amount {
|
if po.RefundedAmount >= po.Amount {
|
||||||
po.PaymentStatus = bizpayment.PaymentStatusRefunded
|
po.PaymentStatus = system.PaymentStatusRefunded
|
||||||
po.RefundStatus = bizpayment.RefundStatusSucceeded
|
po.RefundStatus = system.RefundStatusSucceeded
|
||||||
} else {
|
} else {
|
||||||
po.PaymentStatus = bizpayment.PaymentStatusPartiallyRefunded
|
po.PaymentStatus = system.PaymentStatusPartiallyRefunded
|
||||||
po.RefundStatus = bizpayment.RefundStatusPartial
|
po.RefundStatus = system.RefundStatusPartial
|
||||||
}
|
}
|
||||||
po.RefundNo = ""
|
po.RefundNo = ""
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
@ -431,13 +431,13 @@ func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, t
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*bizpayment.PaymentOrder, error) {
|
func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*system.PaymentOrder, error) {
|
||||||
var result *bizpayment.PaymentOrder
|
var result *system.PaymentOrder
|
||||||
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
var po paymentOrderPO
|
var po paymentOrderPO
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return bizpayment.ErrPaymentOrderNotFound
|
return system.ErrPaymentOrderNotFound
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -450,16 +450,16 @@ func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeN
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *bizpayment.PaymentProviderUpdate) error {
|
func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *system.PaymentProviderUpdate) error {
|
||||||
if update.ProviderTradeNo != "" {
|
if update.ProviderTradeNo != "" {
|
||||||
if po.ProviderTradeNo != nil && *po.ProviderTradeNo != update.ProviderTradeNo {
|
if po.ProviderTradeNo != nil && *po.ProviderTradeNo != update.ProviderTradeNo {
|
||||||
return bizpayment.ErrPaymentProviderConflict
|
return system.ErrPaymentProviderConflict
|
||||||
}
|
}
|
||||||
var other paymentOrderPO
|
var other paymentOrderPO
|
||||||
// This check is repeated under the order transaction so a platform
|
// This check is repeated under the order transaction so a platform
|
||||||
// transaction cannot be attached to a different merchant order.
|
// transaction cannot be attached to a different merchant order.
|
||||||
if err := tx.Where("provider = ? AND provider_trade_no = ? AND id <> ?", po.Provider, update.ProviderTradeNo, po.ID).First(&other).Error; err == nil {
|
if err := tx.Where("provider = ? AND provider_trade_no = ? AND id <> ?", po.Provider, update.ProviderTradeNo, po.ID).First(&other).Error; err == nil {
|
||||||
return bizpayment.ErrPaymentProviderConflict
|
return system.ErrPaymentProviderConflict
|
||||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +468,7 @@ func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *bizpayment.P
|
||||||
}
|
}
|
||||||
if update.QueryID != "" {
|
if update.QueryID != "" {
|
||||||
if po.QueryID != "" && po.QueryID != update.QueryID {
|
if po.QueryID != "" && po.QueryID != update.QueryID {
|
||||||
return bizpayment.ErrPaymentProviderConflict
|
return system.ErrPaymentProviderConflict
|
||||||
}
|
}
|
||||||
po.QueryID = update.QueryID
|
po.QueryID = update.QueryID
|
||||||
}
|
}
|
||||||
|
|
@ -478,11 +478,11 @@ func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *bizpayment.P
|
||||||
func normalizeOrderPaymentStatus(value string) string {
|
func normalizeOrderPaymentStatus(value string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
case "success", "paid", "fulfilled":
|
case "success", "paid", "fulfilled":
|
||||||
return bizpayment.PaymentStatusPaid
|
return system.PaymentStatusPaid
|
||||||
case "pending", "created", "client_pending", "processing":
|
case "pending", "created", "client_pending", "processing":
|
||||||
return bizpayment.PaymentStatusPending
|
return system.PaymentStatusPending
|
||||||
case "failed", "closed", "cancelled", "canceled":
|
case "failed", "closed", "cancelled", "canceled":
|
||||||
return bizpayment.PaymentStatusFailed
|
return system.PaymentStatusFailed
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
bizpayment "kra/internal/biz/payment"
|
"kra/internal/biz/system"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -19,11 +19,11 @@ func newPaymentOrderRepoForTest(t *testing.T) *paymentOrderRepo {
|
||||||
return &paymentOrderRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
|
return &paymentOrderRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPaymentOrder() *bizpayment.PaymentOrder {
|
func testPaymentOrder() *system.PaymentOrder {
|
||||||
return &bizpayment.PaymentOrder{
|
return &system.PaymentOrder{
|
||||||
TradeNo: "order-1", Provider: bizpayment.PaymentAlipay, BusinessType: "game_item", BusinessID: "item-1",
|
TradeNo: "order-1", Provider: system.PaymentAlipay, BusinessType: "game_item", BusinessID: "item-1",
|
||||||
Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: bizpayment.PaymentStatusInitialized,
|
Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: system.PaymentStatusInitialized,
|
||||||
FulfillmentStatus: bizpayment.FulfillmentStatusPending, RefundStatus: bizpayment.RefundStatusNone,
|
FulfillmentStatus: system.FulfillmentStatusPending, RefundStatus: system.RefundStatusNone,
|
||||||
ConfirmationID: "11111111-1111-1111-1111-111111111111", RequestFingerprint: "fingerprint",
|
ConfirmationID: "11111111-1111-1111-1111-111111111111", RequestFingerprint: "fingerprint",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,36 +35,36 @@ func TestPaymentOrderRepositoryPersistsPaymentFulfillmentAndRefundState(t *testi
|
||||||
if err != nil || !created {
|
if err != nil || !created {
|
||||||
t.Fatalf("create order = %#v created=%v err=%v", order, created, err)
|
t.Fatalf("create order = %#v created=%v err=%v", order, created, err)
|
||||||
}
|
}
|
||||||
update := &bizpayment.PaymentProviderUpdate{Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY", EventID: "event-1"}
|
update := &system.PaymentProviderUpdate{Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY", EventID: "event-1"}
|
||||||
order, err = repo.ApplyPaymentResult(ctx, bizpayment.PaymentAlipay, "order-1", update)
|
order, err = repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-1", update)
|
||||||
if err != nil || order.PaymentStatus != bizpayment.PaymentStatusPaid || order.PaidAmount != 100 {
|
if err != nil || order.PaymentStatus != system.PaymentStatusPaid || order.PaidAmount != 100 {
|
||||||
t.Fatalf("apply payment = %#v err=%v", order, err)
|
t.Fatalf("apply payment = %#v err=%v", order, err)
|
||||||
}
|
}
|
||||||
order, token, duplicate, err := repo.BeginPaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", time.Minute)
|
order, token, duplicate, err := repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute)
|
||||||
if err != nil || duplicate || token == "" || order.FulfillmentStatus != bizpayment.FulfillmentStatusProcessing {
|
if err != nil || duplicate || token == "" || order.FulfillmentStatus != system.FulfillmentStatusProcessing {
|
||||||
t.Fatalf("begin fulfillment = %#v token=%q duplicate=%v err=%v", order, token, duplicate, err)
|
t.Fatalf("begin fulfillment = %#v token=%q duplicate=%v err=%v", order, token, duplicate, err)
|
||||||
}
|
}
|
||||||
if _, _, _, err = repo.BeginPaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", time.Minute); err == nil {
|
if _, _, _, err = repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute); err == nil {
|
||||||
t.Fatal("concurrent fulfillment was accepted")
|
t.Fatal("concurrent fulfillment was accepted")
|
||||||
}
|
}
|
||||||
order, err = repo.CompletePaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", token, true, "")
|
order, err = repo.CompletePaymentFulfillment(ctx, system.PaymentAlipay, "order-1", token, true, "")
|
||||||
if err != nil || order.FulfillmentStatus != bizpayment.FulfillmentStatusSucceeded {
|
if err != nil || order.FulfillmentStatus != system.FulfillmentStatusSucceeded {
|
||||||
t.Fatalf("complete fulfillment = %#v err=%v", order, err)
|
t.Fatalf("complete fulfillment = %#v err=%v", order, err)
|
||||||
}
|
}
|
||||||
_, _, duplicate, err = repo.BeginPaymentFulfillment(ctx, bizpayment.PaymentAlipay, "order-1", time.Minute)
|
_, _, duplicate, err = repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute)
|
||||||
if err != nil || !duplicate {
|
if err != nil || !duplicate {
|
||||||
t.Fatalf("duplicate fulfillment = duplicate=%v err=%v", duplicate, err)
|
t.Fatalf("duplicate fulfillment = duplicate=%v err=%v", duplicate, err)
|
||||||
}
|
}
|
||||||
order, token, err = repo.BeginPaymentRefund(ctx, bizpayment.PaymentAlipay, "order-1", 40, time.Minute)
|
order, token, err = repo.BeginPaymentRefund(ctx, system.PaymentAlipay, "order-1", 40, time.Minute)
|
||||||
if err != nil || token == "" || order.RefundStatus != bizpayment.RefundStatusProcessing {
|
if err != nil || token == "" || order.RefundStatus != system.RefundStatusProcessing {
|
||||||
t.Fatalf("begin refund = %#v token=%q err=%v", order, token, err)
|
t.Fatalf("begin refund = %#v token=%q err=%v", order, token, err)
|
||||||
}
|
}
|
||||||
order, err = repo.CompletePaymentRefundRequest(ctx, bizpayment.PaymentAlipay, "order-1", token, true, "")
|
order, err = repo.CompletePaymentRefundRequest(ctx, system.PaymentAlipay, "order-1", token, true, "")
|
||||||
if err != nil || order.RefundStatus != bizpayment.RefundStatusPending || order.RefundRequestedAmount != 40 {
|
if err != nil || order.RefundStatus != system.RefundStatusPending || order.RefundRequestedAmount != 40 {
|
||||||
t.Fatalf("accept refund = %#v err=%v", order, err)
|
t.Fatalf("accept refund = %#v err=%v", order, err)
|
||||||
}
|
}
|
||||||
order, err = repo.ConfirmPaymentRefund(ctx, bizpayment.PaymentAlipay, "order-1", order.RefundNo, 40, true, "")
|
order, err = repo.ConfirmPaymentRefund(ctx, system.PaymentAlipay, "order-1", order.RefundNo, 40, true, "")
|
||||||
if err != nil || order.RefundedAmount != 40 || order.PaymentStatus != bizpayment.PaymentStatusPartiallyRefunded {
|
if err != nil || order.RefundedAmount != 40 || order.PaymentStatus != system.PaymentStatusPartiallyRefunded {
|
||||||
t.Fatalf("confirm refund = %#v err=%v", order, err)
|
t.Fatalf("confirm refund = %#v err=%v", order, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,11 +82,11 @@ func TestPaymentOrderRepositoryRejectsProviderTradeReuse(t *testing.T) {
|
||||||
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
|
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
update := &bizpayment.PaymentProviderUpdate{Status: "success", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY"}
|
update := &system.PaymentProviderUpdate{Status: "success", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY"}
|
||||||
if _, err := repo.ApplyPaymentResult(ctx, bizpayment.PaymentAlipay, "order-1", update); err != nil {
|
if _, err := repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-1", update); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, err := repo.ApplyPaymentResult(ctx, bizpayment.PaymentAlipay, "order-2", update); err != bizpayment.ErrPaymentProviderConflict {
|
if _, err := repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-2", update); err != system.ErrPaymentProviderConflict {
|
||||||
t.Fatalf("provider trade reuse err = %v", err)
|
t.Fatalf("provider trade reuse err = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,13 +100,13 @@ func TestPaymentOrderRepositoryListsWithFilters(t *testing.T) {
|
||||||
}
|
}
|
||||||
second := testPaymentOrder()
|
second := testPaymentOrder()
|
||||||
second.TradeNo = "wechat-order-2"
|
second.TradeNo = "wechat-order-2"
|
||||||
second.Provider = bizpayment.PaymentWechatV3
|
second.Provider = system.PaymentWechatV3
|
||||||
second.BusinessID = "item-2"
|
second.BusinessID = "item-2"
|
||||||
second.ConfirmationID = "33333333-3333-3333-3333-333333333333"
|
second.ConfirmationID = "33333333-3333-3333-3333-333333333333"
|
||||||
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
|
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, bizpayment.PaymentOrderFilter{Provider: bizpayment.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
|
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, system.PaymentOrderFilter{Provider: system.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package payment
|
package payment
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
import (
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
// Provider is the narrow persistence seam required by payment repositories.
|
// Provider is the narrow persistence seam required by payment repositories.
|
||||||
// Keeping it here lets payment remain an independent data module.
|
// Keeping it here lets payment remain an independent data module.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
@ -28,3 +29,32 @@ func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
||||||
}
|
}
|
||||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openIntegrationConfigTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.AutoMigrate(&integrationConfigPO{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if sqlDB, err := db.DB(); err == nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateAll(db *gorm.DB) error {
|
||||||
|
if err := db.AutoMigrate(&integrationConfigPO{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, step := range Migrations() {
|
||||||
|
if err := step.Migrate(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataAccessLogPO struct {
|
type dataAccessLogPO struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|
@ -21,16 +21,16 @@ type DataAccessLogPO struct {
|
||||||
RequestID, Method, Path, Detail string
|
RequestID, Method, Path, Detail string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (DataAccessLogPO) TableName() string { return "sys_data_access_logs" }
|
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
|
||||||
|
|
||||||
func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *system.DataAccessLog) error {
|
func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *system.DataAccessLog) error {
|
||||||
return r.data.DB().WithContext(ctx).Create(&DataAccessLogPO{EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}).Error
|
return r.data.DB().WithContext(ctx).Create(&dataAccessLogPO{EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}).Error
|
||||||
}
|
}
|
||||||
func dataAccessFromPO(v DataAccessLogPO) *system.DataAccessLog {
|
func dataAccessFromPO(v dataAccessLogPO) *system.DataAccessLog {
|
||||||
return &system.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
|
return &system.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
|
||||||
}
|
}
|
||||||
func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *system.DataAccessLog) ([]*system.DataAccessLog, int64, error) {
|
func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *system.DataAccessLog) ([]*system.DataAccessLog, int64, error) {
|
||||||
db := r.data.DB().WithContext(ctx).Model(&DataAccessLogPO{})
|
db := r.data.DB().WithContext(ctx).Model(&dataAccessLogPO{})
|
||||||
if q != nil {
|
if q != nil {
|
||||||
if q.EventType != "" {
|
if q.EventType != "" {
|
||||||
db = db.Where("event_type = ?", q.EventType)
|
db = db.Where("event_type = ?", q.EventType)
|
||||||
|
|
@ -43,7 +43,7 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
|
||||||
if err := db.Count(&total).Error; err != nil {
|
if err := db.Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
var pos []DataAccessLogPO
|
var pos []dataAccessLogPO
|
||||||
if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
@ -54,5 +54,5 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
}
|
}
|
||||||
func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
|
func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error {
|
||||||
return r.data.DB().WithContext(ctx).Delete(&DataAccessLogPO{}, "id IN ?", ids).Error
|
return r.data.DB().WithContext(ctx).Delete(&dataAccessLogPO{}, "id IN ?", ids).Error
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package integration
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
integrationbiz "kra/internal/biz/integration"
|
"kra/internal/biz/system"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigPO struct {
|
type integrationConfigPO struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|
@ -23,56 +23,32 @@ type ConfigPO struct {
|
||||||
Config string `gorm:"type:text;not null"`
|
Config string `gorm:"type:text;not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ConfigPO) TableName() string { return "sys_integration_configs" }
|
func (integrationConfigPO) TableName() string { return "sys_integration_configs" }
|
||||||
|
|
||||||
type integrationConfigRepo struct{ data Provider }
|
type integrationConfigRepo struct{ data Provider }
|
||||||
|
|
||||||
type paymentConfigReader struct{ data Provider }
|
type integrationRuntimeProvider interface {
|
||||||
|
IntegrationRuntime() *runtimeconfig.Store
|
||||||
|
}
|
||||||
|
|
||||||
func NewIntegrationConfigRepo(data Provider) integrationbiz.IntegrationConfigRepo {
|
func NewIntegrationConfigRepo(data Provider) system.IntegrationConfigRepo {
|
||||||
return &integrationConfigRepo{data: data}
|
return &integrationConfigRepo{data: data}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPaymentConfigReader exposes only the raw payment configuration needed by
|
func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*system.IntegrationConfig, error) {
|
||||||
// the payment data module. The ConfigPO and its table name stay private here.
|
var rows []integrationConfigPO
|
||||||
func NewPaymentConfigReader(data Provider) integrationbiz.PaymentConfigReader {
|
|
||||||
return &paymentConfigReader{data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *paymentConfigReader) ReadPaymentConfig(ctx context.Context, provider string) (*integrationbiz.PaymentConfig, error) {
|
|
||||||
if r == nil || r.data == nil || r.data.DB() == nil {
|
|
||||||
return nil, errors.New("集成配置数据库未初始化")
|
|
||||||
}
|
|
||||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
|
||||||
if provider == "" {
|
|
||||||
return nil, errors.New("支付渠道不能为空")
|
|
||||||
}
|
|
||||||
var row ConfigPO
|
|
||||||
if err := r.data.DB().WithContext(ctx).
|
|
||||||
Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindPayment, provider).
|
|
||||||
First(&row).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, integrationbiz.ErrPaymentConfigNotFound
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &integrationbiz.PaymentConfig{Enabled: row.Enabled, Values: append(json.RawMessage(nil), []byte(row.Config)...)}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*integrationbiz.IntegrationConfig, error) {
|
|
||||||
var rows []ConfigPO
|
|
||||||
if err := r.data.DB().WithContext(ctx).Where("kind = ?", kind).Order("provider ASC").Find(&rows).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).Where("kind = ?", kind).Order("provider ASC").Find(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := make([]*integrationbiz.IntegrationConfig, 0, len(rows))
|
result := make([]*system.IntegrationConfig, 0, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
result = append(result, integrationConfigFromPO(row))
|
result = append(result, integrationConfigFromPO(row))
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*integrationbiz.IntegrationConfig, error) {
|
func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*system.IntegrationConfig, error) {
|
||||||
var row ConfigPO
|
var row integrationConfigPO
|
||||||
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).First(&row).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).First(&row).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, errors.New("集成配置不存在")
|
return nil, errors.New("集成配置不存在")
|
||||||
|
|
@ -82,19 +58,19 @@ func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind,
|
||||||
return integrationConfigFromPO(row), nil
|
return integrationConfigFromPO(row), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *integrationbiz.IntegrationConfig) error {
|
func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *system.IntegrationConfig) error {
|
||||||
db := r.data.DB().WithContext(ctx)
|
db := r.data.DB().WithContext(ctx)
|
||||||
var row ConfigPO
|
var row integrationConfigPO
|
||||||
err := db.Where("kind = ? AND provider = ?", config.Kind, config.Provider).First(&row).Error
|
err := db.Where("kind = ? AND provider = ?", config.Kind, config.Provider).First(&row).Error
|
||||||
values := integrationObject(config.Values)
|
values := integrationObject(config.Values)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
if config.Enabled {
|
if config.Enabled {
|
||||||
if err = integrationbiz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
|
if err = system.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
encoded, _ := json.Marshal(values)
|
encoded, _ := json.Marshal(values)
|
||||||
if err := db.Create(&ConfigPO{Kind: config.Kind, Provider: config.Provider, Enabled: config.Enabled, Config: string(encoded)}).Error; err != nil {
|
if err := db.Create(&integrationConfigPO{Kind: config.Kind, Provider: config.Provider, Enabled: config.Enabled, Config: string(encoded)}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.publish(config.Kind, config.Provider, config.Enabled, encoded)
|
r.publish(config.Kind, config.Provider, config.Enabled, encoded)
|
||||||
|
|
@ -105,7 +81,7 @@ func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, confi
|
||||||
}
|
}
|
||||||
mergeIntegrationSecrets(config.Kind, config.Provider, values, integrationObject(json.RawMessage(row.Config)))
|
mergeIntegrationSecrets(config.Kind, config.Provider, values, integrationObject(json.RawMessage(row.Config)))
|
||||||
if config.Enabled {
|
if config.Enabled {
|
||||||
if err = integrationbiz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
|
if err = system.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +94,7 @@ func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, confi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *integrationConfigRepo) DeleteIntegrationConfig(ctx context.Context, kind, provider string) error {
|
func (r *integrationConfigRepo) DeleteIntegrationConfig(ctx context.Context, kind, provider string) error {
|
||||||
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).Delete(&ConfigPO{}).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).Delete(&integrationConfigPO{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if runtime := integrationRuntime(r.data); runtime != nil {
|
if runtime := integrationRuntime(r.data); runtime != nil {
|
||||||
|
|
@ -134,17 +110,17 @@ func (r *integrationConfigRepo) publish(kind, provider string, enabled bool, val
|
||||||
}
|
}
|
||||||
|
|
||||||
func integrationRuntime(provider Provider) *runtimeconfig.Store {
|
func integrationRuntime(provider Provider) *runtimeconfig.Store {
|
||||||
if provider != nil {
|
if value, ok := provider.(integrationRuntimeProvider); ok {
|
||||||
return provider.IntegrationRuntime()
|
return value.IntegrationRuntime()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func integrationConfigFromPO(row ConfigPO) *integrationbiz.IntegrationConfig {
|
func integrationConfigFromPO(row integrationConfigPO) *system.IntegrationConfig {
|
||||||
values := integrationObject(json.RawMessage(row.Config))
|
values := integrationObject(json.RawMessage(row.Config))
|
||||||
maskIntegrationSecrets(row.Kind, row.Provider, values)
|
maskIntegrationSecrets(row.Kind, row.Provider, values)
|
||||||
encoded, _ := json.Marshal(values)
|
encoded, _ := json.Marshal(values)
|
||||||
return &integrationbiz.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded}
|
return &system.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded}
|
||||||
}
|
}
|
||||||
|
|
||||||
func integrationObject(raw json.RawMessage) map[string]any {
|
func integrationObject(raw json.RawMessage) map[string]any {
|
||||||
|
|
@ -189,7 +165,7 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any)
|
||||||
|
|
||||||
func integrationSecretFields(kind, provider string) map[string]bool {
|
func integrationSecretFields(kind, provider string) map[string]bool {
|
||||||
result := map[string]bool{}
|
result := map[string]bool{}
|
||||||
if definition, ok := integrationbiz.IntegrationDefinition(kind, provider); ok {
|
if definition, ok := system.IntegrationDefinition(kind, provider); ok {
|
||||||
for _, field := range definition.Fields {
|
for _, field := range definition.Fields {
|
||||||
if field.Secret {
|
if field.Secret {
|
||||||
result[field.Key] = true
|
result[field.Key] = true
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package integration
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
integrationbiz "kra/internal/biz/integration"
|
"kra/internal/biz/system"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"kra/internal/integration/runtimeconfig"
|
"kra/internal/integration/runtimeconfig"
|
||||||
|
|
@ -21,25 +21,25 @@ func TestIntegrationConfigSavePublishesUnmaskedRuntimeValues(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err = db.AutoMigrate(&ConfigPO{}); err != nil {
|
if err = db.AutoMigrate(&integrationConfigPO{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
provider := &integrationRuntimeTestProvider{Data: &Data{gormDB: newReloadableDB(db, nil)}, store: runtimeconfig.NewStore()}
|
provider := &integrationRuntimeTestProvider{Data: &Data{gormDB: newReloadableDB(db, nil)}, store: runtimeconfig.NewStore()}
|
||||||
repo := &integrationConfigRepo{data: provider}
|
repo := &integrationConfigRepo{data: provider}
|
||||||
|
|
||||||
values := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindMQ, "rabbitmq")
|
values := system.DefaultIntegrationConfig(system.IntegrationKindMQ, "rabbitmq")
|
||||||
values["password"] = "runtime-secret"
|
values["password"] = "runtime-secret"
|
||||||
raw, _ := json.Marshal(values)
|
raw, _ := json.Marshal(values)
|
||||||
if err = repo.SaveIntegrationConfig(context.Background(), &integrationbiz.IntegrationConfig{Kind: integrationbiz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
|
if err = repo.SaveIntegrationConfig(context.Background(), &system.IntegrationConfig{Kind: system.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
values["password"] = "******"
|
values["password"] = "******"
|
||||||
raw, _ = json.Marshal(values)
|
raw, _ = json.Marshal(values)
|
||||||
if err = repo.SaveIntegrationConfig(context.Background(), &integrationbiz.IntegrationConfig{Kind: integrationbiz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
|
if err = repo.SaveIntegrationConfig(context.Background(), &system.IntegrationConfig{Kind: system.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
active, ok := provider.store.Get(integrationbiz.IntegrationKindMQ, "rabbitmq")
|
active, ok := provider.store.Get(system.IntegrationKindMQ, "rabbitmq")
|
||||||
if !ok || !active.Enabled {
|
if !ok || !active.Enabled {
|
||||||
t.Fatalf("runtime config = %#v, ok=%v", active, ok)
|
t.Fatalf("runtime config = %#v, ok=%v", active, ok)
|
||||||
}
|
}
|
||||||
|
|
@ -22,8 +22,8 @@ func Migrations() []migration.Step {
|
||||||
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
||||||
&dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &SecurityConfigPO{},
|
&dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &SecurityConfigPO{},
|
||||||
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
|
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
|
||||||
&operationPO{}, &loginLogPO{}, &DataAccessLogPO{}, &errorRecordPO{},
|
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
|
||||||
&mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
||||||
&announcementPO{},
|
&announcementPO{},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -22,7 +22,8 @@ var ProviderSet = wire.NewSet(
|
||||||
NewAuditRepo,
|
NewAuditRepo,
|
||||||
NewAuditRecorderRepo,
|
NewAuditRecorderRepo,
|
||||||
NewLogFileRepo,
|
NewLogFileRepo,
|
||||||
|
NewTaskRepo,
|
||||||
NewMediaRepo,
|
NewMediaRepo,
|
||||||
NewAnnouncementRepo,
|
NewAnnouncementRepo,
|
||||||
NewMaintenanceRepo,
|
NewIntegrationConfigRepo,
|
||||||
)
|
)
|
||||||
|
|
@ -112,7 +112,7 @@ func (i *tokenIssuer) ReissueToken(source *system.AuthClaims, authorityID uint)
|
||||||
AuthorityID: authorityID, BufferTime: int64(source.BufferTime / time.Second), UserType: source.UserType,
|
AuthorityID: authorityID, BufferTime: int64(source.BufferTime / time.Second), UserType: source.UserType,
|
||||||
MustChangePwd: source.MustChangePwd, PasswordVersion: source.PasswordVersion,
|
MustChangePwd: source.MustChangePwd, PasswordVersion: source.PasswordVersion,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Audience: jwt.ClaimStrings{security.TokenAudience}, Issuer: settings.Issuer,
|
Audience: jwt.ClaimStrings(append([]string(nil), source.Audience...)), Issuer: source.Issuer,
|
||||||
IssuedAt: jwt.NewNumericDate(source.IssuedAt), NotBefore: jwt.NewNumericDate(source.NotBefore), ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
IssuedAt: jwt.NewNumericDate(source.IssuedAt), NotBefore: jwt.NewNumericDate(source.NotBefore), ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -124,8 +124,7 @@ func (i *tokenIssuer) ReissueToken(source *system.AuthClaims, authorityID uint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
|
func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
|
||||||
settings := i.settings.JWTSettings()
|
claims, err := security.Parse(token, i.settings.JWTSettings().SigningKey)
|
||||||
claims, err := security.ParseWithIssuer(token, settings.SigningKey, settings.Issuer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, security.ErrTokenExpired):
|
case errors.Is(err, security.ErrTokenExpired):
|
||||||
|
|
@ -145,12 +144,5 @@ func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
|
||||||
if claims.IssuedAt != nil {
|
if claims.IssuedAt != nil {
|
||||||
issuedAt = claims.IssuedAt.Time
|
issuedAt = claims.IssuedAt.Time
|
||||||
}
|
}
|
||||||
notBefore, expiresAt := time.Time{}, time.Time{}
|
return &system.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
|
||||||
if claims.NotBefore != nil {
|
|
||||||
notBefore = claims.NotBefore.Time
|
|
||||||
}
|
|
||||||
if claims.ExpiresAt != nil {
|
|
||||||
expiresAt = claims.ExpiresAt.Time
|
|
||||||
}
|
|
||||||
return &system.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: notBefore, ExpiresAt: expiresAt}, nil
|
|
||||||
}
|
}
|
||||||
|
|
@ -14,20 +14,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func SeedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, surfaces ...platformmodule.Surface) error {
|
func SeedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, surfaces ...platformmodule.Surface) error {
|
||||||
return seedSystem(ctx, db, input, surfaces...)
|
return seedSystem(ctx, db, input, nil, surfaces...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeedSystemWithCatalog applies module-contributed administration surfaces in
|
// SeedSystemWithCatalog applies module-contributed administration surfaces and
|
||||||
// one transaction. Each data module seeds its own persistent tables.
|
// default timed tasks in one transaction. The system module remains the owner
|
||||||
|
// of the system tables, while other modules contribute through the catalog.
|
||||||
func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, catalog platformmodule.Catalog) error {
|
func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, catalog platformmodule.Catalog) error {
|
||||||
surfaces := make([]platformmodule.Surface, 0, len(catalog.Definitions))
|
surfaces := make([]platformmodule.Surface, 0, len(catalog.Definitions))
|
||||||
for _, definition := range catalog.Definitions {
|
for _, definition := range catalog.Definitions {
|
||||||
surfaces = append(surfaces, definition.Surface)
|
surfaces = append(surfaces, definition.Surface)
|
||||||
}
|
}
|
||||||
return seedSystem(ctx, db, input, surfaces...)
|
return seedSystem(ctx, db, input, catalog.DefaultTimedTasks(), surfaces...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, surfaces ...platformmodule.Surface) error {
|
func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, defaults []platformmodule.TimedTask, surfaces ...platformmodule.Surface) error {
|
||||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
rootParentID := uint(0)
|
rootParentID := uint(0)
|
||||||
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}
|
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}
|
||||||
|
|
@ -111,6 +112,18 @@ func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig,
|
||||||
if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
|
if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var defaultTasks []taskPO
|
||||||
|
if defaults == nil {
|
||||||
|
defaultTasks = []taskPO{{Name: "ClearDB", Description: "定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)", Spec: "@daily", ExecutorType: "method", MethodName: "ClearDB", Enabled: true}, {Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", ExecutorType: "method", MethodName: "CleanStaleUploads", Enabled: true}}
|
||||||
|
}
|
||||||
|
for _, item := range defaults {
|
||||||
|
defaultTasks = append(defaultTasks, taskPO{Name: item.Name, Description: item.Description, Spec: item.Spec, WithSeconds: item.WithSeconds, ExecutorType: "method", MethodName: item.MethodName, Enabled: item.Enabled})
|
||||||
|
}
|
||||||
|
for _, task := range defaultTasks {
|
||||||
|
if err := tx.Where("name = ?", task.Name).FirstOrCreate(&task).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, item := range input.APIs {
|
for _, item := range input.APIs {
|
||||||
if item != nil {
|
if item != nil {
|
||||||
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
|
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package task
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
taskbiz "kra/internal/biz/task"
|
"kra/internal/biz/system"
|
||||||
"kra/pkg/database/gormkit"
|
"kra/pkg/database/gormkit"
|
||||||
"kra/pkg/database/pagination"
|
"kra/pkg/database/pagination"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -46,14 +46,14 @@ func (taskLogPO) TableName() string { return "sys_timed_task_logs" }
|
||||||
|
|
||||||
type taskRepo struct{ data Provider }
|
type taskRepo struct{ data Provider }
|
||||||
|
|
||||||
func NewTaskRepo(data Provider) taskbiz.TaskRepo { return &taskRepo{data: data} }
|
func NewTaskRepo(data Provider) system.TaskRepo { return &taskRepo{data: data} }
|
||||||
func taskToPO(v *taskbiz.TimedTask) taskPO {
|
func taskToPO(v *system.TimedTask) taskPO {
|
||||||
return taskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: gormkit.JSON(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: gormkit.JSON(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
|
return taskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: gormkit.JSON(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: gormkit.JSON(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
|
||||||
}
|
}
|
||||||
func taskFromPO(v taskPO) *taskbiz.TimedTask {
|
func taskFromPO(v taskPO) *system.TimedTask {
|
||||||
return &taskbiz.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
|
return &system.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
|
||||||
}
|
}
|
||||||
func (r *taskRepo) CreateTask(ctx context.Context, v *taskbiz.TimedTask) error {
|
func (r *taskRepo) CreateTask(ctx context.Context, v *system.TimedTask) error {
|
||||||
po := taskToPO(v)
|
po := taskToPO(v)
|
||||||
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -70,26 +70,26 @@ func (r *taskRepo) TaskNameExists(ctx context.Context, name string, excludeID ui
|
||||||
err := db.Count(&count).Error
|
err := db.Count(&count).Error
|
||||||
return count > 0, err
|
return count > 0, err
|
||||||
}
|
}
|
||||||
func (r *taskRepo) UpdateTask(ctx context.Context, v *taskbiz.TimedTask) error {
|
func (r *taskRepo) UpdateTask(ctx context.Context, v *system.TimedTask) error {
|
||||||
po := taskToPO(v)
|
po := taskToPO(v)
|
||||||
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error
|
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error
|
||||||
}
|
}
|
||||||
func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error {
|
func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error {
|
||||||
return r.data.DB().WithContext(ctx).Delete(&taskPO{}, id).Error
|
return r.data.DB().WithContext(ctx).Delete(&taskPO{}, id).Error
|
||||||
}
|
}
|
||||||
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*taskbiz.TimedTask, error) {
|
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*system.TimedTask, error) {
|
||||||
var po taskPO
|
var po taskPO
|
||||||
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return taskFromPO(po), nil
|
return taskFromPO(po), nil
|
||||||
}
|
}
|
||||||
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *taskbiz.TimedTask) ([]*taskbiz.TimedTask, int64, error) {
|
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *system.TimedTask) ([]*system.TimedTask, int64, error) {
|
||||||
// During first-install the data layer intentionally serves a bootstrap
|
// During first-install the data layer intentionally serves a bootstrap
|
||||||
// database without system tables. The scheduler starts before /init/initdb
|
// database without system tables. The scheduler starts before /init/initdb
|
||||||
// and should remain idle instead of logging a missing-table SQL error.
|
// and should remain idle instead of logging a missing-table SQL error.
|
||||||
if !r.data.DatabaseReady() {
|
if !r.data.DatabaseReady() {
|
||||||
return []*taskbiz.TimedTask{}, 0, nil
|
return []*system.TimedTask{}, 0, nil
|
||||||
}
|
}
|
||||||
db := r.data.DB().WithContext(ctx).Model(&taskPO{})
|
db := r.data.DB().WithContext(ctx).Model(&taskPO{})
|
||||||
if q != nil {
|
if q != nil {
|
||||||
|
|
@ -111,7 +111,7 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *taskbiz.Tim
|
||||||
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
out := make([]*taskbiz.TimedTask, 0, len(pos))
|
out := make([]*system.TimedTask, 0, len(pos))
|
||||||
for _, po := range pos {
|
for _, po := range pos {
|
||||||
out = append(out, taskFromPO(po))
|
out = append(out, taskFromPO(po))
|
||||||
}
|
}
|
||||||
|
|
@ -120,16 +120,13 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *taskbiz.Tim
|
||||||
func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error {
|
func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error {
|
||||||
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", id).Update("enabled", enabled).Error
|
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", id).Update("enabled", enabled).Error
|
||||||
}
|
}
|
||||||
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *taskbiz.TimedTaskLog) error {
|
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *system.TimedTaskLog) error {
|
||||||
return r.data.DB().WithContext(ctx).Create(&taskLogPO{TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}).Error
|
return r.data.DB().WithContext(ctx).Create(&taskLogPO{TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}).Error
|
||||||
}
|
}
|
||||||
func taskLogFromPO(v taskLogPO) *taskbiz.TimedTaskLog {
|
func taskLogFromPO(v taskLogPO) *system.TimedTaskLog {
|
||||||
return &taskbiz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
|
return &system.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
|
||||||
}
|
|
||||||
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*taskbiz.TimedTaskLog, int64, error) {
|
|
||||||
if !r.data.DatabaseReady() {
|
|
||||||
return []*taskbiz.TimedTaskLog{}, 0, nil
|
|
||||||
}
|
}
|
||||||
|
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*system.TimedTaskLog, int64, error) {
|
||||||
db := r.data.DB().WithContext(ctx).Model(&taskLogPO{})
|
db := r.data.DB().WithContext(ctx).Model(&taskLogPO{})
|
||||||
if taskID != 0 {
|
if taskID != 0 {
|
||||||
db = db.Where("task_id = ?", taskID)
|
db = db.Where("task_id = ?", taskID)
|
||||||
|
|
@ -145,14 +142,21 @@ func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint
|
||||||
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
out := make([]*taskbiz.TimedTaskLog, 0, len(pos))
|
out := make([]*system.TimedTaskLog, 0, len(pos))
|
||||||
for _, po := range pos {
|
for _, po := range pos {
|
||||||
out = append(out, taskLogFromPO(po))
|
out = append(out, taskLogFromPO(po))
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
}
|
}
|
||||||
func (r *taskRepo) CleanupTaskLogs(ctx context.Context) error {
|
func (r *taskRepo) CleanupLogs(ctx context.Context) error {
|
||||||
return r.data.DB().WithContext(ctx).Unscoped().
|
now := time.Now()
|
||||||
Where("created_at < ?", time.Now().Add(-720*time.Hour)).
|
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
Delete(&taskLogPO{}).Error
|
if err := tx.Unscoped().Where("created_at < ?", now.Add(-2160*time.Hour)).Delete(&operationPO{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Unscoped().Where("created_at < ?", now.Add(-168*time.Hour)).Delete(&jwtBlacklistPO{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Unscoped().Where("created_at < ?", now.Add(-720*time.Hour)).Delete(&taskLogPO{}).Error
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue