feat: 新增合同审查定时任务与积分体系,重构支付模块与余额流水查询
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
// client/ai/pub/jjxDoReview.js
|
||||
module.exports = {
|
||||
main: async (event) => {
|
||||
let { data = {}, userInfo } = event;
|
||||
const { record_id, file_id, user_id, file_name, file_size } = data;
|
||||
|
||||
if (!record_id || !file_id) return { code: -1, msg: '参数错误' };
|
||||
|
||||
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
|
||||
const BOT_ID = '7689821072762011698';
|
||||
const db = uniCloud.database();
|
||||
const _ = db.command;
|
||||
|
||||
try {
|
||||
// 构造请求
|
||||
const objectString = JSON.stringify([
|
||||
{ type: 'file', file_id: file_id },
|
||||
{ type: 'text', text: '请审查这份合同' }
|
||||
]);
|
||||
|
||||
// 调用 Coze 接口(流式模式)
|
||||
const chatRes = await uniCloud.httpclient.request('https://api.coze.cn/v3/chat', {
|
||||
method: 'POST',
|
||||
dataType: 'stream', // 关键:改成 stream
|
||||
contentType: 'json',
|
||||
timeout: 300000, // 5分钟超时
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + COZE_TOKEN,
|
||||
'Accept': 'text/event-stream'
|
||||
},
|
||||
data: {
|
||||
bot_id: BOT_ID,
|
||||
user_id: user_id || 'anonymous',
|
||||
stream: true,
|
||||
auto_save_history: true,
|
||||
additional_messages: [{
|
||||
role: 'user',
|
||||
content: objectString,
|
||||
content_type: 'object_string'
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
// 从流中逐步解析
|
||||
const stream = chatRes.data || chatRes.res;
|
||||
let buffer = '';
|
||||
let answer = '';
|
||||
let conversationId = '';
|
||||
let lastUpdateLen = 0;
|
||||
|
||||
// 返回 Promise,等待流结束
|
||||
return await new Promise((resolve, reject) => {
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
const blocks = buffer.split('\n\n');
|
||||
buffer = blocks.pop();
|
||||
|
||||
for (const block of blocks) {
|
||||
const lines = block.split('\n');
|
||||
let eventName = '';
|
||||
let dataStr = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event:')) eventName = line.slice(6).trim();
|
||||
if (line.startsWith('data:')) dataStr = line.slice(5).trim();
|
||||
}
|
||||
|
||||
if (!dataStr || dataStr === '[DONE]' || dataStr === '"[DONE]"') continue;
|
||||
|
||||
try {
|
||||
const p = JSON.parse(dataStr);
|
||||
if (p.conversation_id) conversationId = p.conversation_id;
|
||||
|
||||
// 处理回答内容
|
||||
if (p.type === 'answer' && p.content) {
|
||||
answer += p.content;
|
||||
|
||||
// 每积累 200 字更新一次数据库(前端轮询能看到进度)
|
||||
if (answer.length - lastUpdateLen >= 200) {
|
||||
lastUpdateLen = answer.length;
|
||||
db.collection('jjx-review-records').doc(record_id).update({
|
||||
result: answer,
|
||||
status: 'reviewing',
|
||||
update_time: new Date()
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理工具响应(报告下载链接)
|
||||
if (p.type === 'tool_response' && p.content) {
|
||||
try {
|
||||
const toolData = JSON.parse(p.content);
|
||||
if (toolData.url) answer += '\n\n📎 报告下载:' + toolData.url;
|
||||
} catch (e) {
|
||||
const urlMatch = p.content.match(/https?:\/\/[^\s\)\]\"\<]+\.pdf[^\s\)\]\"\<]*/i);
|
||||
if (urlMatch) answer += '\n\n📎 报告下载:' + urlMatch[0];
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', async () => {
|
||||
if (!answer) {
|
||||
answer = '审查未能获取结果,请稍后重试';
|
||||
}
|
||||
|
||||
try {
|
||||
// 计算消耗积分
|
||||
const pointsUsed = Math.max(150, Math.ceil((file_size || 0) / (100 * 1024)) * 150);
|
||||
|
||||
// 扣积分
|
||||
const userPoints = await db.collection('jjx-user-points').doc(user_id).get();
|
||||
if (userPoints.data.length && userPoints.data[0].balance >= pointsUsed) {
|
||||
await db.collection('jjx-user-points').doc(user_id).update({
|
||||
balance: _.inc(-pointsUsed),
|
||||
total_spent: _.inc(pointsUsed),
|
||||
update_time: new Date()
|
||||
});
|
||||
|
||||
const updatedPoints = await db.collection('jjx-user-points').doc(user_id).get();
|
||||
await db.collection('jjx-points-log').add({
|
||||
user_id: user_id,
|
||||
type: 'spend',
|
||||
amount: -pointsUsed,
|
||||
balance_after: updatedPoints.data[0] ? updatedPoints.data[0].balance : 0,
|
||||
reason: '合同审查 — ' + (file_name || ''),
|
||||
biz_type: 'contract_review',
|
||||
biz_id: record_id,
|
||||
_add_time: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
// 保存 HTML 报告到云存储
|
||||
let htmlUrl = '';
|
||||
try {
|
||||
// 如果回复本身就是 HTML(包含 <!DOCTYPE html> 或 <html>)
|
||||
let htmlContent = answer;
|
||||
const htmlMatch = answer.match(/```html\s*([\s\S]*?)```/);
|
||||
if (htmlMatch && htmlMatch[1]) {
|
||||
htmlContent = htmlMatch[1].trim();
|
||||
}
|
||||
|
||||
if (htmlContent.includes('<!DOCTYPE') || htmlContent.includes('<html')) {
|
||||
const cloudPath = 'review-reports/' + Date.now() + '_' + (file_name || 'report').replace(/\.[^.]+$/, '') + '.html';
|
||||
const uploadRes = await uniCloud.uploadFile({
|
||||
cloudPath: cloudPath,
|
||||
fileContent: Buffer.from(htmlContent, 'utf-8')
|
||||
});
|
||||
if (uploadRes.fileID) {
|
||||
const urlRes = await uniCloud.getTempFileURL({ fileList: [uploadRes.fileID] });
|
||||
if (urlRes.fileList && urlRes.fileList[0]) {
|
||||
htmlUrl = urlRes.fileList[0].tempFileURL;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('HTML报告上传失败:', e.message);
|
||||
}
|
||||
|
||||
// 解析 JSON 结构化数据(如果有的话)
|
||||
let contractInfo = {};
|
||||
try {
|
||||
const jsonMatch = answer.match(/```json\s*([\s\S]*?)```/);
|
||||
if (jsonMatch) contractInfo = JSON.parse(jsonMatch[1].trim());
|
||||
} catch (e) {}
|
||||
|
||||
// 更新审查记录为完成
|
||||
await db.collection('jjx-review-records').doc(record_id).update({
|
||||
status: 'completed',
|
||||
result: answer,
|
||||
html_url: htmlUrl,
|
||||
contract_info: contractInfo,
|
||||
points_used: pointsUsed,
|
||||
conversation_id: conversationId || '',
|
||||
update_time: new Date()
|
||||
});
|
||||
|
||||
resolve({ code: 0, msg: '审查完成' });
|
||||
|
||||
} catch (e) {
|
||||
// 更新为失败
|
||||
await db.collection('jjx-review-records').doc(record_id).update({
|
||||
status: 'failed',
|
||||
result: '审查失败: ' + e.message,
|
||||
error: e.message,
|
||||
update_time: new Date()
|
||||
});
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', async (err) => {
|
||||
await db.collection('jjx-review-records').doc(record_id).update({
|
||||
status: 'failed',
|
||||
result: '审查失败: ' + err.message,
|
||||
error: err.message,
|
||||
update_time: new Date()
|
||||
});
|
||||
reject(err);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
// 审查失败,更新记录
|
||||
await db.collection('jjx-review-records').doc(record_id).update({
|
||||
status: 'failed',
|
||||
result: '审查失败: ' + e.message,
|
||||
error: e.message,
|
||||
update_time: new Date()
|
||||
});
|
||||
return { code: -1, msg: '审查失败: ' + e.message };
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user