Files
NianAIGC/backend/internal/application/payments.go
T
2026-10-12 13:57:23 +08:00

131 lines
4.7 KiB
Go

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
}