package application import ( "context" "fmt" "log" "net/url" "strconv" "strings" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/postgres" ) func paymentConfig(getenv postgres.Getenv, readFile postgres.ReadFile) (payments.Config, error) { config := payments.Config{Enabled: parseBool(getenv("WECHAT_PAY_ENABLED")), MinAmountFen: 100, MaxAmountFen: 1000000, PresetAmountsFen: []int64{10000, 50000, 100000}, OrderTTL: 15 * time.Minute} if !config.Enabled { return config, nil } config.MchID = strings.TrimSpace(getenv("WECHAT_PAY_MCH_ID")) config.AppID = strings.TrimSpace(getenv("WECHAT_PAY_APP_ID")) config.NotifyURL = strings.TrimSpace(getenv("WECHAT_PAY_NOTIFY_URL")) config.MerchantSerial = strings.TrimSpace(getenv("WECHAT_PAY_MERCHANT_SERIAL")) config.APIv3Key = strings.TrimSpace(getenv("WECHAT_PAY_API_V3_KEY")) config.WechatPublicKeyID = strings.TrimSpace(getenv("WECHAT_PAY_PUBLIC_KEY_ID")) for _, value := range []struct{ name, value string }{ {"WECHAT_PAY_MCH_ID", config.MchID}, {"WECHAT_PAY_APP_ID", config.AppID}, {"WECHAT_PAY_MERCHANT_SERIAL", config.MerchantSerial}, {"WECHAT_PAY_PUBLIC_KEY_ID", config.WechatPublicKeyID}, } { if value.value == "" { return config, fmt.Errorf("%s is required when WECHAT_PAY_ENABLED=true", value.name) } } if len(config.APIv3Key) != 32 { return config, fmt.Errorf("WECHAT_PAY_API_V3_KEY must contain 32 bytes") } notify, err := url.Parse(config.NotifyURL) if err != nil || notify.Scheme != "https" || notify.Host == "" || notify.User != nil || notify.RawQuery != "" || notify.Fragment != "" || notify.Path != "/api/payments/wechat/notify" { return config, fmt.Errorf("WECHAT_PAY_NOTIFY_URL must be an HTTPS URL ending in /api/payments/wechat/notify without query or credentials") } config.PrivateKeyPEM, err = paymentPEM(getenv, readFile, "WECHAT_PAY_PRIVATE_KEY") if err != nil { return config, err } config.WechatPublicKeyPEM, err = paymentPEM(getenv, readFile, "WECHAT_PAY_PUBLIC_KEY") if err != nil { return config, err } for _, bound := range []struct { name string dest *int64 }{ {"WECHAT_PAY_MIN_AMOUNT_FEN", &config.MinAmountFen}, {"WECHAT_PAY_MAX_AMOUNT_FEN", &config.MaxAmountFen}, } { if raw := strings.TrimSpace(getenv(bound.name)); raw != "" { value, err := strconv.ParseInt(raw, 10, 64) if err != nil || value < 1 || value > 1000000 { return config, fmt.Errorf("%s must be an integer between 1 and 1000000", bound.name) } *bound.dest = value } } if config.MinAmountFen > config.MaxAmountFen { return config, fmt.Errorf("payment minimum exceeds maximum") } if raw := strings.TrimSpace(getenv("WECHAT_PAY_PRESET_AMOUNTS_FEN")); raw != "" { config.PresetAmountsFen = nil parts := strings.Split(raw, ",") if len(parts) > 8 { return config, fmt.Errorf("WECHAT_PAY_PRESET_AMOUNTS_FEN allows at most 8 amounts") } seen := map[int64]bool{} for _, part := range parts { value, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64) if err != nil || value < config.MinAmountFen || value > config.MaxAmountFen || seen[value] { return config, fmt.Errorf("WECHAT_PAY_PRESET_AMOUNTS_FEN must contain unique integer amounts within the configured bounds") } seen[value] = true config.PresetAmountsFen = append(config.PresetAmountsFen, value) } } else { valid := make([]int64, 0, len(config.PresetAmountsFen)) for _, value := range config.PresetAmountsFen { if value >= config.MinAmountFen && value <= config.MaxAmountFen { valid = append(valid, value) } } config.PresetAmountsFen = valid } return config, nil } func paymentPEM(getenv postgres.Getenv, readFile postgres.ReadFile, name string) (string, error) { if path := strings.TrimSpace(getenv(name + "_FILE")); path != "" { data, err := readFile(path) if err != nil { return "", fmt.Errorf("unable to read %s_FILE", name) } return string(data), nil } if value := strings.TrimSpace(getenv(name + "_PEM")); value != "" { return strings.ReplaceAll(value, `\n`, "\n"), nil } return "", fmt.Errorf("%s_FILE or %s_PEM is required", name, name) } func startPaymentReconciliation(parent context.Context, service *payments.Service) (context.CancelFunc, <-chan struct{}) { ctx, cancel := context.WithCancel(parent) done := make(chan struct{}) go func() { defer close(done) ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { run, stop := context.WithTimeout(ctx, 25*time.Second) err := service.Reconcile(run) stop() if err != nil && ctx.Err() == nil { log.Print("wechat payment reconciliation encountered an error; pending orders will be retried") } select { case <-ctx.Done(): return case <-ticker.C: } } }() return cancel, done }