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