优化支付配置启用规则
This commit is contained in:
@@ -96,7 +96,7 @@ func (h *Handler) Create(c *gin.Context) {
|
||||
if err == ErrNameRequired || err == ErrMerchantIDRequired || err == ErrGatewayURLRequired ||
|
||||
err == ErrSignKeyRequired || err == ErrNotifyKeyRequired || err == ErrInvalidProvider ||
|
||||
err == ErrNotifyURLRequired || err == ErrInvalidSignType || err == ErrAppIDRequired ||
|
||||
err == ErrSerialNoRequired || err == ErrTermNoRequired {
|
||||
err == ErrInvalidStatus || err == ErrSerialNoRequired || err == ErrTermNoRequired {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func (h *Handler) Update(c *gin.Context) {
|
||||
response.NotFound(c, "配置不存在")
|
||||
return
|
||||
}
|
||||
if err == ErrInvalidSignType || err == ErrNotifyURLRequired {
|
||||
if err == ErrInvalidSignType || err == ErrInvalidStatus || err == ErrNotifyURLRequired {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -171,15 +171,6 @@ func (r *Repository) Create(req CreateRequest, actorID uint64, meta AuditMeta) (
|
||||
|
||||
var dto ConfigDTO
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 如果设置为默认,先取消同 provider 的其他默认配置
|
||||
if req.IsDefault {
|
||||
if err := tx.Model(&model.PaymentMerchantConfig{}).
|
||||
Where("provider = ? AND is_default = ?", req.Provider, true).
|
||||
Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 加密密钥
|
||||
encryptedSignKey, err := r.encryptor.Encrypt(req.SignKey)
|
||||
if err != nil {
|
||||
@@ -198,6 +189,15 @@ func (r *Repository) Create(req CreateRequest, actorID uint64, meta AuditMeta) (
|
||||
if environment == "" {
|
||||
environment = "production"
|
||||
}
|
||||
isDefault := req.IsDefault
|
||||
if status == "active" {
|
||||
if err := deactivateOtherConfigs(tx, 0, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
isDefault = true
|
||||
} else {
|
||||
isDefault = false
|
||||
}
|
||||
|
||||
item := model.PaymentMerchantConfig{
|
||||
Name: req.Name,
|
||||
@@ -212,7 +212,7 @@ func (r *Repository) Create(req CreateRequest, actorID uint64, meta AuditMeta) (
|
||||
JSPayFlag: firstNonEmpty(req.JSPayFlag, "2"),
|
||||
SignType: firstNonEmpty(req.SignType, defaultSignType(req.Provider)),
|
||||
ExtraConfig: req.ExtraConfig,
|
||||
IsDefault: req.IsDefault,
|
||||
IsDefault: isDefault,
|
||||
Status: status,
|
||||
Environment: environment,
|
||||
BusinessTags: req.BusinessTags,
|
||||
@@ -247,6 +247,9 @@ func (r *Repository) Update(id uint64, req UpdateRequest, actorID uint64, meta A
|
||||
if req.SignType != nil && *req.SignType != "" && !isValidSignType(*req.SignType) {
|
||||
return nil, ErrInvalidSignType
|
||||
}
|
||||
if req.Status != nil && !isValidStatus(*req.Status) {
|
||||
return nil, ErrInvalidStatus
|
||||
}
|
||||
if req.NotifyURL != nil && *req.NotifyURL == "" {
|
||||
return nil, ErrNotifyURLRequired
|
||||
}
|
||||
@@ -261,15 +264,6 @@ func (r *Repository) Update(id uint64, req UpdateRequest, actorID uint64, meta A
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果设置为默认,先取消同 provider 的其他默认配置
|
||||
if req.IsDefault != nil && *req.IsDefault && !item.IsDefault {
|
||||
if err := tx.Model(&model.PaymentMerchantConfig{}).
|
||||
Where("provider = ? AND is_default = ? AND id != ?", item.Provider, true, id).
|
||||
Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
updates := make(map[string]any)
|
||||
if req.Name != nil {
|
||||
updates["name"] = *req.Name
|
||||
@@ -326,6 +320,20 @@ func (r *Repository) Update(id uint64, req UpdateRequest, actorID uint64, meta A
|
||||
}
|
||||
updates["updated_by"] = actorID
|
||||
|
||||
finalStatus := item.Status
|
||||
if req.Status != nil {
|
||||
finalStatus = *req.Status
|
||||
}
|
||||
if finalStatus == "active" {
|
||||
if err := deactivateOtherConfigs(tx, id, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
updates["status"] = "active"
|
||||
updates["is_default"] = true
|
||||
} else {
|
||||
updates["is_default"] = false
|
||||
}
|
||||
|
||||
if err := tx.Model(&item).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -430,6 +438,20 @@ func (r *Repository) RecordUsage(configID uint64, paymentOrderID uint64, provide
|
||||
})
|
||||
}
|
||||
|
||||
// deactivateOtherConfigs 保证全局同一时间只有一个启用配置。
|
||||
func deactivateOtherConfigs(tx *gorm.DB, activeID uint64, actorID uint64) error {
|
||||
db := tx.Model(&model.PaymentMerchantConfig{}).
|
||||
Where("status = ? OR is_default = ?", "active", true)
|
||||
if activeID > 0 {
|
||||
db = db.Where("id != ?", activeID)
|
||||
}
|
||||
return db.Updates(map[string]any{
|
||||
"status": "disabled",
|
||||
"is_default": false,
|
||||
"updated_by": actorID,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// toDTO 转换为 DTO
|
||||
func (r *Repository) toDTO(item model.PaymentMerchantConfig, includeSecret bool) (ConfigDTO, error) {
|
||||
dto := ConfigDTO{
|
||||
@@ -492,6 +514,9 @@ func (r *Repository) validateCreateRequest(req CreateRequest) error {
|
||||
if req.SignType != "" && !isValidSignType(req.SignType) {
|
||||
return ErrInvalidSignType
|
||||
}
|
||||
if req.Status != "" && !isValidStatus(req.Status) {
|
||||
return ErrInvalidStatus
|
||||
}
|
||||
|
||||
// leshua 特定验证
|
||||
if req.Provider == "leshua" {
|
||||
@@ -555,6 +580,10 @@ func isValidSignType(value string) bool {
|
||||
return value == "MD5" || value == "SHA256withRSA"
|
||||
}
|
||||
|
||||
func isValidStatus(value string) bool {
|
||||
return value == "active" || value == "disabled" || value == "testing"
|
||||
}
|
||||
|
||||
func extraString(config map[string]any, key string) string {
|
||||
if config == nil {
|
||||
return ""
|
||||
|
||||
@@ -287,7 +287,7 @@ function handleClose() {
|
||||
|
||||
<el-form-item label="是否默认">
|
||||
<el-switch v-model="formData.is_default" :disabled="isReadonly" />
|
||||
<span class="form-hint">每个服务商只能有一个默认配置</span>
|
||||
<span class="form-hint">全局只能启用一个配置,启用后会自动设为默认</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
|
||||
@@ -173,7 +173,7 @@ function deleteDisabledReason(row: PaymentConfig) {
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Payment Configs</p>
|
||||
<h1>支付配置管理</h1>
|
||||
<p>管理多个支付商户配置,支持乐刷、拉卡拉等多种支付渠道。</p>
|
||||
<p>管理多个支付商户配置,支持乐刷、拉卡拉等多种支付渠道,同一时间仅一个配置启用。</p>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { CircleCheck, Coin, Loading, Lock, Money, Refresh, Tickets, Wallet as WalletIcon } from '@element-plus/icons-vue'
|
||||
@@ -33,6 +33,7 @@ const activeRechargePayment = ref<PaymentOrder | null>(null)
|
||||
const rechargeQRCodeURL = ref('')
|
||||
const rechargeQRGenerating = ref(false)
|
||||
const checkingRecharge = ref(false)
|
||||
let rechargePollingTimer: number | null = null
|
||||
|
||||
const walletMetrics = computed(() => {
|
||||
if (!account.value) {
|
||||
@@ -64,6 +65,13 @@ const walletMetrics = computed(() => {
|
||||
})
|
||||
|
||||
onMounted(loadWallet)
|
||||
onBeforeUnmount(stopRechargePolling)
|
||||
|
||||
watch(rechargeDialogVisible, (visible) => {
|
||||
if (!visible) {
|
||||
stopRechargePolling()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadWallet() {
|
||||
loading.value = true
|
||||
@@ -134,8 +142,10 @@ async function handleDevRecharge() {
|
||||
await renderRechargeQRCode(payment)
|
||||
if (payment.paid) {
|
||||
ElMessage.success('充值成功')
|
||||
rechargeDialogVisible.value = false
|
||||
await loadWallet()
|
||||
} else {
|
||||
startRechargePolling()
|
||||
ElMessage.success('测试支付单已创建')
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -145,27 +155,46 @@ async function handleDevRecharge() {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDevRechargeStatus() {
|
||||
async function checkDevRechargeStatus(options: { silent?: boolean } = {}) {
|
||||
if (!activeRechargePayment.value || checkingRecharge.value) return
|
||||
const silent = options.silent === true
|
||||
checkingRecharge.value = true
|
||||
try {
|
||||
const payment = await queryWalletRechargePayment(activeRechargePayment.value.id)
|
||||
activeRechargePayment.value = payment
|
||||
await renderRechargeQRCode(payment)
|
||||
if (payment.paid) {
|
||||
stopRechargePolling()
|
||||
ElMessage.success('充值成功')
|
||||
rechargeDialogVisible.value = false
|
||||
await loadWallet()
|
||||
} else {
|
||||
ElMessage.info('支付暂未完成')
|
||||
if (!silent) {
|
||||
ElMessage.info('支付暂未完成')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '查询支付状态失败'))
|
||||
if (!silent) {
|
||||
ElMessage.error(readError(error, '查询支付状态失败'))
|
||||
}
|
||||
} finally {
|
||||
checkingRecharge.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startRechargePolling() {
|
||||
stopRechargePolling()
|
||||
rechargePollingTimer = window.setInterval(() => {
|
||||
void checkDevRechargeStatus({ silent: true })
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function stopRechargePolling() {
|
||||
if (!rechargePollingTimer) return
|
||||
window.clearInterval(rechargePollingTimer)
|
||||
rechargePollingTimer = null
|
||||
}
|
||||
|
||||
function openRechargePayURL() {
|
||||
const payURL = rechargePayURL()
|
||||
if (!payURL) return
|
||||
|
||||
Reference in New Issue
Block a user