小程序配置增加自动提现多商户管理;解冻成功后联动刷新详情余额。

新增商户列表/保存/证书上传入口(8080a);解冻面板回写余额并提示变动明细。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-14 02:44:44 +08:00
parent e44b1c5d2b
commit 75417417e5
5 changed files with 297 additions and 1 deletions

View File

@@ -67,6 +67,8 @@ const props = defineProps({
isEditMode: { type: Boolean, default: false },
})
const emit = defineEmits(['unfrozen'])
const username = computed(() => localStorage.getItem('username') || '')
const info = reactive({
@@ -153,10 +155,14 @@ const handleUnfreeze = async () => {
beizhu: unfreezeRemark.value,
})
if (res.code === 0) {
ElMessage.success('解冻成功')
const tip = res.data?.unfreeze_result
? `解冻成功:余额 ${res.data.unfreeze_result.balance_before}${res.data.unfreeze_result.balance_after}`
: '解冻成功'
ElMessage.success(tip)
unfreezeAmount.value = 0
unfreezeRemark.value = ''
await load()
emit('unfrozen', res.data || {})
} else {
ElMessage.error(res.msg || '解冻失败')
}

View File

@@ -170,6 +170,110 @@
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="自动提现商户号" name="transfer_mch">
<el-alert
type="warning"
:closable="false"
show-icon
class="shop-hint"
title="仅影响用户收款发起的微信商家转账按优先级从小到大尝试某号失败余额不足/限额等自动换下一号发起成功后打款单会强制记录 mch_id回调按该记录处理"
/>
<div class="asset-toolbar">
<el-button type="primary" @click="openMchDialog()">新增商户</el-button>
<el-button @click="loadMchList">刷新</el-button>
<el-text type="info" size="small">证书目录:{{ mchCertBase || '加载后显示' }}</el-text>
</div>
<el-table :data="mchList" v-loading="mchLoading" border>
<el-table-column prop="priority" label="优先级" width="80" />
<el-table-column prop="mch_id" label="商户号" min-width="120" />
<el-table-column prop="name" label="备注" min-width="120" />
<el-table-column prop="appid" label="AppID" min-width="140" />
<el-table-column label="启用" width="80">
<template #default="{ row }">
<el-tag :type="row.enabled ? 'success' : 'info'" size="small">{{ row.enabled ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="私钥" width="90">
<template #default="{ row }">
<el-tag :type="row.private_key_exists ? 'success' : 'danger'" size="small">
{{ row.private_key_exists ? '已上传' : '缺失' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="平台公钥" width="90">
<template #default="{ row }">
<el-tag :type="row.platform_pub_key_exists ? 'success' : 'warning'" size="small">
{{ row.platform_pub_key_exists ? '已上传' : '可选' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openMchDialog(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="mchDialogVisible" :title="mchForm.id ? '编辑商户' : '新增商户'" width="680px" destroy-on-close>
<el-form label-width="140px">
<el-form-item label="商户号" required>
<el-input v-model="mchForm.mch_id" :disabled="!!mchForm.id" />
</el-form-item>
<el-form-item label="备注名称">
<el-input v-model="mchForm.name" />
</el-form-item>
<el-form-item label="小程序 AppID" required>
<el-input v-model="mchForm.appid" />
</el-form-item>
<el-form-item label="APIv3 密钥" required>
<el-input v-model="mchForm.api_v3_key" type="password" show-password :placeholder="mchForm.id ? '留空不修改' : '必填'" />
</el-form-item>
<el-form-item label="证书序列号" required>
<el-input v-model="mchForm.cert_serial_no" />
</el-form-item>
<el-form-item label="转账场景 ID">
<el-input v-model="mchForm.transfer_scene_id" placeholder="1005" />
</el-form-item>
<el-form-item label="回调 URL">
<el-input v-model="mchForm.notify_url" placeholder="可与默认相同" />
</el-form-item>
<el-form-item label="优先级">
<el-input-number v-model="mchForm.priority" :min="1" :max="9999" />
<el-text type="info" size="small" style="margin-left:8px">数字越小越优先</el-text>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="mchForm.enabled" />
</el-form-item>
<el-form-item label="私钥路径">
<el-input v-model="mchForm.private_key_path" placeholder="上传后自动填也可手写服务器绝对路径" />
<el-upload
:show-file-list="false"
accept=".pem"
:http-request="(opt) => uploadMchFile(opt, 'private_key')"
style="margin-top:8px"
>
<el-button type="primary" link>上传 apiclient_key.pem</el-button>
</el-upload>
</el-form-item>
<el-form-item label="平台公钥路径">
<el-input v-model="mchForm.platform_pub_key_path" placeholder="上传后自动填" />
<el-upload
:show-file-list="false"
accept=".pem"
:http-request="(opt) => uploadMchFile(opt, 'platform_pub_key')"
style="margin-top:8px"
>
<el-button type="primary" link>上传 pub_key.pem</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="mchDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="mchSaving" @click="saveMch">保存</el-button>
</template>
</el-dialog>
</el-tab-pane>
</el-tabs>
</div>
</div>
@@ -209,6 +313,147 @@ const assetList = ref([])
const assetLoading = ref(false)
const assetSaving = ref(false)
const mchLoading = ref(false)
const mchSaving = ref(false)
const mchList = ref([])
const mchCertBase = ref('')
const mchDialogVisible = ref(false)
const mchForm = reactive({
id: null,
mch_id: '',
name: '',
appid: '',
api_v3_key: '',
cert_serial_no: '',
transfer_scene_id: '1005',
notify_url: '',
priority: 100,
enabled: true,
private_key_path: '',
platform_pub_key_path: '',
})
const loadMchList = async () => {
mchLoading.value = true
try {
const res = await request.post('/peizhi/wxmchlb', { username: username.value })
if (res.code === 403) {
hasPermission.value = false
return
}
if (res.code === 0 && res.data) {
mchList.value = res.data.list || []
mchCertBase.value = res.data.cert_base_dir || ''
} else {
ElMessage.error(res.msg || '加载商户失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
mchLoading.value = false
}
}
const openMchDialog = (row) => {
if (row) {
Object.assign(mchForm, {
id: row.id,
mch_id: row.mch_id,
name: row.name || '',
appid: row.appid || '',
api_v3_key: '',
cert_serial_no: row.cert_serial_no || '',
transfer_scene_id: row.transfer_scene_id || '1005',
notify_url: row.notify_url || '',
priority: row.priority ?? 100,
enabled: !!row.enabled,
private_key_path: row.private_key_path || '',
platform_pub_key_path: row.platform_pub_key_path || '',
})
} else {
Object.assign(mchForm, {
id: null,
mch_id: '',
name: '',
appid: sysForm.weixin_appid || '',
api_v3_key: '',
cert_serial_no: '',
transfer_scene_id: '1005',
notify_url: '',
priority: 100,
enabled: true,
private_key_path: '',
platform_pub_key_path: '',
})
}
mchDialogVisible.value = true
}
const saveMch = async () => {
if (!mchForm.mch_id || !mchForm.appid || !mchForm.cert_serial_no) {
ElMessage.warning('请填写商户号、AppID、证书序列号')
return
}
if (!mchForm.id && !mchForm.api_v3_key) {
ElMessage.warning('新建必须填写 APIv3 密钥')
return
}
mchSaving.value = true
try {
const payload = {
username: username.value,
id: mchForm.id,
mch_id: mchForm.mch_id,
name: mchForm.name,
appid: mchForm.appid,
cert_serial_no: mchForm.cert_serial_no,
transfer_scene_id: mchForm.transfer_scene_id || '1005',
notify_url: mchForm.notify_url,
priority: mchForm.priority,
enabled: mchForm.enabled,
private_key_path: mchForm.private_key_path,
platform_pub_key_path: mchForm.platform_pub_key_path,
}
if (mchForm.api_v3_key) payload.api_v3_key = mchForm.api_v3_key
const res = await request.post('/peizhi/wxmchbj', payload)
if (res.code === 0) {
ElMessage.success(res.msg || '保存成功')
mchDialogVisible.value = false
await loadMchList()
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch {
ElMessage.error('网络错误')
} finally {
mchSaving.value = false
}
}
const uploadMchFile = async (opt, fileType) => {
if (!mchForm.mch_id) {
ElMessage.warning('请先填写商户号')
return
}
const formData = new FormData()
formData.append('username', username.value)
formData.append('mch_id', mchForm.mch_id)
formData.append('file_type', fileType)
formData.append('file', opt.file)
try {
const res = await request.post('/peizhi/wxmchscwj', formData)
if (res.code === 0 && res.data) {
ElMessage.success('上传成功')
if (fileType === 'private_key') mchForm.private_key_path = res.data.saved_path
if (fileType === 'platform_pub_key') mchForm.platform_pub_key_path = res.data.saved_path
} else {
ElMessage.error(res.msg || '上传失败')
}
} catch {
ElMessage.error('上传失败')
}
}
const fullUrl = (path) => {
if (!path) return ''
if (path.startsWith('http')) return path
@@ -358,6 +603,7 @@ const uploadAsset = async ({ file }, row) => {
onMounted(() => {
loadSysConfig()
loadAssets()
loadMchList()
})
</script>

View File

@@ -279,6 +279,7 @@
title="管事分红"
:is-edit-mode="true"
class="freeze-section"
@unfrozen="onFreezeUnfrozen"
/>
<!-- 手动修改余额弹窗 -->
@@ -495,6 +496,17 @@ const fetchGuanliDetail = async () => {
}
}
const onFreezeUnfrozen = async (payload) => {
const ur = payload?.unfreeze_result
if (ur) {
const after = parseFloat(ur.balance_after)
if (!Number.isNaN(after)) {
balanceForm.yue = after
}
}
await fetchGuanliDetail()
}
// 进入编辑模式:备份原始数据
const enterEditMode = () => {
originalData = {

View File

@@ -354,6 +354,7 @@
:leixing="1"
title="打手佣金"
:is-edit-mode="true"
@unfrozen="onFreezeUnfrozen"
/>
</el-col>
<el-col :span="12">
@@ -362,6 +363,7 @@
:leixing="5"
title="打手押金"
:is-edit-mode="true"
@unfrozen="onFreezeUnfrozen"
/>
</el-col>
</el-row>
@@ -487,6 +489,24 @@ const fetchDetail = async () => {
}
}
// 解冻后同步上方「可提现余额/押金」展示(冻结面板与详情卡片区联动)
const onFreezeUnfrozen = async (payload) => {
const ur = payload?.unfreeze_result
if (ur) {
const after = parseFloat(ur.balance_after)
if (!Number.isNaN(after)) {
if (payload.leixing === 5) {
playerInfo.value.yajin = after
if (isEditMode.value) editData.value.yajin = after
} else if (payload.leixing === 1) {
playerInfo.value.yue = after
if (isEditMode.value) editData.value.yue = after
}
}
}
await fetchDetail()
}
// 编辑模式
const toggleEditMode = () => {
editData.value = JSON.parse(JSON.stringify(playerInfo.value))

View File

@@ -108,6 +108,7 @@
title="商家余额"
:is-edit-mode="true"
class="freeze-section"
@unfrozen="onFreezeUnfrozen"
/>
</div>
@@ -240,6 +241,17 @@ const fetchMerchantDetail = async () => {
}
}
const onFreezeUnfrozen = async (payload) => {
const ur = payload?.unfreeze_result
if (ur) {
const after = parseFloat(ur.balance_after)
if (!Number.isNaN(after) && merchant.value) {
merchant.value.yue = after
}
}
await fetchMerchantDetail()
}
// 修改余额
const openBalanceDialog = () => {
balanceForm.value = { action: 'add_balance', amount: null }