feat: 合同审查异步方案(定时任务触发、非流式轮询扣子API、token计费、HTML报告下载)
This commit is contained in:
+96
-156
@@ -543,7 +543,7 @@
|
||||
<view class="opt-icon">🔍</view>
|
||||
<view class="opt-title">标准审查</view>
|
||||
<view class="opt-desc">风险条款识别 · 合规性检查</view>
|
||||
<view class="opt-points">{{ reviewPoints }} 积分</view>
|
||||
<!-- <view class="opt-points">{{ reviewPoints }} 积分</view> -->
|
||||
</view>
|
||||
<!-- <view :class="['review-opt', reviewMode === 1 && 'selected']" @click="reviewMode = 1">
|
||||
<view class="opt-icon">📊</view>
|
||||
@@ -556,7 +556,7 @@
|
||||
<el-button type="primary" :loading="reviewLoading"
|
||||
:disabled="pointsBalance < reviewPoints + 200"
|
||||
@click="startReview">
|
||||
{{ reviewLoading ? '审查中...' : '开始审查(' + reviewPoints + ' 积分)' }}
|
||||
{{ reviewLoading ? '审查中...' : '开始审查' }}
|
||||
</el-button>
|
||||
<p v-if="pointsBalance < reviewPoints + 200" style="font-size:12px;color:#f56c6c;margin-top:6px;">
|
||||
⚠️ 积分不足,当前余额 {{ pointsBalance }},需要 {{ reviewPoints }} 积分且审查后余额需保留至少 200 积分。
|
||||
@@ -574,11 +574,15 @@
|
||||
<!-- 审查结果 -->
|
||||
<view v-if="reviewResult">
|
||||
<view class="result-header">
|
||||
<h3>📋 审查结果 — {{ reviewFile.name }}</h3>
|
||||
<span class="result-status">✅ 审查完成</span>
|
||||
<h3>📋 {{ reviewStreaming ? 'AI 正在分析...' : '审查结果' }} — {{ reviewFile.name }}</h3>
|
||||
<span class="result-status">{{ reviewStreaming ? '⏳ 分析中...' : '✅ 审查完成' }}</span>
|
||||
</view>
|
||||
<view class="result-body">
|
||||
<view class="result-body" :style="reviewStreaming ? 'max-height:400px;overflow-y:auto;' : ''">
|
||||
<div v-html="reviewResult"></div>
|
||||
<div v-if="reviewStreaming" style="margin-top:8px;">
|
||||
<span style="display:inline-block;width:8px;height:8px;background:#a92924;border-radius:50%;animation:pulse 1s infinite;"></span>
|
||||
<span style="font-size:12px;color:#999;margin-left:6px;">AI 正在输出分析结果...</span>
|
||||
</div>
|
||||
</view>
|
||||
<view class="result-footer">
|
||||
<el-button v-if="reviewHtmlUrl" type="success" size="small" @click="openUrl(reviewHtmlUrl)">📄 查看完整 HTML 报告</el-button>
|
||||
@@ -603,12 +607,13 @@
|
||||
<view class="report-time">{{ r.time }} · 消耗 {{ Math.abs(r.amount) }} 积分</view>
|
||||
</view>
|
||||
<el-tag v-if="r.status === 'uploaded'" size="mini" type="info">待审查</el-tag>
|
||||
<el-tag v-else-if="r.status === 'reviewing'" size="mini" type="warning">审查中</el-tag>
|
||||
<el-tag v-else-if="r.status === 'reviewing'" size="mini" type="warning">排队中</el-tag>
|
||||
<el-tag v-else-if="r.status === 'processing'" size="mini" type="warning">审查中</el-tag>
|
||||
<el-tag v-else-if="r.status === 'completed'" size="mini" type="success">已完成</el-tag>
|
||||
<el-tag v-else-if="r.status === 'failed'" size="mini" type="danger">失败</el-tag>
|
||||
<el-button v-if="r.htmlUrl" size="mini" type="text" @click="openUrl(r.htmlUrl)">查看报告</el-button>
|
||||
<el-button v-if="r.fileUrl" size="mini" type="text" @click="previewFile(r)">查看文件</el-button>
|
||||
<!-- <el-button v-if="r.status === 'completed'" size="mini" type="text" @click="viewReport(r)">查看摘要</el-button> -->
|
||||
<el-button v-if="r.status === 'completed'" size="mini" type="text" @click="viewReport(r)">查看摘要</el-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -872,6 +877,7 @@
|
||||
reviewHtmlUrl: '',
|
||||
reviewPdfUrl: '',
|
||||
reviewContractInfo: {},
|
||||
reviewStreaming: false,
|
||||
reviewUploading: false,
|
||||
reviewMode: 0,
|
||||
reviewPoints: 100,
|
||||
@@ -882,34 +888,34 @@
|
||||
// 积分充值
|
||||
selectedRecharge: 1,
|
||||
rechargePackages: [{
|
||||
points: 500,
|
||||
price: '30元',
|
||||
points: 10000,
|
||||
price: '10元',
|
||||
bonus: ''
|
||||
},
|
||||
{
|
||||
points: 1000,
|
||||
points: 50000,
|
||||
price: '50元',
|
||||
bonus: '送100积分'
|
||||
bonus: ''
|
||||
},
|
||||
{
|
||||
points: 2000,
|
||||
price: '98元',
|
||||
bonus: '送300积分'
|
||||
points: 100000,
|
||||
price: '100元',
|
||||
bonus: ''
|
||||
},
|
||||
{
|
||||
points: 5000,
|
||||
price: '198元',
|
||||
bonus: '送1000积分'
|
||||
points: 450000,
|
||||
price: '450元',
|
||||
bonus: ''
|
||||
},
|
||||
{
|
||||
points: 10000,
|
||||
price: '368元',
|
||||
bonus: '送3000积分'
|
||||
points: 1000000,
|
||||
price: '1000元',
|
||||
bonus: ''
|
||||
},
|
||||
{
|
||||
points: 20000,
|
||||
price: '688元',
|
||||
bonus: '送8000积分'
|
||||
points: 2000000,
|
||||
price: '2000元',
|
||||
bonus: ''
|
||||
}
|
||||
],
|
||||
// 我的报告
|
||||
@@ -1671,6 +1677,14 @@
|
||||
openUrl(url) {
|
||||
if (url) window.open(url, '_blank');
|
||||
},
|
||||
fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result.split(',')[1]);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
},
|
||||
formatReviewResult(text) {
|
||||
if (!text) return '';
|
||||
let html = text;
|
||||
@@ -1836,135 +1850,57 @@
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
// 前端 startReview() 完整改造
|
||||
async startReview() {
|
||||
if (!this.reviewFileId) {
|
||||
this.$message.warning('文件尚未上传完成,请稍候');
|
||||
return;
|
||||
}
|
||||
|
||||
const pointsNeeded = this.reviewPoints;
|
||||
if (this.pointsBalance < pointsNeeded + 200) {
|
||||
this.$message.warning('积分不足,当前余额 ' + this.pointsBalance + ',需要 ' + pointsNeeded + ' 且审查后余额需保留至少 200 积分');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reviewLoading = true;
|
||||
this.reviewProgress = 0;
|
||||
|
||||
// 模拟进度条
|
||||
const progressTimer = setInterval(() => {
|
||||
this.reviewProgress = Math.min(this.reviewProgress + 1, 90);
|
||||
}, 1500);
|
||||
|
||||
try {
|
||||
// 1. 发起审查(只更新状态,立即返回)
|
||||
const res = await vk.callFunction({
|
||||
url: 'client/ai/pub/review',
|
||||
data: {
|
||||
file_id: this.reviewFileId,
|
||||
record_id: this.reviewRecordId,
|
||||
file_name: this.reviewFile.name,
|
||||
file_size: this.reviewFile.size
|
||||
}
|
||||
});
|
||||
|
||||
if (res.code !== 0) {
|
||||
this.$message.error(res.msg || '审查发起失败');
|
||||
this.reviewLoading = false;
|
||||
clearInterval(progressTimer);
|
||||
return;
|
||||
}
|
||||
|
||||
const recordId = res.data.record_id;
|
||||
|
||||
// 2. 轮询结果(每 5 秒查一次,最多 10 分钟)
|
||||
let result = '';
|
||||
let pointsUsed = 0;
|
||||
let htmlUrl = '';
|
||||
let contractInfo = {};
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
await new Promise(r => setTimeout(r, 10000));
|
||||
|
||||
const pollRes = await vk.callFunction({
|
||||
url: 'client/ai/pub/getReviewResult',
|
||||
data: { record_id: recordId }
|
||||
});
|
||||
|
||||
if (pollRes.code === 0 && pollRes.data) {
|
||||
if (pollRes.data.status === 'completed') {
|
||||
result = pollRes.data.result;
|
||||
pointsUsed = pollRes.data.points_used || 0;
|
||||
htmlUrl = pollRes.data.html_url || '';
|
||||
contractInfo = pollRes.data.contract_info || {};
|
||||
break;
|
||||
}
|
||||
if (pollRes.data.status === 'failed') {
|
||||
this.$message.error(pollRes.data.result || '审查失败');
|
||||
this.reviewLoading = false;
|
||||
clearInterval(progressTimer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearInterval(progressTimer);
|
||||
this.reviewProgress = 100;
|
||||
|
||||
if (result) {
|
||||
// 解析 JSON 结果
|
||||
try {
|
||||
const jsonMatch = result.match(/```json\s*([\s\S]*?)```/);
|
||||
if (jsonMatch) {
|
||||
const info = JSON.parse(jsonMatch[1].trim());
|
||||
this.reviewContractInfo = info;
|
||||
// 生成摘要文本用于页面显示
|
||||
let summary = '<strong>合同名称:</strong>' + (info.contract_name || '-') + '<br>';
|
||||
summary += '<strong>合同类型:</strong>' + (info.contract_type || '-') + '<br>';
|
||||
summary += '<strong>综合风险:</strong><span style="color:#f56c6c;font-weight:700;">' + (info.risk_level || '-') + '</span><br>';
|
||||
summary += '<strong>风险统计:</strong>高 ' + (info.risk_count && info.risk_count.high || 0) + ' / 中 ' + (info.risk_count && info.risk_count.medium || 0) + ' / 低 ' + (info.risk_count && info.risk_count.low || 0) + '<br>';
|
||||
summary += '<strong>总体评估:</strong>' + (info.risk_summary || '-') + '<br>';
|
||||
// 高风险摘要
|
||||
const highRisks = (info.risks || []).filter(r => r.level === 'high');
|
||||
if (highRisks.length > 0) {
|
||||
summary += '<br><strong style="color:#f56c6c;">⚠️ 高风险摘要:</strong><br>';
|
||||
highRisks.forEach(r => {
|
||||
summary += '<div style="padding:8px 12px;margin:6px 0;background:#fff1f0;border-left:3px solid #f56c6c;border-radius:0 4px 4px 0;font-size:13px;">';
|
||||
summary += '<strong>风险 #' + r.id + '</strong> ' + (r.location || '') + ':' + (r.problem || '');
|
||||
summary += '</div>';
|
||||
});
|
||||
}
|
||||
this.reviewResult = summary;
|
||||
} else {
|
||||
this.reviewResult = result;
|
||||
}
|
||||
} catch (e) {
|
||||
this.reviewResult = result;
|
||||
}
|
||||
this.reviewHtmlUrl = htmlUrl;
|
||||
this.pointsBalance -= pointsUsed;
|
||||
// 保存报告到本地
|
||||
const reports = JSON.parse(localStorage.getItem('jjx_review_reports') || '[]');
|
||||
reports.unshift({
|
||||
fileName: this.reviewFile.name,
|
||||
fileSize: this.reviewFile.size,
|
||||
time: new Date().toLocaleString(),
|
||||
points: pointsUsed,
|
||||
result: result,
|
||||
htmlUrl: htmlUrl
|
||||
});
|
||||
localStorage.setItem('jjx_review_reports', JSON.stringify(reports.slice(0, 50)));
|
||||
this.getReportList();
|
||||
this.$message.success('合同审查完成,消耗 ' + pointsUsed + ' 积分');
|
||||
} else {
|
||||
this.$message.warning('审查时间较长,请稍后在「我的报告」中查看结果');
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(progressTimer);
|
||||
this.$message.error('审查异常,请稍后重试');
|
||||
}
|
||||
|
||||
this.reviewLoading = false;
|
||||
if (!this.reviewFileId) {
|
||||
this.$message.warning('请先选择合同文件并等待上传完成');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 检查积分
|
||||
const balanceRes = await vk.callFunction({
|
||||
url: 'client/points/kh/getBalance'
|
||||
});
|
||||
if (balanceRes.code !== 0 || !balanceRes.data || balanceRes.data.balance < 150) {
|
||||
this.$message.warning('积分不足,请先充值');
|
||||
return;
|
||||
}
|
||||
|
||||
this.reviewLoading = true;
|
||||
|
||||
// 2. 发起审查(更新已有记录状态为 reviewing,用 selectReviewFile 时保存的 file_id)
|
||||
const reviewRes = await vk.callFunction({
|
||||
url: 'client/ai/pub/review',
|
||||
data: {
|
||||
record_id: this.reviewRecordId,
|
||||
file_id: this.reviewFileId,
|
||||
file_name: this.reviewFile.name,
|
||||
file_size: this.reviewFile.size,
|
||||
uid: this.userInfo.uid
|
||||
}
|
||||
});
|
||||
|
||||
this.reviewLoading = false;
|
||||
|
||||
if (reviewRes.code !== 0) {
|
||||
this.$message.error(reviewRes.msg || '发起审查失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 提示用户 + 跳转到我的报告
|
||||
this.$alert(
|
||||
'合同已提交审查,AI 正在分析中,预计 3-5 分钟完成。您可以在「我的报告」中查看审查结果。',
|
||||
'审查已提交',
|
||||
{
|
||||
confirmButtonText: '查看我的报告',
|
||||
callback: () => {
|
||||
this.menuSelect('5');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 重置上传区
|
||||
this.resetReview();
|
||||
},
|
||||
resetReview() {
|
||||
this.reviewFile = null;
|
||||
@@ -1978,12 +1914,12 @@
|
||||
async doRecharge() {
|
||||
const pkg = this.rechargePackages[this.selectedRecharge];
|
||||
const priceMap = {
|
||||
'30元': 3000,
|
||||
'10元': 1000,
|
||||
'50元': 5000,
|
||||
'98元': 9800,
|
||||
'198元': 19800,
|
||||
'368元': 36800,
|
||||
'688元': 1
|
||||
'100元': 10000,
|
||||
'450元': 45000,
|
||||
'1000元': 100000,
|
||||
'2000元': 200000
|
||||
};
|
||||
const price = priceMap[pkg.price] || 5000;
|
||||
const pointsMatch = pkg.points;
|
||||
@@ -2869,6 +2805,10 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
}
|
||||
.disclaimer-notice {
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
background: #fff8e1; border: 1px solid #ffe082; border-radius: 8px;
|
||||
|
||||
Reference in New Issue
Block a user