feat: 新增合同审查定时任务与积分体系,重构支付模块与余额流水查询
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"name": "http",
|
||||||
|
"type": "http",
|
||||||
|
"config": {
|
||||||
|
"path": "/review-stream",
|
||||||
|
"method": ["POST"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.main = async (event, context) => {
|
||||||
|
// uniCloud HTTP 触发器:event 本身就是请求信息
|
||||||
|
// event.httpMethod = 'GET'/'POST'/'OPTIONS'
|
||||||
|
// event.body = 请求体
|
||||||
|
// event.headers = 请求头
|
||||||
|
// context.callbackWaitsForEmptyEventLoop = true 保持连接
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Max-Age': '86400'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理 CORS 预检
|
||||||
|
if (event.httpMethod === 'OPTIONS') {
|
||||||
|
return {
|
||||||
|
statusCode: 204,
|
||||||
|
headers: corsHeaders,
|
||||||
|
body: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置等待空事件循环(保持 SSE 连接)
|
||||||
|
context.callbackWaitsForEmptyEventLoop = false;
|
||||||
|
|
||||||
|
// 解析请求体
|
||||||
|
let body = {};
|
||||||
|
try {
|
||||||
|
const raw = event.body || '';
|
||||||
|
if (event.isBase64Encoded) {
|
||||||
|
body = JSON.parse(Buffer.from(raw, 'base64').toString());
|
||||||
|
} else {
|
||||||
|
try { body = JSON.parse(raw); } catch (e) {
|
||||||
|
const params = new URLSearchParams(raw);
|
||||||
|
for (const [k, v] of params) body[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const { file_id, user_id, file_name, file_size, record_id } = body;
|
||||||
|
|
||||||
|
if (!file_id) {
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ error: '缺少 file_id' })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
|
||||||
|
const BOT_ID = '7689821072762011698';
|
||||||
|
|
||||||
|
const objectString = JSON.stringify([
|
||||||
|
{ type: 'file', file_id: file_id },
|
||||||
|
{ type: 'text', text: '请审查这份合同' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 调用扣子(流式)
|
||||||
|
try {
|
||||||
|
const chatRes = await uniCloud.httpclient.request('https://api.coze.cn/v3/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
dataType: 'text',
|
||||||
|
contentType: 'json',
|
||||||
|
timeout: 300000,
|
||||||
|
headers: { 'Authorization': 'Bearer ' + COZE_TOKEN },
|
||||||
|
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'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回 SSE 格式
|
||||||
|
const text = chatRes.data || '';
|
||||||
|
const sseBody = text.split('\n').map(line => {
|
||||||
|
if (line.startsWith('data:')) return line + '\n\n';
|
||||||
|
return '';
|
||||||
|
}).join('') + 'data: [DONE]\n\n';
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders,
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive'
|
||||||
|
},
|
||||||
|
body: sseBody
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ error: e.message })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "jjxReviewStream",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "合同审查流式输出云函数",
|
||||||
|
"main": "index.js"
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 定时任务:每分钟检查 reviewing 状态的记录,触发审查
|
||||||
|
const db = uniCloud.database();
|
||||||
|
const _ = db.command;
|
||||||
|
|
||||||
|
// 导入审查执行函数(从 router 的 service 目录)
|
||||||
|
// 注意:定时任务是独立云函数,不能直接 require router 的 service
|
||||||
|
// 所以把审查逻辑直接写在这里
|
||||||
|
|
||||||
|
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
|
||||||
|
const BOT_ID = '7689821072762011698';
|
||||||
|
|
||||||
|
async function doReview(record) {
|
||||||
|
const { _id: record_id, file_id, user_id, file_name, file_size } = record;
|
||||||
|
|
||||||
|
const objectString = JSON.stringify([
|
||||||
|
{ type: 'file', file_id: file_id },
|
||||||
|
{ type: 'text', text: '请审查这份合同' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 非流式发起对话(立即返回 chat_id)
|
||||||
|
const chatRes = await uniCloud.httpclient.request('https://api.coze.cn/v3/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
contentType: 'json',
|
||||||
|
timeout: 30000,
|
||||||
|
headers: { 'Authorization': 'Bearer ' + COZE_TOKEN },
|
||||||
|
data: {
|
||||||
|
bot_id: BOT_ID,
|
||||||
|
user_id: user_id || 'anonymous',
|
||||||
|
stream: false,
|
||||||
|
auto_save_history: true,
|
||||||
|
additional_messages: [{
|
||||||
|
role: 'user',
|
||||||
|
content: objectString,
|
||||||
|
content_type: 'object_string'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatData = chatRes.data && chatRes.data.data;
|
||||||
|
const chatId = chatData && chatData.id;
|
||||||
|
const conversationId = chatData && chatData.conversation_id;
|
||||||
|
console.log('[审查] chat_id:', chatId, 'conversation_id:', conversationId);
|
||||||
|
|
||||||
|
if (!chatId) {
|
||||||
|
throw new Error('未获取到 chat_id: ' + JSON.stringify(chatRes.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存 chat_id 和 conversation_id 到数据库
|
||||||
|
await db.collection('jjx-review-records').doc(record_id).update({
|
||||||
|
chat_id: chatId,
|
||||||
|
conversation_id: conversationId || '',
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
console.log('[审查] 已保存 chat_id:', chatId);
|
||||||
|
|
||||||
|
// 2. 轮询对话状态(每5秒查一次,最多5分钟)
|
||||||
|
let answer = '';
|
||||||
|
let tokenCount = 0;
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
await new Promise(r => setTimeout(r, 5000));
|
||||||
|
|
||||||
|
const statusRes = await uniCloud.httpclient.request(
|
||||||
|
'https://api.coze.cn/v3/chat/retrieve?chat_id=' + chatId + '&conversation_id=' + conversationId,
|
||||||
|
{ method: 'GET', dataType: 'json', headers: { 'Authorization': 'Bearer ' + COZE_TOKEN }, timeout: 15000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const status = statusRes.data && statusRes.data.data && statusRes.data.data.status;
|
||||||
|
console.log('[审查] 第', i + 1, '次轮询, 状态:', status);
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
// 保存 token 消耗信息
|
||||||
|
const usage = statusRes.data && statusRes.data.data && statusRes.data.data.usage;
|
||||||
|
tokenCount = (usage && usage.token_count) || 0;
|
||||||
|
console.log('[审查] Token 消耗:', tokenCount);
|
||||||
|
|
||||||
|
// 3. 获取消息列表
|
||||||
|
const msgRes = await uniCloud.httpclient.request(
|
||||||
|
'https://api.coze.cn/v3/chat/message/list?chat_id=' + chatId + '&conversation_id=' + conversationId,
|
||||||
|
{ method: 'GET', dataType: 'json', headers: { 'Authorization': 'Bearer ' + COZE_TOKEN }, timeout: 15000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = msgRes.data && msgRes.data.data;
|
||||||
|
if (Array.isArray(messages)) {
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.type === 'answer' && msg.role === 'assistant' && msg.content) {
|
||||||
|
answer = msg.content; // answer 类型的完整内容
|
||||||
|
}
|
||||||
|
if (msg.type === 'tool_response' && msg.content) {
|
||||||
|
try {
|
||||||
|
const toolData = JSON.parse(msg.content);
|
||||||
|
if (toolData.url) answer += '\n\n📎 报告下载:' + toolData.url;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
|
throw new Error('对话失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!answer) answer = '审查未能获取结果,请稍后重试';
|
||||||
|
console.log('[审查] 结果长度:', answer.length);
|
||||||
|
|
||||||
|
// 计算消耗积分
|
||||||
|
// 积分 = token消耗 / 100(最低1积分)
|
||||||
|
const pointsUsed = Math.max(1, Math.round(tokenCount / 100));
|
||||||
|
console.log('[积分] token_count:', tokenCount, '→ 扣除积分:', pointsUsed);
|
||||||
|
|
||||||
|
// 扣积分
|
||||||
|
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 || '') + '(' + tokenCount + ' token)',
|
||||||
|
biz_type: 'contract_review',
|
||||||
|
biz_id: record_id,
|
||||||
|
_add_time: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存 HTML 报告到云存储
|
||||||
|
let htmlUrl = '';
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新记录为完成
|
||||||
|
await db.collection('jjx-review-records').doc(record_id).update({
|
||||||
|
status: 'completed',
|
||||||
|
result: answer,
|
||||||
|
html_url: htmlUrl,
|
||||||
|
token_count: tokenCount,
|
||||||
|
points_used: pointsUsed,
|
||||||
|
conversation_id: conversationId || '',
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('审查完成:', record_id, file_name);
|
||||||
|
return { 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()
|
||||||
|
});
|
||||||
|
console.error('审查失败:', record_id, e.message);
|
||||||
|
return { code: -1, msg: '审查失败: ' + e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.main = async (event, context) => {
|
||||||
|
// 先把卡在 processing 超过10分钟的记录重置为 reviewing(超时保护)
|
||||||
|
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||||
|
const stuckRecords = await db.collection('jjx-review-records')
|
||||||
|
.where({ status: 'processing', update_time: _.lt(tenMinutesAgo) })
|
||||||
|
.get();
|
||||||
|
for (const stuck of stuckRecords.data) {
|
||||||
|
await db.collection('jjx-review-records').doc(stuck._id).update({
|
||||||
|
status: 'reviewing',
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询所有 reviewing 状态的记录(超过1分钟的,避免重复触发)
|
||||||
|
const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
|
||||||
|
|
||||||
|
const pendingRecords = await db.collection('jjx-review-records')
|
||||||
|
.where({
|
||||||
|
status: 'reviewing',
|
||||||
|
review_started_at: _.lt(oneMinuteAgo)
|
||||||
|
})
|
||||||
|
.limit(5) // 每次最多处理5条
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!pendingRecords.data.length) {
|
||||||
|
return { code: 0, msg: '无待处理记录' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('发现待处理审查记录:', pendingRecords.data.length, '条');
|
||||||
|
|
||||||
|
// 逐条处理(串行,避免并发问题)
|
||||||
|
const results = [];
|
||||||
|
for (const record of pendingRecords.data) {
|
||||||
|
// 先标记为 processing,避免重复触发
|
||||||
|
await db.collection('jjx-review-records').doc(record._id).update({
|
||||||
|
status: 'processing',
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await doReview(record);
|
||||||
|
results.push({ record_id: record._id, ...result });
|
||||||
|
} catch (e) {
|
||||||
|
// 审查失败,重置为 reviewing 以便下次重试
|
||||||
|
await db.collection('jjx-review-records').doc(record._id).update({
|
||||||
|
status: 'reviewing',
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
console.error('审查异常,已重置状态:', record._id, e.message);
|
||||||
|
results.push({ record_id: record._id, code: -1, msg: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 0, msg: '处理完成', data: results };
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// 本文件中的json内容将在云函数【运行】时作为参数传给云函数。
|
||||||
|
// 配置教程参考:https://uniapp.dcloud.net.cn/uniCloud/rundebug.html#runparam
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "jjxReviewTimer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "合同审查定时任务",
|
||||||
|
"main": "index.js",
|
||||||
|
"cloudfunction-config": {
|
||||||
|
"memorySize": 256,
|
||||||
|
"timeout": 600,
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"name": "myTrigger",
|
||||||
|
"type": "timer",
|
||||||
|
"config": "0 */1 * * * * *"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"path": "",
|
||||||
|
"runtime": "Nodejs16"
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-22
@@ -35,63 +35,64 @@
|
|||||||
},
|
},
|
||||||
"node_modules/charenc": {
|
"node_modules/charenc": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"license": "BSD-3-Clause",
|
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crypt": {
|
"node_modules/crypt": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"license": "BSD-3-Clause",
|
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/crypto-js": {
|
"node_modules/crypto-js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"license": "MIT"
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||||
|
"deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained."
|
||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.10",
|
"version": "1.11.23",
|
||||||
"license": "MIT"
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
|
||||||
},
|
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "3.2.7",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "^2.1.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/is-buffer": {
|
"node_modules/is-buffer": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
"license": "MIT"
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
|
||||||
},
|
},
|
||||||
"node_modules/md5": {
|
"node_modules/md5": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"license": "BSD-3-Clause",
|
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"charenc": "0.0.2",
|
"charenc": "0.0.2",
|
||||||
"crypt": "0.0.2",
|
"crypt": "0.0.2",
|
||||||
"is-buffer": "~1.1.6"
|
"is-buffer": "~1.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/node-ensure": {
|
"node_modules/node-ensure": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT"
|
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw=="
|
||||||
},
|
},
|
||||||
"node_modules/pdf-parse": {
|
"node_modules/pdf-parse": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.4",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^3.1.0",
|
|
||||||
"node-ensure": "^0.0.0"
|
"node-ensure": "^0.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.8.1"
|
"node": ">=6.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uni-config-center": {
|
"node_modules/uni-config-center": {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"concurrency": 1,
|
"concurrency": 1,
|
||||||
"memorySize": 512,
|
"memorySize": 512,
|
||||||
"path": "/http/router",
|
"path": "/http/router",
|
||||||
"timeout": 60,
|
"timeout": 600,
|
||||||
"triggers": [],
|
"triggers": [],
|
||||||
"keepRunningAfterReturn": false
|
"keepRunningAfterReturn": false
|
||||||
},
|
},
|
||||||
|
|||||||
+25
-67
@@ -8,17 +8,15 @@ module.exports = {
|
|||||||
* @param {String} accNo 账号
|
* @param {String} accNo 账号
|
||||||
* @param {String} reqDate 请求日期,格式:yyyyMMdd
|
* @param {String} reqDate 请求日期,格式:yyyyMMdd
|
||||||
* @param {String} reqJournalNo 请求流水号
|
* @param {String} reqJournalNo 请求流水号
|
||||||
* @param {String} logId 可选,流水ID,传入后会回写状态
|
|
||||||
*/
|
*/
|
||||||
main: async (event) => {
|
main: async (event) => {
|
||||||
let { data = {}, util } = event;
|
let { data = {}, util } = event;
|
||||||
let { vk } = util;
|
let { vk } = util;
|
||||||
let { accNo, reqDate, reqJournalNo, logId } = data;
|
let { accNo, reqDate, reqJournalNo } = data;
|
||||||
|
|
||||||
accNo = String(accNo || '').trim();
|
accNo = String(accNo || '').trim();
|
||||||
reqDate = String(reqDate || '').trim();
|
reqDate = String(reqDate || '').trim();
|
||||||
reqJournalNo = String(reqJournalNo || '').trim();
|
reqJournalNo = String(reqJournalNo || '').trim();
|
||||||
logId = String(logId || '').trim();
|
|
||||||
|
|
||||||
if (!accNo) {
|
if (!accNo) {
|
||||||
return {
|
return {
|
||||||
@@ -48,83 +46,43 @@ module.exports = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = await balanceUtil.requestActionLog({
|
let res = await vk.request({
|
||||||
vk,
|
url: 'http://jjx-union-pay.gxwebsoft.com/api/union-pay-jjx/getActionLog',
|
||||||
|
method: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
header: {
|
||||||
|
'content-type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
accNo,
|
accNo,
|
||||||
reqDate,
|
reqDate,
|
||||||
reqJournalNo
|
reqJournalNo
|
||||||
|
},
|
||||||
|
timeout: 60000
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && typeof res === 'object' && typeof res.code === 'undefined') {
|
if (res && typeof res === 'object' && typeof res.code === 'undefined') {
|
||||||
res.code = 0;
|
res.code = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = balanceUtil.getActionStatus(res);
|
|
||||||
const statusText = balanceUtil.getActionStatusTextByStatus(status);
|
|
||||||
|
|
||||||
if (status !== null) {
|
|
||||||
let logInfo = null;
|
|
||||||
if (logId) {
|
|
||||||
logInfo = await vk.baseDao.findByWhereJson({
|
|
||||||
dbName: 'jjx-balance-log',
|
|
||||||
whereJson: {
|
|
||||||
_id: logId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!logInfo) {
|
|
||||||
logInfo = await vk.baseDao.findByWhereJson({
|
|
||||||
dbName: 'jjx-balance-log',
|
|
||||||
whereJson: {
|
|
||||||
srcReqId: reqJournalNo,
|
|
||||||
merchantId: accNo
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logInfo && logInfo._id) {
|
|
||||||
logId = logInfo._id;
|
|
||||||
const prevStatus = Number(logInfo.status);
|
|
||||||
const nextStatus = Number(status);
|
|
||||||
const updateData = {
|
|
||||||
status: nextStatus
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新提现单(balanceDeducted === false):成功时再扣减余额缓存
|
|
||||||
// 旧提现单无该字段:创建时已扣减,这里只更新状态
|
|
||||||
const needDeductOnSuccess = logInfo.balanceDeducted === false
|
|
||||||
&& prevStatus === 1
|
|
||||||
&& nextStatus === 2;
|
|
||||||
|
|
||||||
if (needDeductOnSuccess) {
|
|
||||||
const amount = balanceUtil.normalizeAmount(logInfo.amount);
|
|
||||||
await balanceUtil.applyMerchantBalanceDelta({
|
|
||||||
vk,
|
|
||||||
merchantId: accNo,
|
|
||||||
delta: -amount,
|
|
||||||
lastLogId: logId
|
|
||||||
});
|
|
||||||
updateData.balanceDeducted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await vk.baseDao.updateById({
|
|
||||||
dbName: 'jjx-balance-log',
|
|
||||||
id: logId,
|
|
||||||
dataJson: updateData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseObjects = balanceUtil.getRemoteResponseObjects(res);
|
const responseObjects = balanceUtil.getRemoteResponseObjects(res);
|
||||||
|
const list = balanceUtil.pickFirstDefined([
|
||||||
|
Array.isArray(res.list) ? res.list : undefined,
|
||||||
|
Array.isArray(res.rows) ? res.rows : undefined,
|
||||||
|
Array.isArray(res.data) ? res.data : undefined,
|
||||||
|
Array.isArray(res.data && res.data.list) ? res.data.list : undefined,
|
||||||
|
Array.isArray(res.data && res.data.rows) ? res.data.rows : undefined,
|
||||||
|
Array.isArray(res.info && res.info.list) ? res.info.list : undefined,
|
||||||
|
Array.isArray(res.info && res.info.rows) ? res.info.rows : undefined,
|
||||||
|
Array.isArray(res.content && res.content.list) ? res.content.list : undefined,
|
||||||
|
Array.isArray(res.content && res.content.rows) ? res.content.rows : undefined
|
||||||
|
]) || [];
|
||||||
|
|
||||||
if (responseObjects.data && typeof responseObjects.data === 'object' && !Array.isArray(responseObjects.data)) {
|
if (responseObjects.data && typeof responseObjects.data === 'object' && !Array.isArray(responseObjects.data)) {
|
||||||
responseObjects.data.status = status;
|
responseObjects.data.list = list;
|
||||||
responseObjects.data.statusText = statusText;
|
|
||||||
responseObjects.data.logId = logId;
|
|
||||||
} else if (res && typeof res === 'object') {
|
} else if (res && typeof res === 'object') {
|
||||||
res.data = {
|
res.data = {
|
||||||
status,
|
list
|
||||||
statusText,
|
|
||||||
logId
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const { BALANCE_DOC_ID, getBalanceInfoList, getAvailableWithdrawBalance, normalizeAmount } = require('./util.js');
|
const { BALANCE_DOC_ID, getBalanceInfoList } = require('./util.js');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
@@ -25,24 +25,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const balanceInfoList = getBalanceInfoList(info.balanceInfo || {});
|
const balanceInfoList = getBalanceInfoList(info.balanceInfo || {});
|
||||||
const enrichedList = [];
|
|
||||||
for (let i = 0; i < balanceInfoList.length; i++) {
|
|
||||||
const item = balanceInfoList[i];
|
|
||||||
const {
|
|
||||||
pendingAmount,
|
|
||||||
availableBalance
|
|
||||||
} = await getAvailableWithdrawBalance({
|
|
||||||
vk,
|
|
||||||
merchantId: item.accNo,
|
|
||||||
merchantBalance: item.balance
|
|
||||||
});
|
|
||||||
enrichedList.push({
|
|
||||||
...item,
|
|
||||||
balance: normalizeAmount(item.balance),
|
|
||||||
pendingAmount,
|
|
||||||
availableBalance
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
@@ -50,7 +32,7 @@ module.exports = {
|
|||||||
data: {
|
data: {
|
||||||
balance: Number(Number(info.balance || 0).toFixed(2)),
|
balance: Number(Number(info.balance || 0).toFixed(2)),
|
||||||
updateTime: info.updateTime || info._update_time_str || info._add_time_str || '',
|
updateTime: info.updateTime || info._update_time_str || info._add_time_str || '',
|
||||||
balanceInfoList: enrichedList
|
balanceInfoList
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,64 +213,6 @@ function getActionStatusTextByStatus(status) {
|
|||||||
return statusMap[Number(status)] || '';
|
return statusMap[Number(status)] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyMerchantBalanceDelta({
|
|
||||||
vk,
|
|
||||||
transaction,
|
|
||||||
merchantId,
|
|
||||||
delta,
|
|
||||||
lastLogId = '',
|
|
||||||
updateTime = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
}) {
|
|
||||||
const accNo = String(merchantId || '').trim();
|
|
||||||
if (!accNo) {
|
|
||||||
throw new Error('提现账号不能为空');
|
|
||||||
}
|
|
||||||
const changeAmount = normalizeAmount(delta);
|
|
||||||
const balanceRecord = await ensureBalanceRecord({ transaction, vk });
|
|
||||||
const balanceInfoMap = buildBalanceInfoMap(balanceRecord.balanceInfo || {});
|
|
||||||
const currentMerchantInfo = balanceInfoMap[accNo] || {
|
|
||||||
accNo,
|
|
||||||
merchantName: getMerchantLabel(accNo),
|
|
||||||
balance: 0,
|
|
||||||
updateTime: ''
|
|
||||||
};
|
|
||||||
const currentMerchantBalance = normalizeAmount(currentMerchantInfo.balance || 0);
|
|
||||||
const nextMerchantBalance = normalizeAmount(currentMerchantBalance + changeAmount);
|
|
||||||
if (nextMerchantBalance < 0) {
|
|
||||||
throw new Error('余额不足');
|
|
||||||
}
|
|
||||||
const nextBalanceInfoMap = {
|
|
||||||
...balanceInfoMap,
|
|
||||||
[accNo]: {
|
|
||||||
accNo,
|
|
||||||
merchantName: currentMerchantInfo.merchantName || getMerchantLabel(accNo),
|
|
||||||
balance: nextMerchantBalance,
|
|
||||||
updateTime
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const nextTotalBalance = normalizeAmount(Number(balanceRecord.balance || 0) + changeAmount);
|
|
||||||
const dataJson = {
|
|
||||||
balance: nextTotalBalance,
|
|
||||||
balanceInfo: nextBalanceInfoMap,
|
|
||||||
updateTime
|
|
||||||
};
|
|
||||||
if (lastLogId) {
|
|
||||||
dataJson.lastLogId = lastLogId;
|
|
||||||
}
|
|
||||||
await vk.baseDao.updateById({
|
|
||||||
db: transaction,
|
|
||||||
dbName: 'jjx-balance',
|
|
||||||
id: balanceRecord._id,
|
|
||||||
dataJson
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
balance: nextTotalBalance,
|
|
||||||
merchantBalance: nextMerchantBalance,
|
|
||||||
updateTime,
|
|
||||||
balanceInfoMap: nextBalanceInfoMap
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncMerchantBalance({
|
async function syncMerchantBalance({
|
||||||
vk,
|
vk,
|
||||||
accNo,
|
accNo,
|
||||||
@@ -381,49 +323,6 @@ async function checkBalanceAvailable({ transaction, vk, changeAmount, excludeLog
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 统计指定商户处理中的提现金额(status=1)
|
|
||||||
*/
|
|
||||||
async function getPendingWithdrawAmount({ transaction, vk, merchantId }) {
|
|
||||||
const accNo = String(merchantId || '').trim();
|
|
||||||
if (!accNo) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const res = await vk.baseDao.select({
|
|
||||||
db: transaction,
|
|
||||||
dbName: 'jjx-balance-log',
|
|
||||||
whereJson: {
|
|
||||||
merchantId: accNo,
|
|
||||||
type: 'expense',
|
|
||||||
source: 'withdraw',
|
|
||||||
status: 1
|
|
||||||
},
|
|
||||||
pageIndex: 1,
|
|
||||||
pageSize: 1000,
|
|
||||||
getCount: false
|
|
||||||
});
|
|
||||||
const rows = (res && res.rows) || [];
|
|
||||||
return normalizeAmount(rows.reduce((sum, item) => sum + Number(item.amount || 0), 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可提现余额 = 当前余额 - 处理中提现金额
|
|
||||||
*/
|
|
||||||
async function getAvailableWithdrawBalance({ transaction, vk, merchantId, merchantBalance }) {
|
|
||||||
const pendingAmount = await getPendingWithdrawAmount({
|
|
||||||
transaction,
|
|
||||||
vk,
|
|
||||||
merchantId
|
|
||||||
});
|
|
||||||
const balance = normalizeAmount(merchantBalance);
|
|
||||||
const availableBalance = normalizeAmount(Math.max(0, balance - pendingAmount));
|
|
||||||
return {
|
|
||||||
balance,
|
|
||||||
pendingAmount,
|
|
||||||
availableBalance
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateBalanceWithDelta({ transaction, vk, delta, lastLogId = '' }) {
|
async function updateBalanceWithDelta({ transaction, vk, delta, lastLogId = '' }) {
|
||||||
const balanceInfo = await ensureBalanceRecord({ transaction, vk });
|
const balanceInfo = await ensureBalanceRecord({ transaction, vk });
|
||||||
const nextBalance = Number((Number(balanceInfo.balance || 0) + Number(delta || 0)).toFixed(2));
|
const nextBalance = Number((Number(balanceInfo.balance || 0) + Number(delta || 0)).toFixed(2));
|
||||||
@@ -473,13 +372,10 @@ module.exports = {
|
|||||||
parseOrderSet,
|
parseOrderSet,
|
||||||
getActionStatus,
|
getActionStatus,
|
||||||
getActionStatusTextByStatus,
|
getActionStatusTextByStatus,
|
||||||
applyMerchantBalanceDelta,
|
|
||||||
syncMerchantBalance,
|
syncMerchantBalance,
|
||||||
ensureBalanceRecord,
|
ensureBalanceRecord,
|
||||||
recalculateBalance,
|
recalculateBalance,
|
||||||
checkBalanceAvailable,
|
checkBalanceAvailable,
|
||||||
getPendingWithdrawAmount,
|
|
||||||
getAvailableWithdrawBalance,
|
|
||||||
updateBalanceWithDelta,
|
updateBalanceWithDelta,
|
||||||
rollbackTransaction
|
rollbackTransaction
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
const dayjs = require('dayjs');
|
||||||
const balanceUtil = require('./util.js');
|
const balanceUtil = require('./util.js');
|
||||||
const merchantMap = balanceUtil.MERCHANT_MAP;
|
const merchantMap = balanceUtil.MERCHANT_MAP;
|
||||||
|
|
||||||
@@ -33,28 +34,11 @@ module.exports = {
|
|||||||
updateTime: ''
|
updateTime: ''
|
||||||
};
|
};
|
||||||
const merchantBalance = balanceUtil.normalizeAmount(merchantBalanceInfo.balance || 0);
|
const merchantBalance = balanceUtil.normalizeAmount(merchantBalanceInfo.balance || 0);
|
||||||
const {
|
if (merchantBalance <= 0) {
|
||||||
pendingAmount,
|
throw new Error('当前企业余额小于等于0,无法提现');
|
||||||
availableBalance
|
|
||||||
} = await balanceUtil.getAvailableWithdrawBalance({
|
|
||||||
transaction,
|
|
||||||
vk,
|
|
||||||
merchantId,
|
|
||||||
merchantBalance
|
|
||||||
});
|
|
||||||
if (availableBalance <= 0) {
|
|
||||||
throw new Error(
|
|
||||||
pendingAmount > 0
|
|
||||||
? `可提现余额不足(处理中提现¥${pendingAmount.toFixed(2)}已占用)`
|
|
||||||
: '当前企业余额小于等于0,无法提现'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (amount > availableBalance) {
|
if (amount > merchantBalance) {
|
||||||
throw new Error(
|
throw new Error('提现金额不能大于当前企业余额');
|
||||||
pendingAmount > 0
|
|
||||||
? `提现金额不能大于可提现余额¥${availableBalance.toFixed(2)}(已排除处理中提现¥${pendingAmount.toFixed(2)})`
|
|
||||||
: '提现金额不能大于当前企业余额'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const description = String(data.description || '').trim() || `余额提现-${merchantMap[merchantId]}`;
|
const description = String(data.description || '').trim() || `余额提现-${merchantMap[merchantId]}`;
|
||||||
|
|
||||||
@@ -96,28 +80,52 @@ module.exports = {
|
|||||||
merchantId,
|
merchantId,
|
||||||
merchantName: merchantMap[merchantId],
|
merchantName: merchantMap[merchantId],
|
||||||
srcReqId: requestRes.srcReqId,
|
srcReqId: requestRes.srcReqId,
|
||||||
status: 1,
|
status: 1
|
||||||
// 新逻辑:处理中不扣余额,成功后再扣;用于兼容旧数据(旧单默认已扣)
|
|
||||||
balanceDeducted: false
|
|
||||||
};
|
};
|
||||||
|
const changeAmount = balanceUtil.buildBalanceChange(logData);
|
||||||
|
|
||||||
await vk.baseDao.add({
|
await balanceUtil.checkBalanceAvailable({
|
||||||
|
transaction,
|
||||||
|
vk,
|
||||||
|
changeAmount
|
||||||
|
});
|
||||||
|
|
||||||
|
const addRes = await vk.baseDao.add({
|
||||||
db: transaction,
|
db: transaction,
|
||||||
dbName: 'jjx-balance-log',
|
dbName: 'jjx-balance-log',
|
||||||
dataJson: logData
|
dataJson: logData
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理中提现不立刻扣减余额缓存,避免与远端余额同步互相覆盖;
|
const updateTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
// 可提现额度通过「余额 - 处理中提现」计算,防止重复提现超总额。
|
const nextMerchantBalance = Number((merchantBalance + changeAmount).toFixed(2));
|
||||||
|
const nextBalance = Number((Number(balanceRecord.balance || 0) + changeAmount).toFixed(2));
|
||||||
|
await vk.baseDao.updateById({
|
||||||
|
db: transaction,
|
||||||
|
dbName: 'jjx-balance',
|
||||||
|
id: balanceRecord._id,
|
||||||
|
dataJson: {
|
||||||
|
balance: nextBalance,
|
||||||
|
lastLogId: addRes.id,
|
||||||
|
updateTime,
|
||||||
|
balanceInfo: {
|
||||||
|
...balanceInfoMap,
|
||||||
|
[merchantId]: {
|
||||||
|
accNo: merchantId,
|
||||||
|
merchantName: merchantMap[merchantId],
|
||||||
|
balance: nextMerchantBalance,
|
||||||
|
updateTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
msg: '',
|
msg: '',
|
||||||
data: {
|
data: {
|
||||||
balance: merchantBalance,
|
balance: nextBalance,
|
||||||
pendingAmount: balanceUtil.normalizeAmount(pendingAmount + amount),
|
merchantBalance: nextMerchantBalance
|
||||||
availableBalance: balanceUtil.normalizeAmount(Math.max(0, availableBalance - amount)),
|
|
||||||
merchantBalance
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let answer = '暂时无法回复,请拨打 400-686-5559 咨询';
|
let answer = '';
|
||||||
let conversationId = '';
|
let conversationId = '';
|
||||||
const text = res.data || '';
|
const text = res.data || '';
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
@@ -37,10 +37,11 @@ module.exports = {
|
|||||||
const p = JSON.parse(d);
|
const p = JSON.parse(d);
|
||||||
if (p.conversation_id) conversationId = p.conversation_id;
|
if (p.conversation_id) conversationId = p.conversation_id;
|
||||||
if (p.type === 'answer' && p.content) {
|
if (p.type === 'answer' && p.content) {
|
||||||
answer = p.content;
|
answer += p.content; // 拼接,不是覆盖
|
||||||
}
|
}
|
||||||
} catch (e) { /* 跳过 */ }
|
} catch (e) { /* 跳过 */ }
|
||||||
}
|
}
|
||||||
|
if (!answer) answer = '暂时无法回复,请拨打 400-686-5559 咨询';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// client/ai/pub/doReviewTimer.js
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {} } = event;
|
||||||
|
const { record_id, file_id, user_id, file_name, file_size } = data;
|
||||||
|
|
||||||
|
if (!record_id || !file_id) return { code: -1, msg: '参数错误' };
|
||||||
|
|
||||||
|
const db = uniCloud.database();
|
||||||
|
|
||||||
|
// 检查是否已经在审查中(防止重复触发)
|
||||||
|
const record = await db.collection('jjx-review-records').doc(record_id).get();
|
||||||
|
if (record.data.length && record.data[0].status === 'reviewing') {
|
||||||
|
// 检查是否刚触发(5分钟内不重复触发)
|
||||||
|
const lastUpdate = record.data[0].update_time;
|
||||||
|
if (lastUpdate && (Date.now() - new Date(lastUpdate).getTime() < 5 * 60 * 1000)) {
|
||||||
|
return { code: 0, msg: '已在审查中,跳过' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态为审查中
|
||||||
|
await db.collection('jjx-review-records').doc(record_id).update({
|
||||||
|
status: 'reviewing',
|
||||||
|
review_started_at: new Date(),
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 调用实际的审查逻辑
|
||||||
|
try {
|
||||||
|
const doReview = require('./jjxDoReview.js');
|
||||||
|
await doReview.main({
|
||||||
|
data: {
|
||||||
|
record_id,
|
||||||
|
file_id,
|
||||||
|
user_id: user_id || 'anonymous',
|
||||||
|
file_name: file_name || '',
|
||||||
|
file_size: file_size || 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { code: 0, msg: '审查完成' };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('审查失败:', 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util } = event;
|
||||||
|
let { db } = util;
|
||||||
|
const { pageIndex = 1, pageSize = 50 } = data;
|
||||||
|
|
||||||
|
const uid = (data && data.uid) || '';
|
||||||
|
if (!uid) return { code: -1, msg: '未登录' };
|
||||||
|
|
||||||
|
const where = { user_id: uid };
|
||||||
|
|
||||||
|
const countRes = await db.collection('jjx-review-records').where(where).count();
|
||||||
|
|
||||||
|
const listRes = await db.collection('jjx-review-records')
|
||||||
|
.where(where)
|
||||||
|
.orderBy('_add_time', 'desc')
|
||||||
|
.skip((pageIndex - 1) * pageSize)
|
||||||
|
.limit(pageSize)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
msg: 'ok',
|
||||||
|
data: {
|
||||||
|
total: countRes.total,
|
||||||
|
rows: listRes.data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// 云函数:client/ai/pub/getReviewResult
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util } = event;
|
||||||
|
let { db } = util;
|
||||||
|
const { record_id } = data;
|
||||||
|
|
||||||
|
if (!record_id) return { code: -1, msg: '缺少 record_id' };
|
||||||
|
|
||||||
|
const record = await db.collection('jjx-review-records').doc(record_id).get();
|
||||||
|
if (!record.data.length) return { code: -1, msg: '记录不存在' };
|
||||||
|
|
||||||
|
const rec = record.data[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
status: rec.status,
|
||||||
|
result: rec.result || '',
|
||||||
|
points_used: rec.points_used || 0,
|
||||||
|
html_url: rec.html_url || '',
|
||||||
|
contract_info: rec.contract_info || {},
|
||||||
|
error: rec.error || ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// client/ai/pub/review.js
|
||||||
|
// Markdown 转 HTML
|
||||||
|
function mdToHtml(md) {
|
||||||
|
if (!md) return '';
|
||||||
|
let html = md;
|
||||||
|
// 标题
|
||||||
|
html = html.replace(/^####\s+(.+)$/gm, '<h5>$1</h5>');
|
||||||
|
html = html.replace(/^###\s+(.+)$/gm, '<h4>$1</h4>');
|
||||||
|
html = html.replace(/^##\s+(.+)$/gm, '<h3>$1</h3>');
|
||||||
|
html = html.replace(/^#\s+(.+)$/gm, '<h2>$1</h2>');
|
||||||
|
// 加粗
|
||||||
|
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||||
|
// 链接
|
||||||
|
html = html.replace(/(https?:\/\/[^\s\)\]\"\<]+)/g, '<a href="$1" target="_blank">$1</a>');
|
||||||
|
// 代码块
|
||||||
|
html = html.replace(/```[\w]*\n([\s\S]*?)```/g, '<pre class="code-block">$1</pre>');
|
||||||
|
// 行内代码
|
||||||
|
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||||
|
// 列表
|
||||||
|
html = html.replace(/^[\-\•]\s+(.+)$/gm, '<li>$1</li>');
|
||||||
|
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<li>$1. $2</li>');
|
||||||
|
// 换行
|
||||||
|
html = html.replace(/\n\n/g, '</p><p>');
|
||||||
|
html = html.replace(/\n/g, '<br>');
|
||||||
|
html = '<p>' + html + '</p>';
|
||||||
|
// 清理空段落
|
||||||
|
html = html.replace(/<p>\s*<\/p>/g, '');
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成精美 HTML 报告
|
||||||
|
function generateHtmlReport(markdown, fileName, reportDate, info) {
|
||||||
|
const riskLevel = info.risk_level || '待评估';
|
||||||
|
const riskSummary = info.risk_summary || '';
|
||||||
|
const riskCount = info.risk_count || { high: 0, medium: 0, low: 0 };
|
||||||
|
const contractName = info.contract_name || fileName || '合同审查报告';
|
||||||
|
const contractType = info.contract_type || '';
|
||||||
|
|
||||||
|
// 风险等级颜色
|
||||||
|
let riskColor = '#67c23a';
|
||||||
|
if (riskLevel.includes('高')) riskColor = '#f56c6c';
|
||||||
|
else if (riskLevel.includes('中高')) riskColor = '#e6a23c';
|
||||||
|
else if (riskLevel.includes('中')) riskColor = '#e6a23c';
|
||||||
|
else if (riskLevel.includes('中低')) riskColor = '#409eff';
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${contractName} - 审查报告</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; background: #f5f7fa; color: #333; line-height: 1.8; }
|
||||||
|
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
|
||||||
|
.header { background: linear-gradient(135deg, #a92924, #d4574e); color: #fff; padding: 40px; border-radius: 12px 12px 0 0; }
|
||||||
|
.header h1 { font-size: 24px; margin-bottom: 8px; }
|
||||||
|
.header .subtitle { opacity: 0.85; font-size: 14px; }
|
||||||
|
.summary-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding: 24px 40px; background: #fff; border-bottom: 1px solid #eee; }
|
||||||
|
.summary-card { text-align: center; padding: 16px; border-radius: 8px; background: #f9f9f9; }
|
||||||
|
.summary-card .label { font-size: 12px; color: #999; margin-bottom: 4px; }
|
||||||
|
.summary-card .value { font-size: 20px; font-weight: 700; }
|
||||||
|
.body { background: #fff; padding: 32px 40px; border-radius: 0 0 12px 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); }
|
||||||
|
.body h2 { font-size: 18px; color: #222; margin: 28px 0 12px; padding-bottom: 8px; border-bottom: 2px solid #a92924; }
|
||||||
|
.body h3 { font-size: 16px; color: #333; margin: 20px 0 8px; }
|
||||||
|
.body h4 { font-size: 14px; color: #444; margin: 16px 0 6px; }
|
||||||
|
.body p { margin: 8px 0; font-size: 14px; }
|
||||||
|
.body li { margin: 4px 0 4px 20px; font-size: 14px; }
|
||||||
|
.body strong { color: #a92924; }
|
||||||
|
.body a { color: #a92924; text-decoration: underline; }
|
||||||
|
.risk-badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 600; color: #fff; }
|
||||||
|
.risk-high { background: #f56c6c; }
|
||||||
|
.risk-medium { background: #e6a23c; }
|
||||||
|
.risk-low { background: #67c23a; }
|
||||||
|
.code-block { background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 12px 16px; font-size: 13px; overflow-x: auto; margin: 8px 0; font-family: "SFMono-Regular", Consolas, monospace; }
|
||||||
|
.disclaimer { margin-top: 32px; padding: 16px 20px; background: #fff8e1; border-left: 4px solid #e6a23c; border-radius: 4px; font-size: 12px; color: #b8860b; }
|
||||||
|
.footer { text-align: center; padding: 24px; font-size: 12px; color: #999; }
|
||||||
|
@media print { body { background: #fff; } .container { padding: 0; } .header { border-radius: 0; } .body { box-shadow: none; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📋 合同审查报告</h1>
|
||||||
|
<div class="subtitle">${contractName} | ${reportDate}</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-cards">
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="label">综合风险等级</div>
|
||||||
|
<div class="value" style="color:${riskColor}">${riskLevel}</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="label">风险统计</div>
|
||||||
|
<div class="value"><span style="color:#f56c6c">${riskCount.high||0}</span> / <span style="color:#e6a23c">${riskCount.medium||0}</span> / <span style="color:#67c23a">${riskCount.low||0}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card">
|
||||||
|
<div class="label">合同类型</div>
|
||||||
|
<div class="value" style="font-size:14px">${contractType || '待识别'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<h2>总体评估</h2>
|
||||||
|
<p>${riskSummary || '暂无总体评估'}</p>
|
||||||
|
<h2>详细审查意见</h2>
|
||||||
|
${mdToHtml(markdown.replace(/```json[\s\S]*?```/, ''))}
|
||||||
|
<div class="disclaimer">
|
||||||
|
⚠️ <strong>免责声明</strong>:本报告由 AI 系统自动生成,仅供参考,不构成正式法律意见。涉及重大金额或复杂交易的合同,建议咨询执业律师。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
金久信签约宝 · AI 合同审查服务 | 报告生成时间:${new Date().toLocaleString('zh-CN')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo } = event;
|
||||||
|
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
|
||||||
|
const BOT_ID = '7689821072762011698';
|
||||||
|
const db = uniCloud.database();
|
||||||
|
const _ = db.command;
|
||||||
|
|
||||||
|
let fileId = data.file_id;
|
||||||
|
|
||||||
|
// ===== 模式1:上传文件 + 创建待审查记录 =====
|
||||||
|
if (data.upload_only && data.file_base64) {
|
||||||
|
const fileBuffer = Buffer.from(data.file_base64, 'base64');
|
||||||
|
const boundary = '----FormBoundary' + Date.now();
|
||||||
|
const header = Buffer.from(
|
||||||
|
'--' + boundary + '\r\n' +
|
||||||
|
'Content-Disposition: form-data; name="file"; filename="' + (data.file_name || 'file') + '"\r\n' +
|
||||||
|
'Content-Type: application/octet-stream\r\n\r\n'
|
||||||
|
);
|
||||||
|
const footer = Buffer.from('\r\n--' + boundary + '--\r\n');
|
||||||
|
const uploadBody = Buffer.concat([header, fileBuffer, footer]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uploadRes = await uniCloud.httpclient.request('https://api.coze.cn/v1/files/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + COZE_TOKEN,
|
||||||
|
'Content-Type': 'multipart/form-data; boundary=' + boundary,
|
||||||
|
'Content-Length': uploadBody.length.toString()
|
||||||
|
},
|
||||||
|
content: uploadBody,
|
||||||
|
timeout: 600000
|
||||||
|
});
|
||||||
|
if (uploadRes.data.code !== 0) {
|
||||||
|
return { code: -1, msg: '文件上传失败: ' + uploadRes.data.msg };
|
||||||
|
}
|
||||||
|
fileId = uploadRes.data.data.id;
|
||||||
|
|
||||||
|
// 同时上传到 uniCloud 云存储(用于查看/下载)
|
||||||
|
let fileUrl = '';
|
||||||
|
try {
|
||||||
|
const cloudPath = 'review-files/' + Date.now() + '_' + (data.file_name || 'file');
|
||||||
|
const uploadFileRes = await uniCloud.uploadFile({
|
||||||
|
cloudPath: cloudPath,
|
||||||
|
fileContent: fileBuffer
|
||||||
|
});
|
||||||
|
if (uploadFileRes.fileID) {
|
||||||
|
const urlRes = await uniCloud.getTempFileURL({
|
||||||
|
fileList: [uploadFileRes.fileID]
|
||||||
|
});
|
||||||
|
if (urlRes.fileList && urlRes.fileList[0]) {
|
||||||
|
fileUrl = urlRes.fileList[0].tempFileURL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('云存储上传失败(不影响审查):', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建待审查记录
|
||||||
|
const recordRes = await db.collection('jjx-review-records').add({
|
||||||
|
user_id: (data && data.uid) || 'anonymous',
|
||||||
|
file_name: data.file_name || '',
|
||||||
|
file_size: data.file_size || 0,
|
||||||
|
file_id: fileId,
|
||||||
|
file_url: fileUrl,
|
||||||
|
chat_id: '',
|
||||||
|
conversation_id: '',
|
||||||
|
status: 'uploaded',
|
||||||
|
result: '',
|
||||||
|
points_used: 0,
|
||||||
|
_add_time: new Date(),
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 0, msg: 'ok', data: { file_id: fileId, record_id: recordRes.id, file_url: fileUrl } };
|
||||||
|
} catch (e) {
|
||||||
|
return { code: -1, msg: '上传异常: ' + e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 模式2:发起审查(只更新状态,立即返回)=====
|
||||||
|
if (!fileId) return { code: -1, msg: '缺少 file_id' };
|
||||||
|
|
||||||
|
const recordId = data.record_id;
|
||||||
|
if (!recordId) return { code: -1, msg: '缺少 record_id' };
|
||||||
|
|
||||||
|
// 检查当前状态
|
||||||
|
const record = await db.collection('jjx-review-records').doc(recordId).get();
|
||||||
|
if (record.data.length) {
|
||||||
|
const rec = record.data[0];
|
||||||
|
|
||||||
|
// 已完成,直接返回结果
|
||||||
|
if (rec.status === 'completed') {
|
||||||
|
return {
|
||||||
|
code: 0, msg: 'ok',
|
||||||
|
data: {
|
||||||
|
record_id: recordId,
|
||||||
|
status: 'completed',
|
||||||
|
result: rec.result,
|
||||||
|
points_used: rec.points_used,
|
||||||
|
html_url: rec.html_url || '',
|
||||||
|
contract_info: rec.contract_info || {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正在审查中,直接返回
|
||||||
|
if (rec.status === 'reviewing') {
|
||||||
|
return { code: 0, msg: 'ok', data: { record_id: recordId, status: 'reviewing' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 失败,返回错误
|
||||||
|
if (rec.status === 'failed') {
|
||||||
|
return { code: 0, msg: 'ok', data: { record_id: recordId, status: 'failed', result: rec.result || '审查失败' } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态为审查中
|
||||||
|
await db.collection('jjx-review-records').doc(recordId).update({
|
||||||
|
file_id: fileId,
|
||||||
|
file_name: data.file_name || '',
|
||||||
|
file_size: data.file_size || 0,
|
||||||
|
status: 'reviewing',
|
||||||
|
review_started_at: new Date(),
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 立即返回,审查由定时任务触发 =====
|
||||||
|
return {
|
||||||
|
code: 0, msg: 'ok',
|
||||||
|
data: { record_id: recordId, status: 'reviewing' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { BALANCE_DOC_ID, normalizeAmount } = require("../admin/finance/balance/sys/util.js");
|
const {
|
||||||
|
BALANCE_DOC_ID,
|
||||||
|
normalizeAmount
|
||||||
|
} = require("../admin/finance/balance/sys/util.js");
|
||||||
const notaryPay = require("./order/kh/util/notaryPay");
|
const notaryPay = require("./order/kh/util/notaryPay");
|
||||||
var vk; // 全局vk实例
|
var vk; // 全局vk实例
|
||||||
const orderTypeList = [
|
const orderTypeList = [
|
||||||
@@ -19,6 +22,11 @@ const orderTypeList = [
|
|||||||
'司法调证',
|
'司法调证',
|
||||||
'金久信签约宝次数',
|
'金久信签约宝次数',
|
||||||
'金久信自营询价',
|
'金久信自营询价',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'AI积分充值'
|
||||||
];
|
];
|
||||||
// 涉及的表名
|
// 涉及的表名
|
||||||
const dbName = {
|
const dbName = {
|
||||||
@@ -57,7 +65,7 @@ var cloudObject = {
|
|||||||
* 请求前处理,主要用于调用方法之前进行预处理,一般用于拦截器、统一的身份验证、参数校验、定义全局对象等。
|
* 请求前处理,主要用于调用方法之前进行预处理,一般用于拦截器、统一的身份验证、参数校验、定义全局对象等。
|
||||||
* 文档地址:https://vkdoc.fsq.pub/client/uniCloud/cloudfunctions/cloudObject.html#before-预处理
|
* 文档地址:https://vkdoc.fsq.pub/client/uniCloud/cloudfunctions/cloudObject.html#before-预处理
|
||||||
*/
|
*/
|
||||||
_before: async function () {
|
_before: async function() {
|
||||||
vk = this.vk; // 将vk定义为全局对象
|
vk = this.vk; // 将vk定义为全局对象
|
||||||
// let { customUtil, uniID, config, pubFun } = this.getUtil(); // 获取工具包
|
// let { customUtil, uniID, config, pubFun } = this.getUtil(); // 获取工具包
|
||||||
},
|
},
|
||||||
@@ -65,8 +73,11 @@ var cloudObject = {
|
|||||||
* 请求后处理,主要用于处理本次调用方法的返回结果或者抛出的错误
|
* 请求后处理,主要用于处理本次调用方法的返回结果或者抛出的错误
|
||||||
* 文档地址:https://vkdoc.fsq.pub/client/uniCloud/cloudfunctions/cloudObject.html#after-后处理
|
* 文档地址:https://vkdoc.fsq.pub/client/uniCloud/cloudfunctions/cloudObject.html#after-后处理
|
||||||
*/
|
*/
|
||||||
_after: async function (options) {
|
_after: async function(options) {
|
||||||
let {err, res} = options;
|
let {
|
||||||
|
err,
|
||||||
|
res
|
||||||
|
} = options;
|
||||||
if (err) {
|
if (err) {
|
||||||
return; // 如果方法抛出错误,直接return;不处理
|
return; // 如果方法抛出错误,直接return;不处理
|
||||||
}
|
}
|
||||||
@@ -76,17 +87,26 @@ var cloudObject = {
|
|||||||
* 获取列表
|
* 获取列表
|
||||||
* @url client/payment.getList 前端调用的url参数地址
|
* @url client/payment.getList 前端调用的url参数地址
|
||||||
*/
|
*/
|
||||||
pub_payNotify: async function (data) {
|
pub_payNotify: async function(data) {
|
||||||
let res = {code: 0, msg: ''};
|
console.log("=== 支付回调触发 ===", JSON.stringify(data));
|
||||||
|
console.log("订单号:", data.billNo || data.merOrderId);
|
||||||
|
console.log("支付状态:", data.billStatus);
|
||||||
|
let res = {
|
||||||
|
code: 0,
|
||||||
|
msg: ''
|
||||||
|
};
|
||||||
// 业务逻辑开始-----------------------------------------------------------
|
// 业务逻辑开始-----------------------------------------------------------
|
||||||
console.log("pay notify data", data)
|
console.log("pay notify data", data)
|
||||||
let orderNo = data.billNo
|
let orderNo = data.billNo
|
||||||
if (!orderNo) orderNo = data.merOrderId
|
if (!orderNo) orderNo = data.merOrderId
|
||||||
let payTime = null, payAmount = null, payNo = null
|
let payTime = null,
|
||||||
|
payAmount = null,
|
||||||
|
payNo = null
|
||||||
if (data.billPayment) {
|
if (data.billPayment) {
|
||||||
const billPayment = JSON.parse(data.billPayment)
|
const billPayment = JSON.parse(data.billPayment)
|
||||||
payTime = pickFirstDefined(billPayment.payTime, data.payTime, data.createTime)
|
payTime = pickFirstDefined(billPayment.payTime, data.payTime, data.createTime)
|
||||||
payAmount = toFenAmount(pickFirstDefined(billPayment.receiptAmount, billPayment.totalAmount, data.receiptAmount, data.totalAmount))
|
payAmount = toFenAmount(pickFirstDefined(billPayment.receiptAmount, billPayment.totalAmount, data
|
||||||
|
.receiptAmount, data.totalAmount))
|
||||||
payNo = pickFirstDefined(billPayment.paySeqId, billPayment.seqId, data.seqId, data.targetOrderId)
|
payNo = pickFirstDefined(billPayment.paySeqId, billPayment.seqId, data.seqId, data.targetOrderId)
|
||||||
} else {
|
} else {
|
||||||
payTime = pickFirstDefined(data.payTime, data.createTime)
|
payTime = pickFirstDefined(data.payTime, data.createTime)
|
||||||
@@ -218,7 +238,7 @@ var cloudObject = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([2, 3, 11, 12, 13].includes(order.type)) {
|
if ([2, 3, 11, 12, 13,20].includes(order.type)) {
|
||||||
await vk.baseDao.updateById({
|
await vk.baseDao.updateById({
|
||||||
dbName: "jjx-order",
|
dbName: "jjx-order",
|
||||||
id: order._id,
|
id: order._id,
|
||||||
@@ -618,6 +638,74 @@ var cloudObject = {
|
|||||||
remainNum
|
remainNum
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (order.type === 20) {
|
||||||
|
// 积分充值到账
|
||||||
|
const goods = await vk.baseDao.findByWhereJson({
|
||||||
|
dbName: "jjx-order-goods",
|
||||||
|
whereJson: {
|
||||||
|
orderId: order._id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (goods) {
|
||||||
|
const points = parseInt(goods.goodsName.replace('积分充值_', '').replace('积分', ''));
|
||||||
|
if (points > 0) {
|
||||||
|
// 增加积分余额
|
||||||
|
const userPoints = await vk.baseDao.findByWhereJson({
|
||||||
|
dbName: "jjx-user-points",
|
||||||
|
whereJson: {
|
||||||
|
_id: order.uid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (userPoints) {
|
||||||
|
await vk.baseDao.updateById({
|
||||||
|
dbName: "jjx-user-points",
|
||||||
|
id: order.uid,
|
||||||
|
dataJson: {
|
||||||
|
balance: _.inc(points),
|
||||||
|
total_earned: _.inc(points),
|
||||||
|
update_time: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await vk.baseDao.add({
|
||||||
|
dbName: "jjx-user-points",
|
||||||
|
dataJson: {
|
||||||
|
_id: order.uid,
|
||||||
|
balance: points,
|
||||||
|
total_earned: points,
|
||||||
|
total_spent: 0,
|
||||||
|
gift: 0,
|
||||||
|
update_time: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 记录流水
|
||||||
|
const updatedPoints = await vk.baseDao.findById({
|
||||||
|
dbName: "jjx-user-points",
|
||||||
|
id: order.uid
|
||||||
|
});
|
||||||
|
await vk.baseDao.add({
|
||||||
|
dbName: "jjx-points-log",
|
||||||
|
dataJson: {
|
||||||
|
user_id: order.uid,
|
||||||
|
type: 'earn',
|
||||||
|
amount: points,
|
||||||
|
balance_after: updatedPoints ? updatedPoints.balance : points,
|
||||||
|
reason: '积分充值(' + order.offAmount + '元)',
|
||||||
|
biz_type: 'recharge',
|
||||||
|
biz_id: order._id,
|
||||||
|
_add_time: new Date()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await vk.baseDao.updateById({
|
||||||
|
dbName: "jjx-order",
|
||||||
|
id: order._id,
|
||||||
|
dataJson: {
|
||||||
|
orderStatus: 4
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!order) {
|
if (!order) {
|
||||||
@@ -629,7 +717,8 @@ var cloudObject = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('pay list order data', order)
|
console.log('pay list order data', order)
|
||||||
if (order && !order.sellerConfirm && (data.billStatus === 'PAID' || data.status === 'TRADE_SUCCESS')) {
|
if (order && !order.sellerConfirm && (data.billStatus === 'PAID' || data.status ===
|
||||||
|
'TRADE_SUCCESS')) {
|
||||||
await vk.baseDao.updateById({
|
await vk.baseDao.updateById({
|
||||||
dbName: "jjx-order-pay-list",
|
dbName: "jjx-order-pay-list",
|
||||||
id: order._id,
|
id: order._id,
|
||||||
@@ -649,9 +738,14 @@ var cloudObject = {
|
|||||||
* 模板函数
|
* 模板函数
|
||||||
* @url client/payment.test 前端调用的url参数地址
|
* @url client/payment.test 前端调用的url参数地址
|
||||||
*/
|
*/
|
||||||
test: async function (data) {
|
test: async function(data) {
|
||||||
let res = {code: 0, msg: ''};
|
let res = {
|
||||||
let {uid} = this.getClientInfo(); // 获取客户端信息
|
code: 0,
|
||||||
|
msg: ''
|
||||||
|
};
|
||||||
|
let {
|
||||||
|
uid
|
||||||
|
} = this.getClientInfo(); // 获取客户端信息
|
||||||
// 业务逻辑开始-----------------------------------------------------------
|
// 业务逻辑开始-----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//创建积分充值订单
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util } = event;
|
||||||
|
let { vk, db, _ } = util;
|
||||||
|
const { points, price } = data; // points=积分数量 price=价格(分→元)
|
||||||
|
|
||||||
|
const amount = price / 100; // 分转元
|
||||||
|
const offAmount = amount;
|
||||||
|
const orderId = await vk.baseDao.add({
|
||||||
|
dbName: "jjx-order",
|
||||||
|
dataJson: {
|
||||||
|
orderNo: vk.pubfn.createOrderNo("39XU", 20),
|
||||||
|
uid: userInfo.uid,
|
||||||
|
orderStatus: 0,
|
||||||
|
type: 20, // 20=积分充值
|
||||||
|
amount,
|
||||||
|
offAmount,
|
||||||
|
saveAmount: 0,
|
||||||
|
offRate: 100,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await vk.baseDao.add({
|
||||||
|
dbName: "jjx-order-goods",
|
||||||
|
dataJson: {
|
||||||
|
orderId,
|
||||||
|
goodsId: 'points_recharge',
|
||||||
|
num: 1,
|
||||||
|
goodsName: `积分充值_${points}积分`,
|
||||||
|
goodsPrice: amount,
|
||||||
|
goodsCover: null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 0, msg: 'ok', data: orderId };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//扣积分(合同审查用)
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util } = event;
|
||||||
|
let { db, _ } = util;
|
||||||
|
const { amount, reason, biz_type, biz_id } = data;
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
return { code: -1, msg: '扣减数量无效' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查余额
|
||||||
|
const userRes = await db.collection('jjx-user-points').doc(userInfo.uid).get();
|
||||||
|
if (!userRes.data.length) {
|
||||||
|
return { code: -1, msg: '积分账户不存在,请先获取积分' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBalance = userRes.data[0].balance;
|
||||||
|
if (currentBalance < amount) {
|
||||||
|
return { code: -1, msg: '积分不足', data: { balance: currentBalance, needed: amount } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBalance = currentBalance - amount;
|
||||||
|
|
||||||
|
// 扣减余额
|
||||||
|
await db.collection('jjx-user-points').doc(userInfo.uid).update({
|
||||||
|
balance: newBalance,
|
||||||
|
total_spent: _.inc(amount),
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 记录流水
|
||||||
|
await db.collection('jjx-points-log').add({
|
||||||
|
user_id: userInfo.uid,
|
||||||
|
type: 'spend',
|
||||||
|
amount: -amount,
|
||||||
|
balance_after: newBalance,
|
||||||
|
reason: reason || '积分消耗',
|
||||||
|
biz_type: biz_type || 'other',
|
||||||
|
biz_id: biz_id || '',
|
||||||
|
_add_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 0, msg: 'ok', data: { balance: newBalance } };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// 获取积分余额
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util } = event;
|
||||||
|
let { db, _ } = util;
|
||||||
|
//默认积分500
|
||||||
|
let amount = 500;
|
||||||
|
|
||||||
|
const res = await db.collection('jjx-user-points').doc(userInfo.uid).get();
|
||||||
|
|
||||||
|
if (res.data.length === 0) {
|
||||||
|
// 新用户,自动赠送 500 积分
|
||||||
|
await db.collection('jjx-user-points').add({
|
||||||
|
_id: userInfo.uid,
|
||||||
|
balance: amount,
|
||||||
|
total_earned: amount,
|
||||||
|
total_spent: 0,
|
||||||
|
gift: amount,
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.collection('jjx-points-log').add({
|
||||||
|
user_id: userInfo.uid,
|
||||||
|
type: 'earn',
|
||||||
|
amount: amount,
|
||||||
|
balance_after: amount,
|
||||||
|
reason: '新用户注册赠送',
|
||||||
|
biz_type: 'gift',
|
||||||
|
_add_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 0, msg: 'ok', data: { balance: amount, total_earned: amount, total_spent: 0, gift: amount } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code: 0, msg: 'ok', data: res.data[0] };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//查询积分流水
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util } = event;
|
||||||
|
let { vk, db, _ } = util;
|
||||||
|
const { pageIndex = 1, pageSize = 50, biz_type } = data;
|
||||||
|
|
||||||
|
console.log('getLog uid:', userInfo.uid);
|
||||||
|
|
||||||
|
// 先查全部看看有没有数据
|
||||||
|
const allRes = await db.collection('jjx-points-log').limit(5).get();
|
||||||
|
console.log('getLog all data count:', allRes.data.length);
|
||||||
|
if (allRes.data.length > 0) {
|
||||||
|
console.log('getLog first record:', JSON.stringify(allRes.data[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const where = { user_id: userInfo.uid };
|
||||||
|
if (biz_type) where.biz_type = biz_type;
|
||||||
|
|
||||||
|
const countRes = await db.collection('jjx-points-log').where(where).count();
|
||||||
|
console.log('getLog filtered count:', countRes.total);
|
||||||
|
|
||||||
|
const listRes = await db.collection('jjx-points-log')
|
||||||
|
.where(where)
|
||||||
|
.orderBy('_add_time', 'desc')
|
||||||
|
.skip((pageIndex - 1) * pageSize)
|
||||||
|
.limit(pageSize)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
msg: 'ok',
|
||||||
|
data: {
|
||||||
|
total: countRes.total,
|
||||||
|
rows: listRes.data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//积分充值
|
||||||
|
module.exports = {
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util } = event;
|
||||||
|
let { db, _ } = util;
|
||||||
|
const { order_id } = data;
|
||||||
|
|
||||||
|
if (!order_id) {
|
||||||
|
return { code: -1, msg: '缺少订单ID' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查订单
|
||||||
|
const orderRes = await db.collection('vk-pay-orders').doc(order_id).get();
|
||||||
|
if (!orderRes.data.length) {
|
||||||
|
return { code: -1, msg: '订单不存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = orderRes.data[0];
|
||||||
|
if (order.status !== 1) {
|
||||||
|
return { code: -1, msg: '订单未支付' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析充值积分数量
|
||||||
|
const attachedData = JSON.parse(order.attached_data || '{}');
|
||||||
|
const points = attachedData.points;
|
||||||
|
if (!points) {
|
||||||
|
return { code: -1, msg: '订单数据异常' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加余额
|
||||||
|
await db.collection('jjx-user-points').doc(userInfo.uid).update({
|
||||||
|
balance: _.inc(points),
|
||||||
|
total_earned: _.inc(points),
|
||||||
|
update_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 查最新余额
|
||||||
|
const userRes = await db.collection('jjx-user-points').doc(userInfo.uid).get();
|
||||||
|
const balance = userRes.data[0].balance;
|
||||||
|
|
||||||
|
// 记录流水
|
||||||
|
await db.collection('jjx-points-log').add({
|
||||||
|
user_id: userInfo.uid,
|
||||||
|
type: 'earn',
|
||||||
|
amount: points,
|
||||||
|
balance_after: balance,
|
||||||
|
reason: '积分充值(' + (order.total_fee / 100) + '元)',
|
||||||
|
biz_type: 'recharge',
|
||||||
|
biz_id: order_id,
|
||||||
|
_add_time: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 0, msg: 'ok', data: { balance, points_added: points } };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* 此函数名称
|
||||||
|
* @url template/test/sys/test 前端调用的url参数地址
|
||||||
|
* data 请求参数 说明
|
||||||
|
* res 返回参数说明
|
||||||
|
* @params {Number} code 错误码,0表示成功
|
||||||
|
* @params {String} msg 详细信息
|
||||||
|
*/
|
||||||
|
main: async (event) => {
|
||||||
|
let { data = {}, userInfo, util, originalParam } = event;
|
||||||
|
let { uniID, config, pubFun, vk , db, _ } = util;
|
||||||
|
let { uid } = data;
|
||||||
|
let res = { code : 0, msg : '' };
|
||||||
|
// 业务逻辑开始-----------------------------------------------------------
|
||||||
|
res.msg = "ok";
|
||||||
|
// 业务逻辑结束-----------------------------------------------------------
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"bsonType":"object","required":[],"properties":{"_id":{"description":"记录ID,系统自动生成","bsonType":"string"},"user_id":{"bsonType":"string","title":"用户ID","description":"关联 uni-id-users._id","foreignKey":"uni-id-users._id"},"type":{"bsonType":"string","title":"变动类型","description":"earn=获得 spend=消耗","enum":[{"text":"获得","value":"earn"},{"text":"消耗","value":"spend"}]},"amount":{"bsonType":"number","title":"变动数量","description":"正数为获得,负数为消耗"},"balance_after":{"bsonType":"number","title":"变动后余额","description":"本次变动后的积分余额"},"reason":{"bsonType":"string","title":"变动原因","description":"如:新用户赠送、积分充值、合同审查消耗","trim":"both"},"biz_type":{"bsonType":"string","title":"业务类型","description":"关联的业务类型","enum":[{"text":"赠送","value":"gift"},{"text":"充值","value":"recharge"},{"text":"合同审查","value":"contract_review"},{"text":"签到","value":"checkin"},{"text":"其他","value":"other"}]},"biz_id":{"bsonType":"string","title":"业务ID","description":"关联的业务记录ID(如订单ID)"},"_add_time":{"bsonType":"date","title":"创建时间","description":"记录创建时间,系统自动填充"}}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"bsonType":"object","required":[],"properties":{"_id":{"description":"记录ID,系统自动生成","bsonType":"string"},"user_id":{"bsonType":"string","title":"用户ID","description":"关联 uni-id-users._id","foreignKey":"uni-id-users._id"},"file_name":{"bsonType":"string","title":"文件名","description":"上传的合同文件名","trim":"both"},"file_size":{"bsonType":"number","title":"文件大小","description":"文件大小(字节)"},"file_id":{"bsonType":"string","title":"扣子文件ID","description":"上传到扣子后的 file_id"},"file_url":{"bsonType":"string","title":"文件预览地址","description":"uniCloud 云存储临时链接,用于查看\/下载原文件"},"html_url":{"bsonType":"string","title":"HTML报告地址","description":"精美 HTML 审查报告链接"},"pdf_url":{"bsonType":"string","title":"PDF报告地址","description":"PDF 审查报告下载链接"},"contract_info":{"bsonType":"object","title":"合同结构化信息","description":"AI 返回的合同名称、风险等级、风险统计等"},"chat_id":{"bsonType":"string","title":"对话ID","description":"扣子对话 chat_id"},"conversation_id":{"bsonType":"string","title":"会话ID","description":"扣子会话 conversation_id"},"status":{"bsonType":"string","title":"审查状态","description":"uploaded=待审查 reviewing=等待处理 processing=审查中 completed=已完成 failed=失败","enum":[{"text":"待审查","value":"uploaded"},{"text":"等待处理","value":"reviewing"},{"text":"审查中","value":"processing"},{"text":"已完成","value":"completed"},{"text":"失败","value":"failed"}]},"result":{"bsonType":"string","title":"审查结果","description":"AI 返回的审查结果内容"},"points_used":{"bsonType":"number","title":"消耗积分","description":"本次审查消耗的积分数量(token_count \/ 100)"},"token_count":{"bsonType":"number","title":"Token消耗","description":"扣子返回的 token 总消耗"},"_add_time":{"bsonType":"date","title":"创建时间","description":"记录创建时间"},"update_time":{"bsonType":"date","title":"更新时间","description":"最后更新时间"}}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"bsonType":"object","required":[],"properties":{"_id":{"description":"用户ID(与 uni-id-users._id 对应)","bsonType":"string"},"balance":{"bsonType":"number","title":"积分余额","description":"当前可用积分","defaultValue":0,"minimum":0},"total_earned":{"bsonType":"number","title":"累计获得","description":"累计获得的积分总数","defaultValue":0},"total_spent":{"bsonType":"number","title":"累计消耗","description":"累计消耗的积分总数","defaultValue":0},"gift":{"bsonType":"number","title":"赠送积分","description":"新用户赠送的积分数量","defaultValue":500},"update_time":{"bsonType":"date","title":"更新时间","description":"最后更新时间"}}}
|
||||||
Reference in New Issue
Block a user