327 lines
11 KiB
Go
327 lines
11 KiB
Go
// 支付渠道配置:渠道列表、管理员配置保存(密钥加密存储)、清单与到期策略。
|
|
//
|
|
// 渠道实现全部来自已校验的支付插件包;没有可用插件时渠道列表为空,下单失败关闭,
|
|
// 绝不回退到宿主内置实现。
|
|
|
|
package app
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
"infinite-canvas/backend/internal/model"
|
|
"infinite-canvas/backend/internal/payment"
|
|
"infinite-canvas/backend/internal/protocol"
|
|
)
|
|
|
|
func (s *Service) PaymentNotificationResponse(providerID string, success bool) (int, string, string) {
|
|
return PaymentNotificationResponseForWithRegistry(s.paymentRegistry, providerID, success, http.StatusInternalServerError)
|
|
}
|
|
|
|
func (s *Service) PaymentNotificationFailureResponse(providerID string, status int) (int, string, string) {
|
|
return PaymentNotificationResponseForWithRegistry(s.paymentRegistry, providerID, false, status)
|
|
}
|
|
|
|
func PaymentNotificationResponseFor(providerID string, success bool, failureStatus int) (int, string, string) {
|
|
registry, _ := payment.NewRegistry()
|
|
return PaymentNotificationResponseForWithRegistry(registry, providerID, success, failureStatus)
|
|
}
|
|
|
|
func PaymentNotificationResponseForWithRegistry(registry *payment.Registry, providerID string, success bool, failureStatus int) (int, string, string) {
|
|
if registry != nil {
|
|
if provider, ok := registry.Get(providerID); ok {
|
|
descriptor := provider.Descriptor()
|
|
response := descriptor.NotificationFailure
|
|
if success {
|
|
response = descriptor.NotificationSuccess
|
|
}
|
|
status := response.Status
|
|
if status == 0 {
|
|
status = failureStatus
|
|
}
|
|
contentType, body := response.ContentType, response.Body
|
|
if contentType == "" {
|
|
contentType = "text/plain; charset=utf-8"
|
|
}
|
|
return status, contentType, body
|
|
}
|
|
}
|
|
if success {
|
|
return http.StatusNoContent, "", ""
|
|
}
|
|
return failureStatus, "", ""
|
|
}
|
|
|
|
func (s *Service) PaymentProviders(actor *model.User) ([]PaymentProviderView, error) {
|
|
if actor == nil {
|
|
return nil, Unauthorized("请先登录")
|
|
}
|
|
if err := s.RequireFeature(FeatureCredits); err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]PaymentProviderView, 0)
|
|
for _, descriptor := range s.paymentRegistry.Descriptors() {
|
|
view, _, err := s.paymentProviderView(descriptor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if view.Enabled && view.Configured {
|
|
items = append(items, view)
|
|
}
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *Service) AdminPaymentProviders(actor *model.User) ([]AdminPaymentProviderView, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]AdminPaymentProviderView, 0)
|
|
for _, descriptor := range s.paymentRegistry.Descriptors() {
|
|
base, config, err := s.paymentProviderView(descriptor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
manifest, _ := s.paymentManifestForProvider(descriptor.ID)
|
|
view := AdminPaymentProviderView{PaymentProviderView: base, Values: map[string]string{}, SecretConfigured: map[string]bool{}, ConfigFields: manifest.Configuration.Fields}
|
|
if config != nil {
|
|
values, err := s.decryptPaymentConfig(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
view.ConfigID = config.ID
|
|
view.ConfigEnabled = config.Enabled
|
|
view.Version = config.Version
|
|
view.UpdatedAt = &config.CreatedAt
|
|
for _, field := range manifest.Configuration.Fields {
|
|
if field.Secret {
|
|
view.SecretConfigured[field.Name] = strings.TrimSpace(values[field.Name]) != ""
|
|
continue
|
|
}
|
|
view.Values[field.Name] = values[field.Name]
|
|
}
|
|
}
|
|
items = append(items, view)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *Service) UpdatePaymentProviderConfig(actor *model.User, providerID string, request UpdatePaymentProviderConfigRequest) (*AdminPaymentProviderView, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
provider, ok := s.paymentRegistry.Get(providerID)
|
|
if !ok {
|
|
return nil, BadAuthRequest("未知支付渠道")
|
|
}
|
|
descriptor := provider.Descriptor()
|
|
manifest, ok := s.paymentManifestForProvider(descriptor.ID)
|
|
if !ok {
|
|
return nil, BadAuthRequest("支付插件清单不存在")
|
|
}
|
|
policy, ok := paymentExpiryPolicy(manifest, descriptor.ID)
|
|
if !ok {
|
|
return nil, BadAuthRequest("支付插件清单不存在")
|
|
}
|
|
if request.CloseAfterMinutes < policy.MinMinutes || request.CloseAfterMinutes > policy.MaxMinutes {
|
|
return nil, BadAuthRequest(fmt.Sprintf("未支付关闭时间必须为 %d-%d 分钟", policy.MinMinutes, policy.MaxMinutes))
|
|
}
|
|
values := make(payment.Config)
|
|
previousIdentity := make(map[string]string)
|
|
current, err := s.repo.LatestPaymentProviderConfig(providerID)
|
|
if err == nil {
|
|
values, err = s.decryptPaymentConfig(current)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, field := range descriptor.IdentityFields {
|
|
previousIdentity[field] = strings.TrimSpace(values[field])
|
|
}
|
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
for _, field := range manifest.Configuration.Fields {
|
|
value, supplied := request.Values[field.Name]
|
|
value = strings.TrimSpace(value)
|
|
if field.Secret && (!supplied || value == "") {
|
|
continue
|
|
}
|
|
if !supplied && field.Default != nil && strings.TrimSpace(values[field.Name]) == "" {
|
|
value = strings.TrimSpace(fmt.Sprint(field.Default))
|
|
}
|
|
values[field.Name] = value
|
|
}
|
|
changedIdentityField := ""
|
|
for field, previous := range previousIdentity {
|
|
if previous != "" && strings.TrimSpace(values[field]) != previous {
|
|
changedIdentityField = field
|
|
break
|
|
}
|
|
}
|
|
if changedIdentityField != "" {
|
|
orderCount, err := s.repo.PaymentOrderCountForProvider(providerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if orderCount > 0 {
|
|
return nil, BadAuthRequest("该渠道已有历史订单,首期不支持切换商户身份;请保持 " + changedIdentityField + " 不变")
|
|
}
|
|
}
|
|
if base := values["publicBaseUrl"]; base != "" {
|
|
if err := validatePaymentPublicBaseURL(base); err != nil {
|
|
return nil, BadAuthRequest(err.Error())
|
|
}
|
|
}
|
|
if request.Enabled {
|
|
if strings.TrimSpace(values["publicBaseUrl"]) == "" {
|
|
return nil, BadAuthRequest("启用支付渠道前必须填写服务器公网地址")
|
|
}
|
|
if err := provider.ValidateConfig(values); err != nil {
|
|
return nil, BadAuthRequest(err.Error())
|
|
}
|
|
}
|
|
plain, err := json.Marshal(values)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ciphertext, err := s.encryptSettingSecret(string(plain))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
digest := sha256.Sum256(plain)
|
|
config := &model.PaymentProviderConfig{
|
|
ID: newID(), ProviderID: descriptor.ID, PluginID: descriptor.PluginID, PluginVersion: descriptor.PluginVersion,
|
|
Enabled: request.Enabled, CloseAfterMinutes: request.CloseAfterMinutes,
|
|
ConfigCipher: ciphertext, ConfigDigest: hex.EncodeToString(digest[:]), CreatedBy: actor.ID,
|
|
}
|
|
if err := s.repo.CreatePaymentProviderConfig(config); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.appendAdminAudit(actor, "payment_provider.config.update", "payment_provider", providerID, "更新支付渠道配置", map[string]any{
|
|
"version": config.Version, "enabled": config.Enabled, "closeAfterMinutes": config.CloseAfterMinutes,
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
items, err := s.AdminPaymentProviders(actor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for index := range items {
|
|
if items[index].ID == providerID {
|
|
return &items[index], nil
|
|
}
|
|
}
|
|
return nil, errors.New("保存支付渠道配置后未找到渠道")
|
|
}
|
|
|
|
func (s *Service) paymentProviderView(descriptor payment.Descriptor) (PaymentProviderView, *model.PaymentProviderConfig, error) {
|
|
view := PaymentProviderView{ID: descriptor.ID, PluginID: descriptor.PluginID, Name: descriptor.Name, Icon: descriptor.Icon, CheckoutMode: descriptor.CheckoutMode}
|
|
if manifest, ok := s.paymentManifestForProvider(descriptor.ID); ok {
|
|
if policy, found := paymentExpiryPolicy(manifest, descriptor.ID); found {
|
|
view.CloseAfterMinutes = policy.DefaultMinutes
|
|
}
|
|
}
|
|
state, err := s.pluginStateForUser(nil, descriptor.PluginID, s.Plugins())
|
|
if err == nil {
|
|
view.PluginEnabled = state.PlatformAvailable
|
|
view.Enabled = state.PlatformAvailable
|
|
}
|
|
config, configErr := s.repo.LatestPaymentProviderConfig(descriptor.ID)
|
|
if errors.Is(configErr, gorm.ErrRecordNotFound) {
|
|
return view, nil, nil
|
|
}
|
|
if configErr != nil {
|
|
return view, nil, configErr
|
|
}
|
|
view.Configured = strings.TrimSpace(config.ConfigCipher) != ""
|
|
view.Enabled = view.Enabled && config.Enabled
|
|
view.CloseAfterMinutes = config.CloseAfterMinutes
|
|
return view, config, nil
|
|
}
|
|
|
|
func (s *Service) paymentManifestForProvider(providerID string) (protocol.Manifest, bool) {
|
|
providerID = strings.TrimSpace(providerID)
|
|
if providerID == "" {
|
|
return protocol.Manifest{}, false
|
|
}
|
|
if s != nil {
|
|
for _, plugin := range s.Plugins() {
|
|
for _, contribution := range plugin.Manifest.Contributes.PaymentProviders {
|
|
if contribution.ID == providerID {
|
|
return protocolManifestFromPluginView(plugin), true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bundledPaymentManifestForProvider(providerID)
|
|
}
|
|
|
|
func bundledPaymentManifestForProvider(providerID string) (protocol.Manifest, bool) {
|
|
for _, manifest := range bundledPaymentPluginManifests() {
|
|
for _, contribution := range manifest.Contributes.PaymentProviders {
|
|
if contribution.ID == providerID {
|
|
return manifest, true
|
|
}
|
|
}
|
|
}
|
|
return protocol.Manifest{}, false
|
|
}
|
|
|
|
func protocolManifestFromPluginView(plugin PluginView) protocol.Manifest {
|
|
return protocol.Manifest{
|
|
APIVersion: plugin.Manifest.APIVersion,
|
|
Metadata: protocol.Metadata{
|
|
ID: plugin.Manifest.ID, Version: plugin.Manifest.Version, Name: plugin.Manifest.Name,
|
|
Vendor: plugin.Manifest.Author, Description: plugin.Manifest.Description,
|
|
Documentation: plugin.Manifest.Documentation,
|
|
},
|
|
Surfaces: plugin.Manifest.Surfaces,
|
|
Runtime: plugin.Manifest.Runtime,
|
|
Permissions: plugin.Manifest.Permissions,
|
|
Configuration: plugin.Manifest.Configuration,
|
|
Contributes: plugin.Manifest.Contributes,
|
|
}
|
|
}
|
|
|
|
func paymentExpiryPolicy(manifest protocol.Manifest, providerID string) (protocol.ManifestPaymentExpiryPolicy, bool) {
|
|
for _, contribution := range manifest.Contributes.PaymentProviders {
|
|
if contribution.ID == providerID {
|
|
return contribution.ExpiryPolicy, true
|
|
}
|
|
}
|
|
if len(manifest.Contributes.PaymentProviders) == 1 {
|
|
return manifest.Contributes.PaymentProviders[0].ExpiryPolicy, true
|
|
}
|
|
return protocol.ManifestPaymentExpiryPolicy{}, false
|
|
}
|
|
|
|
func (s *Service) decryptPaymentConfig(config *model.PaymentProviderConfig) (payment.Config, error) {
|
|
if config == nil {
|
|
return nil, errors.New("支付渠道配置不存在")
|
|
}
|
|
plain, err := s.decryptSettingSecret(config.ConfigCipher)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values := make(payment.Config)
|
|
if err := json.Unmarshal([]byte(plain), &values); err != nil {
|
|
return nil, errors.New("支付渠道配置内容无效")
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func validatePaymentPublicBaseURL(value string) error {
|
|
parsed, err := url.Parse(strings.TrimRight(strings.TrimSpace(value), "/"))
|
|
if err != nil || (parsed.Scheme != "https" && parsed.Scheme != "http") || parsed.Host == "" || parsed.User != nil || parsed.Fragment != "" || parsed.RawQuery != "" || (parsed.Path != "" && parsed.Path != "/") {
|
|
return errors.New("服务器公网地址必须是有效的 HTTP(S) 根地址")
|
|
}
|
|
return nil
|
|
}
|