fix: 修复收入统计 404,并隔离支付测试/生产配置

Dashboard 收入接口路径对齐;登录页去掉「授权管理平台」;Waffo 测试/生产密钥与 Product 映射分环境保存。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-12 22:37:22 +08:00
1 parent 76bba729f1
commit 9b79eabd34
9 files changed
+391 -80

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>opsx-licence 授权管理平台</title>
<title>opsx-licence</title>
</head>
<body>
<div id="app"></div>
+1 -1
View File
@@ -31,7 +31,7 @@ export function getProductDistribution() {
}
export function getRevenueAnalytics() {
return request({ url: '/analytics/revenue', method: 'get' })
return request({ url: '/dashboard/revenue', method: 'get' })
}
export function getGeoDistribution() {
+11 -16
View File
@@ -2,9 +2,7 @@
<div class="login-container">
<div class="login-card">
<div class="login-header">
<el-icon size="40" color="#409eff"><Key /></el-icon>
<h2 class="login-title">opsx-licence</h2>
<p class="login-subtitle">授权管理平台</p>
</div>
<el-form
@@ -74,7 +72,6 @@ async function handleLogin() {
loading.value = true
try {
await userStore.login(loginForm)
// 登录成功后跳转到 redirect 或首页 // Logan.Li
const redirect = route.query.redirect || '/'
router.push(redirect)
} catch {
@@ -92,27 +89,25 @@ async function handleLogin() {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #f3f5f7;
}
.login-card {
width: 400px;
padding: 40px;
width: 380px;
padding: 36px 32px 28px;
background: #fff;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid #ebeef5;
border-radius: 10px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.login-header {
text-align: center;
margin-bottom: 30px;
margin-bottom: 28px;
}
.login-title {
margin: 12px 0 4px;
font-size: 24px;
margin: 0;
font-size: 20px;
font-weight: 600;
letter-spacing: 0.02em;
color: #303133;
}
.login-subtitle {
margin: 0;
color: #909399;
font-size: 14px;
}
</style>
+115 -32
View File
@@ -18,7 +18,7 @@
type="info"
:closable="false"
show-icon
title="商户 ID / Store / 私钥来自 Waffo Dashboard「集成」页。复制密钥后粘贴保存即可,无需手改服务器 .env。"
title="测试 / 生产环境的私钥与 Product ID 映射相互隔离。切换到生产后需单独填写生产密钥;保存后才会生效。"
style="margin-bottom: 16px"
/>
@@ -26,6 +26,24 @@
<el-form-item label="启用">
<el-switch v-model="waffo.enabled" />
</el-form-item>
<el-form-item label="环境">
<div class="env-row">
<span :class="{ muted: isLive }">测试</span>
<el-switch
v-model="isLive"
inline-prompt
active-text="生产"
inactive-text="测试"
style="margin: 0 10px"
@change="onEnvSwitch"
/>
<span :class="{ muted: !isLive }">生产</span>
<el-tag size="small" :type="isLive ? 'danger' : 'success'" style="margin-left: 12px">
{{ isLive ? 'live' : 'test' }}
</el-tag>
</div>
<div class="hint">切换环境会载入该环境的 Product 映射;若该环境尚未配置私钥,需重新粘贴保存。</div>
</el-form-item>
<el-form-item label="商户 ID">
<el-input v-model="waffo.merchant_id" placeholder="MER_…" />
</el-form-item>
@@ -35,16 +53,10 @@
<el-form-item label="Store Slug">
<el-input v-model="waffo.store_slug" placeholder="如 opsxshell-pwsi6m4n" />
</el-form-item>
<el-form-item label="环境">
<el-radio-group v-model="waffo.environment">
<el-radio value="test">测试 (test)</el-radio>
<el-radio value="live">生产 (live)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="API 私钥">
<div class="secret-row">
<el-tag :type="waffo.private_key_configured ? 'success' : 'warning'" size="small">
{{ waffo.private_key_configured ? '已配置' : '未配置' }}
{{ waffo.private_key_configured ? `已配置(${waffo.environment})` : `未配置(${waffo.environment})` }}
</el-tag>
<el-button size="small" @click="showPrivateKey = !showPrivateKey">
{{ showPrivateKey ? '取消更换' : (waffo.private_key_configured ? '更换' : '填写') }}
@@ -52,11 +64,11 @@
<el-button v-if="waffo.private_key_configured" size="small" type="danger" link @click="clearPrivateKey">清除</el-button>
</div>
<el-input
v-if="showPrivateKey"
v-if="showPrivateKey || !waffo.private_key_configured"
v-model="privateKeyInput"
type="textarea"
:rows="4"
placeholder="粘贴 PEM 或 Dashboard Base64 密钥(保存后不再回显)"
:placeholder="`粘贴 ${waffo.environment === 'live' ? '生产' : '测试'} 环境 PEM / Base64 密钥(保存后不再回显)`"
style="margin-top: 8px"
/>
</el-form-item>
@@ -80,7 +92,7 @@
<el-card shadow="never" style="margin-top: 16px">
<template #header>
<div class="card-header">
<span class="title">Product ID 映射</span>
<span class="title">Product ID 映射({{ isLive ? '生产' : '测试' }})</span>
<el-button size="small" type="primary" @click="addMapping">新增行</el-button>
</div>
</template>
@@ -88,7 +100,7 @@
type="warning"
:closable="false"
show-icon
title="将计划类型 / 业务 key 映射到 Waffo Product ID(如 yearly → PROD_xxx)。保存时会同步兼容字段 product_id_yearly / product_id_perpetual。"
title="测试与生产的 Product ID 通常不同,已按环境分别保存。切换开关即可编辑对应环境映射。"
style="margin-bottom: 12px"
/>
<el-table :data="productMappings" border stripe>
@@ -132,7 +144,7 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getWaffoSettings, updateWaffoSettings, testWaffoConnection } from '@/api/setting'
@@ -146,6 +158,8 @@ const privateKeyInput = ref('')
const clearKeyFlag = ref(false)
const debugResult = ref('')
const productMappings = ref([])
const profiles = ref({ test: null, live: null })
const keyByEnv = ref({ test: false, live: false })
const waffo = reactive({
enabled: true,
@@ -163,6 +177,20 @@ const waffo = reactive({
private_key_configured: false,
})
const isLive = computed({
get: () => waffo.environment === 'live',
set: (v) => {
waffo.environment = v ? 'live' : 'test'
},
})
function defaultMappings() {
return [
{ plan_key: 'yearly', label: '包年', product_id: '' },
{ plan_key: 'perpetual', label: '永久', product_id: '' },
]
}
function normalizeMappings(data = {}) {
if (Array.isArray(data.product_mappings) && data.product_mappings.length) {
return data.product_mappings.map((m) => ({
@@ -182,13 +210,7 @@ function normalizeMappings(data = {}) {
product_id: data.product_id_perpetual || data.product_id,
})
}
if (!rows.length) {
rows.push(
{ plan_key: 'yearly', label: '包年', product_id: '' },
{ plan_key: 'perpetual', label: '永久', product_id: '' },
)
}
return rows
return rows.length ? rows : defaultMappings()
}
function mappingsToCompatFields(rows) {
@@ -212,6 +234,33 @@ function mappingsToCompatFields(rows) {
}
}
function stashCurrentEnv() {
const env = waffo.environment === 'live' ? 'live' : 'test'
profiles.value[env] = {
product_mappings: productMappings.value.map((r) => ({ ...r })),
private_key_configured: waffo.private_key_configured,
}
}
function applyEnv(env) {
const target = env === 'live' ? 'live' : 'test'
waffo.environment = target
const profile = profiles.value[target]
productMappings.value = normalizeMappings(profile || {})
const configured = !!(keyByEnv.value[target] || profile?.private_key_configured)
waffo.private_key_configured = configured
privateKeyInput.value = ''
clearKeyFlag.value = false
// 未配置私钥时强制展开输入框,避免仍显示另一环境密钥状态 // Logan.Li
showPrivateKey.value = !configured
}
function onEnvSwitch(live) {
stashCurrentEnv()
applyEnv(live ? 'live' : 'test')
ElMessage.info(live ? '已切换到生产环境视图(保存后生效)' : '已切换到测试环境视图(保存后生效)')
}
function addMapping() {
productMappings.value.push({ plan_key: '', label: '', product_id: '' })
}
@@ -225,12 +274,28 @@ async function load() {
loading.value = true
try {
const res = await getWaffoSettings()
Object.assign(waffo, res.data || {})
const data = res.data || {}
Object.assign(waffo, data)
if (!waffo.environment) waffo.environment = waffo.sandbox_mode === false ? 'live' : 'test'
productMappings.value = normalizeMappings(res.data || {})
showPrivateKey.value = false
privateKeyInput.value = ''
clearKeyFlag.value = false
waffo.environment = waffo.environment === 'live' ? 'live' : 'test'
keyByEnv.value = {
test: !!(data.private_key_configured_by_env?.test),
live: !!(data.private_key_configured_by_env?.live),
}
profiles.value = {
test: data.profiles?.test || { product_mappings: [] },
live: data.profiles?.live || { product_mappings: [] },
}
// 当前环境优先用顶层字段(已 ApplyEnvProfile) // Logan.Li
profiles.value[waffo.environment] = {
product_mappings: data.product_mappings || [],
product_id: data.product_id,
product_id_yearly: data.product_id_yearly,
product_id_perpetual: data.product_id_perpetual,
private_key_configured: data.private_key_configured,
}
applyEnv(waffo.environment)
debugResult.value = ''
} finally {
loading.value = false
@@ -240,6 +305,7 @@ async function load() {
async function save() {
saving.value = true
try {
stashCurrentEnv()
const compat = mappingsToCompatFields(productMappings.value)
const payload = {
enabled: waffo.enabled,
@@ -252,15 +318,29 @@ async function save() {
clear_private_key: clearKeyFlag.value,
...compat,
}
if (showPrivateKey.value && privateKeyInput.value.trim()) {
if (privateKeyInput.value.trim()) {
payload.private_key = privateKeyInput.value.trim()
payload.clear_private_key = false
} else if (!waffo.private_key_configured) {
// 当前环境无密钥且未填写:明确清除该环境密钥位 // Logan.Li
payload.clear_private_key = true
}
const res = await updateWaffoSettings(payload)
Object.assign(waffo, res.data || {})
productMappings.value = normalizeMappings(res.data || { ...compat })
showPrivateKey.value = false
privateKeyInput.value = ''
clearKeyFlag.value = false
const data = res.data || {}
keyByEnv.value = {
test: !!(data.private_key_configured_by_env?.test),
live: !!(data.private_key_configured_by_env?.live),
}
profiles.value = {
test: data.profiles?.test || profiles.value.test,
live: data.profiles?.live || profiles.value.live,
}
profiles.value[waffo.environment] = {
product_mappings: data.product_mappings || [],
private_key_configured: data.private_key_configured,
}
applyEnv(waffo.environment)
ElMessage.success('支付配置已保存')
} catch { /* */ } finally {
saving.value = false
@@ -270,9 +350,10 @@ async function save() {
function clearPrivateKey() {
clearKeyFlag.value = true
waffo.private_key_configured = false
keyByEnv.value[waffo.environment] = false
privateKeyInput.value = ''
showPrivateKey.value = false
ElMessage.info('将在保存后清除私钥')
showPrivateKey.value = true
ElMessage.info('将在保存后清除当前环境私钥')
}
async function copyWebhook() {
@@ -315,6 +396,8 @@ onMounted(load)
.title { font-weight: 600; }
.header-actions { display: flex; gap: 8px; }
.secret-row { display: flex; align-items: center; gap: 10px; }
.env-row { display: flex; align-items: center; }
.muted { color: #c0c4cc; }
.hint { margin-top: 6px; font-size: 12px; color: #909399; }
.debug-box {
margin: 0;