feat: AI routing DB-driven, payment gateway full integration, WeChat mini-program CI/CD
- AI routing rules now stored in system_configs DB table instead of hardcoded config - Multi-model support via name|model composite key for same-provider routing - UnifiedPayService with HMAC-SHA256 gateway integration (alipay/wechat) - Admin payment panel: list, stats, search, filter, refund - WeChat mini-program CI/CD via miniprogram-ci (v1.0.9) - Translation quota extended to LLM provider tier - SearchService with DB-driven provider config (bing/google_cse/searxng) - Footer cleanup across admin/workspace/uni-app - Private key excluded from git tracking
This commit is contained in:
@@ -58,4 +58,8 @@ export function processInvoice(id, action) {
|
||||
export function aiChat(message, history = []) { return http.post('/ai/chat', { message, history }) }
|
||||
export function aiQuickQuestions() { return http.get('/ai/quick-questions') }
|
||||
|
||||
export function listPayments(params) { return http.get('/admin/payments', { params }) }
|
||||
export function getPaymentStats() { return http.get('/admin/payments/stats') }
|
||||
export function adminRefund(order_no, reason = '') { return http.post('/admin/payments/refund', { order_no, reason }) }
|
||||
|
||||
export default http
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>日志</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/payments">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
<span>支付管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/config">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>配置</span>
|
||||
@@ -93,37 +97,13 @@
|
||||
|
||||
<el-footer class="footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<div class="footer-brand">TradeMate</div>
|
||||
<p class="footer-tagline">AI 外贸小助手 · 让外贸更简单</p>
|
||||
<div class="qrcode-row">
|
||||
<div class="qrcode-item">
|
||||
<img src="/images/yzr/yuzhiran.jpg" alt="微信公众号" class="qrcode-img" />
|
||||
<span>微信公众号</span>
|
||||
</div>
|
||||
<div class="qrcode-item">
|
||||
<img src="/images/yzr/yuzhiran-tech.jpg" alt="微信服务号" class="qrcode-img" />
|
||||
<span>微信服务号</span>
|
||||
</div>
|
||||
<div class="qrcode-item">
|
||||
<img src="/images/yzr/yuzhiran-yhl.jpg" alt="小程序" class="qrcode-img" />
|
||||
<span>小程序</span>
|
||||
</div>
|
||||
<div class="qrcode-item">
|
||||
<img src="/images/yzr/kefu.png" alt="客服" class="qrcode-img" />
|
||||
<span>微信客服</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© {{ new Date().getFullYear() }} TradeMate 外贸小助手. 保留所有权利.</p>
|
||||
<div class="footer-links">
|
||||
<a href="http://beian.miit.gov.cn/" target="_blank">{{ beianInfo.icp }}</a>
|
||||
<a v-if="beianInfo.showGongan" :href="beianInfo.gonganLink" target="_blank" rel="noreferrer" class="gongan-link">
|
||||
<img src="/images/beian/gongan-beian.png" alt="公安备案" class="gongan-icon" />
|
||||
{{ beianInfo.gongan }}
|
||||
</a>
|
||||
</div>
|
||||
<p>© {{ new Date().getFullYear() }} TradeMate</p>
|
||||
<div class="footer-links">
|
||||
<a href="http://beian.miit.gov.cn/" target="_blank">{{ beianInfo.icp }}</a>
|
||||
<a v-if="beianInfo.showGongan" :href="beianInfo.gonganLink" target="_blank" rel="noreferrer" class="gongan-link">
|
||||
<img src="/images/beian/gongan-beian.png" alt="公安备案" class="gongan-icon" />
|
||||
{{ beianInfo.gongan }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</el-footer>
|
||||
@@ -172,14 +152,7 @@ const beianInfo = computed(() => {
|
||||
.user-name { font-size: 14px; color: #333; }
|
||||
.main-content { background: #f5f5f5; padding: 20px; overflow-y: auto; }
|
||||
.footer { padding: 0; background: #fff; border-top: 1px solid #e8e8e8; color: #666; font-size: 12px; }
|
||||
.footer-content { padding: 20px 24px 16px; }
|
||||
.footer-section { margin-bottom: 16px; }
|
||||
.footer-brand { font-size: 15px; font-weight: 700; color: #1890ff; margin-bottom: 4px; }
|
||||
.footer-tagline { color: #999; font-size: 12px; margin-bottom: 12px; }
|
||||
.qrcode-row { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.qrcode-item { display: flex; flex-direction: column; align-items: center; gap: 4px; color: #999; font-size: 11px; }
|
||||
.qrcode-img { width: 48px; height: 48px; border-radius: 6px; }
|
||||
.footer-bottom { border-top: 1px solid #eee; padding-top: 12px; display: flex; justify-content: center; align-items: center; gap: 16px; flex-wrap: wrap; }
|
||||
.footer-content { padding: 8px 24px; display: flex; justify-content: center; align-items: center; gap: 16px; flex-wrap: wrap; }
|
||||
.footer-links { display: flex; gap: 16px; align-items: center; }
|
||||
.footer-links a { color: #999; text-decoration: none; }
|
||||
.footer-links a:hover { color: #1890ff; }
|
||||
|
||||
@@ -36,6 +36,14 @@ const routes = [
|
||||
{ path: '', name: 'Logs', component: () => import('@/views/Logs.vue'), meta: { title: '日志' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/payments',
|
||||
component: AdminLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{ path: '', name: 'Payments', component: () => import('@/views/Payments.vue'), meta: { title: '支付管理' } },
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
component: AdminLayout,
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
<div class="cfg-group-title">{{ fieldLabelsMap.ai_routing?.[taskKey] || taskKey }}</div>
|
||||
<div class="cfg-field">
|
||||
<span class="cfg-label">主选</span>
|
||||
<el-select v-model="edits[cfg.key][taskKey].primary" size="small" style="width:300px" filterable>
|
||||
<el-option v-for="p in providers" :key="p.provider_type" :value="p.provider_type" :label="p.name + ' — ' + p.model_name" />
|
||||
<el-select v-model="edits[cfg.key][taskKey].primary" size="small" style="width:380px" filterable>
|
||||
<el-option v-for="p in providers" :key="p.name + '|' + p.model_name" :value="p.name + '|' + p.model_name" :label="p.name + ' — ' + p.model_name" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="cfg-field">
|
||||
<span class="cfg-label">备用</span>
|
||||
<el-select v-model="edits[cfg.key][taskKey].fallback" size="small" style="width:400px" multiple filterable collapse-tags>
|
||||
<el-option v-for="p in providers" :key="p.provider_type" :value="p.provider_type" :label="p.name + ' — ' + p.model_name" />
|
||||
<el-select v-model="edits[cfg.key][taskKey].fallback" size="small" style="width:500px" multiple filterable collapse-tags>
|
||||
<el-option v-for="p in providers" :key="p.name + '|' + p.model_name" :value="p.name + '|' + p.model_name" :label="p.name + ' — ' + p.model_name" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="16" style="margin-bottom:20px">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">总收入</div>
|
||||
<div class="stat-value">¥{{ (stats.total_revenue || 0).toFixed(2) }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已支付</div>
|
||||
<div class="stat-value" style="color:#52c41a">{{ stats.paid_count || 0 }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">待支付</div>
|
||||
<div class="stat-value" style="color:#faad14">{{ stats.pending_count || 0 }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">已退款</div>
|
||||
<div class="stat-value" style="color:#ff4d4f">{{ stats.refunded_count || 0 }}</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>交易记录</span>
|
||||
<div style="display:flex;gap:8px">
|
||||
<el-select v-model="filters.status" placeholder="状态" clearable style="width:120px" @change="loadData">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待支付" value="pending" />
|
||||
<el-option label="已支付" value="paid" />
|
||||
<el-option label="已退款" value="refunded" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
<el-select v-model="filters.pay_type" placeholder="支付方式" clearable style="width:120px" @change="loadData">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="支付宝" value="alipay" />
|
||||
<el-option label="微信" value="wechat" />
|
||||
</el-select>
|
||||
<el-input v-model="filters.user_id" placeholder="用户ID" clearable style="width:180px" @clear="loadData" @keyup.enter="loadData" />
|
||||
<el-button type="primary" @click="loadData">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="list" v-loading="loading" stripe style="width:100%">
|
||||
<el-table-column prop="order_no" label="订单号" width="200" />
|
||||
<el-table-column prop="user_id" label="用户" min-width="180" />
|
||||
<el-table-column prop="plan" label="套餐" width="100" />
|
||||
<el-table-column label="金额" width="100">
|
||||
<template #default="{ row }">¥{{ (row.amount || 0).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付方式" width="80">
|
||||
<template #default="{ row }">{{ row.pay_type === 'wechat' ? '微信' : '支付宝' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status === 'paid'" type="success" size="small">已支付</el-tag>
|
||||
<el-tag v-else-if="row.status === 'pending'" type="warning" size="small">待支付</el-tag>
|
||||
<el-tag v-else-if="row.status === 'refunded'" type="danger" size="small">已退款</el-tag>
|
||||
<el-tag v-else type="info" size="small">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" width="170">
|
||||
<template #default="{ row }">{{ row.paid_at || row.created_at }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 'paid'" type="danger" size="small" @click="handleRefund(row)">退款</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="margin-top:16px;display:flex;justify-content:flex-end">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="refundDialog.visible" title="确认退款" width="400px">
|
||||
<p>订单号:{{ refundDialog.order_no }}</p>
|
||||
<p>退款金额:¥{{ (refundDialog.amount || 0).toFixed(2) }}</p>
|
||||
<el-input v-model="refundDialog.reason" type="textarea" placeholder="退款原因(可选)" :rows="3" />
|
||||
<template #footer>
|
||||
<el-button @click="refundDialog.visible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="refundDialog.loading" @click="confirmRefund">确认退款</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { listPayments, getPaymentStats, adminRefund } from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
const loading = ref(false)
|
||||
const stats = ref({})
|
||||
|
||||
const filters = reactive({ status: '', pay_type: '', user_id: '' })
|
||||
|
||||
const refundDialog = reactive({ visible: false, loading: false, order_no: '', amount: 0, reason: '' })
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
loadData()
|
||||
})
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await getPaymentStats()
|
||||
stats.value = res.data || res
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, size: size.value }
|
||||
if (filters.status) params.status = filters.status
|
||||
if (filters.pay_type) params.pay_type = filters.pay_type
|
||||
if (filters.user_id) params.user_id = filters.user_id
|
||||
const res = await listPayments(params)
|
||||
const d = res.data || res
|
||||
list.value = d.items || []
|
||||
total.value = d.total || 0
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function handleRefund(row) {
|
||||
refundDialog.order_no = row.order_no
|
||||
refundDialog.amount = row.amount
|
||||
refundDialog.reason = ''
|
||||
refundDialog.visible = true
|
||||
}
|
||||
|
||||
async function confirmRefund() {
|
||||
refundDialog.loading = true
|
||||
try {
|
||||
await adminRefund(refundDialog.order_no, refundDialog.reason)
|
||||
ElMessage.success('退款成功')
|
||||
refundDialog.visible = false
|
||||
loadData()
|
||||
loadStats()
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.detail || '退款失败')
|
||||
} finally {
|
||||
refundDialog.loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-item { text-align: center; padding: 8px 0; }
|
||||
.stat-label { font-size: 13px; color: #999; margin-bottom: 4px; }
|
||||
.stat-value { font-size: 24px; font-weight: 700; color: #1890ff; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user