From 555bddf5c3e58435df41179de3c0c7e43ff339fe Mon Sep 17 00:00:00 2001 From: qian Date: Thu, 21 May 2026 23:06:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(payment):=E9=87=8D=E6=9E=84=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/admin.go | 6 +- api/admin/v1/dashboard.go | 4 + api/admin/v1/payment.go | 54 +- api/tenant/tenant.go | 1 + api/tenant/v1/order_tenant.go | 25 +- internal/cmd/cmd.go | 7 +- .../admin/admin_v1_payment_channel_create.go | 12 - .../admin/admin_v1_payment_channel_delete.go | 12 - .../admin/admin_v1_payment_channel_detail.go | 12 - .../admin/admin_v1_payment_channel_save.go | 12 + .../admin/admin_v1_payment_channel_toggle.go | 12 - .../admin/admin_v1_payment_channel_update.go | 12 - .../tenant_v1_tenant_recharge_create.go | 12 + internal/dao/internal/bil_transactions.go | 8 +- internal/dao/internal/ord_payment_channels.go | 99 --- internal/dao/internal/tsk_model_tasks.go | 4 +- internal/dao/ord_payment_channels.go | 22 - internal/handler/public/payment.go | 42 +- internal/logic/admin/dashboard.go | 2 +- internal/logic/admin/order.go | 103 +-- internal/logic/billing/settlement.go | 6 +- internal/logic/billing/task_billing.go | 3 +- internal/logic/billing/wallet.go | 5 +- internal/logic/payment/callback.go | 34 +- internal/logic/payment/config.go | 183 +++- internal/logic/payment/mock.go | 73 -- internal/logic/payment/provider.go | 6 +- internal/logic/payment/stripe.go | 233 ------ internal/logic/task/async_polling.go | 23 +- internal/logic/tenant/billing.go | 18 +- internal/logic/tenant/order.go | 178 ++-- internal/model/do/bil_transactions.go | 5 +- internal/model/do/ord_payment_channels.go | 26 - internal/model/do/tsk_model_tasks.go | 2 +- internal/model/entity/bil_transactions.go | 27 +- internal/model/entity/ord_payment_channels.go | 24 - internal/model/entity/tsk_model_tasks.go | 50 +- internal/service/admin.go | 14 +- internal/service/tenant.go | 2 + .../000005_add_transaction_context_fields.sql | 22 + .../000006_drop_ord_payment_channels.sql | 30 + .../src/views/finance/PaymentSettingsPage.vue | 538 +++--------- .../src/views/tenant/TenantDetailPage.vue | 3 + .../src/views/finance/TransactionsPage.vue | 10 +- web/tenant/src/views/finance/WalletPage.vue | 779 +++++++++++++++--- 45 files changed, 1297 insertions(+), 1458 deletions(-) delete mode 100644 internal/controller/admin/admin_v1_payment_channel_create.go delete mode 100644 internal/controller/admin/admin_v1_payment_channel_delete.go delete mode 100644 internal/controller/admin/admin_v1_payment_channel_detail.go create mode 100644 internal/controller/admin/admin_v1_payment_channel_save.go delete mode 100644 internal/controller/admin/admin_v1_payment_channel_toggle.go delete mode 100644 internal/controller/admin/admin_v1_payment_channel_update.go create mode 100644 internal/controller/tenant/tenant_v1_tenant_recharge_create.go delete mode 100644 internal/dao/internal/ord_payment_channels.go delete mode 100644 internal/dao/ord_payment_channels.go delete mode 100644 internal/logic/payment/mock.go delete mode 100644 internal/logic/payment/stripe.go delete mode 100644 internal/model/do/ord_payment_channels.go delete mode 100644 internal/model/entity/ord_payment_channels.go create mode 100644 migrations/000005_add_transaction_context_fields.sql create mode 100644 migrations/000006_drop_ord_payment_channels.sql diff --git a/api/admin/admin.go b/api/admin/admin.go index 9dc5151..dc93016 100644 --- a/api/admin/admin.go +++ b/api/admin/admin.go @@ -146,11 +146,7 @@ type IAdminV1 interface { AdminMemberEnable(ctx context.Context, req *v1.AdminMemberEnableReq) (res *v1.AdminMemberEnableRes, err error) AdminMemberResetPassword(ctx context.Context, req *v1.AdminMemberResetPasswordReq) (res *v1.AdminMemberResetPasswordRes, err error) PaymentChannelList(ctx context.Context, req *v1.PaymentChannelListReq) (res *v1.PaymentChannelListRes, err error) - PaymentChannelCreate(ctx context.Context, req *v1.PaymentChannelCreateReq) (res *v1.PaymentChannelCreateRes, err error) - PaymentChannelDetail(ctx context.Context, req *v1.PaymentChannelDetailReq) (res *v1.PaymentChannelDetailRes, err error) - PaymentChannelUpdate(ctx context.Context, req *v1.PaymentChannelUpdateReq) (res *v1.PaymentChannelUpdateRes, err error) - PaymentChannelDelete(ctx context.Context, req *v1.PaymentChannelDeleteReq) (res *v1.PaymentChannelDeleteRes, err error) - PaymentChannelToggle(ctx context.Context, req *v1.PaymentChannelToggleReq) (res *v1.PaymentChannelToggleRes, err error) + PaymentChannelSave(ctx context.Context, req *v1.PaymentChannelSaveReq) (res *v1.PaymentChannelSaveRes, err error) PaymentSettingsGet(ctx context.Context, req *v1.PaymentSettingsGetReq) (res *v1.PaymentSettingsGetRes, err error) PaymentSettingsUpdate(ctx context.Context, req *v1.PaymentSettingsUpdateReq) (res *v1.PaymentSettingsUpdateRes, err error) AdminPermissionList(ctx context.Context, req *v1.AdminPermissionListReq) (res *v1.AdminPermissionListRes, err error) diff --git a/api/admin/v1/dashboard.go b/api/admin/v1/dashboard.go index 59e569a..a8f3a31 100644 --- a/api/admin/v1/dashboard.go +++ b/api/admin/v1/dashboard.go @@ -277,6 +277,10 @@ type AdminWalletTransactionItem struct { BalanceAfter float64 `json:"balance_after"` FrozenAfter float64 `json:"frozen_after"` Description string `json:"description"` + UserId int64 `json:"user_id"` + Username string `json:"username"` + RequestId string `json:"request_id"` + ModelName string `json:"model_name"` CreatedAt string `json:"created_at"` } diff --git a/api/admin/v1/payment.go b/api/admin/v1/payment.go index 0088fe5..dbd40eb 100644 --- a/api/admin/v1/payment.go +++ b/api/admin/v1/payment.go @@ -41,61 +41,25 @@ type AdminMemberResetPasswordRes struct { NewPassword string `json:"new_password"` } -// === 支付渠道管理 === +// === 支付渠道管理(单例配置模式:每种渠道类型只有一个配置) === +// PaymentChannelListReq 获取所有渠道配置 type PaymentChannelListReq struct { - g.Meta `path:"/payment-channels" method:"get" mime:"json" tags:"管理后台-支付" summary:"支付渠道列表"` + g.Meta `path:"/payment-channels" method:"get" mime:"json" tags:"管理后台-支付" summary:"获取所有渠道配置"` } type PaymentChannelListRes struct { List []map[string]any `json:"list"` } -type PaymentChannelCreateReq struct { - g.Meta `path:"/payment-channels" method:"post" mime:"json" tags:"管理后台-支付" summary:"创建支付渠道"` - Channel string `json:"channel" v:"required#请输入渠道类型" dc:"渠道类型"` - Name string `json:"name" v:"required#请输入渠道名称" dc:"渠道名称"` - PaymentType string `json:"payment_type" dc:"支付类型"` - Config string `json:"config" dc:"配置(JSON)"` - SortOrder int `json:"sort_order" dc:"排序"` +// PaymentChannelSaveReq 保存指定渠道的配置(整体覆盖) +type PaymentChannelSaveReq struct { + g.Meta `path:"/payment-channels/{channel}" method:"put" mime:"json" tags:"管理后台-支付" summary:"保存渠道配置"` + Channel string `json:"channel" in:"path" v:"required|in:epay#请指定渠道类型|不支持的渠道类型" dc:"渠道类型"` + Config string `json:"config" v:"required#请提供配置" dc:"完整的 JSON 配置(含 is_enabled)"` } -type PaymentChannelCreateRes struct { - ID int64 `json:"id"` -} - -type PaymentChannelDetailReq struct { - g.Meta `path:"/payment-channels/{id}" method:"get" mime:"json" tags:"管理后台-支付" summary:"支付渠道详情"` - Id int64 `json:"id" in:"path" v:"required|min:1" dc:"渠道ID"` -} - -type PaymentChannelDetailRes struct { - Data map[string]any `json:"data"` -} - -type PaymentChannelUpdateReq struct { - g.Meta `path:"/payment-channels/{id}" method:"put" mime:"json" tags:"管理后台-支付" summary:"更新支付渠道"` - Id int64 `json:"id" in:"path" v:"required|min:1" dc:"渠道ID"` - Update map[string]interface{} `json:"update" dc:"更新字段"` -} - -type PaymentChannelUpdateRes struct{} - -type PaymentChannelDeleteReq struct { - g.Meta `path:"/payment-channels/{id}" method:"delete" mime:"json" tags:"管理后台-支付" summary:"删除支付渠道"` - Id int64 `json:"id" in:"path" v:"required|min:1" dc:"渠道ID"` -} - -type PaymentChannelDeleteRes struct{} - -type PaymentChannelToggleReq struct { - g.Meta `path:"/payment-channels/{id}/toggle" method:"put" mime:"json" tags:"管理后台-支付" summary:"切换支付渠道状态"` - Id int64 `json:"id" in:"path" v:"required|min:1" dc:"渠道ID"` -} - -type PaymentChannelToggleRes struct { - IsEnabled bool `json:"is_enabled"` -} +type PaymentChannelSaveRes struct{} // === 支付设置 === diff --git a/api/tenant/tenant.go b/api/tenant/tenant.go index 7c6e4a2..659fc87 100644 --- a/api/tenant/tenant.go +++ b/api/tenant/tenant.go @@ -105,6 +105,7 @@ type ITenantV1 interface { TenantOrderList(ctx context.Context, req *v1.TenantOrderListReq) (res *v1.TenantOrderListRes, err error) TenantOrderDetail(ctx context.Context, req *v1.TenantOrderDetailReq) (res *v1.TenantOrderDetailRes, err error) TenantOrderCancel(ctx context.Context, req *v1.TenantOrderCancelReq) (res *v1.TenantOrderCancelRes, err error) + TenantRechargeCreate(ctx context.Context, req *v1.TenantRechargeCreateReq) (res *v1.TenantRechargeCreateRes, err error) TenantPaymentInfo(ctx context.Context, req *v1.TenantPaymentInfoReq) (res *v1.TenantPaymentInfoRes, err error) TenantOrderExport(ctx context.Context, req *v1.TenantOrderExportReq) (res *v1.TenantOrderExportRes, err error) TenantOrgInfo(ctx context.Context, req *v1.TenantOrgInfoReq) (res *v1.TenantOrgInfoRes, err error) diff --git a/api/tenant/v1/order_tenant.go b/api/tenant/v1/order_tenant.go index a923825..0ae1962 100644 --- a/api/tenant/v1/order_tenant.go +++ b/api/tenant/v1/order_tenant.go @@ -19,10 +19,10 @@ type TenantOrderCreateRes struct { } type TenantOrderPayReq struct { - g.Meta `path:"/orders/{id}/pay" method:"post" mime:"json" tags:"租户控制台-订单" summary:"支付订单"` - Id int64 `json:"id" in:"path" v:"required|min:1"` - PaymentChannelID int64 `json:"payment_channel_id"` - PaymentMethod string `json:"payment_method"` + g.Meta `path:"/orders/{id}/pay" method:"post" mime:"json" tags:"租户控制台-订单" summary:"支付订单"` + Id int64 `json:"id" in:"path" v:"required|min:1"` + PaymentChannel string `json:"payment_channel" dc:"支付渠道类型(epay/stripe/mock)"` + PaymentMethod string `json:"payment_method"` } type TenantOrderPayRes struct { @@ -84,12 +84,27 @@ type TenantOrderCancelReq struct { type TenantOrderCancelRes struct{} +type TenantRechargeCreateReq struct { + g.Meta `path:"/recharge/create" method:"post" mime:"json" tags:"租户控制台-订单" summary:"创建充值订单"` + Amount float64 `json:"amount" v:"required|min:0.01#请输入充值金额|充值金额不能小于 0.01"` + PaymentChannel string `json:"payment_channel" v:"required|in:epay#请选择支付渠道|不支持的支付渠道"` + PaymentMethod string `json:"payment_method" v:"required#请选择支付方式"` +} + +type TenantRechargeCreateRes struct { + Data map[string]any `json:"data"` +} + type TenantPaymentInfoReq struct { g.Meta `path:"/payment-info" method:"get" mime:"json" tags:"租户控制台-订单" summary:"支付信息"` } type TenantPaymentInfoRes struct { - Data map[string]any `json:"data"` + Channels []map[string]any `json:"channels"` + AmountOptions []int `json:"amount_options,omitempty"` + AmountDiscount map[int]float64 `json:"amount_discount,omitempty"` + MinTopUp int `json:"min_topup,omitempty"` + Currency string `json:"currency,omitempty"` } // TenantOrderExportReq 导出订单列表请求 diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index da3c3f5..aa0fac3 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -313,10 +313,9 @@ func printBanner() { // registerPaymentCallbacks registers payment callback routes. func registerPaymentCallbacks(group *ghttp.RouterGroup) { group.Group("/payment", func(g *ghttp.RouterGroup) { - g.POST("/callback/{channel_id}", public.HandlePaymentCallback) - g.GET("/callback/{channel_id}", public.HandlePaymentCallback) - g.GET("/epay/return/{channel_id}", public.HandlePaymentEpayReturn) - g.POST("/stripe/webhook/{channel_id}", public.HandlePaymentStripeWebhook) + g.POST("/callback/{channel}", public.HandlePaymentCallback) + g.GET("/callback/{channel}", public.HandlePaymentCallback) + g.GET("/epay/return", public.HandlePaymentEpayReturn) }) } diff --git a/internal/controller/admin/admin_v1_payment_channel_create.go b/internal/controller/admin/admin_v1_payment_channel_create.go deleted file mode 100644 index c80ff29..0000000 --- a/internal/controller/admin/admin_v1_payment_channel_create.go +++ /dev/null @@ -1,12 +0,0 @@ -package admin - -import ( - "context" - - "github.com/qianfree/team-api/api/admin/v1" - "github.com/qianfree/team-api/internal/service" -) - -func (c *ControllerV1) PaymentChannelCreate(ctx context.Context, req *v1.PaymentChannelCreateReq) (res *v1.PaymentChannelCreateRes, err error) { - return service.Admin().CreatePaymentChannel(ctx, req) -} diff --git a/internal/controller/admin/admin_v1_payment_channel_delete.go b/internal/controller/admin/admin_v1_payment_channel_delete.go deleted file mode 100644 index 1a94e01..0000000 --- a/internal/controller/admin/admin_v1_payment_channel_delete.go +++ /dev/null @@ -1,12 +0,0 @@ -package admin - -import ( - "context" - - "github.com/qianfree/team-api/api/admin/v1" - "github.com/qianfree/team-api/internal/service" -) - -func (c *ControllerV1) PaymentChannelDelete(ctx context.Context, req *v1.PaymentChannelDeleteReq) (res *v1.PaymentChannelDeleteRes, err error) { - return service.Admin().DeletePaymentChannel(ctx, req) -} diff --git a/internal/controller/admin/admin_v1_payment_channel_detail.go b/internal/controller/admin/admin_v1_payment_channel_detail.go deleted file mode 100644 index 9344bd3..0000000 --- a/internal/controller/admin/admin_v1_payment_channel_detail.go +++ /dev/null @@ -1,12 +0,0 @@ -package admin - -import ( - "context" - - "github.com/qianfree/team-api/api/admin/v1" - "github.com/qianfree/team-api/internal/service" -) - -func (c *ControllerV1) PaymentChannelDetail(ctx context.Context, req *v1.PaymentChannelDetailReq) (res *v1.PaymentChannelDetailRes, err error) { - return service.Admin().GetPaymentChannel(ctx, req) -} diff --git a/internal/controller/admin/admin_v1_payment_channel_save.go b/internal/controller/admin/admin_v1_payment_channel_save.go new file mode 100644 index 0000000..1e2ef1f --- /dev/null +++ b/internal/controller/admin/admin_v1_payment_channel_save.go @@ -0,0 +1,12 @@ +package admin + +import ( + "context" + + "github.com/qianfree/team-api/api/admin/v1" + "github.com/qianfree/team-api/internal/service" +) + +func (c *ControllerV1) PaymentChannelSave(ctx context.Context, req *v1.PaymentChannelSaveReq) (res *v1.PaymentChannelSaveRes, err error) { + return service.Admin().SavePaymentChannel(ctx, req) +} diff --git a/internal/controller/admin/admin_v1_payment_channel_toggle.go b/internal/controller/admin/admin_v1_payment_channel_toggle.go deleted file mode 100644 index 2639b06..0000000 --- a/internal/controller/admin/admin_v1_payment_channel_toggle.go +++ /dev/null @@ -1,12 +0,0 @@ -package admin - -import ( - "context" - - "github.com/qianfree/team-api/api/admin/v1" - "github.com/qianfree/team-api/internal/service" -) - -func (c *ControllerV1) PaymentChannelToggle(ctx context.Context, req *v1.PaymentChannelToggleReq) (res *v1.PaymentChannelToggleRes, err error) { - return service.Admin().TogglePaymentChannel(ctx, req) -} diff --git a/internal/controller/admin/admin_v1_payment_channel_update.go b/internal/controller/admin/admin_v1_payment_channel_update.go deleted file mode 100644 index 5b966c4..0000000 --- a/internal/controller/admin/admin_v1_payment_channel_update.go +++ /dev/null @@ -1,12 +0,0 @@ -package admin - -import ( - "context" - - "github.com/qianfree/team-api/api/admin/v1" - "github.com/qianfree/team-api/internal/service" -) - -func (c *ControllerV1) PaymentChannelUpdate(ctx context.Context, req *v1.PaymentChannelUpdateReq) (res *v1.PaymentChannelUpdateRes, err error) { - return service.Admin().UpdatePaymentChannel(ctx, req) -} diff --git a/internal/controller/tenant/tenant_v1_tenant_recharge_create.go b/internal/controller/tenant/tenant_v1_tenant_recharge_create.go new file mode 100644 index 0000000..e288e0f --- /dev/null +++ b/internal/controller/tenant/tenant_v1_tenant_recharge_create.go @@ -0,0 +1,12 @@ +package tenant + +import ( + "context" + + "github.com/qianfree/team-api/api/tenant/v1" + "github.com/qianfree/team-api/internal/service" +) + +func (c *ControllerV1) TenantRechargeCreate(ctx context.Context, req *v1.TenantRechargeCreateReq) (res *v1.TenantRechargeCreateRes, err error) { + return service.Tenant().RechargeCreate(ctx, req) +} diff --git a/internal/dao/internal/bil_transactions.go b/internal/dao/internal/bil_transactions.go index fa60f3d..f8b70ad 100644 --- a/internal/dao/internal/bil_transactions.go +++ b/internal/dao/internal/bil_transactions.go @@ -24,7 +24,7 @@ type BilTransactionsColumns struct { Id string // 主键ID TenantId string // 租户ID WalletId string // 关联钱包ID - Type string // 类型:recharge(充值)/ pre_deduct(预扣)/ settle(结算)/ refund(退款)/ adjust(调整)/ freeze(冻结)/ unfreeze(解冻) + Type string // 类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃) Amount string // 变动金额(正数=收入,负数=支出) BalanceAfter string // 变动后总余额 FrozenAfter string // 变动后冻结余额 @@ -33,6 +33,9 @@ type BilTransactionsColumns struct { Description string // 交易描述 CreatedAt string // 创建时间 UpdatedAt string // 更新时间 + UserId string // 关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空) + RequestId string // 关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空) + ModelName string // 关联模型名(consume 类型为调用的模型名,其他类型为空) } // bilTransactionsColumns holds the columns for the table bil_transactions. @@ -49,6 +52,9 @@ var bilTransactionsColumns = BilTransactionsColumns{ Description: "description", CreatedAt: "created_at", UpdatedAt: "updated_at", + UserId: "user_id", + RequestId: "request_id", + ModelName: "model_name", } // NewBilTransactionsDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/ord_payment_channels.go b/internal/dao/internal/ord_payment_channels.go deleted file mode 100644 index 05be03d..0000000 --- a/internal/dao/internal/ord_payment_channels.go +++ /dev/null @@ -1,99 +0,0 @@ -// ========================================================================== -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ========================================================================== - -package internal - -import ( - "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" -) - -// OrdPaymentChannelsDao is the data access object for the table ord_payment_channels. -type OrdPaymentChannelsDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns OrdPaymentChannelsColumns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. -} - -// OrdPaymentChannelsColumns defines and stores column names for the table ord_payment_channels. -type OrdPaymentChannelsColumns struct { - Id string // 主键ID - Channel string // 渠道标识(alipay/wechat/stripe/mock) - Name string // 显示名称 - Config string // 渠道配置(JSONB,含 API 密钥等敏感信息) - IsEnabled string // 是否启用 - SortOrder string // 排序权重 - CreatedAt string // 创建时间 - UpdatedAt string // 更新时间 - PaymentType string // 子支付方式(alipay/wxpay 等,空表示该渠道支持所有方式) - CallbackUrl string // 支付回调地址覆盖(为空则使用系统默认) - ReturnUrl string // 支付完成后前端跳转地址覆盖 -} - -// ordPaymentChannelsColumns holds the columns for the table ord_payment_channels. -var ordPaymentChannelsColumns = OrdPaymentChannelsColumns{ - Id: "id", - Channel: "channel", - Name: "name", - Config: "config", - IsEnabled: "is_enabled", - SortOrder: "sort_order", - CreatedAt: "created_at", - UpdatedAt: "updated_at", - PaymentType: "payment_type", - CallbackUrl: "callback_url", - ReturnUrl: "return_url", -} - -// NewOrdPaymentChannelsDao creates and returns a new DAO object for table data access. -func NewOrdPaymentChannelsDao(handlers ...gdb.ModelHandler) *OrdPaymentChannelsDao { - return &OrdPaymentChannelsDao{ - group: "default", - table: "ord_payment_channels", - columns: ordPaymentChannelsColumns, - handlers: handlers, - } -} - -// DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *OrdPaymentChannelsDao) DB() gdb.DB { - return g.DB(dao.group) -} - -// Table returns the table name of the current DAO. -func (dao *OrdPaymentChannelsDao) Table() string { - return dao.table -} - -// Columns returns all column names of the current DAO. -func (dao *OrdPaymentChannelsDao) Columns() OrdPaymentChannelsColumns { - return dao.columns -} - -// Group returns the database configuration group name of the current DAO. -func (dao *OrdPaymentChannelsDao) Group() string { - return dao.group -} - -// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *OrdPaymentChannelsDao) Ctx(ctx context.Context) *gdb.Model { - model := dao.DB().Model(dao.table) - for _, handler := range dao.handlers { - model = handler(model) - } - return model.Safe().Ctx(ctx) -} - -// Transaction wraps the transaction logic using function f. -// It rolls back the transaction and returns the error if function f returns a non-nil error. -// It commits the transaction and returns nil if function f returns nil. -// -// Note: Do not commit or roll back the transaction in function f, -// as it is automatically handled by this function. -func (dao *OrdPaymentChannelsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { - return dao.Ctx(ctx).Transaction(ctx, f) -} diff --git a/internal/dao/internal/tsk_model_tasks.go b/internal/dao/internal/tsk_model_tasks.go index 936eb1a..2cadd47 100644 --- a/internal/dao/internal/tsk_model_tasks.go +++ b/internal/dao/internal/tsk_model_tasks.go @@ -23,7 +23,6 @@ type TskModelTasksDao struct { type TskModelTasksColumns struct { Id string // PublicTaskId string // - RequestId string // Platform string // Action string // Status string // @@ -46,13 +45,13 @@ type TskModelTasksColumns struct { FinishTime string // CreatedAt string // UpdatedAt string // + RequestId string // 任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id } // tskModelTasksColumns holds the columns for the table tsk_model_tasks. var tskModelTasksColumns = TskModelTasksColumns{ Id: "id", PublicTaskId: "public_task_id", - RequestId: "request_id", Platform: "platform", Action: "action", Status: "status", @@ -75,6 +74,7 @@ var tskModelTasksColumns = TskModelTasksColumns{ FinishTime: "finish_time", CreatedAt: "created_at", UpdatedAt: "updated_at", + RequestId: "request_id", } // NewTskModelTasksDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/ord_payment_channels.go b/internal/dao/ord_payment_channels.go deleted file mode 100644 index bf5e669..0000000 --- a/internal/dao/ord_payment_channels.go +++ /dev/null @@ -1,22 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "github.com/qianfree/team-api/internal/dao/internal" -) - -// ordPaymentChannelsDao is the data access object for the table ord_payment_channels. -// You can define custom methods on it to extend its functionality as needed. -type ordPaymentChannelsDao struct { - *internal.OrdPaymentChannelsDao -} - -var ( - // OrdPaymentChannels is a globally accessible object for table ord_payment_channels operations. - OrdPaymentChannels = ordPaymentChannelsDao{internal.NewOrdPaymentChannelsDao()} -) - -// Add your custom methods and functionality below. diff --git a/internal/handler/public/payment.go b/internal/handler/public/payment.go index 7cf496d..65a5bca 100644 --- a/internal/handler/public/payment.go +++ b/internal/handler/public/payment.go @@ -2,21 +2,21 @@ package public import ( "github.com/gogf/gf/v2/net/ghttp" - "github.com/gogf/gf/v2/util/gconv" "github.com/qianfree/team-api/internal/logic/payment" ) -// handlePaymentCallback 处理支付渠道的异步回调通知(Epay 等支持 GET/POST 两种方式)。 +// HandlePaymentCallback 处理支付渠道的异步回调通知(支持 GET/POST)。 +// channel 从 URL 路径获取(如 "epay")。 func HandlePaymentCallback(r *ghttp.Request) { - channelID := gconv.Int64(r.Get("channel_id").String()) - if channelID <= 0 { + channel := r.Get("channel").String() + if channel == "" { r.Response.WriteStatus(400) r.Response.Write("fail") return } - err := payment.ProcessCallback(r.Context(), r.Request, channelID) + err := payment.ProcessCallback(r.Context(), r.Request, channel) if err != nil { r.Response.WriteStatus(200) r.Response.Write("fail") @@ -27,38 +27,14 @@ func HandlePaymentCallback(r *ghttp.Request) { r.Response.Write("success") } -// handlePaymentEpayReturn 处理 Epay 支付完成后的浏览器同步跳转。 +// HandlePaymentEpayReturn 处理 Epay 支付完成后的浏览器同步跳转。 func HandlePaymentEpayReturn(r *ghttp.Request) { - channelID := gconv.Int64(r.Get("channel_id").String()) - if channelID <= 0 { - r.Response.WriteStatus(302) - r.Response.Header().Set("Location", "/console/topup?pay=fail") - return - } - - err := payment.ProcessCallback(r.Context(), r.Request, channelID) + err := payment.ProcessCallback(r.Context(), r.Request, "epay") if err != nil { r.Response.WriteStatus(302) - r.Response.Header().Set("Location", "/console/topup?pay=fail") + r.Response.Header().Set("Location", "/console/wallet?pay=fail") return } r.Response.WriteStatus(302) - r.Response.Header().Set("Location", "/console/topup?pay=success") -} - -// handlePaymentStripeWebhook 处理 Stripe Webhook 回调。 -func HandlePaymentStripeWebhook(r *ghttp.Request) { - channelID := gconv.Int64(r.Get("channel_id").String()) - if channelID <= 0 { - r.Response.WriteStatus(400) - return - } - - err := payment.ProcessCallback(r.Context(), r.Request, channelID) - if err != nil { - r.Response.WriteStatus(400) - return - } - - r.Response.WriteStatus(200) + r.Response.Header().Set("Location", "/console/wallet?pay=success") } diff --git a/internal/logic/admin/dashboard.go b/internal/logic/admin/dashboard.go index 04df3d9..43542f7 100644 --- a/internal/logic/admin/dashboard.go +++ b/internal/logic/admin/dashboard.go @@ -467,7 +467,7 @@ func (s *sAdmin) GetWalletTransactions(ctx context.Context, req *v1.AdminWalletT var total int records := make([]*v1.AdminWalletTransactionItem, 0) err = query.OrderDesc("created_at"). - Fields("id, type, amount, balance_after, frozen_after, description, created_at"). + Fields("id, type, amount, balance_after, frozen_after, description, user_id, request_id, model_name, created_at"). Page(page, pageSize). ScanAndCount(&records, &total, false) if err != nil { diff --git a/internal/logic/admin/order.go b/internal/logic/admin/order.go index f8fefe0..7627a07 100644 --- a/internal/logic/admin/order.go +++ b/internal/logic/admin/order.go @@ -3,16 +3,15 @@ package admin import ( "context" "fmt" - "github.com/qianfree/team-api/internal/dao" - do "github.com/qianfree/team-api/internal/model/do" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" - "github.com/gogf/gf/v2/util/gconv" v1 "github.com/qianfree/team-api/api/admin/v1" + "github.com/qianfree/team-api/internal/dao" + do "github.com/qianfree/team-api/internal/model/do" + "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/logic/payment" - "github.com/qianfree/team-api/internal/model/entity" "github.com/qianfree/team-api/internal/utility/export" ) @@ -129,98 +128,22 @@ func (s *sAdmin) OrderComplete(ctx context.Context, req *v1.OrderCompleteReq) (* return &v1.OrderCompleteRes{}, nil } -// GetPaymentChannels 获取支付渠道配置 +// GetPaymentChannels 获取所有渠道配置(单例模式,从 sys_options 读取) func (s *sAdmin) GetPaymentChannels(ctx context.Context, _ *v1.PaymentChannelListReq) (*v1.PaymentChannelListRes, error) { - channels := make([]map[string]any, 0) - err := dao.OrdPaymentChannels.Ctx(ctx). - OrderAsc("sort_order"). - Scan(&channels) - if err != nil { - return nil, err - } - return &v1.PaymentChannelListRes{List: channels}, nil + return &v1.PaymentChannelListRes{List: payment.ListAllChannels(ctx)}, nil } -// CreatePaymentChannel 创建支付渠道(默认禁用)。 -func (s *sAdmin) CreatePaymentChannel(ctx context.Context, req *v1.PaymentChannelCreateReq) (*v1.PaymentChannelCreateRes, error) { - result, err := dao.OrdPaymentChannels.Ctx(ctx).Insert(do.OrdPaymentChannels{ - Channel: req.Channel, - Name: req.Name, - Config: req.Config, - IsEnabled: false, - SortOrder: req.SortOrder, - }) +// SavePaymentChannel 保存指定渠道的配置(整体覆盖) +func (s *sAdmin) SavePaymentChannel(ctx context.Context, req *v1.PaymentChannelSaveReq) (*v1.PaymentChannelSaveRes, error) { + // 校验 config JSON 是否合法 + _, err := payment.ParseChannelConfig(req.Channel, req.Config) if err != nil { + return nil, common.NewBadRequestError("配置 JSON 格式无效: " + err.Error()) + } + if err := payment.SaveChannelConfig(ctx, req.Channel, req.Config); err != nil { return nil, err } - id, _ := result.LastInsertId() - return &v1.PaymentChannelCreateRes{ID: id}, nil -} - -// GetPaymentChannel 获取单个支付渠道详情。 -func (s *sAdmin) GetPaymentChannel(ctx context.Context, req *v1.PaymentChannelDetailReq) (*v1.PaymentChannelDetailRes, error) { - var ch entity.OrdPaymentChannels - err := dao.OrdPaymentChannels.Ctx(ctx). - Where("id", req.Id).Scan(&ch) - if err != nil { - return nil, err - } - if ch.Id == 0 { - return nil, common.NewNotFoundError("支付渠道") - } - return &v1.PaymentChannelDetailRes{Data: gconv.Map(ch)}, nil -} - -// UpdatePaymentChannel 更新支付渠道配置 -func (s *sAdmin) UpdatePaymentChannel(ctx context.Context, req *v1.PaymentChannelUpdateReq) (*v1.PaymentChannelUpdateRes, error) { - result, err := dao.OrdPaymentChannels.Ctx(ctx). - Where("id", req.Id). - Data(req.Update). - Update() - if err != nil { - return nil, err - } - rows, _ := result.RowsAffected() - if rows == 0 { - return nil, common.NewNotFoundError("支付渠道") - } - return &v1.PaymentChannelUpdateRes{}, nil -} - -// DeletePaymentChannel 删除支付渠道。 -func (s *sAdmin) DeletePaymentChannel(ctx context.Context, req *v1.PaymentChannelDeleteReq) (*v1.PaymentChannelDeleteRes, error) { - result, err := dao.OrdPaymentChannels.Ctx(ctx). - Where("id", req.Id).Delete() - if err != nil { - return nil, err - } - rows, _ := result.RowsAffected() - if rows == 0 { - return nil, common.NewNotFoundError("支付渠道") - } - return &v1.PaymentChannelDeleteRes{}, nil -} - -// TogglePaymentChannel 切换支付渠道启用/禁用状态。 -func (s *sAdmin) TogglePaymentChannel(ctx context.Context, req *v1.PaymentChannelToggleReq) (*v1.PaymentChannelToggleRes, error) { - var ch struct { - IsEnabled bool `json:"is_enabled"` - } - err := dao.OrdPaymentChannels.Ctx(ctx). - Where("id", req.Id).Scan(&ch) - if err != nil { - return nil, err - } - newState := !ch.IsEnabled - _, err = dao.OrdPaymentChannels.Ctx(ctx). - Where("id", req.Id). - Data(do.OrdPaymentChannels{ - IsEnabled: newState, - }).Update() - if err != nil { - return nil, err - } - return &v1.PaymentChannelToggleRes{IsEnabled: newState}, nil + return &v1.PaymentChannelSaveRes{}, nil } // GetPaymentSettings 获取全局支付设置。 diff --git a/internal/logic/billing/settlement.go b/internal/logic/billing/settlement.go index f4d3a90..acda5ce 100644 --- a/internal/logic/billing/settlement.go +++ b/internal/logic/billing/settlement.go @@ -97,7 +97,8 @@ func Settle(ctx context.Context, tenantID, userID, apiKeyID, channelID int64, // 6. 记录消费流水(一条汇总) recordTransaction(ctx, wallet.ID, tenantID, "consume", -actualCost, - fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.4f actual=%.4f", requestID, modelName, inputTokens, outputTokens, preDeductAmount, actualCost)) + fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.4f actual=%.4f", requestID, modelName, inputTokens, outputTokens, preDeductAmount, actualCost), + userID, requestID, modelName) return &SettlementResult{ PreDeductAmount: preDeductAmount, @@ -201,7 +202,8 @@ func SettleWithUsage(ctx context.Context, tenantID, userID, apiKeyID, channelID // 8. 记录消费流水(一条汇总) recordTransaction(ctx, wallet.ID, tenantID, "consume", -actualCost, - fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.4f actual=%.4f", requestID, modelName, breakdown.InputTokens, breakdown.OutputTokens, preDeductAmount, actualCost)) + fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.4f actual=%.4f", requestID, modelName, breakdown.InputTokens, breakdown.OutputTokens, preDeductAmount, actualCost), + userID, requestID, modelName) return settlementResult, nil } diff --git a/internal/logic/billing/task_billing.go b/internal/logic/billing/task_billing.go index 29ec9b0..7290b69 100644 --- a/internal/logic/billing/task_billing.go +++ b/internal/logic/billing/task_billing.go @@ -187,7 +187,8 @@ func (b *TaskBillingProviderImpl) SettleTaskSuccess(ctx context.Context, tenantI // 6. 记录消费流水(一条汇总) recordTransaction(ctx, wallet.ID, tenantID, "consume", -actualCost, - fmt.Sprintf("consume: %s model=%s pre_deduct=%.4f actual=%.4f", requestID, modelName, preDeductAmount, actualCost)) + fmt.Sprintf("consume: %s model=%s pre_deduct=%.4f actual=%.4f", requestID, modelName, preDeductAmount, actualCost), + userID, requestID, modelName) return result, nil } diff --git a/internal/logic/billing/wallet.go b/internal/logic/billing/wallet.go index f226a9f..9b8ab53 100644 --- a/internal/logic/billing/wallet.go +++ b/internal/logic/billing/wallet.go @@ -317,7 +317,7 @@ func GetPreDeductAmount(ctx context.Context, requestID string) (float64, bool) { } // recordTransaction 记录钱包流水 -func recordTransaction(ctx context.Context, walletID, tenantID int64, txnType string, amount float64, description string) { +func recordTransaction(ctx context.Context, walletID, tenantID int64, txnType string, amount float64, description string, userID int64, requestID string, modelName string) { type walletRow struct { Balance float64 `json:"balance"` FrozenBalance float64 `json:"frozen_balance"` @@ -342,6 +342,9 @@ func recordTransaction(ctx context.Context, walletID, tenantID int64, txnType st BalanceAfter: balanceAfter, FrozenAfter: frozenAfter, Description: description, + UserId: userID, + RequestId: requestID, + ModelName: modelName, }) if err != nil { g.Log().Errorf(ctx, "record transaction failed: %v", err) diff --git a/internal/logic/payment/callback.go b/internal/logic/payment/callback.go index ae7a94a..dcc55ca 100644 --- a/internal/logic/payment/callback.go +++ b/internal/logic/payment/callback.go @@ -2,13 +2,14 @@ package payment import ( "context" - do "github.com/qianfree/team-api/internal/model/do" "net/http" "sync" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/os/gtime" + do "github.com/qianfree/team-api/internal/model/do" + "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/common" ) @@ -57,33 +58,18 @@ func UnlockOrder(orderNo string) { } // ProcessCallback 统一回调处理流程。 -func ProcessCallback(ctx context.Context, r *http.Request, channelID int64) error { - // 1. 加载支付渠道配置 - var channel *struct { - Channel string `json:"channel"` - Config string `json:"config"` - IsEnabled bool `json:"is_enabled"` - } - err := dao.OrdPaymentChannels.Ctx(ctx). - Where("id", channelID).Scan(&channel) +// channelType 为渠道类型字符串(如 "epay")。 +func ProcessCallback(ctx context.Context, r *http.Request, channelType string) error { + // 1. 从 sys_options 加载渠道配置 + cfg, err := GetChannelConfigAndProvider(ctx, channelType) if err != nil { - return gerror.Wrapf(err, "加载支付渠道失败") - } - if channel == nil { - return common.NewNotFoundError("支付渠道") - } - if !channel.IsEnabled { - return common.NewBadRequestError("支付渠道已禁用") + return gerror.Wrapf(err, "加载支付渠道配置失败") } - // 2. 解析配置并获取 Provider - cfg, err := ParseChannelConfig(channel.Channel, channel.Config) - if err != nil { - return gerror.Wrapf(err, "解析渠道配置失败") - } - provider := GetProvider(channel.Channel) + // 2. 获取 Provider + provider := GetProvider(channelType) if provider == nil { - return gerror.Newf("不支持的支付渠道: %s", channel.Channel) + return gerror.Newf("不支持的支付渠道: %s", channelType) } // 3. 调用 Provider 验签并解析回调 diff --git a/internal/logic/payment/config.go b/internal/logic/payment/config.go index c8d58db..73ef800 100644 --- a/internal/logic/payment/config.go +++ b/internal/logic/payment/config.go @@ -1,47 +1,150 @@ package payment -import "encoding/json" +import ( + "context" + "encoding/json" + "fmt" + + "github.com/gogf/gf/v2/frame/g" + + lcommon "github.com/qianfree/team-api/internal/logic/common" +) + +// 已注册的渠道类型列表。 +var channelTypes = []string{"epay"} // EpayConfig 易支付渠道配置。 type EpayConfig struct { - PayAddress string `json:"pay_address"` // 易支付网关地址 - MerchantID string `json:"merchant_id"` // 商户 ID - MerchantKey string `json:"merchant_key"` // 商户密钥(用于签名) - PayMethods []EpayPayMethod `json:"pay_methods"` // 可用支付方式列表 + IsEnabled bool `json:"is_enabled"` + PayAddress string `json:"pay_address"` + MerchantID string `json:"merchant_id"` + MerchantKey string `json:"merchant_key"` + PayMethods []EpayPayMethod `json:"pay_methods"` } // EpayPayMethod 易支付子支付方式。 type EpayPayMethod struct { - Name string `json:"name"` // 显示名称(如"支付宝"、"微信支付") - Type string `json:"type"` // 方式标识(alipay/wxpay/qqpay 等) - Color string `json:"color"` // UI 颜色提示 -} - -// StripeConfig Stripe 渠道配置。 -type StripeConfig struct { - APISecret string `json:"api_secret"` // Stripe 密钥(sk_... / rk_...) - WebhookSecret string `json:"webhook_secret"` // Webhook 签名密钥(whsec_...) - PriceID string `json:"price_id"` // Stripe Price ID(用于 Checkout Session) - UnitPrice float64 `json:"unit_price"` // 每单位价格(USD),当 PriceID 为空时使用 - MinTopup int `json:"min_topup"` // 最低充值数量 - TestMode bool `json:"test_mode"` // 是否测试模式 -} - -// MockConfig Mock 支付渠道配置(开发环境)。 -type MockConfig struct { - AutoFulfill bool `json:"auto_fulfill"` // 是否自动履约 + Name string `json:"name"` + Type string `json:"type"` + Color string `json:"color"` } // GlobalPaymentSettings 全局支付设置。 type GlobalPaymentSettings struct { - AmountOptions []int `json:"amount_options"` // 预设充值金额选项 - AmountDiscount map[int]float64 `json:"amount_discount"` // 金额折扣映射,如 {100: 0.9} 表示充 100 享 9 折 - MinTopUp int `json:"min_topup"` // 最低充值金额 - Currency string `json:"currency"` // 默认货币代码 - CallbackBaseURL string `json:"callback_base_url"` // 回调基础 URL(为空则使用请求 Host) + AmountOptions []int `json:"amount_options"` + AmountDiscount map[int]float64 `json:"amount_discount"` + MinTopUp int `json:"min_topup"` + Currency string `json:"currency"` + CallbackBaseURL string `json:"callback_base_url"` } -// ParseChannelConfig 按渠道类型将 JSONB 配置字符串解析为对应的类型化结构体。 +// channelConfigKey 返回渠道在 sys_options 中的存储键。 +func channelConfigKey(channelType string) string { + return "payment_channel_" + channelType +} + +// LoadChannelConfig 从 sys_options 加载渠道配置,返回原始 JSON 字符串和解析后的配置。 +func LoadChannelConfig(ctx context.Context, channelType string) (jsonStr string, cfg interface{}, err error) { + jsonStr = lcommon.Config().GetOption(ctx, channelConfigKey(channelType)) + if jsonStr == "" { + cfg = defaultChannelConfig(channelType) + return + } + cfg, err = ParseChannelConfig(channelType, jsonStr) + return +} + +// SaveChannelConfig 将渠道配置 JSON 保存到 sys_options。 +func SaveChannelConfig(ctx context.Context, channelType string, configJSON string) error { + return lcommon.Config().SetOption(ctx, channelConfigKey(channelType), configJSON) +} + +// ListAllChannels 返回所有已注册渠道的配置(含未配置的,返回默认值)。 +func ListAllChannels(ctx context.Context) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(channelTypes)) + for _, ct := range channelTypes { + _, cfg, _ := LoadChannelConfig(ctx, ct) + item := map[string]interface{}{ + "channel": ct, + } + if c, ok := cfg.(*EpayConfig); ok { + item["is_enabled"] = c.IsEnabled + item["config"] = c + } + result = append(result, item) + } + return result +} + +// GetEnabledChannels 返回所有已启用的渠道及其支付方式(供租户 PaymentInfo 使用)。 +func GetEnabledChannels(ctx context.Context) []map[string]interface{} { + result := make([]map[string]interface{}, 0) + for _, ct := range channelTypes { + jsonStr, cfg, err := LoadChannelConfig(ctx, ct) + if err != nil { + g.Log().Warningf(ctx, "[Payment] channel %s config parse error: %v", ct, err) + continue + } + if cfg == nil { + g.Log().Debugf(ctx, "[Payment] channel %s config is nil, jsonStr=%q", ct, jsonStr) + continue + } + + var isEnabled bool + var payMethods []map[string]interface{} + + if c, ok := cfg.(*EpayConfig); ok { + isEnabled = c.IsEnabled + for _, m := range c.PayMethods { + payMethods = append(payMethods, map[string]interface{}{ + "type": m.Type, + "name": m.Name, + "color": m.Color, + }) + } + g.Log().Debugf(ctx, "[Payment] channel %s: enabled=%v, pay_methods_count=%d, json_len=%d", + ct, isEnabled, len(c.PayMethods), len(jsonStr)) + } else { + g.Log().Warningf(ctx, "[Payment] channel %s config type mismatch: %T", ct, cfg) + } + + if !isEnabled { + continue + } + + item := map[string]interface{}{ + "channel": ct, + } + if len(payMethods) > 0 { + item["pay_methods"] = payMethods + } + result = append(result, item) + } + g.Log().Debugf(ctx, "[Payment] GetEnabledChannels returned %d channels", len(result)) + return result +} + +// GetChannelConfigAndProvider 获取已启用渠道的配置和 Provider,未启用或不存在时返回错误。 +func GetChannelConfigAndProvider(ctx context.Context, channelType string) (interface{}, error) { + _, cfg, err := LoadChannelConfig(ctx, channelType) + if err != nil { + return nil, fmt.Errorf("支付渠道配置无效") + } + + var isEnabled bool + if c, ok := cfg.(*EpayConfig); ok { + isEnabled = c.IsEnabled + } else { + return nil, fmt.Errorf("不支持的支付渠道: %s", channelType) + } + + if !isEnabled { + return nil, fmt.Errorf("支付渠道 %s 未启用", channelType) + } + return cfg, nil +} + +// ParseChannelConfig 按渠道类型将 JSON 字符串解析为对应的类型化结构体。 func ParseChannelConfig(channelType string, configJSON string) (interface{}, error) { switch channelType { case "epay": @@ -50,19 +153,17 @@ func ParseChannelConfig(channelType string, configJSON string) (interface{}, err return nil, err } return cfg, nil - case "stripe": - cfg := &StripeConfig{} - if err := json.Unmarshal([]byte(configJSON), cfg); err != nil { - return nil, err - } - return cfg, nil - case "mock": - cfg := &MockConfig{} - if err := json.Unmarshal([]byte(configJSON), cfg); err != nil { - return nil, err - } - return cfg, nil default: return nil, nil } } + +// defaultChannelConfig 返回渠道类型的默认配置。 +func defaultChannelConfig(channelType string) interface{} { + switch channelType { + case "epay": + return &EpayConfig{PayMethods: []EpayPayMethod{}} + default: + return nil + } +} diff --git a/internal/logic/payment/mock.go b/internal/logic/payment/mock.go deleted file mode 100644 index 2781ef6..0000000 --- a/internal/logic/payment/mock.go +++ /dev/null @@ -1,73 +0,0 @@ -package payment - -import ( - "context" - do "github.com/qianfree/team-api/internal/model/do" - "net/http" - - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" - - "github.com/qianfree/team-api/internal/dao" -) - -// MockProvider Mock 支付渠道(开发环境)。 -type MockProvider struct{} - -func (p *MockProvider) CreatePayment(ctx context.Context, order *PaymentOrder, config interface{}) (*PaymentResult, error) { - return &PaymentResult{ - PaymentURL: "", - PaymentNo: "MOCK_" + order.OrderNo, - Params: map[string]string{"mock": "true"}, - IsRedirect: false, - }, nil -} - -func (p *MockProvider) HandleCallback(ctx context.Context, r *http.Request, config interface{}) (*CallbackResult, error) { - return &CallbackResult{ - OrderNo: r.URL.Query().Get("order_no"), - TradeNo: "MOCK_CALLBACK", - TradeStatus: "TRADE_SUCCESS", - Success: true, - }, nil -} - -func (p *MockProvider) Refund(ctx context.Context, refund *RefundRequest, config interface{}) error { - return nil -} - -func (p *MockProvider) QueryPaymentStatus(ctx context.Context, paymentNo string, config interface{}) (*PaymentStatus, error) { - return &PaymentStatus{ - TradeNo: paymentNo, - Status: "paid", - }, nil -} - -// MockPay 模拟支付(开发环境),向后兼容。 -func MockPay(ctx context.Context, orderID int64) error { - var status string - err := dao.OrdOrders.Ctx(ctx). - Where("id", orderID). - Fields("status"). - Scan(&status) - if err != nil { - return err - } - if status != "pending" { - return nil - } - - _, err = dao.OrdOrders.Ctx(ctx). - Where("id", orderID). - Where("status", "pending"). - Data(do.OrdOrders{ - Status: "paid", - PaidAt: gtime.Now(), - PaymentNo: "MOCK_" + g.Cfg().MustGet(ctx, "server.name").String(), - }).Update() - if err != nil { - return err - } - - return FulfillOrder(ctx, orderID) -} diff --git a/internal/logic/payment/provider.go b/internal/logic/payment/provider.go index 2581d30..71740a3 100644 --- a/internal/logic/payment/provider.go +++ b/internal/logic/payment/provider.go @@ -29,7 +29,7 @@ type PaymentOrder struct { Currency string // 货币代码 OrderType string // 订单类型(new_plan/renew/upgrade/recharge) Description string // 订单描述(用于支付页面展示) - PaymentMethod string // 子支付方式(alipay/wxpay/stripe 等) + PaymentMethod string // 子支付方式(alipay/wxpay 等) NotifyURL string // 异步回调地址 ReturnURL string // 支付完成前端跳转地址 } @@ -74,10 +74,6 @@ func GetProvider(channelType string) PaymentProvider { switch channelType { case "epay": return &EpayProvider{} - case "stripe": - return &StripeProvider{} - case "mock": - return &MockProvider{} default: return nil } diff --git a/internal/logic/payment/stripe.go b/internal/logic/payment/stripe.go deleted file mode 100644 index bd335fc..0000000 --- a/internal/logic/payment/stripe.go +++ /dev/null @@ -1,233 +0,0 @@ -package payment - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/util/gconv" - "github.com/stripe/stripe-go/v81" - "github.com/stripe/stripe-go/v81/checkout/session" - "github.com/stripe/stripe-go/v81/refund" - stripewebhook "github.com/stripe/stripe-go/v81/webhook" - - "github.com/qianfree/team-api/internal/consts" - lcommon "github.com/qianfree/team-api/internal/logic/common" -) - -// StripeProvider Stripe 支付渠道实现。 -type StripeProvider struct{} - -func (p *StripeProvider) CreatePayment(ctx context.Context, order *PaymentOrder, config interface{}) (*PaymentResult, error) { - cfg, ok := config.(*StripeConfig) - if !ok { - return nil, lcommon.NewBusinessError(consts.CodePaymentInvalidConfig, consts.MsgPaymentInvalidConfig) - } - if cfg.APISecret == "" { - return nil, lcommon.NewBusinessError(consts.CodePaymentInvalidConfig, "Stripe API 密钥未配置") - } - - stripe.Key = cfg.APISecret - - // 构建 Checkout Session 参数 - params := &stripe.CheckoutSessionParams{ - ClientReferenceID: stripe.String(order.OrderNo), - Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), - SuccessURL: stripe.String(order.ReturnURL + "?session_id={CHECKOUT_SESSION_ID}"), - CancelURL: stripe.String(order.ReturnURL + "?cancelled=true"), - Metadata: map[string]string{ - "tenant_id": gconv.String(order.TenantID), - "order_id": gconv.String(order.OrderID), - "order_no": order.OrderNo, - "order_type": order.OrderType, - }, - } - - // 使用 PriceID 或自定义单价 - if cfg.PriceID != "" { - params.LineItems = []*stripe.CheckoutSessionLineItemParams{ - { - Price: stripe.String(cfg.PriceID), - Quantity: stripe.Int64(int64(order.Amount)), - }, - } - } else { - unitPrice := cfg.UnitPrice - if unitPrice <= 0 { - unitPrice = 1.0 - } - // Stripe 金额以分为单位 - amountInCents := int64(order.Amount * unitPrice * 100) - params.LineItems = []*stripe.CheckoutSessionLineItemParams{ - { - PriceData: &stripe.CheckoutSessionLineItemPriceDataParams{ - Currency: stripe.String(strings.ToLower(order.Currency)), - ProductData: &stripe.CheckoutSessionLineItemPriceDataProductDataParams{ - Name: stripe.String(order.Description), - }, - UnitAmount: stripe.Int64(amountInCents), - }, - Quantity: stripe.Int64(1), - }, - } - } - - s, err := session.New(params) - if err != nil { - return nil, gerror.Wrapf(err, "创建 Stripe Checkout Session 失败") - } - - return &PaymentResult{ - PaymentURL: s.URL, - PaymentNo: s.ID, - Params: map[string]string{ - "session_id": s.ID, - "payment_intent": s.PaymentIntent.ID, - }, - IsRedirect: true, - }, nil -} - -func (p *StripeProvider) HandleCallback(ctx context.Context, r *http.Request, config interface{}) (*CallbackResult, error) { - cfg, ok := config.(*StripeConfig) - if !ok { - return nil, lcommon.NewBusinessError(consts.CodePaymentInvalidConfig, consts.MsgPaymentInvalidConfig) - } - if cfg.WebhookSecret == "" { - return nil, lcommon.NewBusinessError(consts.CodePaymentInvalidConfig, "Stripe Webhook 密钥未配置") - } - - body, err := ReadBody(r) - if err != nil { - return nil, gerror.Wrapf(err, "读取请求体失败") - } - - signature := r.Header.Get("Stripe-Signature") - event, err := stripewebhook.ConstructEventWithOptions(body, signature, cfg.WebhookSecret, stripewebhook.ConstructEventOptions{ - IgnoreAPIVersionMismatch: true, - }) - if err != nil { - return nil, gerror.Wrapf(err, "Stripe Webhook 签名验证失败") - } - - switch event.Type { - case stripe.EventTypeCheckoutSessionCompleted: - return p.handleSessionCompleted(event) - case stripe.EventTypeCheckoutSessionExpired: - return p.handleSessionExpired(event) - default: - return nil, lcommon.NewBusinessError(consts.CodePaymentCallbackFailed, fmt.Sprintf("不支持的 Stripe 事件类型: %s", event.Type)) - } -} - -func (p *StripeProvider) handleSessionCompleted(event stripe.Event) (*CallbackResult, error) { - var cs stripe.CheckoutSession - if err := json.Unmarshal(event.Data.Raw, &cs); err != nil { - return nil, gerror.Wrapf(err, "解析 CheckoutSession 数据失败") - } - - if cs.PaymentStatus != stripe.CheckoutSessionPaymentStatusPaid { - return &CallbackResult{ - OrderNo: cs.ClientReferenceID, - TradeNo: cs.ID, - TradeStatus: string(cs.PaymentStatus), - Success: false, - RawData: string(event.Data.Raw), - }, nil - } - - // 解析金额(Stripe 以分为单位) - var paidAmount float64 - if cs.AmountTotal > 0 { - paidAmount = float64(cs.AmountTotal) / 100.0 - } - - return &CallbackResult{ - OrderNo: cs.ClientReferenceID, - TradeNo: cs.ID, - TradeStatus: "TRADE_SUCCESS", - PaidAmount: paidAmount, - Success: true, - RawData: string(event.Data.Raw), - }, nil -} - -func (p *StripeProvider) handleSessionExpired(event stripe.Event) (*CallbackResult, error) { - var cs stripe.CheckoutSession - if err := json.Unmarshal(event.Data.Raw, &cs); err != nil { - return nil, gerror.Wrapf(err, "解析 CheckoutSession 数据失败") - } - - return &CallbackResult{ - OrderNo: cs.ClientReferenceID, - TradeNo: cs.ID, - TradeStatus: "expired", - Success: false, - RawData: string(event.Data.Raw), - }, nil -} - -func (p *StripeProvider) Refund(ctx context.Context, refundReq *RefundRequest, config interface{}) error { - cfg, ok := config.(*StripeConfig) - if !ok { - return lcommon.NewBusinessError(consts.CodePaymentInvalidConfig, consts.MsgPaymentInvalidConfig) - } - if cfg.APISecret == "" { - return lcommon.NewBusinessError(consts.CodePaymentInvalidConfig, "Stripe API 密钥未配置") - } - - stripe.Key = cfg.APISecret - - // 通过 Checkout Session ID 获取 PaymentIntent,然后发起退款 - params := &stripe.RefundParams{ - PaymentIntent: stripe.String(refundReq.PaymentNo), - Amount: stripe.Int64(int64(refundReq.RefundAmount * 100)), // 转为分 - Reason: stripe.String(string(stripe.RefundReasonRequestedByCustomer)), - Metadata: map[string]string{ - "order_id": gconv.String(refundReq.OrderID), - "order_no": refundReq.OrderNo, - "refund_reason": refundReq.Reason, - }, - } - - _, err := refund.New(params) - if err != nil { - return gerror.Wrapf(err, "Stripe 退款失败") - } - - return nil -} - -func (p *StripeProvider) QueryPaymentStatus(ctx context.Context, paymentNo string, config interface{}) (*PaymentStatus, error) { - cfg, ok := config.(*StripeConfig) - if !ok { - return nil, lcommon.NewBusinessError(consts.CodePaymentInvalidConfig, consts.MsgPaymentInvalidConfig) - } - - stripe.Key = cfg.APISecret - - s, err := session.Get(paymentNo, nil) - if err != nil { - return nil, gerror.Wrapf(err, "查询 Stripe Session 失败") - } - - status := "pending" - var paidAmount float64 - if s.PaymentStatus == stripe.CheckoutSessionPaymentStatusPaid { - status = "paid" - if s.AmountTotal > 0 { - paidAmount = float64(s.AmountTotal) / 100.0 - } - } else if s.Status == stripe.CheckoutSessionStatusExpired { - status = "expired" - } - - return &PaymentStatus{ - TradeNo: s.ID, - Status: status, - PaidAmount: paidAmount, - }, nil -} diff --git a/internal/logic/task/async_polling.go b/internal/logic/task/async_polling.go index 2643cac..bc2dd4f 100644 --- a/internal/logic/task/async_polling.go +++ b/internal/logic/task/async_polling.go @@ -111,6 +111,13 @@ func handleTimedOutTasks(ctx context.Context) { } g.Log().Infof(ctx, "poll: task %s timed out", t.PublicTaskID) + // 查询渠道信息并记录用量日志 + var ch *common.ChannelBasicInfo + if t.ChannelID > 0 { + ch, _ = DefaultAsyncProvider.GetChannelByID(ctx, t.ChannelID) + } + recordTaskUsage(t, ch, false, "task timed out", nil) + // 更新审计记录 recordTaskCompletionAudit(t, "TIMEOUT", "", nil) } @@ -323,7 +330,7 @@ func pollSingleTask(ctx context.Context, adaptor common.TaskAdaptor, channel *co g.Log().Infof(ctx, "poll: task %s completed", task.PublicTaskID) // 记录用量日志 - recordTaskUsage(task, true, "", settleResult) + recordTaskUsage(task, channel, true, "", settleResult) // 更新审计记录 recordTaskCompletionAudit(task, "SUCCESS", string(body), upstreamRespHeaders(resp)) @@ -343,7 +350,7 @@ func pollSingleTask(ctx context.Context, adaptor common.TaskAdaptor, channel *co g.Log().Infof(ctx, "poll: task %s failed: %s", task.PublicTaskID, task.FailReason) // 记录用量日志 - recordTaskUsage(task, false, task.FailReason, nil) + recordTaskUsage(task, channel, false, task.FailReason, nil) // 更新审计记录 recordTaskCompletionAudit(task, "FAILURE", string(body), upstreamRespHeaders(resp)) @@ -351,12 +358,20 @@ func pollSingleTask(ctx context.Context, adaptor common.TaskAdaptor, channel *co } // recordTaskUsage 异步记录视频/音乐等异步任务的用量日志 -func recordTaskUsage(task *common.AsyncTask, success bool, errMsg string, settleResult *common.SettlementResult) { +func recordTaskUsage(task *common.AsyncTask, channel *common.ChannelBasicInfo, success bool, errMsg string, settleResult *common.SettlementResult) { latencyMs := 0 if task.SubmitTime != nil && task.FinishTime != nil { latencyMs = int(task.FinishTime.Sub(*task.SubmitTime).Milliseconds()) } + // 提取渠道名称和类型 + var channelName string + var channelType int + if channel != nil { + channelName = channel.Name + channelType = channel.Type + } + status := "success" if !success { status = "error" @@ -367,6 +382,8 @@ func recordTaskUsage(task *common.AsyncTask, success bool, errMsg string, settle UserID: task.UserID, ApiKeyID: task.ApiKeyID, ChannelID: task.ChannelID, + ChannelName: channelName, + ChannelType: channelType, ModelName: task.ModelName, RelayMode: int(constant.RelayModeVideoGenerations), RequestType: 3, // async diff --git a/internal/logic/tenant/billing.go b/internal/logic/tenant/billing.go index 5d76878..9129dfa 100644 --- a/internal/logic/tenant/billing.go +++ b/internal/logic/tenant/billing.go @@ -91,14 +91,19 @@ func (s *sTenant) WalletTransactions(ctx context.Context, req *v1.TenantWalletTr FrozenAfter float64 `json:"frozen_after"` RelatedType string `json:"related_type"` Description string `json:"description"` + UserId int64 `json:"user_id"` + Username string `json:"username"` + RequestId string `json:"request_id"` + ModelName string `json:"model_name"` CreatedAt *gtime.Time `json:"created_at"` } var records []transactionRow var err error var total int - err = query.Fields("id, type, amount, balance_after, frozen_after, related_type, description, created_at"). - OrderDesc("created_at"). + err = query.Fields("bil_transactions.id, bil_transactions.type, bil_transactions.amount, bil_transactions.balance_after, bil_transactions.frozen_after, bil_transactions.related_type, bil_transactions.description, bil_transactions.user_id, COALESCE(tu.username, '') AS username, bil_transactions.request_id, bil_transactions.model_name, bil_transactions.created_at"). + LeftJoin("tnt_users tu", "bil_transactions.user_id = tu.id AND bil_transactions.tenant_id = tu.tenant_id"). + OrderDesc("bil_transactions.created_at"). Page(page, pageSize). ScanAndCount(&records, &total, false) if err != nil { @@ -118,6 +123,10 @@ func (s *sTenant) WalletTransactions(ctx context.Context, req *v1.TenantWalletTr "frozen_after": r.FrozenAfter, "related_type": r.RelatedType, "description": r.Description, + "user_id": r.UserId, + "username": r.Username, + "request_id": r.RequestId, + "model_name": r.ModelName, "created_at": r.CreatedAt, }) } @@ -505,6 +514,9 @@ func (s *sTenant) ExportWalletTransactions(ctx context.Context, req *v1.TenantWa Type string `json:"type"` Amount float64 `json:"amount"` BalanceAfter float64 `json:"balance_after"` + UserId int64 `json:"user_id"` + RequestId string `json:"request_id"` + ModelName string `json:"model_name"` Description string `json:"description"` CreatedAt *gtime.Time `json:"created_at"` } @@ -512,7 +524,7 @@ func (s *sTenant) ExportWalletTransactions(ctx context.Context, req *v1.TenantWa var records []transactionRow err := dao.BilTransactions.Ctx(ctx). Where("tenant_id", tenantID). - Fields("id, type, amount, balance_after, description, created_at"). + Fields("id, type, amount, balance_after, user_id, request_id, model_name, description, created_at"). OrderDesc("created_at"). Limit(1000).Offset(offset). Scan(&records) diff --git a/internal/logic/tenant/order.go b/internal/logic/tenant/order.go index adb6081..228910f 100644 --- a/internal/logic/tenant/order.go +++ b/internal/logic/tenant/order.go @@ -3,9 +3,6 @@ package tenant import ( "context" "fmt" - "github.com/gogf/gf/v2/util/gconv" - "github.com/qianfree/team-api/internal/dao" - do "github.com/qianfree/team-api/internal/model/do" "time" v1 "github.com/qianfree/team-api/api/tenant/v1" @@ -13,6 +10,9 @@ import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" + "github.com/qianfree/team-api/internal/dao" + do "github.com/qianfree/team-api/internal/model/do" + lcommon "github.com/qianfree/team-api/internal/logic/common" "github.com/qianfree/team-api/internal/logic/payment" "github.com/qianfree/team-api/internal/utility/export" @@ -109,7 +109,6 @@ func (s *sTenant) OrderCreate(ctx context.Context, req *v1.TenantOrderCreateReq) amount = plan.MonthlyPrice * float64(months) } - // 生成订单号 orderNo := fmt.Sprintf("ORD%s%04d", time.Now().Format("20060102150405"), time.Now().UnixNano()%10000) result, err := dao.OrdOrders.Ctx(ctx).Insert(do.OrdOrders{ @@ -122,7 +121,7 @@ func (s *sTenant) OrderCreate(ctx context.Context, req *v1.TenantOrderCreateReq) DiscountAmount: 0, FinalAmount: amount, Currency: "CNY", - PaymentChannel: "mock", + PaymentChannel: "", Status: "pending", ExpiredAt: gtime.Now().Add(30 * time.Minute), Description: fmt.Sprintf("Order for new_plan"), @@ -174,56 +173,36 @@ func (s *sTenant) OrderPay(ctx context.Context, req *v1.TenantOrderPayReq) (*v1. tenantID := ctxTenantID(ctx) orderID := req.Id - // channelID=0 走 mock 支付(向后兼容) - if req.PaymentChannelID == 0 { - if err := payment.MockPay(ctx, orderID); err != nil { - return nil, err - } - return &v1.TenantOrderPayRes{Data: g.Map{"status": "paid"}}, nil + // 支付渠道 + if req.PaymentChannel == "" { + return nil, lcommon.NewBusinessError(422, "请选择支付渠道") } - // 真实支付渠道 + // orderNo, finalAmount, currency, orderType, description, err := getOrderForPay(ctx, tenantID, orderID) if err != nil { return nil, err } - var channel *struct { - Channel string `json:"channel"` - Config string `json:"config"` - IsEnabled bool `json:"is_enabled"` - } - err = dao.OrdPaymentChannels.Ctx(ctx). - Where("id", req.PaymentChannelID).Scan(&channel) + cfg, err := payment.GetChannelConfigAndProvider(ctx, req.PaymentChannel) if err != nil { - return nil, err - } - if channel == nil { - return nil, lcommon.NewBusinessError(422, "支付渠道不存在") - } - if !channel.IsEnabled { - return nil, lcommon.NewBusinessError(422, "支付渠道已禁用") + return nil, lcommon.NewBusinessError(422, err.Error()) } - updateOrderPaymentChannel(ctx, orderID, channel.Channel, req.PaymentMethod) - - provider := payment.GetProvider(channel.Channel) + provider := payment.GetProvider(req.PaymentChannel) if provider == nil { return nil, lcommon.NewBusinessError(422, "不支持的支付渠道") } - cfg, err := payment.ParseChannelConfig(channel.Channel, channel.Config) - if err != nil { - return nil, err - } + updateOrderPaymentChannel(ctx, orderID, req.PaymentChannel, req.PaymentMethod) settings, _ := payment.GetGlobalPaymentSettings(ctx) - baseURL := settings.CallbackBaseURL - notifyURL := baseURL + "/api/payment/callback/" + gconv.String(req.PaymentChannelID) - if channel.Channel == "stripe" { - notifyURL = baseURL + "/api/payment/stripe/webhook/" + gconv.String(req.PaymentChannelID) + baseURL := "" + if settings != nil { + baseURL = settings.CallbackBaseURL } - + notifyURL := baseURL + "/api/payment/callback/" + req.PaymentChannel + returnURL := baseURL + "/api/payment/epay/return" payOrder := &payment.PaymentOrder{ OrderID: orderID, OrderNo: orderNo, @@ -234,6 +213,7 @@ func (s *sTenant) OrderPay(ctx context.Context, req *v1.TenantOrderPayReq) (*v1. Description: description, PaymentMethod: req.PaymentMethod, NotifyURL: notifyURL, + ReturnURL: returnURL, } result, err := provider.CreatePayment(ctx, payOrder, cfg) @@ -255,19 +235,20 @@ func (s *sTenant) PaymentInfo(ctx context.Context, req *v1.TenantPaymentInfoReq) if role != "owner" && role != "admin" { return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") } - channels := make([]map[string]any, 0) - err := dao.OrdPaymentChannels.Ctx(ctx). - Where("is_enabled", true). - OrderAsc("sort_order"). - Fields("id, channel, name, payment_type"). - Scan(&channels) - if err != nil { - return nil, err + + res := &v1.TenantPaymentInfoRes{ + Channels: payment.GetEnabledChannels(ctx), } - return &v1.TenantPaymentInfoRes{Data: g.Map{ - "channels": channels, - }}, nil + settings, _ := payment.GetGlobalPaymentSettings(ctx) + if settings != nil { + res.AmountOptions = settings.AmountOptions + res.AmountDiscount = settings.AmountDiscount + res.MinTopUp = settings.MinTopUp + res.Currency = settings.Currency + } + + return res, nil } // getOrderForPay 获取待支付订单信息(供 OrderPay 内部调用) @@ -306,6 +287,105 @@ func updateOrderPaymentChannel(ctx context.Context, orderID int64, channel, paym }).Update() } +// RechargeCreate 创建充值订单并发起支付(一步完成) +func (s *sTenant) RechargeCreate(ctx context.Context, req *v1.TenantRechargeCreateReq) (*v1.TenantRechargeCreateRes, error) { + role := ctxUserRole(ctx) + if role != "owner" && role != "admin" { + return nil, lcommon.NewForbiddenError("需要 owner 或 admin 权限") + } + tenantID := ctxTenantID(ctx) + userID := ctxUserID(ctx) + + // 1. 校验金额 + settings, _ := payment.GetGlobalPaymentSettings(ctx) + minTopup := 1 + if settings != nil && settings.MinTopUp > 0 { + minTopup = settings.MinTopUp + } + if req.Amount < float64(minTopup) { + return nil, lcommon.NewBusinessError(422, fmt.Sprintf("充值金额不能小于 %d", minTopup)) + } + + // 2. 从 sys_options 加载渠道配置 + cfg, err := payment.GetChannelConfigAndProvider(ctx, req.PaymentChannel) + if err != nil { + return nil, lcommon.NewBusinessError(422, err.Error()) + } + + provider := payment.GetProvider(req.PaymentChannel) + if provider == nil { + return nil, lcommon.NewBusinessError(422, "不支持的支付渠道") + } + + // 3. 计算折扣后金额 + finalAmount := req.Amount + if settings != nil { + if discount, ok := settings.AmountDiscount[int(req.Amount)]; ok && discount > 0 { + finalAmount = req.Amount * discount + } + } + + // 4. 生成订单号并创建订单 + orderNo := fmt.Sprintf("RCH%s%04d", time.Now().Format("20060102150405"), time.Now().UnixNano()%10000) + description := fmt.Sprintf("钱包充值 ¥%.2f", req.Amount) + + result, err := dao.OrdOrders.Ctx(ctx).Insert(do.OrdOrders{ + OrderNo: orderNo, + TenantId: tenantID, + UserId: userID, + OrderType: "recharge", + Amount: req.Amount, + DiscountAmount: req.Amount - finalAmount, + FinalAmount: finalAmount, + Currency: "CNY", + PaymentChannel: req.PaymentChannel, + PaymentMethod: req.PaymentMethod, + Status: "pending", + ExpiredAt: gtime.Now().Add(30 * time.Minute), + Description: description, + }) + if err != nil { + return nil, err + } + orderID, _ := result.LastInsertId() + + // 5. 构建回调 URL 和 ReturnURL + baseURL := "" + if settings != nil { + baseURL = settings.CallbackBaseURL + } + notifyURL := baseURL + "/api/payment/callback/" + req.PaymentChannel + returnURL := baseURL + "/api/payment/epay/return" + // 6. 调用 Provider 生成支付链接 + payResult, err := provider.CreatePayment(ctx, &payment.PaymentOrder{ + OrderID: orderID, + OrderNo: orderNo, + TenantID: tenantID, + Amount: finalAmount, + Currency: "CNY", + OrderType: "recharge", + Description: description, + PaymentMethod: req.PaymentMethod, + NotifyURL: notifyURL, + ReturnURL: returnURL, + }, cfg) + if err != nil { + dao.OrdOrders.Ctx(ctx).Where("id", orderID). + Data(do.OrdOrders{Status: "cancelled"}).Update() + return nil, err + } + + return &v1.TenantRechargeCreateRes{Data: g.Map{ + "order_id": orderID, + "order_no": orderNo, + "payment_url": payResult.PaymentURL, + "payment_no": payResult.PaymentNo, + "params": payResult.Params, + "is_redirect": payResult.IsRedirect, + "final_amount": finalAmount, + }}, nil +} + // ExportOrders exports the tenant order list as CSV or Excel. func (s *sTenant) ExportOrders(ctx context.Context, req *v1.TenantOrderExportReq) (*v1.TenantOrderExportRes, error) { role := ctxUserRole(ctx) diff --git a/internal/model/do/bil_transactions.go b/internal/model/do/bil_transactions.go index ab3f10c..40a7a60 100644 --- a/internal/model/do/bil_transactions.go +++ b/internal/model/do/bil_transactions.go @@ -15,7 +15,7 @@ type BilTransactions struct { Id any // 主键ID TenantId any // 租户ID WalletId any // 关联钱包ID - Type any // 类型:recharge(充值)/ pre_deduct(预扣)/ settle(结算)/ refund(退款)/ adjust(调整)/ freeze(冻结)/ unfreeze(解冻) + Type any // 类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃) Amount any // 变动金额(正数=收入,负数=支出) BalanceAfter any // 变动后总余额 FrozenAfter any // 变动后冻结余额 @@ -24,4 +24,7 @@ type BilTransactions struct { Description any // 交易描述 CreatedAt *gtime.Time // 创建时间 UpdatedAt *gtime.Time // 更新时间 + UserId any // 关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空) + RequestId any // 关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空) + ModelName any // 关联模型名(consume 类型为调用的模型名,其他类型为空) } diff --git a/internal/model/do/ord_payment_channels.go b/internal/model/do/ord_payment_channels.go deleted file mode 100644 index ab5b895..0000000 --- a/internal/model/do/ord_payment_channels.go +++ /dev/null @@ -1,26 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package do - -import ( - "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/os/gtime" -) - -// OrdPaymentChannels is the golang structure of table ord_payment_channels for DAO operations like Where/Data. -type OrdPaymentChannels struct { - g.Meta `orm:"table:ord_payment_channels, do:true"` - Id any // 主键ID - Channel any // 渠道标识(alipay/wechat/stripe/mock) - Name any // 显示名称 - Config any // 渠道配置(JSONB,含 API 密钥等敏感信息) - IsEnabled any // 是否启用 - SortOrder any // 排序权重 - CreatedAt *gtime.Time // 创建时间 - UpdatedAt *gtime.Time // 更新时间 - PaymentType any // 子支付方式(alipay/wxpay 等,空表示该渠道支持所有方式) - CallbackUrl any // 支付回调地址覆盖(为空则使用系统默认) - ReturnUrl any // 支付完成后前端跳转地址覆盖 -} diff --git a/internal/model/do/tsk_model_tasks.go b/internal/model/do/tsk_model_tasks.go index 824cc50..ae22026 100644 --- a/internal/model/do/tsk_model_tasks.go +++ b/internal/model/do/tsk_model_tasks.go @@ -14,7 +14,6 @@ type TskModelTasks struct { g.Meta `orm:"table:tsk_model_tasks, do:true"` Id any // PublicTaskId any // - RequestId any // 任务提交时的原始请求 ID Platform any // Action any // Status any // @@ -37,4 +36,5 @@ type TskModelTasks struct { FinishTime *gtime.Time // CreatedAt *gtime.Time // UpdatedAt *gtime.Time // + RequestId any // 任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id } diff --git a/internal/model/entity/bil_transactions.go b/internal/model/entity/bil_transactions.go index 314735f..9b34e39 100644 --- a/internal/model/entity/bil_transactions.go +++ b/internal/model/entity/bil_transactions.go @@ -10,16 +10,19 @@ import ( // BilTransactions is the golang structure for table bil_transactions. type BilTransactions struct { - Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID - TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID - WalletId int64 `json:"wallet_id" orm:"wallet_id" description:"关联钱包ID"` // 关联钱包ID - Type string `json:"type" orm:"type" description:"类型:recharge(充值)/ pre_deduct(预扣)/ settle(结算)/ refund(退款)/ adjust(调整)/ freeze(冻结)/ unfreeze(解冻)"` // 类型:recharge(充值)/ pre_deduct(预扣)/ settle(结算)/ refund(退款)/ adjust(调整)/ freeze(冻结)/ unfreeze(解冻) - Amount float64 `json:"amount" orm:"amount" description:"变动金额(正数=收入,负数=支出)"` // 变动金额(正数=收入,负数=支出) - BalanceAfter float64 `json:"balance_after" orm:"balance_after" description:"变动后总余额"` // 变动后总余额 - FrozenAfter float64 `json:"frozen_after" orm:"frozen_after" description:"变动后冻结余额"` // 变动后冻结余额 - RelatedId int64 `json:"related_id" orm:"related_id" description:"关联业务ID(如计费记录ID、订单ID等)"` // 关联业务ID(如计费记录ID、订单ID等) - RelatedType string `json:"related_type" orm:"related_type" description:"关联业务类型:billing_record / order / refund / adjustment / redemption"` // 关联业务类型:billing_record / order / refund / adjustment / redemption - Description string `json:"description" orm:"description" description:"交易描述"` // 交易描述 - CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 - UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 + Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID + WalletId int64 `json:"wallet_id" orm:"wallet_id" description:"关联钱包ID"` // 关联钱包ID + Type string `json:"type" orm:"type" description:"类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃)"` // 类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃) + Amount float64 `json:"amount" orm:"amount" description:"变动金额(正数=收入,负数=支出)"` // 变动金额(正数=收入,负数=支出) + BalanceAfter float64 `json:"balance_after" orm:"balance_after" description:"变动后总余额"` // 变动后总余额 + FrozenAfter float64 `json:"frozen_after" orm:"frozen_after" description:"变动后冻结余额"` // 变动后冻结余额 + RelatedId int64 `json:"related_id" orm:"related_id" description:"关联业务ID(如计费记录ID、订单ID等)"` // 关联业务ID(如计费记录ID、订单ID等) + RelatedType string `json:"related_type" orm:"related_type" description:"关联业务类型:billing_record / order / refund / adjustment / redemption"` // 关联业务类型:billing_record / order / refund / adjustment / redemption + Description string `json:"description" orm:"description" description:"交易描述"` // 交易描述 + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 + UserId int64 `json:"user_id" orm:"user_id" description:"关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空)"` // 关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空) + RequestId string `json:"request_id" orm:"request_id" description:"关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空)"` // 关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空) + ModelName string `json:"model_name" orm:"model_name" description:"关联模型名(consume 类型为调用的模型名,其他类型为空)"` // 关联模型名(consume 类型为调用的模型名,其他类型为空) } diff --git a/internal/model/entity/ord_payment_channels.go b/internal/model/entity/ord_payment_channels.go deleted file mode 100644 index 78776cc..0000000 --- a/internal/model/entity/ord_payment_channels.go +++ /dev/null @@ -1,24 +0,0 @@ -// ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// ================================================================================= - -package entity - -import ( - "github.com/gogf/gf/v2/os/gtime" -) - -// OrdPaymentChannels is the golang structure for table ord_payment_channels. -type OrdPaymentChannels struct { - Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID - Channel string `json:"channel" orm:"channel" description:"渠道标识(alipay/wechat/stripe/mock)"` // 渠道标识(alipay/wechat/stripe/mock) - Name string `json:"name" orm:"name" description:"显示名称"` // 显示名称 - Config string `json:"config" orm:"config" description:"渠道配置(JSONB,含 API 密钥等敏感信息)"` // 渠道配置(JSONB,含 API 密钥等敏感信息) - IsEnabled bool `json:"is_enabled" orm:"is_enabled" description:"是否启用"` // 是否启用 - SortOrder int `json:"sort_order" orm:"sort_order" description:"排序权重"` // 排序权重 - CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 - UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 - PaymentType string `json:"payment_type" orm:"payment_type" description:"子支付方式(alipay/wxpay 等,空表示该渠道支持所有方式)"` // 子支付方式(alipay/wxpay 等,空表示该渠道支持所有方式) - CallbackUrl string `json:"callback_url" orm:"callback_url" description:"支付回调地址覆盖(为空则使用系统默认)"` // 支付回调地址覆盖(为空则使用系统默认) - ReturnUrl string `json:"return_url" orm:"return_url" description:"支付完成后前端跳转地址覆盖"` // 支付完成后前端跳转地址覆盖 -} diff --git a/internal/model/entity/tsk_model_tasks.go b/internal/model/entity/tsk_model_tasks.go index 867d802..585d6c6 100644 --- a/internal/model/entity/tsk_model_tasks.go +++ b/internal/model/entity/tsk_model_tasks.go @@ -10,29 +10,29 @@ import ( // TskModelTasks is the golang structure for table tsk_model_tasks. type TskModelTasks struct { - Id int64 `json:"id" orm:"id" description:""` // - PublicTaskId string `json:"public_task_id" orm:"public_task_id" description:""` // - RequestId string `json:"request_id" orm:"request_id" description:"任务提交时的原始请求 ID"` // - Platform string `json:"platform" orm:"platform" description:""` // - Action string `json:"action" orm:"action" description:""` // - Status string `json:"status" orm:"status" description:""` // - Progress string `json:"progress" orm:"progress" description:""` // - FailReason string `json:"fail_reason" orm:"fail_reason" description:""` // - TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` // - UserId int64 `json:"user_id" orm:"user_id" description:""` // - ApiKeyId int64 `json:"api_key_id" orm:"api_key_id" description:""` // - ChannelId int64 `json:"channel_id" orm:"channel_id" description:""` // - ModelName string `json:"model_name" orm:"model_name" description:""` // - UpstreamModel string `json:"upstream_model" orm:"upstream_model" description:""` // - PreDeductAmount float64 `json:"pre_deduct_amount" orm:"pre_deduct_amount" description:""` // - ActualCost float64 `json:"actual_cost" orm:"actual_cost" description:""` // - BillingSettled bool `json:"billing_settled" orm:"billing_settled" description:""` // - ResultUrl string `json:"result_url" orm:"result_url" description:""` // - Data string `json:"data" orm:"data" description:""` // - PrivateData string `json:"private_data" orm:"private_data" description:""` // - SubmitTime *gtime.Time `json:"submit_time" orm:"submit_time" description:""` // - StartTime *gtime.Time `json:"start_time" orm:"start_time" description:""` // - FinishTime *gtime.Time `json:"finish_time" orm:"finish_time" description:""` // - CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // - UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // + Id int64 `json:"id" orm:"id" description:""` // + PublicTaskId string `json:"public_task_id" orm:"public_task_id" description:""` // + Platform string `json:"platform" orm:"platform" description:""` // + Action string `json:"action" orm:"action" description:""` // + Status string `json:"status" orm:"status" description:""` // + Progress string `json:"progress" orm:"progress" description:""` // + FailReason string `json:"fail_reason" orm:"fail_reason" description:""` // + TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` // + UserId int64 `json:"user_id" orm:"user_id" description:""` // + ApiKeyId int64 `json:"api_key_id" orm:"api_key_id" description:""` // + ChannelId int64 `json:"channel_id" orm:"channel_id" description:""` // + ModelName string `json:"model_name" orm:"model_name" description:""` // + UpstreamModel string `json:"upstream_model" orm:"upstream_model" description:""` // + PreDeductAmount float64 `json:"pre_deduct_amount" orm:"pre_deduct_amount" description:""` // + ActualCost float64 `json:"actual_cost" orm:"actual_cost" description:""` // + BillingSettled bool `json:"billing_settled" orm:"billing_settled" description:""` // + ResultUrl string `json:"result_url" orm:"result_url" description:""` // + Data string `json:"data" orm:"data" description:""` // + PrivateData string `json:"private_data" orm:"private_data" description:""` // + SubmitTime *gtime.Time `json:"submit_time" orm:"submit_time" description:""` // + StartTime *gtime.Time `json:"start_time" orm:"start_time" description:""` // + FinishTime *gtime.Time `json:"finish_time" orm:"finish_time" description:""` // + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // + RequestId string `json:"request_id" orm:"request_id" description:"任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id"` // 任务提交时的原始请求 ID(req_xxxxx),关联 aud_request_logs.request_id } diff --git a/internal/service/admin.go b/internal/service/admin.go index 3a099d2..6e2b311 100644 --- a/internal/service/admin.go +++ b/internal/service/admin.go @@ -248,18 +248,10 @@ type ( RefundOrder(ctx context.Context, req *v1.OrderRefundReq) (*v1.OrderRefundRes, error) // OrderComplete 手动完成订单 OrderComplete(ctx context.Context, req *v1.OrderCompleteReq) (*v1.OrderCompleteRes, error) - // GetPaymentChannels 获取支付渠道配置 + // GetPaymentChannels 获取所有渠道配置(单例模式,从 sys_options 读取) GetPaymentChannels(ctx context.Context, _ *v1.PaymentChannelListReq) (*v1.PaymentChannelListRes, error) - // CreatePaymentChannel 创建支付渠道(默认禁用)。 - CreatePaymentChannel(ctx context.Context, req *v1.PaymentChannelCreateReq) (*v1.PaymentChannelCreateRes, error) - // GetPaymentChannel 获取单个支付渠道详情。 - GetPaymentChannel(ctx context.Context, req *v1.PaymentChannelDetailReq) (*v1.PaymentChannelDetailRes, error) - // UpdatePaymentChannel 更新支付渠道配置 - UpdatePaymentChannel(ctx context.Context, req *v1.PaymentChannelUpdateReq) (*v1.PaymentChannelUpdateRes, error) - // DeletePaymentChannel 删除支付渠道。 - DeletePaymentChannel(ctx context.Context, req *v1.PaymentChannelDeleteReq) (*v1.PaymentChannelDeleteRes, error) - // TogglePaymentChannel 切换支付渠道启用/禁用状态。 - TogglePaymentChannel(ctx context.Context, req *v1.PaymentChannelToggleReq) (*v1.PaymentChannelToggleRes, error) + // SavePaymentChannel 保存指定渠道的配置(整体覆盖) + SavePaymentChannel(ctx context.Context, req *v1.PaymentChannelSaveReq) (*v1.PaymentChannelSaveRes, error) // GetPaymentSettings 获取全局支付设置。 GetPaymentSettings(ctx context.Context, _ *v1.PaymentSettingsGetReq) (*v1.PaymentSettingsGetRes, error) // UpdatePaymentSettings 更新全局支付设置。 diff --git a/internal/service/tenant.go b/internal/service/tenant.go index 04151c7..b30ecf0 100644 --- a/internal/service/tenant.go +++ b/internal/service/tenant.go @@ -203,6 +203,8 @@ type ( OrderPay(ctx context.Context, req *v1.TenantOrderPayReq) (*v1.TenantOrderPayRes, error) // PaymentInfo 获取租户可用的支付信息(渠道列表、金额选项、折扣) PaymentInfo(ctx context.Context, req *v1.TenantPaymentInfoReq) (*v1.TenantPaymentInfoRes, error) + // RechargeCreate 创建充值订单并发起支付(一步完成) + RechargeCreate(ctx context.Context, req *v1.TenantRechargeCreateReq) (*v1.TenantRechargeCreateRes, error) // ExportOrders exports the tenant order list as CSV or Excel. ExportOrders(ctx context.Context, req *v1.TenantOrderExportReq) (*v1.TenantOrderExportRes, error) // GetOrgInfo returns tenant organization info. diff --git a/migrations/000005_add_transaction_context_fields.sql b/migrations/000005_add_transaction_context_fields.sql new file mode 100644 index 0000000..9923eb5 --- /dev/null +++ b/migrations/000005_add_transaction_context_fields.sql @@ -0,0 +1,22 @@ +-- +goose Up +-- bil_transactions 增加关联字段,支持按用户、模型、请求维度查询和导出交易记录 + +ALTER TABLE bil_transactions ADD COLUMN IF NOT EXISTS user_id BIGINT; +ALTER TABLE bil_transactions ADD COLUMN IF NOT EXISTS request_id VARCHAR(64); +ALTER TABLE bil_transactions ADD COLUMN IF NOT EXISTS model_name VARCHAR(100); + +COMMENT ON COLUMN bil_transactions.user_id IS '关联用户ID(consume 类型为实际消费用户,recharge 类型为操作用户,adjust 类型为空)'; +COMMENT ON COLUMN bil_transactions.request_id IS '关联请求ID(consume 类型对应 API 调用的 request_id,其他类型为空)'; +COMMENT ON COLUMN bil_transactions.model_name IS '关联模型名(consume 类型为调用的模型名,其他类型为空)'; + +-- 按用户查询交易记录 +CREATE INDEX IF NOT EXISTS idx_bil_transactions_user_created ON bil_transactions (tenant_id, user_id, created_at DESC); +-- 按模型查询消费记录 +CREATE INDEX IF NOT EXISTS idx_bil_transactions_model_created ON bil_transactions (tenant_id, model_name, created_at DESC) WHERE model_name IS NOT NULL; + +-- +goose Down +DROP INDEX IF EXISTS idx_bil_transactions_model_created; +DROP INDEX IF EXISTS idx_bil_transactions_user_created; +ALTER TABLE bil_transactions DROP COLUMN IF EXISTS model_name; +ALTER TABLE bil_transactions DROP COLUMN IF EXISTS request_id; +ALTER TABLE bil_transactions DROP COLUMN IF EXISTS user_id; diff --git a/migrations/000006_drop_ord_payment_channels.sql b/migrations/000006_drop_ord_payment_channels.sql new file mode 100644 index 0000000..2430d54 --- /dev/null +++ b/migrations/000006_drop_ord_payment_channels.sql @@ -0,0 +1,30 @@ +-- +goose Up +DROP TABLE IF EXISTS ord_payment_channels; + +-- +goose Down +CREATE TABLE ord_payment_channels ( + id BIGSERIAL PRIMARY KEY, + channel VARCHAR(20) NOT NULL, + name VARCHAR(100) NOT NULL, + config JSONB DEFAULT '{}' NOT NULL, + is_enabled BOOLEAN DEFAULT false NOT NULL, + sort_order INTEGER DEFAULT 0 NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + payment_type VARCHAR(20) DEFAULT '' NOT NULL, + callback_url VARCHAR(500) DEFAULT '' NOT NULL, + return_url VARCHAR(500) DEFAULT '' NOT NULL +); +CREATE INDEX idx_ord_payment_channels_channel ON ord_payment_channels USING btree (channel); +COMMENT ON TABLE ord_payment_channels IS '支付渠道配置'; +COMMENT ON COLUMN ord_payment_channels.id IS '主键ID'; +COMMENT ON COLUMN ord_payment_channels.channel IS '渠道标识(alipay/wechat/stripe/mock)'; +COMMENT ON COLUMN ord_payment_channels.name IS '显示名称'; +COMMENT ON COLUMN ord_payment_channels.config IS '渠道配置(JSONB,含 API 密钥等敏感信息)'; +COMMENT ON COLUMN ord_payment_channels.is_enabled IS '是否启用'; +COMMENT ON COLUMN ord_payment_channels.sort_order IS '排序权重'; +COMMENT ON COLUMN ord_payment_channels.created_at IS '创建时间'; +COMMENT ON COLUMN ord_payment_channels.updated_at IS '更新时间'; +COMMENT ON COLUMN ord_payment_channels.payment_type IS '子支付方式(alipay/wxpay 等,空表示该渠道支持所有方式)'; +COMMENT ON COLUMN ord_payment_channels.callback_url IS '支付回调地址覆盖(为空则使用系统默认)'; +COMMENT ON COLUMN ord_payment_channels.return_url IS '支付完成后前端跳转地址覆盖'; diff --git a/web/admin/src/views/finance/PaymentSettingsPage.vue b/web/admin/src/views/finance/PaymentSettingsPage.vue index d2a761a..1244311 100644 --- a/web/admin/src/views/finance/PaymentSettingsPage.vue +++ b/web/admin/src/views/finance/PaymentSettingsPage.vue @@ -1,302 +1,90 @@ + +