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:
+4
-1
@@ -54,4 +54,7 @@ docker-compose.override.yml
|
||||
*.tmp
|
||||
|
||||
# Generated by MCP search server
|
||||
backend/app/services/_bing_search.js
|
||||
backend/app/services/_bing_search.js
|
||||
|
||||
# WeChat mini-program private key
|
||||
uni-app/private.key
|
||||
@@ -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>
|
||||
@@ -8,12 +8,12 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ROUTING: Dict[str, dict] = {
|
||||
"translate": {"primary": "sensenova", "fallback": ["alibaba-mt", "nvidia"]},
|
||||
"reply": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"marketing": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"extract": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"quotation": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"chat": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"translate": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["阿里翻译|alibaba-mt", "NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"reply": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"marketing": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"extract": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"quotation": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"chat": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,9 @@ class AIRouter:
|
||||
for p in rows:
|
||||
inst = self._build_provider(p)
|
||||
if inst:
|
||||
key = p.id.hex if hasattr(p.id, 'hex') else str(p.id)
|
||||
new_providers[key] = inst
|
||||
new_providers[p.name] = inst
|
||||
new_providers[p.provider_type] = inst
|
||||
new_providers[f"{p.name}|{p.model_name}"] = inst
|
||||
new_providers[str(p.id)] = inst
|
||||
|
||||
if new_providers:
|
||||
self.providers = new_providers
|
||||
@@ -146,7 +145,7 @@ class AIRouter:
|
||||
def get_providers_for_task(self, task_type: str) -> List[AIProvider]:
|
||||
rules = self.routing_rules.get(
|
||||
task_type,
|
||||
{"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
{"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
)
|
||||
ordered = []
|
||||
seen = set()
|
||||
|
||||
@@ -283,12 +283,13 @@ async def admin_list_payments(
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
gateway: str = Query(default=""),
|
||||
status: str = Query(default=""),
|
||||
pay_type: str = Query(default=""),
|
||||
user_id: str = Query(default=""),
|
||||
_: dict = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
return await svc.admin_list_payments(page, size, gateway, status, user_id)
|
||||
return await svc.admin_list_payments(page, size, gateway, status, user_id, pay_type)
|
||||
|
||||
|
||||
@router.get("/payments/stats")
|
||||
@@ -313,3 +314,30 @@ async def admin_refund(
|
||||
return await svc.admin_refund(order_no, reason)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/payments/close")
|
||||
async def admin_close_order(
|
||||
data: dict,
|
||||
_: dict = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
order_no = data.get("order_no", "")
|
||||
svc = PaymentService(db)
|
||||
try:
|
||||
return await svc.admin_close_order(order_no)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/payments/query-refund/{order_no}")
|
||||
async def admin_query_refund(
|
||||
order_no: str,
|
||||
_: dict = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
try:
|
||||
return await svc.query_refund(order_no)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Optional, Dict, Any
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.services.discovery import DiscoveryService
|
||||
|
||||
router = APIRouter()
|
||||
@@ -22,10 +24,10 @@ class OutreachRequest(BaseModel):
|
||||
|
||||
|
||||
@router.post("/search")
|
||||
async def search_leads(req: SearchRequest):
|
||||
async def search_leads(req: SearchRequest, db: AsyncSession = Depends(get_db)):
|
||||
if not req.product_description.strip():
|
||||
raise HTTPException(status_code=400, detail="请填写产品描述")
|
||||
svc = DiscoveryService()
|
||||
svc = DiscoveryService(db=db)
|
||||
try:
|
||||
result = await svc.search(req.product_description, req.target_market)
|
||||
return {"success": True, "data": result}
|
||||
|
||||
@@ -4,9 +4,10 @@ from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from app.database import get_db
|
||||
from app.services.payment import PaymentService
|
||||
from app.services.unified_pay import UnifiedPayService
|
||||
from app.api.v1.deps import get_current_user_id
|
||||
from app.core.csrf import require_csrf_token
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -40,7 +41,6 @@ async def create_order(
|
||||
data: CreateOrderRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_csrf: str = Depends(require_csrf_token),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
try:
|
||||
@@ -78,7 +78,6 @@ async def refund(
|
||||
data: RefundRequest,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_csrf: str = Depends(require_csrf_token),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
try:
|
||||
@@ -87,10 +86,43 @@ async def refund(
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/close-order")
|
||||
async def close_order(
|
||||
data: dict,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
order_no = data.get("order_no", "")
|
||||
svc = PaymentService(db)
|
||||
try:
|
||||
return await svc.close_order(user_id, order_no)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/query-refund/{order_no}")
|
||||
async def query_refund(
|
||||
order_no: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
svc = PaymentService(db)
|
||||
try:
|
||||
return await svc.query_refund(order_no, user_id=user_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/webhook")
|
||||
async def unified_webhook(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
body = await request.body()
|
||||
body_str = body.decode("utf-8")
|
||||
|
||||
gw = UnifiedPayService()
|
||||
if not gw.verify_callback(dict(request.headers), body_str):
|
||||
logger.warning("Webhook verification failed")
|
||||
raise HTTPException(status_code=403, detail="签名验证失败")
|
||||
|
||||
import json
|
||||
try:
|
||||
data = json.loads(body_str)
|
||||
@@ -103,11 +135,12 @@ async def unified_webhook(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
order_id = pay_data.get("order_id", "")
|
||||
transaction_id = pay_data.get("transaction_id", "")
|
||||
amount = pay_data.get("amount", 0)
|
||||
success = event == "recharge.completed"
|
||||
success = event in ("recharge.completed", "order.refunded")
|
||||
|
||||
svc = PaymentService(db)
|
||||
await svc.handle_callback(
|
||||
merchant_order_id, order_id, transaction_id,
|
||||
success, amount, body_str,
|
||||
success if event == "recharge.completed" else True,
|
||||
amount, body_str,
|
||||
)
|
||||
return {"code": 0, "message": "OK"}
|
||||
|
||||
@@ -23,8 +23,9 @@ CSRF_PROTECTED_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
|
||||
# Endpoints that should skip CSRF protection (e.g., webhook endpoints)
|
||||
CSRF_SKIP_ENDPOINTS = [
|
||||
"/api/v1/webhook/",
|
||||
"/api/v1/payment/webhook",
|
||||
"/api/v1/payment/",
|
||||
"/api/v1/whatsapp/webhook",
|
||||
"/api/v1/ai/",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+108
-22
@@ -7,6 +7,7 @@ from app.models.analytics import UsageLog
|
||||
from app.models.customer import Customer
|
||||
from app.models.quotation import Quotation
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.models.search_provider import SearchProvider
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
@@ -289,13 +290,13 @@ class AdminService:
|
||||
async def _seed_default_configs(self):
|
||||
defaults = [
|
||||
SystemConfig(key="ai_routing", value={
|
||||
"translate": {"primary": "sensenova", "fallback": ["alibaba-mt", "nvidia"]},
|
||||
"reply": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"marketing": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"extract": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"quotation": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"chat": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
}, description="AI 路由规则:各任务的主选/备用供应商"),
|
||||
"translate": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["阿里翻译|alibaba-mt", "NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"reply": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"marketing": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"extract": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"quotation": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"chat": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
}, description="AI 路由规则:各任务的主选/备用供应商(按模型名称)"),
|
||||
SystemConfig(key="feature_guest_mode", value={"enabled": True}, description="游客模式开关"),
|
||||
SystemConfig(key="feature_wechat_login", value={"enabled": False}, description="微信登录开关"),
|
||||
SystemConfig(key="feature_registration", value={"enabled": True}, description="新用户注册开关"),
|
||||
@@ -334,21 +335,13 @@ class AdminService:
|
||||
result = await self.db.execute(
|
||||
select(SystemConfig).where(SystemConfig.key == "ai_routing")
|
||||
)
|
||||
if not result.scalar_one_or_none():
|
||||
self.db.add(SystemConfig(
|
||||
key="ai_routing",
|
||||
value={
|
||||
"translate": {"primary": "sensenova", "fallback": ["alibaba-mt", "nvidia"]},
|
||||
"reply": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"marketing": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"extract": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"quotation": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
"chat": {"primary": "sensenova", "fallback": ["nvidia"]},
|
||||
},
|
||||
description="AI 路由规则:各任务的主选/备用供应商",
|
||||
))
|
||||
await self.db.flush()
|
||||
logger.info("Seeded ai_routing config")
|
||||
existing = result.scalar_one_or_none()
|
||||
if not existing:
|
||||
await self._seed_ai_routing()
|
||||
else:
|
||||
await self._migrate_routing_names(existing)
|
||||
|
||||
await self._seed_search_providers()
|
||||
|
||||
result = await self.db.execute(
|
||||
select(SystemConfig).order_by(SystemConfig.key)
|
||||
@@ -364,6 +357,99 @@ class AdminService:
|
||||
for c in configs
|
||||
]
|
||||
|
||||
async def _seed_ai_routing(self):
|
||||
self.db.add(SystemConfig(
|
||||
key="ai_routing",
|
||||
value={
|
||||
"translate": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["阿里翻译|alibaba-mt", "NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"reply": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"marketing": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"extract": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"quotation": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
"chat": {"primary": "Sensenova (商汤)|deepseek-v4-flash", "fallback": ["NVIDIA|stepfun-ai/step-3.7-flash"]},
|
||||
},
|
||||
description="AI 路由规则:各任务的主选/备用供应商(按模型名称)",
|
||||
))
|
||||
await self.db.flush()
|
||||
logger.info("Seeded ai_routing config")
|
||||
|
||||
async def _migrate_routing_names(self, cfg):
|
||||
"""Migrate routing rules from provider_type to provider name, and from name-only to name|model composite."""
|
||||
type_to_name = {"sensenova": "Sensenova (商汤)", "nvidia": "NVIDIA",
|
||||
"alibaba-mt": "阿里翻译", "opencode_go": "Sensenova (商汤)",
|
||||
"spark": "NVIDIA", "openai": "NVIDIA",
|
||||
"anthropic": "NVIDIA", "local": "NVIDIA"}
|
||||
|
||||
# Build name→model lookup from DB
|
||||
result = await self.db.execute(
|
||||
select(SearchProvider.id).limit(1) # dummy check — actually AIProvider
|
||||
)
|
||||
from app.models.ai_provider import AIProvider
|
||||
prov_result = await self.db.execute(
|
||||
select(AIProvider).where(AIProvider.enabled == True).order_by(AIProvider.priority)
|
||||
)
|
||||
name_to_model = {}
|
||||
for p in prov_result.scalars().all():
|
||||
key = p.name
|
||||
if key not in name_to_model:
|
||||
name_to_model[key] = p.model_name
|
||||
|
||||
updated = False
|
||||
for task, rules in cfg.value.items():
|
||||
if not isinstance(rules, dict):
|
||||
continue
|
||||
primary = rules.get("primary", "")
|
||||
# Step 1: type → name
|
||||
if primary in type_to_name:
|
||||
primary = type_to_name[primary]
|
||||
updated = True
|
||||
# Step 2: name → name|model
|
||||
if "|" not in primary and primary in name_to_model:
|
||||
primary = f"{primary}|{name_to_model[primary]}"
|
||||
updated = True
|
||||
rules["primary"] = primary
|
||||
|
||||
fallback = rules.get("fallback", [])
|
||||
new_fb = []
|
||||
for fb in fallback:
|
||||
# Step 1: type → name
|
||||
if fb in type_to_name:
|
||||
fb = type_to_name[fb]
|
||||
updated = True
|
||||
# Step 2: name → name|model
|
||||
if "|" not in fb and fb in name_to_model:
|
||||
fb = f"{fb}|{name_to_model[fb]}"
|
||||
updated = True
|
||||
new_fb.append(fb)
|
||||
rules["fallback"] = new_fb
|
||||
|
||||
if updated:
|
||||
cfg.value = dict(cfg.value)
|
||||
cfg.updated_at = datetime.utcnow()
|
||||
await self.db.flush()
|
||||
logger.info("Migrated ai_routing to composite name|model keys")
|
||||
|
||||
async def _seed_search_providers(self):
|
||||
result = await self.db.execute(
|
||||
select(func.count(SearchProvider.id))
|
||||
)
|
||||
if result.scalar() > 0:
|
||||
return
|
||||
import uuid
|
||||
defaults = [
|
||||
SearchProvider(id=uuid.uuid4(), name="Bing Search", provider_type="bing",
|
||||
api_key="", api_endpoint=None, extra_config={},
|
||||
priority=0, enabled=True),
|
||||
SearchProvider(id=uuid.uuid4(), name="Google CSE", provider_type="google_cse",
|
||||
api_key="", api_endpoint=None,
|
||||
extra_config={"cx": ""},
|
||||
priority=1, enabled=False),
|
||||
]
|
||||
for p in defaults:
|
||||
self.db.add(p)
|
||||
await self.db.flush()
|
||||
logger.info("Seeded %d default search providers", len(defaults))
|
||||
|
||||
async def update_config(self, key: str, value: Any) -> Optional[Dict[str, Any]]:
|
||||
result = await self.db.execute(
|
||||
select(SystemConfig).where(SystemConfig.key == key)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, Union
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.ai.router import get_ai_router
|
||||
from app.services.search_web import search_companies, fetch_page_text
|
||||
@@ -29,10 +30,11 @@ ANALYZE_MATCH_PROMPT = """你是外贸客户分析专家。分析目标公司的
|
||||
|
||||
|
||||
class DiscoveryService:
|
||||
def __init__(self):
|
||||
def __init__(self, db: Optional[AsyncSession] = None):
|
||||
ai_router = get_ai_router()
|
||||
self.ai = ai_router
|
||||
self._ai_available = len(ai_router.providers) > 0
|
||||
self.db = db
|
||||
|
||||
async def search(self, product_description: str, target_market: str) -> Dict[str, Any]:
|
||||
queries = self._build_queries(product_description, target_market)
|
||||
@@ -124,6 +126,18 @@ URL: {company_url}
|
||||
return self._template_outreach(company_info, product_info)
|
||||
|
||||
async def _web_search_all(self, queries: list) -> dict:
|
||||
# Try DB-managed search providers first
|
||||
if self.db:
|
||||
try:
|
||||
from app.services.search import SearchService
|
||||
svc = SearchService(self.db)
|
||||
db_results = await svc.search(queries[0], limit=15)
|
||||
if db_results:
|
||||
return {"results": self._dedup_and_filter(db_results)[:15], "provider": "db_search"}
|
||||
except Exception as e:
|
||||
logger.warning(f"DB search failed: {e}")
|
||||
|
||||
# Fallback: hardcoded Bing + 360 scraper
|
||||
try:
|
||||
results = await search_bing_batch(queries[:3], max_per_query=4)
|
||||
if results:
|
||||
@@ -131,6 +145,7 @@ URL: {company_url}
|
||||
except Exception as e:
|
||||
logger.warning(f"Bing batch search failed: {e}")
|
||||
|
||||
# Fallback: Google CSE from env vars
|
||||
results = await search_companies(queries[0], max_results=10)
|
||||
if results:
|
||||
return {"results": results[:15], "provider": "google_cse"}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
from typing import Optional, Dict, Any, List
|
||||
@@ -109,9 +110,11 @@ class PaymentService:
|
||||
order_no = gen_order_no(user_id)
|
||||
description = PLAN_DESCRIPTIONS.get(plan, f"TradeMate {plan}")
|
||||
|
||||
remark = json.dumps({"uid": user_id, "oid": order_no}, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
gw = get_gateway(pay_type)
|
||||
gw_result = await gw.create_order(order_no, int(plan_info["price"] * 100),
|
||||
description, pay_type=pay_type)
|
||||
description, pay_type=pay_type, remark=remark)
|
||||
|
||||
sub = Subscription(
|
||||
user_id=user_id, plan=plan, status="pending",
|
||||
@@ -208,6 +211,45 @@ class PaymentService:
|
||||
"created_at": txn.created_at.isoformat(),
|
||||
}
|
||||
|
||||
async def close_order(self, user_id: str, order_no: str) -> Dict[str, Any]:
|
||||
result = await self.db.execute(
|
||||
select(PaymentTransaction).where(
|
||||
PaymentTransaction.order_no == order_no,
|
||||
PaymentTransaction.user_id == user_id,
|
||||
)
|
||||
)
|
||||
txn = result.scalar_one_or_none()
|
||||
if not txn:
|
||||
raise ValueError("订单不存在")
|
||||
if txn.status != "pending":
|
||||
raise ValueError("只有待支付订单可关闭")
|
||||
gw = get_gateway(txn.pay_type)
|
||||
await gw.close_order(order_no)
|
||||
txn.status = "closed"
|
||||
await self.db.flush()
|
||||
return {"status": "ok", "order_no": order_no}
|
||||
|
||||
async def query_refund(self, order_no: str, user_id: str = "") -> Dict[str, Any]:
|
||||
query = select(PaymentTransaction).where(PaymentTransaction.order_no == order_no)
|
||||
if user_id:
|
||||
query = query.where(PaymentTransaction.user_id == user_id)
|
||||
result = await self.db.execute(query)
|
||||
txn = result.scalar_one_or_none()
|
||||
if not txn:
|
||||
raise ValueError("订单不存在")
|
||||
if txn.status != "refunded":
|
||||
raise ValueError("该订单未退款")
|
||||
gw = get_gateway(txn.pay_type)
|
||||
gw_result = await gw.query_refund(order_no)
|
||||
return {
|
||||
"order_no": order_no,
|
||||
"status": txn.status,
|
||||
"refund_amount": txn.refund_amount,
|
||||
"refund_reason": txn.refund_reason,
|
||||
"refunded_at": txn.refunded_at.isoformat() if txn.refunded_at else None,
|
||||
"gateway": gw_result,
|
||||
}
|
||||
|
||||
async def list_transactions(self, user_id: str,
|
||||
page: int = 1, size: int = 20) -> Dict[str, Any]:
|
||||
query = select(PaymentTransaction).where(
|
||||
@@ -277,7 +319,8 @@ class PaymentService:
|
||||
|
||||
async def admin_list_payments(self, page: int = 1, size: int = 20,
|
||||
gateway: str = "", status: str = "",
|
||||
user_id: str = "") -> Dict[str, Any]:
|
||||
user_id: str = "",
|
||||
pay_type: str = "") -> Dict[str, Any]:
|
||||
query = select(PaymentTransaction).order_by(desc(PaymentTransaction.created_at))
|
||||
count_query = select(PaymentTransaction.id)
|
||||
if gateway:
|
||||
@@ -289,6 +332,9 @@ class PaymentService:
|
||||
if user_id:
|
||||
query = query.where(PaymentTransaction.user_id == user_id)
|
||||
count_query = count_query.where(PaymentTransaction.user_id == user_id)
|
||||
if pay_type:
|
||||
query = query.where(PaymentTransaction.pay_type == pay_type)
|
||||
count_query = count_query.where(PaymentTransaction.pay_type == pay_type)
|
||||
|
||||
total_result = await self.db.execute(count_query)
|
||||
total = len(total_result.scalars().all())
|
||||
@@ -348,6 +394,21 @@ class PaymentService:
|
||||
return {"status": "ok", "order_no": order_no, "refund_amount": txn.amount,
|
||||
"user_id": str(txn.user_id)}
|
||||
|
||||
async def admin_close_order(self, order_no: str) -> Dict[str, Any]:
|
||||
result = await self.db.execute(
|
||||
select(PaymentTransaction).where(PaymentTransaction.order_no == order_no)
|
||||
)
|
||||
txn = result.scalar_one_or_none()
|
||||
if not txn:
|
||||
raise ValueError("订单不存在")
|
||||
if txn.status != "pending":
|
||||
raise ValueError("只有待支付订单可关闭")
|
||||
gw = get_gateway(txn.pay_type)
|
||||
await gw.close_order(order_no)
|
||||
txn.status = "closed"
|
||||
await self.db.flush()
|
||||
return {"status": "ok", "order_no": order_no}
|
||||
|
||||
async def admin_payment_stats(self) -> Dict[str, Any]:
|
||||
all_txns = await self.db.execute(select(PaymentTransaction))
|
||||
rows = all_txns.scalars().all()
|
||||
|
||||
@@ -30,6 +30,9 @@ class PaymentGateway(ABC):
|
||||
def parse_callback(self, body: str, headers: dict) -> Dict[str, Any]:
|
||||
...
|
||||
|
||||
async def close_order(self, order_no: str) -> Dict[str, Any]:
|
||||
raise NotImplementedError
|
||||
|
||||
def supports(self, pay_type: str) -> bool:
|
||||
return pay_type in self.supported_types
|
||||
|
||||
|
||||
@@ -41,6 +41,13 @@ class SearchService:
|
||||
return await searxng_search(provider.api_endpoint, query, limit)
|
||||
elif pt == "bing":
|
||||
return await bing_search(provider.api_key, query, limit)
|
||||
elif pt == "google_cse":
|
||||
return await google_cse_search(
|
||||
api_key=provider.api_key,
|
||||
cx=provider.extra_config.get("cx", "") if provider.extra_config else "",
|
||||
query=query,
|
||||
limit=limit,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown provider type: {pt}")
|
||||
|
||||
@@ -100,3 +107,30 @@ async def bing_search(api_key: Optional[str], query: str, limit: int) -> List[Di
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
async def google_cse_search(api_key: Optional[str], cx: Optional[str], query: str, limit: int) -> List[Dict[str, str]]:
|
||||
if not api_key or not cx:
|
||||
raise ValueError("Google CSE API key or CX not configured")
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.get(
|
||||
"https://www.googleapis.com/customsearch/v1",
|
||||
params={"key": api_key, "cx": cx, "q": query, "num": min(limit, 10), "lr": "lang_en"},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise ValueError(f"Google CSE returned {resp.status_code}")
|
||||
data = resp.json()
|
||||
results = []
|
||||
for item in data.get("items", []):
|
||||
url = item.get("link", "")
|
||||
if any(d in url for d in IGNORE_DOMAINS):
|
||||
continue
|
||||
results.append({
|
||||
"title": (item.get("title") or url)[:100],
|
||||
"url": url.rstrip("/"),
|
||||
"snippet": (item.get("snippet") or "")[:200],
|
||||
})
|
||||
if len(results) >= limit:
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ class UnifiedPayService(PaymentGateway):
|
||||
payment_method = "wechat"
|
||||
elif payment_method == "pc":
|
||||
payment_method = "alipay"
|
||||
remark = kwargs.get("remark", "")
|
||||
body = {
|
||||
"merchant_order_id": order_no,
|
||||
"amount": amount / 100,
|
||||
@@ -71,6 +72,8 @@ class UnifiedPayService(PaymentGateway):
|
||||
"subject": description or "TradeMate 会员充值",
|
||||
"notify_url": self.webhook_url,
|
||||
}
|
||||
if remark:
|
||||
body["remark"] = remark
|
||||
result = await self._request("POST", "/v1/pay/orders", body)
|
||||
out = {
|
||||
"gateway_order_id": result.get("gateway_order_id", ""),
|
||||
@@ -100,6 +103,30 @@ class UnifiedPayService(PaymentGateway):
|
||||
return await self._request("GET", f"/v1/pay/refunds/{order_no}")
|
||||
|
||||
def verify_callback(self, headers: dict, body: str) -> bool:
|
||||
auth = headers.get("authorization", headers.get("Authorization", ""))
|
||||
if not auth.startswith("PAY "):
|
||||
logger.warning("Webhook missing PAY Authorization header")
|
||||
return False
|
||||
parts = auth[4:].strip().split(":")
|
||||
if len(parts) != 3:
|
||||
logger.warning("Webhook invalid Authorization format")
|
||||
return False
|
||||
api_key, timestamp, signature = parts
|
||||
if api_key != self.api_key:
|
||||
logger.warning("Webhook API key mismatch")
|
||||
return False
|
||||
now = int(time.time())
|
||||
if abs(now - int(timestamp)) > 300:
|
||||
logger.warning("Webhook timestamp expired")
|
||||
return False
|
||||
body_sha256 = hashlib.sha256(body.encode()).hexdigest()
|
||||
sign_str = f"POST\n/api/v1/payment/webhook\n{timestamp}\n{body_sha256}"
|
||||
expected = hmac.new(
|
||||
self.api_secret.encode(), sign_str.encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(expected, signature):
|
||||
logger.warning("Webhook signature mismatch")
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse_callback(self, body: str, headers: dict) -> Dict[str, Any]:
|
||||
@@ -115,3 +142,6 @@ class UnifiedPayService(PaymentGateway):
|
||||
"success": event == "recharge.completed",
|
||||
"raw": payload,
|
||||
}
|
||||
|
||||
async def close_order(self, order_no: str) -> Dict[str, Any]:
|
||||
return await self._request("POST", f"/v1/pay/orders/{order_no}/close")
|
||||
|
||||
Generated
+11601
-69
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,16 @@
|
||||
"dev:mp-weixin": "uni -p mp-weixin",
|
||||
"build:mp-weixin": "uni build -p mp-weixin",
|
||||
"dev:h5": "uni",
|
||||
"build:h5": "uni build"
|
||||
"build:h5": "uni build",
|
||||
"upload": "node scripts/upload.js",
|
||||
"preview": "node scripts/upload.js preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4010520240507001",
|
||||
"@dcloudio/uni-components": "3.0.0-4010520240507001",
|
||||
"@dcloudio/uni-h5": "3.0.0-4010520240507001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4010520240507001",
|
||||
"miniprogram-ci": "^2.1.31",
|
||||
"vue": "3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const ci = require('miniprogram-ci')
|
||||
const path = require('path')
|
||||
|
||||
const APPID = process.env.WX_APPID || 'wxdad62baf4ccd09e3'
|
||||
const KEY_PATH = process.env.WX_PRIVATE_KEY_PATH || path.resolve(__dirname, '../private.key')
|
||||
const VERSION = process.env.WX_VERSION || '1.0.6'
|
||||
|
||||
// miniprogram-ci 2.x 已知 bug:上传完成后进程残留 setTimeout 无法自动退出
|
||||
// 设置强制退出计时器,防止进程卡死
|
||||
const FORCE_EXIT_MS = 600_000
|
||||
let forceExitTimer = setTimeout(() => {
|
||||
console.error('Upload timed out, forcing exit')
|
||||
process.exit(1)
|
||||
}, FORCE_EXIT_MS)
|
||||
|
||||
async function main() {
|
||||
const project = new ci.Project({
|
||||
appid: APPID,
|
||||
type: 'miniProgram',
|
||||
projectPath: path.resolve(__dirname, '../dist/build/mp-weixin/'),
|
||||
privateKeyPath: KEY_PATH,
|
||||
ignores: ['node_modules/**/*'],
|
||||
})
|
||||
|
||||
const action = process.argv[2] || 'upload'
|
||||
|
||||
if (action === 'preview') {
|
||||
const qrcodeDest = path.resolve(__dirname, '../dist/qrcode.jpg')
|
||||
await ci.preview({
|
||||
project,
|
||||
version: VERSION,
|
||||
desc: process.env.WX_DESC || '自动预览',
|
||||
setting: { minify: true, es6: true, autoPrefixWXSS: true },
|
||||
qrcodeFormat: 'image',
|
||||
qrcodeOutputDest: qrcodeDest,
|
||||
onProgressUpdate: console.log,
|
||||
})
|
||||
console.log('Preview QR:', qrcodeDest)
|
||||
} else {
|
||||
console.log(`Uploading v${VERSION} ...`)
|
||||
const result = await ci.upload({
|
||||
project,
|
||||
version: VERSION,
|
||||
desc: process.env.WX_DESC || '自动构建上传',
|
||||
setting: { minify: true, es6: true, autoPrefixWXSS: true },
|
||||
onProgressUpdate: console.log,
|
||||
})
|
||||
console.log('Upload done:', JSON.stringify(result))
|
||||
}
|
||||
}
|
||||
|
||||
main().then(() => {
|
||||
clearTimeout(forceExitTimer)
|
||||
process.exit(0)
|
||||
}).catch(e => {
|
||||
console.error('Upload failed:', e.message)
|
||||
clearTimeout(forceExitTimer)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -6,8 +6,12 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* #ifdef H5 */
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
/* #endif */
|
||||
html, body, #app { height: 100%; width: 100%; }
|
||||
/* #ifdef H5 */
|
||||
uni-page { overflow-y: auto !important; }
|
||||
uni-page-body { overflow-y: auto !important; min-height: 100% !important; }
|
||||
/* #endif */
|
||||
</style>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "",
|
||||
"appid": "wxdad62baf4ccd09e3",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
|
||||
@@ -277,63 +277,22 @@
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view class="footer-qrcode">
|
||||
<view class="qrcode-item">
|
||||
<image src="/static/images/yzr/yuzhiran.jpg" class="qrcode-img" mode="aspectFill" />
|
||||
<text class="qrcode-label">公众号</text>
|
||||
</view>
|
||||
<view class="qrcode-item">
|
||||
<image src="/static/images/yzr/yuzhiran-tech.jpg" class="qrcode-img" mode="aspectFill" />
|
||||
<text class="qrcode-label">服务号</text>
|
||||
</view>
|
||||
<view class="qrcode-item">
|
||||
<image src="/static/images/yzr/yuzhiran-yhl.jpg" class="qrcode-img" mode="aspectFill" />
|
||||
<text class="qrcode-label">小程序</text>
|
||||
</view>
|
||||
<view class="qrcode-item">
|
||||
<image src="/static/images/yzr/kefu.png" class="qrcode-img" mode="aspectFill" />
|
||||
<text class="qrcode-label">客服</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer-links">
|
||||
<text class="footer-link" @click="goToPage(PAGES.AGREEMENT_PRIVACY)">隐私政策</text>
|
||||
<text class="footer-divider">|</text>
|
||||
<text class="footer-link" @click="goToPage(PAGES.AGREEMENT_TERMS)">用户协议</text>
|
||||
</view>
|
||||
<view class="footer-beian">
|
||||
<a class="footer-beian-link" :href="beianUrl" target="_blank">{{ beianIcp }}</a>
|
||||
<text class="footer-divider">|</text>
|
||||
<a class="footer-beian-link" :href="beianPsbUrl" target="_blank">{{ beianPsb }}</a>
|
||||
</view>
|
||||
<text class="footer-copyright">© {{ copyrightYear }} 北京宇之然科技中心. 保留所有权利.</text>
|
||||
</view>
|
||||
<AiAssistant />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { authApi, customerApi, analyticsApi, onboardingApi, notificationApi, followupApi, translateApi, BASE_URL } from '@/utils/api.js'
|
||||
import AiAssistant from '@/components/ai-assistant.vue'
|
||||
import { STORAGE_KEYS, PAGES, EXTERNAL_URLS, APP_INFO, EXTRACT_FIELD_LABELS } from '@/config.js'
|
||||
|
||||
const beianInfo = computed(() => {
|
||||
let hostname = ''
|
||||
try { hostname = window.location.hostname } catch {}
|
||||
if (hostname === 'yuzhiran.com' || hostname === 'www.yuzhiran.com') {
|
||||
return { icp: '京ICP备2026007249号-1', psb: '京公网安备11011502039545号', psbUrl: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039545' }
|
||||
}
|
||||
if (hostname === 'yuzhiran.com.cn' || hostname === 'www.yuzhiran.com.cn') {
|
||||
return { icp: '京ICP备2026007249号-2', psb: '京公网安备11011502039622号', psbUrl: 'https://beian.mps.gov.cn/#/query/webSearch?code=11011502039622' }
|
||||
}
|
||||
return { icp: APP_INFO.ICP, psb: APP_INFO.PSB, psbUrl: EXTERNAL_URLS.BEIAN_PSB }
|
||||
})
|
||||
const beianUrl = computed(() => EXTERNAL_URLS.BEIAN)
|
||||
const beianIcp = computed(() => beianInfo.value.icp)
|
||||
const beianPsb = computed(() => beianInfo.value.psb)
|
||||
const beianPsbUrl = computed(() => beianInfo.value.psbUrl)
|
||||
const copyrightYear = computed(() => new Date().getFullYear())
|
||||
import { STORAGE_KEYS, PAGES, EXTRACT_FIELD_LABELS } from '@/config.js'
|
||||
|
||||
const showAnnouncement = ref(false)
|
||||
const currentAnnouncement = ref(0)
|
||||
|
||||
@@ -49,21 +49,31 @@
|
||||
<text class="menu-text">意见反馈</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goAgreement('privacy')">
|
||||
<text class="menu-icon">📄</text>
|
||||
<text class="menu-text">隐私政策</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">关于我们</view>
|
||||
<view class="about-item">
|
||||
<text class="about-label">版本</text>
|
||||
<text class="about-value">1.0.0</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goAgreement('terms')">
|
||||
<text class="menu-icon">📋</text>
|
||||
<text class="menu-text">用户协议</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
<view class="about-item" @click="goAgreement('privacy')">
|
||||
<text class="about-label">隐私政策</text>
|
||||
<text class="about-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item">
|
||||
<text class="menu-icon">ℹ️</text>
|
||||
<text class="menu-text">版本</text>
|
||||
<text class="menu-value">1.0.0</text>
|
||||
<view class="about-item" @click="goAgreement('terms')">
|
||||
<text class="about-label">用户协议</text>
|
||||
<text class="about-arrow">›</text>
|
||||
</view>
|
||||
<view class="about-item">
|
||||
<text class="about-label">ICP 备案</text>
|
||||
<text class="about-value">京ICP备2026007249号-1</text>
|
||||
</view>
|
||||
<view class="about-item">
|
||||
<text class="about-label">公安备案</text>
|
||||
<text class="about-value">京公网安备11011502039545号</text>
|
||||
</view>
|
||||
<view class="about-copyright">© 2026 北京宇之然科技中心. 保留所有权利.</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" v-if="user.tier !== 'guest'" @click="logout">退出登录</view>
|
||||
@@ -412,4 +422,36 @@ onShow(loadUser)
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.about-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.about-item:last-child { border-bottom: none; }
|
||||
|
||||
.about-label {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.about-value {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.about-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.about-copyright {
|
||||
text-align: center;
|
||||
padding: 24rpx 30rpx;
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { STORAGE_KEYS, PAGES } from '@/config.js'
|
||||
|
||||
export const BASE_URL = '/api/v1'
|
||||
// #ifdef MP-WEIXIN
|
||||
const API_HOST = 'https://trade.yuzhiran.com'
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
const API_HOST = ''
|
||||
// #endif
|
||||
export const BASE_URL = `${API_HOST}/api/v1`
|
||||
|
||||
const getAuthHeader = () => {
|
||||
const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
|
||||
|
||||
@@ -102,7 +102,7 @@ export function markAllRead() { return http.post('/notifications/read-all') }
|
||||
|
||||
export function getPlans() { return http.get('/payment/plans') }
|
||||
export function getSubscription() { return http.get('/payment/subscription') }
|
||||
export function createOrder(planId) { return http.post('/payment/create-order', { plan_id: planId }) }
|
||||
export function createOrder(plan, payType = 'alipay') { return http.post('/payment/create-order', { plan, pay_type: payType }) }
|
||||
|
||||
export function submitCertification(data) { return http.post('/certification/submit', data) }
|
||||
export function getCertificationStatus() { return http.get('/certification/status') }
|
||||
|
||||
@@ -67,37 +67,13 @@
|
||||
|
||||
<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>
|
||||
</footer>
|
||||
@@ -160,15 +136,8 @@ function handleLogout() {
|
||||
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
||||
.notif-badge :deep(.el-badge__content) { top: 8px; right: 4px; }
|
||||
.content { flex: 1; padding: 24px; overflow-y: auto; background: #f5f5f5; }
|
||||
.footer { text-align: center; color: #999; font-size: 12px; border-top: 1px solid #e8e8e8; background: #fff; flex-shrink: 0; }
|
||||
.footer-content { padding: 16px 24px 12px; }
|
||||
.footer-section { margin-bottom: 12px; }
|
||||
.footer-brand { font-size: 14px; font-weight: 700; color: #1890ff; margin-bottom: 2px; }
|
||||
.footer-tagline { color: #999; font-size: 11px; margin-bottom: 10px; }
|
||||
.qrcode-row { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
|
||||
.qrcode-item { display: flex; flex-direction: column; align-items: center; gap: 3px; color: #999; font-size: 10px; }
|
||||
.qrcode-img { width: 44px; height: 44px; border-radius: 6px; }
|
||||
.footer-bottom { border-top: 1px solid #eee; padding-top: 10px; display: flex; justify-content: center; align-items: center; gap: 14px; flex-wrap: wrap; }
|
||||
.footer { text-align: center; color: #999; font-size: 12px; border-top: 1px solid #e8e8e8; background: #fff; flex-shrink: 0; padding: 8px 24px; }
|
||||
.footer-content { display: flex; justify-content: center; align-items: center; gap: 14px; flex-wrap: wrap; }
|
||||
.footer-links { display: flex; gap: 14px; align-items: center; }
|
||||
.footer-links a { color: #999; text-decoration: none; }
|
||||
.footer-links a:hover { color: #1890ff; }
|
||||
|
||||
@@ -23,17 +23,51 @@
|
||||
<div style="text-align:center;margin-top:16px">
|
||||
<el-button v-if="p.id === currentPlan" type="default" disabled>当前套餐</el-button>
|
||||
<el-button v-else-if="p.id === 'free'" @click="handleFree">当前套餐</el-button>
|
||||
<el-button v-else type="primary" :loading="loadingId === p.id" @click="upgrade(p.id)">{{ p.price === 0 ? '当前套餐' : '升级' }}</el-button>
|
||||
<el-button v-else type="primary" :loading="loadingId === p.id" @click="showPayDialog(p.id)">{{ p.price === 0 ? '当前套餐' : '升级' }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-if="!plans.length" description="暂无套餐信息" />
|
||||
|
||||
<el-dialog v-model="payDialog.visible" title="选择支付方式" width="400px" :close-on-click-modal="false">
|
||||
<div style="text-align:center;padding:20px 0" v-if="!payDialog.orderCreated">
|
||||
<el-radio-group v-model="payDialog.payType" style="margin-bottom:24px">
|
||||
<el-radio-button value="alipay">
|
||||
<span style="display:flex;align-items:center;gap:6px;padding:0 20px">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#1677ff"><path d="M21.422 15.358c-3.22-1.386-6.847-2.408-10.564-2.828 1.102-2.279 2.38-4.49 3.735-6.59H9.878c-.185-.413-.262-.912-.04-1.436.454-1.072 1.92-1.348 1.92-1.348s.162-.09.026-.207c-.137-.117-1.866-.313-2.666-.363-2.348-.155-4.99.22-5.733 1.181-1.14 1.48.067 2.925.401 3.337.337.412 1.256.498 1.256.498s-1.466.536-1.992 1.2c-.525.665-.264 1.383.13 1.664.394.281.756.388 1.07.482.707.21 1.818.431 2.795.555 1.454.184 2.957.1 4.312-.184 1.408-2.06 2.83-4.017 4.285-5.907l3.192 1.558c.289.142.66.028.827-.256a.63.63 0 0 0-.086-.74L15.734 7.56c.7-.878 1.426-1.727 2.18-2.537 1.938-2.083 4.298-3.876 6.377-4.707a12.29 12.29 0 0 0-6.648-1.99c-6.427 0-11.66 4.996-11.66 11.116 0 1.49.294 2.913.825 4.215-.374.314-.707.674-.99 1.075-2.316 3.277-.477 6.101 1.046 7.247 1.518 1.144 4.464 1.772 7.155.875 2.798-.93 5.256-3.103 6.822-5.531 1.654-2.563 2.549-5.435 2.549-8.367a12.9 12.9 0 0 0-.316-2.81c-1.178-.022-3.226.306-5.354 1.522z"/></svg>
|
||||
支付宝
|
||||
</span>
|
||||
</el-radio-button>
|
||||
<el-radio-button value="wechat">
|
||||
<span style="display:flex;align-items:center;gap:6px;padding:0 20px">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#07c160"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.271.271 0 0 0 .14.045c.134 0 .24-.11.24-.245 0-.06-.024-.12-.04-.178l-.325-1.233a.49.49 0 0 1 .178-.553C23.028 18.125 24 16.539 24 14.711c0-3.396-3.637-6.02-7.062-5.853zm-2.06 1.964c.535 0 .968.44.968.982a.975.975 0 0 1-.968.983.975.975 0 0 1-.969-.983c0-.542.434-.982.969-.982zm4.844 0c.535 0 .969.44.969.982a.975.975 0 0 1-.969.983.975.975 0 0 1-.968-.983c0-.542.433-.982.968-.982z"/></svg>
|
||||
微信支付
|
||||
</span>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div>
|
||||
<el-button type="primary" size="large" :loading="payDialog.loading" @click="handleUpgrade">立即支付</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:center;padding:20px 0" v-else>
|
||||
<div v-if="payDialog.codeUrl">
|
||||
<p style="margin-bottom:16px;color:#666">请使用微信扫描下方二维码支付</p>
|
||||
<img :src="payDialog.codeUrl" style="width:200px;height:200px;border:1px solid #eee;border-radius:8px" />
|
||||
<p style="margin-top:12px;font-size:12px;color:#999">支付成功后自动生效</p>
|
||||
</div>
|
||||
<div v-else-if="payDialog.payUrl">
|
||||
<p style="margin-bottom:16px;color:#666">正在跳转支付宝...</p>
|
||||
<el-button type="primary" @click="openPayUrl">前往支付</el-button>
|
||||
</div>
|
||||
<el-button style="margin-top:16px" @click="payDialog.visible = false">关闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { getPlans, getSubscription, createOrder } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
@@ -41,6 +75,16 @@ const plans = ref([])
|
||||
const currentPlan = ref(null)
|
||||
const loadingId = ref(null)
|
||||
|
||||
const payDialog = reactive({
|
||||
visible: false,
|
||||
planId: null,
|
||||
payType: 'alipay',
|
||||
loading: false,
|
||||
orderCreated: false,
|
||||
payUrl: '',
|
||||
codeUrl: '',
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [plansRes, subRes] = await Promise.all([getPlans(), getSubscription().catch(() => null)])
|
||||
@@ -53,14 +97,37 @@ onMounted(async () => {
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
|
||||
async function upgrade(planId) {
|
||||
loadingId.value = planId
|
||||
function showPayDialog(planId) {
|
||||
payDialog.planId = planId
|
||||
payDialog.payType = 'alipay'
|
||||
payDialog.orderCreated = false
|
||||
payDialog.payUrl = ''
|
||||
payDialog.codeUrl = ''
|
||||
payDialog.visible = true
|
||||
}
|
||||
|
||||
async function handleUpgrade() {
|
||||
payDialog.loading = true
|
||||
try {
|
||||
const res = await createOrder(planId, 'native')
|
||||
ElMessage.success('订单已创建' + (res.pay_url ? ',正在跳转支付...' : ''))
|
||||
if (res.pay_url) window.open(res.pay_url)
|
||||
} catch (e) { ElMessage.error(e?.detail || '升级失败') }
|
||||
finally { loadingId.value = null }
|
||||
const res = await createOrder(payDialog.planId, payDialog.payType)
|
||||
payDialog.orderCreated = true
|
||||
if (res.code_url) {
|
||||
payDialog.codeUrl = res.code_url
|
||||
} else if (res.pay_url) {
|
||||
payDialog.payUrl = res.pay_url
|
||||
window.open(res.pay_url)
|
||||
} else {
|
||||
ElMessage.success('订单已创建,请稍后查看')
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.detail || '下单失败')
|
||||
} finally {
|
||||
payDialog.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPayUrl() {
|
||||
if (payDialog.payUrl) window.open(payDialog.payUrl)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user