diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 7b5eac2..ceb530d 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -172,6 +172,9 @@ var ( } return nil }) + cs.Register("order_expiration", "*/5 * * * *", func(ctx context.Context) error { + return task.ExpirePendingOrders(ctx) + }) cs.StartBackground(ctx) // Initialize update manager diff --git a/internal/handler/public/payment.go b/internal/handler/public/payment.go index 260e98c..a332e2b 100644 --- a/internal/handler/public/payment.go +++ b/internal/handler/public/payment.go @@ -1,6 +1,8 @@ package public import ( + "time" + "github.com/gogf/gf/v2/net/ghttp" "github.com/qianfree/team-api/internal/logic/payment" @@ -31,10 +33,24 @@ func HandlePaymentCallback(r *ghttp.Request) { // 仅查询订单状态决定跳转目标,履约由异步回调完成。 func HandlePaymentEpayReturn(r *ghttp.Request) { orderNo := r.GetQuery("out_trade_no").String() - paid := payment.QueryOrderPaid(r.Context(), orderNo) + if orderNo == "" { + r.Response.RedirectTo("/console/wallet?pay=fail") + return + } + + ctx := r.Context() + // 异步 notify 可能晚于浏览器回跳到达:最多轮询约 4 秒等待回调入账。 + // 仍未到账不一定是失败(回调可能仍在途),引导到"处理中"页由前端继续刷新, + // 避免给用户展示误导性的"失败"。 + paid := payment.QueryOrderPaid(ctx, orderNo) + for i := 0; !paid && i < 8; i++ { + time.Sleep(500 * time.Millisecond) + paid = payment.QueryOrderPaid(ctx, orderNo) + } + if paid { r.Response.RedirectTo("/console/wallet?pay=success") return } - r.Response.RedirectTo("/console/wallet?pay=fail") + r.Response.RedirectTo("/console/wallet?pay=processing") } diff --git a/internal/logic/common/config.go b/internal/logic/common/config.go index e31ff68..b55d87e 100644 --- a/internal/logic/common/config.go +++ b/internal/logic/common/config.go @@ -197,9 +197,18 @@ func (s *ConfigService) SetOption(ctx context.Context, key, value string) error } if count > 0 { + data := do.SysOptions{Value: value} + // Sync metadata from registry so that changes to IsPublic/Category/Description + // are reflected in the DB row on the next write (e.g. a setting newly marked + // as IsPublic will become visible to GetPublicOptions). + if def := GetSettingDef(key); def != nil { + data.Category = def.Category + data.Description = def.Label + data.IsPublic = def.IsPublic + } _, err = dao.SysOptions.Ctx(ctx). Where("key", key). - Data(do.SysOptions{Value: value}). + Data(data). Update() } else { def := GetSettingDef(key) diff --git a/internal/logic/common/settings_registry.go b/internal/logic/common/settings_registry.go index 84dd689..8aa471e 100644 --- a/internal/logic/common/settings_registry.go +++ b/internal/logic/common/settings_registry.go @@ -120,9 +120,9 @@ var Registry = []SettingDef{ {Key: "password_min_length", Type: SettingTypeInt, Default: "8", Category: "security", Label: "密码最小长度", Validation: "min:6,max:32"}, {Key: "turnstile_enabled", Type: SettingTypeBool, Default: "false", Category: "security", - Label: "启用 Turnstile 人机验证"}, + Label: "启用 Turnstile 人机验证", IsPublic: true}, {Key: "turnstile_site_key", Type: SettingTypeString, Default: "", Category: "security", - Label: "Turnstile Site Key", Sensitive: false}, + Label: "Turnstile Site Key", Sensitive: false, IsPublic: true}, {Key: "turnstile_secret_key", Type: SettingTypeString, Default: "", Category: "security", Label: "Turnstile Secret Key", Sensitive: true}, {Key: "captcha_expire_seconds", Type: SettingTypeInt, Default: "300", Category: "security", diff --git a/internal/logic/payment/config.go b/internal/logic/payment/config.go index 97b678e..657afef 100644 --- a/internal/logic/payment/config.go +++ b/internal/logic/payment/config.go @@ -57,9 +57,42 @@ func LoadChannelConfig(ctx context.Context, channelType string) (jsonStr string, // SaveChannelConfig 将渠道配置 JSON 保存到 sys_options。 func SaveChannelConfig(ctx context.Context, channelType string, configJSON string) error { + // 启用线上支付渠道前,必须先配置全局回调地址,否则网关异步通知无法回传, + // 支付成功也无法入账(发起支付时还有 RequireCallbackBaseURL 二次校验)。 + if err := validateChannelEnable(ctx, channelType, configJSON); err != nil { + return err + } return lcommon.Config().SetOption(ctx, channelConfigKey(channelType), configJSON) } +// validateChannelEnable 校验"启用渠道"的前置条件:启用易支付要求回调地址已配置。 +func validateChannelEnable(ctx context.Context, channelType string, configJSON string) error { + if channelType != "epay" { + return nil + } + cfg, err := ParseChannelConfig(channelType, configJSON) + if err != nil || cfg == nil { + return nil // JSON 合法性由调用方校验;此处只关心启用前置条件 + } + ec, ok := cfg.(*EpayConfig) + if !ok || !ec.IsEnabled { + return nil + } + if strings.TrimSpace(GetCallbackBaseURL(ctx)) == "" { + return lcommon.NewBadRequestError("请先在「支付设置」中配置回调基础URL,再启用支付渠道;否则支付成功后无法回调入账") + } + return nil +} + +// RequireCallbackBaseURL 发起支付前的二次防护:回调地址未配置则拒绝发起。 +// 渠道启用时已拦截一次,这里防止"启用后被清空回调地址"等场景下仍能发起支付。 +func RequireCallbackBaseURL(ctx context.Context) error { + if strings.TrimSpace(GetCallbackBaseURL(ctx)) == "" { + return lcommon.NewBusinessError(422, "支付回调地址未配置,请联系管理员在「支付设置」中配置回调基础URL") + } + return nil +} + // ListAllChannels 返回所有已注册渠道的配置(含未配置的,返回默认值)。 func ListAllChannels(ctx context.Context) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(channelTypes)) diff --git a/internal/logic/payment/epay.go b/internal/logic/payment/epay.go index e42ed3f..cda4b4a 100644 --- a/internal/logic/payment/epay.go +++ b/internal/logic/payment/epay.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "net/url" "sort" "strings" @@ -43,10 +42,14 @@ func (p *EpayProvider) CreatePayment(ctx context.Context, order *PaymentOrder, c params["sign"] = sign params["sign_type"] = "MD5" - redirectURL := fmt.Sprintf("%s/submit.php?%s", strings.TrimRight(cfg.PayAddress, "/"), buildQuery(params)) + // 采用 POST 表单提交:这里只返回 submit.php 的 action 地址,签名后的全部 + // 参数放入 Params,由前端构造