131 lines
4.7 KiB
Go
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
|
|
}
|