From 78117bb4e1990449c06965e7a2d5508656bff391 Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Fri, 16 Oct 2026 17:38:17 +0800 Subject: [PATCH] feat: security hardening, wechat pay v3 platform cert integration, companion certification revocation, and audit closed-loop --- .../src/views/activity/list/index.vue | 17 + .../src/views/banner/manage/index.vue | 8 +- .../src/views/banner/nav-category/index.vue | 8 +- .../src/views/companion/audit/index.vue | 63 +- .../src/views/companion/list/index.vue | 112 +- .../workplace/components/action-hub.vue | 27 +- .../workplace/components/content-chart.vue | 50 +- .../workplace/components/data-panel.vue | 25 +- .../workplace/components/quick-todo-list.vue | 12 +- .../workplace/components/quick-verifier.vue | 9 +- .../src/views/finance/config/index.vue | 361 +++- .../src/views/finance/coupons/index.vue | 8 +- .../views/finance/recharge-tiers/index.vue | 8 +- .../views/safety/platform-config/index.vue | 426 +++++ .../src/views/user/list/index.vue | 88 +- pwuniapp/App.vue | 14 +- pwuniapp/common/api.js | 294 ++- pwuniapp/common/icons.js | 15 + pwuniapp/common/location.js | 83 + pwuniapp/common/request.js | 7 + pwuniapp/common/role.js | 119 +- pwuniapp/components/CustomTabBar.vue | 5 +- pwuniapp/manifest.json | 13 +- pwuniapp/pages.json | 30 + pwuniapp/pages/activity/create.vue | 393 +++- pwuniapp/pages/activity/detail.vue | 836 +++++++-- pwuniapp/pages/auth/login.vue | 104 +- pwuniapp/pages/community/detail.vue | 1244 +++++++++++++ pwuniapp/pages/community/index.vue | 802 ++++++++- pwuniapp/pages/expert/detail.vue | 135 +- pwuniapp/pages/expert/hall.vue | 789 ++++++++- pwuniapp/pages/index/index.vue | 15 + pwuniapp/pages/order/create.vue | 148 +- pwuniapp/pages/order/list.vue | 39 +- pwuniapp/pages/user/favorites.vue | 30 +- pwuniapp/pages/user/index.vue | 270 +-- pwuniapp/pages/user/recharge.vue | 1407 +++++++++++++++ pwuniapp/pages/user/reports.vue | 1190 +++++++++++++ pwuniapp/pages/user/settings.vue | 178 +- server/src/core/pay/wechat-pay-v3.ts | 215 +++ .../core/security/content-security.service.ts | 45 + server/src/core/security/user-token.ts | 54 + server/src/db/mysql.ts | 125 +- server/src/db/repository.ts | 1295 ++++++++++++-- server/src/index.ts | 1578 +++++++++++++++-- server/tests/admin-features.spec.ts | 82 +- server/tests/admin-observability.spec.ts | 69 + server/tests/client-features.spec.ts | 36 + server/tests/content-security.spec.ts | 116 +- server/tests/dashboard-reality.spec.ts | 49 + .../tests/identity-binding-hardening.spec.ts | 84 + server/tests/identity-hardening.spec.ts | 152 ++ server/tests/order-activity-integrity.spec.ts | 93 + .../tests/order-lifecycle-hardening.spec.ts | 133 ++ server/tests/ownership-hardening.spec.ts | 74 + server/tests/rbac-permission.spec.ts | 13 + server/tests/security-hardening.spec.ts | 116 ++ server/tests/wechat-pay-v3.spec.ts | 212 +++ 58 files changed, 12971 insertions(+), 952 deletions(-) create mode 100644 pwuniapp/common/location.js create mode 100644 pwuniapp/pages/community/detail.vue create mode 100644 pwuniapp/pages/user/recharge.vue create mode 100644 pwuniapp/pages/user/reports.vue create mode 100644 server/src/core/pay/wechat-pay-v3.ts create mode 100644 server/src/core/security/user-token.ts create mode 100644 server/tests/admin-observability.spec.ts create mode 100644 server/tests/dashboard-reality.spec.ts create mode 100644 server/tests/identity-binding-hardening.spec.ts create mode 100644 server/tests/identity-hardening.spec.ts create mode 100644 server/tests/order-activity-integrity.spec.ts create mode 100644 server/tests/order-lifecycle-hardening.spec.ts create mode 100644 server/tests/ownership-hardening.spec.ts create mode 100644 server/tests/security-hardening.spec.ts create mode 100644 server/tests/wechat-pay-v3.spec.ts diff --git a/admin/arco-design-pro-vite/src/views/activity/list/index.vue b/admin/arco-design-pro-vite/src/views/activity/list/index.vue index 0a8b678..db42a4b 100644 --- a/admin/arco-design-pro-vite/src/views/activity/list/index.vue +++ b/admin/arco-design-pro-vite/src/views/activity/list/index.vue @@ -176,6 +176,16 @@ + + + @@ -240,6 +250,7 @@ title: '', gameCategory: '同城陪吃', location: '', + startTime: '', payType: 1, maxParticipants: 4, perPersonFee: 100, @@ -307,6 +318,7 @@ title: '', gameCategory: '同城陪吃', location: '', + startTime: '', payType: 1, maxParticipants: 4, perPersonFee: 100, @@ -319,12 +331,17 @@ Message.warning('请填写完整的组局标题与公开地点'); return; } + if (!createForm.value.startTime) { + Message.warning('请选择活动时段'); + return; + } try { await axios.post('/api/activities', { creatorId: 1001, title: createForm.value.title, gameCategory: createForm.value.gameCategory, location: createForm.value.location, + startTime: createForm.value.startTime, payType: createForm.value.payType, maxParticipants: createForm.value.maxParticipants, perPersonFeeCents: createForm.value.perPersonFee * 100, diff --git a/admin/arco-design-pro-vite/src/views/banner/manage/index.vue b/admin/arco-design-pro-vite/src/views/banner/manage/index.vue index beadf7a..8291d16 100644 --- a/admin/arco-design-pro-vite/src/views/banner/manage/index.vue +++ b/admin/arco-design-pro-vite/src/views/banner/manage/index.vue @@ -355,14 +355,14 @@ try { if (isEdit.value && editingId.value) { const res: any = await axios.put(`/api/admin/banners/${editingId.value}`, formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('轮播图修改成功'); modalVisible.value = false; fetchData(); } } else { const res: any = await axios.post('/api/admin/banners', formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('轮播图创建成功,已同步至小程序端'); modalVisible.value = false; fetchData(); @@ -380,7 +380,7 @@ const res: any = await axios.post(`/api/admin/banners/${record.id}/status`, { status: newStatus, }); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { record.status = newStatus; Message.success(newStatus === 1 ? '已上架至小程序端' : '已下线隐藏'); } @@ -394,7 +394,7 @@ const handleDelete = async (id: number) => { try { const res: any = await axios.delete(`/api/admin/banners/${id}`); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('轮播图已删除'); fetchData(); } diff --git a/admin/arco-design-pro-vite/src/views/banner/nav-category/index.vue b/admin/arco-design-pro-vite/src/views/banner/nav-category/index.vue index 7e1aef7..3d1f9b2 100644 --- a/admin/arco-design-pro-vite/src/views/banner/nav-category/index.vue +++ b/admin/arco-design-pro-vite/src/views/banner/nav-category/index.vue @@ -311,14 +311,14 @@ try { if (isEdit.value && editingId.value) { const res: any = await axios.put(`/api/admin/nav-categories/${editingId.value}`, formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('品类导航修改成功'); modalVisible.value = false; fetchData(); } } else { const res: any = await axios.post('/api/admin/nav-categories', formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('品类导航新增成功,已同步至小程序首页'); modalVisible.value = false; fetchData(); @@ -336,7 +336,7 @@ const res: any = await axios.post(`/api/admin/nav-categories/${record.id}/status`, { status: newStatus, }); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { record.status = newStatus; Message.success(newStatus === 1 ? '品类已上架至小程序' : '品类已下线隐藏'); } @@ -350,7 +350,7 @@ const handleDelete = async (id: number) => { try { const res: any = await axios.delete(`/api/admin/nav-categories/${id}`); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('品类导航已彻底删除'); fetchData(); } diff --git a/admin/arco-design-pro-vite/src/views/companion/audit/index.vue b/admin/arco-design-pro-vite/src/views/companion/audit/index.vue index 25aa7e1..e632bd6 100644 --- a/admin/arco-design-pro-vite/src/views/companion/audit/index.vue +++ b/admin/arco-design-pro-vite/src/views/companion/audit/index.vue @@ -78,15 +78,21 @@ - + - + - + - + @@ -143,9 +170,10 @@ const statusTab = ref('ALL'); const playingId = ref(null); - // 驳回弹窗 + // 驳回/解除认证弹窗 const rejectModalVisible = ref(false); const currentRejectItem = ref(null); + const rejectActionType = ref<'reject' | 'revoke'>('reject'); const rejectForm = ref({ reason: '', }); @@ -195,32 +223,39 @@ }, 3000); }; - const handleAudit = async (id: number, action: 'approve' | 'reject', reason?: string) => { + const handleAudit = async (id: number, action: 'approve' | 'reject' | 'revoke', reason?: string) => { try { await axios.post(`/api/companions/${id}/audit`, { action, reason, }); - Message.success(action === 'approve' ? '审核通过,已开通接单资质' : '已成功驳回'); + Message.success( + action === 'approve' + ? '审核通过,已开通接单资质' + : action === 'revoke' + ? '已解除大神认证资质' + : '已成功驳回' + ); fetchData(); } catch (err: any) { Message.error(err.message || '操作失败'); } }; - const openRejectModal = (record: any) => { + const openRejectModal = (record: any, actionType: 'reject' | 'revoke' = 'reject') => { currentRejectItem.value = record; + rejectActionType.value = actionType; rejectForm.value.reason = ''; rejectModalVisible.value = true; }; const handleRejectSubmit = () => { if (!rejectForm.value.reason) { - Message.warning('请填写具体的驳回原因'); + Message.warning(rejectActionType.value === 'revoke' ? '请填写解除认证的原因' : '请填写具体的驳回原因'); return; } if (currentRejectItem.value) { - handleAudit(currentRejectItem.value.id, 'reject', rejectForm.value.reason); + handleAudit(currentRejectItem.value.id, rejectActionType.value, rejectForm.value.reason); rejectModalVisible.value = false; } }; diff --git a/admin/arco-design-pro-vite/src/views/companion/list/index.vue b/admin/arco-design-pro-vite/src/views/companion/list/index.vue index 93fffbb..d8d586f 100644 --- a/admin/arco-design-pro-vite/src/views/companion/list/index.vue +++ b/admin/arco-design-pro-vite/src/views/companion/list/index.vue @@ -131,20 +131,64 @@ /> - + + + + + + 解除【{{ currentRevokeItem?.name }}】的大神认证资质后,该大神将立即从小程序大神列表下架,并退回为普通玩家身份(如需恢复接单须重新提交资质审核或由运营在审核列表恢复)。 + + + + + + {{ preset }} + + + + + + @@ -160,6 +204,18 @@ const statusFilter = ref('ALL'); const playingId = ref(null); + const revokeModalVisible = ref(false); + const currentRevokeItem = ref(null); + const revokeForm = ref({ + reason: '', + }); + const revokePresets = [ + '服务评分低于平台考核标准,暂停接单资质', + '多次收到用户违约或服务态度投诉,清退整改', + '实名身份或语音资料复核不符,需重新提交认证', + '大神本人主动申请退网解约', + ]; + const fetchData = async () => { loading.value = true; try { @@ -216,9 +272,43 @@ } }; - const toggleStatus = (record: any) => { - record.status = record.status === '接单中' ? '休息中' : '接单中'; - Message.success(`大神【${record.name}】状态已切换为【${record.status}】`); + const toggleStatus = async (record: any) => { + const nextStatus = record.status === '接单中' ? '休息中' : '接单中'; + try { + await axios.put(`/api/companions/${record.id}`, { + status: nextStatus, + }); + record.status = nextStatus; + Message.success(`大神【${record.name}】状态已切换为【${nextStatus}】`); + } catch (err: any) { + Message.error(err.message || '切换接单状态失败'); + } + }; + + const openRevokeModal = (record: any) => { + currentRevokeItem.value = record; + revokeForm.value.reason = ''; + revokeModalVisible.value = true; + }; + + const handleRevokeSubmit = async () => { + const reason = revokeForm.value.reason.trim(); + if (!reason) { + Message.warning('请填写或选择解除认证的原因'); + return; + } + if (!currentRevokeItem.value) return; + try { + await axios.post(`/api/companions/${currentRevokeItem.value.id}/audit`, { + action: 'revoke', + reason, + }); + Message.success(`已解除【${currentRevokeItem.value.name}】的大神认证资质`); + revokeModalVisible.value = false; + fetchData(); + } catch (err: any) { + Message.error(err.message || '解除认证失败'); + } }; onMounted(() => { diff --git a/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/action-hub.vue b/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/action-hub.vue index b5cb2c6..8ac19f1 100644 --- a/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/action-hub.vue +++ b/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/action-hub.vue @@ -1,10 +1,13 @@ @@ -19,6 +22,7 @@ diff --git a/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/quick-todo-list.vue b/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/quick-todo-list.vue index 748102e..7985484 100644 --- a/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/quick-todo-list.vue +++ b/admin/arco-design-pro-vite/src/views/dashboard/workplace/components/quick-todo-list.vue @@ -1,5 +1,8 @@
+ + 组局活动列表加载失败,暂无法选择要核销的活动;请刷新页面或检查服务端接口后再试。 +
现场用户出示4位纯数字即可即时到场校验
([]); const lastVerifiedResult = ref(''); + const activitiesFailed = ref(false); const fetchActivities = async () => { try { @@ -63,8 +67,11 @@ activityList.value = res.data; selectedActivityId.value = res.data[0].id; } + activitiesFailed.value = false; } catch { - // 保持兜底 + // 不再静默兜底为空活动列表:显式标记失败,避免现场核销误判"无活动可选" + activitiesFailed.value = true; + Message.error('组局活动列表加载失败,暂无法核销,请刷新重试'); } }; diff --git a/admin/arco-design-pro-vite/src/views/finance/config/index.vue b/admin/arco-design-pro-vite/src/views/finance/config/index.vue index 70d66c8..c4bf921 100644 --- a/admin/arco-design-pro-vite/src/views/finance/config/index.vue +++ b/admin/arco-design-pro-vite/src/views/finance/config/index.vue @@ -5,6 +5,15 @@ 分销与抽成配置 + + 检测到配置读取失败。为避免用页面默认值覆盖线上正确配置,保存与自检按钮已暂时禁用;请刷新页面或检查服务端接口后重试。 + + 参数全局生效说明:修改抽成与分销比例后,新生成的订单将即刻采用最新费率进行自动清结算分账;老订单按照下单时的历史固定抽成费率履约。 @@ -91,13 +100,240 @@ - + 保存并实时生效 + + + + + + + 当您申请下来微信支付商户号后,在此填入商户号 (mchid)、APIv3 密钥及商户私钥证书 (apiclient_key.pem) 并保存,小程序端下单与钱包充值即可无缝切换为真实微信支付收银台。未配置完整前默认运行于沙箱模拟收银台模式,不影响功能验收。 + + + + + + + + 自动识别切换 (填齐证书即开启实盘,未填齐走沙箱) + 强制微信支付实盘模式 (LIVE - 生产环境) + 强制沙箱收银台模式 (SANDBOX - 演示测试) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 商户 API 证书与私钥内容 (PEM 格式) + + +
+
+ + 包含 -----BEGIN PRIVATE KEY----- 至 -----END PRIVATE KEY----- + + + 读取本地 apiclient_key.pem 文件 + +
+ +
+
+ + +
+
+ + 包含 -----BEGIN CERTIFICATE----- 至 -----END CERTIFICATE----- + + + 读取本地 apiclient_cert.pem 文件 + +
+ +
+
+ + + + + + + + + + + + + + + + + +
+ +
+
+ + {{ chk.pass ? '通过' : '待完善' }} + + {{ chk.item }}: + {{ chk.detail }} +
+
+
+
+ + + + + + 保存微信支付商户配置 + + + 执行参数与证书签名自检 + + + +
+
@@ -107,6 +343,9 @@ import { Message } from '@arco-design/web-vue'; const saving = ref(false); + // 配置读取是否成功:失败时锁定保存,避免用页面默认值覆盖线上配置 + const configLoaded = ref(false); + const wxPayLoaded = ref(false); const form = ref({ platformRatePercent: 10, firstReferralPercent: 5, @@ -116,18 +355,67 @@ withdrawFeePercent: 0, }); + // 微信支付 V3 商户配置表单 + const wxPaySaving = ref(false); + const wxPayTesting = ref(false); + const wxPayEffectiveMode = ref<'LIVE' | 'SANDBOX'>('SANDBOX'); + const wxPayTestResult = ref(null); + const pemFileInputRef = ref(null); + const currentPemTargetField = ref<'privateKey' | 'certPem'>('privateKey'); + + const wxPayForm = ref({ + mode: 'AUTO', + appId: '', + appSecret: '', + mchId: '', + apiV3Key: '', + serialNo: '', + privateKey: '', + certPem: '', + platformPublicKeyId: '', + platformPublicKey: '', + notifyUrl: 'https://peiww.gynhc.com/api/pay/wechat/notify', + remark: '微信支付V3直连商户参数配置' + }); + const fetchData = async () => { try { const res: any = await axios.get('/api/finance/config'); if (res && res.data) { form.value = res.data; } + configLoaded.value = true; } catch { - // 保持兜底 + // 加载失败绝不能用页面默认值兜底,否则保存会覆盖线上正确费率 + configLoaded.value = false; + Message.error('财务抽成配置加载失败,已锁定保存按钮,避免以默认值覆盖线上配置'); + } + + try { + const wxRes: any = await axios.get('/api/admin/pay/wechat/config'); + const data = wxRes?.data?.data || wxRes?.data || wxRes; + if (data && typeof data === 'object') { + wxPayForm.value = { + ...wxPayForm.value, + ...data + }; + if (data.effectiveMode) { + wxPayEffectiveMode.value = data.effectiveMode; + } + } + wxPayLoaded.value = true; + } catch { + // 加载失败绝不能展示空表单,否则保存会清空线上商户号/密钥/证书 + wxPayLoaded.value = false; + Message.error('微信支付商户配置加载失败,已锁定保存按钮,避免清空线上商户参数'); } }; const handleSave = async () => { + if (!configLoaded.value) { + Message.warning('配置尚未成功加载,请刷新页面后再保存,避免覆盖线上费率'); + return; + } saving.value = true; try { await axios.post('/api/finance/config', form.value); @@ -139,6 +427,75 @@ } }; + const triggerPemFileSelect = (field: 'privateKey' | 'certPem') => { + currentPemTargetField.value = field; + if (pemFileInputRef.value) { + pemFileInputRef.value.value = ''; + pemFileInputRef.value.click(); + } + }; + + const handlePemFileChange = (e: Event) => { + const input = e.target as HTMLInputElement; + const file = input?.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + const content = String(reader.result || '').trim(); + wxPayForm.value[currentPemTargetField.value] = content; + Message.success(`已成功读取证书文件: ${file.name}`); + }; + reader.onerror = () => { + Message.error('读取证书文件失败,请手动复制粘贴文本内容'); + }; + reader.readAsText(file, 'utf-8'); + }; + + const handleSaveWxPay = async () => { + if (!wxPayLoaded.value) { + Message.warning('商户配置尚未成功加载,请刷新页面后再保存,避免清空线上商户参数'); + return; + } + wxPaySaving.value = true; + try { + const res: any = await axios.post('/api/admin/pay/wechat/config', wxPayForm.value); + const data = res?.data?.data || res?.data || res; + if (data && data.effectiveMode) { + wxPayEffectiveMode.value = data.effectiveMode; + } + Message.success( + wxPayEffectiveMode.value === 'LIVE' + ? '微信支付商户配置已保存,实盘支付模式 (LIVE) 已激活生效' + : '微信支付配置已保存,当前运行于沙箱收银台模式 (SANDBOX)' + ); + } catch (err: any) { + Message.error(err.response?.data?.message || err.message || '保存微信支付配置失败'); + } finally { + wxPaySaving.value = false; + } + }; + + const handleTestWxPay = async () => { + if (!wxPayLoaded.value) { + Message.warning('商户配置尚未成功加载,请刷新页面后再自检'); + return; + } + wxPayTesting.value = true; + try { + const res: any = await axios.post('/api/admin/pay/wechat/test', wxPayForm.value); + const data = res?.data?.data || res?.data || res; + wxPayTestResult.value = data; + if (data && data.effectiveMode) { + wxPayEffectiveMode.value = data.effectiveMode; + } + Message.success('微信支付配置与签名自检完成'); + } catch (err: any) { + Message.error(err.response?.data?.message || err.message || '配置自检失败'); + } finally { + wxPayTesting.value = false; + } + }; + onMounted(() => { fetchData(); }); diff --git a/admin/arco-design-pro-vite/src/views/finance/coupons/index.vue b/admin/arco-design-pro-vite/src/views/finance/coupons/index.vue index c42487b..ace970a 100644 --- a/admin/arco-design-pro-vite/src/views/finance/coupons/index.vue +++ b/admin/arco-design-pro-vite/src/views/finance/coupons/index.vue @@ -285,14 +285,14 @@ try { if (isEdit.value && editingId.value) { const res: any = await axios.put(`/api/admin/coupons/${editingId.value}`, formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('优惠券修改成功'); modalVisible.value = false; fetchData(); } } else { const res: any = await axios.post('/api/admin/coupons', formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('优惠券创建成功,已同步至小程序'); modalVisible.value = false; fetchData(); @@ -310,7 +310,7 @@ const res: any = await axios.put(`/api/admin/coupons/${record.id}`, { status: newStatus, }); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { record.status = newStatus; Message.success(newStatus === 1 ? '优惠券已上架发放' : '优惠券已停用下架'); } @@ -324,7 +324,7 @@ const handleDelete = async (id: number) => { try { const res: any = await axios.delete(`/api/admin/coupons/${id}`); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('优惠券已彻底删除'); fetchData(); } diff --git a/admin/arco-design-pro-vite/src/views/finance/recharge-tiers/index.vue b/admin/arco-design-pro-vite/src/views/finance/recharge-tiers/index.vue index 0c69c34..6679603 100644 --- a/admin/arco-design-pro-vite/src/views/finance/recharge-tiers/index.vue +++ b/admin/arco-design-pro-vite/src/views/finance/recharge-tiers/index.vue @@ -215,14 +215,14 @@ try { if (isEdit.value && editingId.value) { const res: any = await axios.put(`/api/admin/recharge-tiers/${editingId.value}`, formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('充值阶梯修改成功'); modalVisible.value = false; fetchData(); } } else { const res: any = await axios.post('/api/admin/recharge-tiers', formData.value); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('充值阶梯创建成功,已同步至小程序'); modalVisible.value = false; fetchData(); @@ -240,7 +240,7 @@ const res: any = await axios.put(`/api/admin/recharge-tiers/${record.id}`, { status: newStatus, }); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { record.status = newStatus; Message.success(newStatus === 1 ? '充值档位已上架' : '充值档位已下线'); } @@ -254,7 +254,7 @@ const handleDelete = async (id: number) => { try { const res: any = await axios.delete(`/api/admin/recharge-tiers/${id}`); - if (res && (res.data.code === 20000 || res.status === 200)) { + if (res && res.code === 20000) { Message.success('充值阶梯已彻底删除'); fetchData(); } diff --git a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue index e13acc9..5c6d9bf 100644 --- a/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue +++ b/admin/arco-design-pro-vite/src/views/safety/platform-config/index.vue @@ -705,6 +705,311 @@ + + + +
+ + 在此配置微信小程序支付 (JSAPI) 所需的直连商户号 (mchid)、APIv3 密钥、商户证书序列号及私钥 (apiclient_key.pem)。保存后无需重启服务器即可实时生效;未配置完整前自动运行于沙箱模拟收银台模式。 + + + + + + + + + + + 自动识别切换 (填齐商户号与证书即开启实盘) + 强制微信支付实盘模式 (LIVE - 生产环境) + 强制沙箱收银台模式 (SANDBOX - 演示测试) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + 支持直接粘贴文本,或点击右侧按钮读取本地 apiclient_key.pem 文件 + + + 读取本地 apiclient_key.pem 文件 + +
+ +
+
+ + +
+
+ + 支持直接粘贴文本,或点击右侧按钮读取本地 apiclient_cert.pem 文件并自动解析序列号 + + + + 从证书提取序列号 + + + 读取本地 apiclient_cert.pem 文件 + + +
+ +
+
+ + + 微信支付平台公钥配置(用于商家转账到零钱实名加密与异步回调验签,选填) + + + + + + + + + + + +
+
+ + 支持直接粘贴文本,或点击右侧按钮读取从微信商户后台下载的本地 pub_key.pem 公钥文件 + + + 读取本地 pub_key.pem 文件 + +
+ +
+
+ + + +
+ +
+ + {{ chk.pass ? '通过' : '待完善' }} + + {{ chk.item }}: + {{ chk.detail }} +
+
+
+ +
+ + + 保存微信支付商户配置 + + + 执行参数与证书签名自检 + + +
+
+
+
+
+ + + +
+ + 在此维护微信小程序的地图定位服务参数(腾讯位置服务 Tencent LBS Key)。用于同城组局发布时的地图选点(chooseLocation)、经纬度逆地址解析及附近搭子/活动测距。 +
+ 配置指引:登录腾讯位置服务控制台 (lbs.qq.com) -> 创建应用并添加 Key -> 勾选 WebServiceAPI 与微信小程序授权(填入小程序 AppID: wx0c929f7e54024e67)。 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + 保存地图定位配置 + + +
+
+
+
+
@@ -1030,6 +1335,117 @@ const saveMessageTemplateConfig = () => { saveConfig('message_template_rules', messageTemplateForm); }; +// Tab 12 微信支付商户号与证书配置 +const wxPayForm = reactive({ + name: '微信支付商户号与证书配置', + description: '微信支付V3商户号、APIv3密钥与证书参数配置', + status: 1, + valueJson: { + mode: 'AUTO', + appId: '', + appSecret: '', + mchId: '', + apiV3Key: '', + serialNo: '', + privateKey: '', + certPem: '', + platformPublicKeyId: '', + platformPublicKey: '', + notifyUrl: 'https://peiww.gynhc.com/api/pay/wechat/notify', + remark: '微信支付V3直连商户参数配置' + } +}); + +const wxPayTesting = ref(false); +const wxPayTestResult = ref(null); +const pemInputRef = ref(null); +const activePemField = ref<'privateKey' | 'certPem' | 'platformPublicKey'>('privateKey'); + +const triggerPemSelect = (field: 'privateKey' | 'certPem' | 'platformPublicKey') => { + activePemField.value = field; + if (pemInputRef.value) { + pemInputRef.value.value = ''; + pemInputRef.value.click(); + } +}; + +const extractCertSerialNo = async () => { + if (!wxPayForm.valueJson.certPem) { + Message.warning('请先读取或粘贴 apiclient_cert.pem 证书内容'); + return; + } + try { + const res: any = await axios.post('/api/admin/pay/wechat/parse-cert', { + certPem: wxPayForm.valueJson.certPem + }); + const data = res?.data?.data || res?.data || res; + if (data && data.serialNo) { + wxPayForm.valueJson.serialNo = data.serialNo; + Message.success(`已自动提取 40 位证书序列号: ${data.serialNo}`); + } + } catch (err: any) { + Message.warning(err.response?.data?.message || err.message || '未能自动提取序列号,请手动填写'); + } +}; + +const onPemFileSelected = (e: Event) => { + const input = e.target as HTMLInputElement; + const file = input?.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = async () => { + const content = String(reader.result || '').trim(); + wxPayForm.valueJson[activePemField.value] = content; + Message.success(`已读取证书文件: ${file.name}`); + if (activePemField.value === 'certPem' && content.includes('BEGIN CERTIFICATE')) { + await extractCertSerialNo(); + } + }; + reader.readAsText(file, 'utf-8'); +}; + +const saveWechatPayConfig = () => { + saveConfig('wechat_pay_config', wxPayForm); +}; + +// Tab 13 地图定位与LBS服务配置 +const mapLocationForm = reactive({ + name: '地图定位与LBS服务配置', + description: '腾讯位置服务Key、选点与周边测距参数配置', + status: 1, + valueJson: { + provider: 'tencent', + mapKey: '', + secretKey: '', + serviceName: '腾讯位置服务 (Tencent LBS)', + enableChooseLocation: 1, + enableDistanceCalculation: 1, + defaultCity: '北京市', + remark: '腾讯位置服务 Key(需在 lbs.qq.com 申请并勾选 WebServiceAPI 与微信小程序 AppID: wx0c929f7e54024e67)' + } +}); + +const saveMapLocationConfig = () => { + saveConfig('map_location_config', mapLocationForm); +}; + +const testWechatPayConfig = async () => { + wxPayTesting.value = true; + try { + const res: any = await axios.post('/api/admin/pay/wechat/test', wxPayForm.valueJson); + const data = res?.data?.data || res?.data || res; + wxPayTestResult.value = data; + if (data?.serialNo && !wxPayForm.valueJson.serialNo) { + wxPayForm.valueJson.serialNo = data.serialNo; + } + Message.success('微信支付配置与签名自检完成'); + } catch (err: any) { + Message.error(err.response?.data?.message || err.message || '自检请求失败'); + } finally { + wxPayTesting.value = false; + } +}; + const sendTestMessage = async () => { testSending.value = true; try { @@ -1125,6 +1541,16 @@ const fetchConfigs = async () => { messageTemplateForm.description = item.description; messageTemplateForm.status = item.status; messageTemplateForm.valueJson = { ...messageTemplateForm.valueJson, ...(item.valueJson || {}) }; + } else if (item.configKey === 'wechat_pay_config') { + wxPayForm.name = item.name; + wxPayForm.description = item.description; + wxPayForm.status = item.status; + wxPayForm.valueJson = { ...wxPayForm.valueJson, ...(item.valueJson || {}) }; + } else if (item.configKey === 'map_location_config') { + mapLocationForm.name = item.name; + mapLocationForm.description = item.description; + mapLocationForm.status = item.status; + mapLocationForm.valueJson = { ...mapLocationForm.valueJson, ...(item.valueJson || {}) }; } }); } diff --git a/admin/arco-design-pro-vite/src/views/user/list/index.vue b/admin/arco-design-pro-vite/src/views/user/list/index.vue index 42c4681..5a01c6c 100644 --- a/admin/arco-design-pro-vite/src/views/user/list/index.vue +++ b/admin/arco-design-pro-vite/src/views/user/list/index.vue @@ -77,6 +77,20 @@