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": {
|
||||
"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": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/crypt": {
|
||||
"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": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"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": {
|
||||
"version": "1.11.10",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
"version": "1.11.23",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
|
||||
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="
|
||||
},
|
||||
"node_modules/is-buffer": {
|
||||
"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": {
|
||||
"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": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-ensure": {
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.4.tgz",
|
||||
"integrity": "sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==",
|
||||
"dependencies": {
|
||||
"debug": "^3.1.0",
|
||||
"node-ensure": "^0.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.8.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/mehmet-kozan"
|
||||
}
|
||||
},
|
||||
"node_modules/uni-config-center": {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"concurrency": 1,
|
||||
"memorySize": 512,
|
||||
"path": "/http/router",
|
||||
"timeout": 60,
|
||||
"timeout": 600,
|
||||
"triggers": [],
|
||||
"keepRunningAfterReturn": false
|
||||
},
|
||||
|
||||
+28
-70
@@ -8,17 +8,15 @@ module.exports = {
|
||||
* @param {String} accNo 账号
|
||||
* @param {String} reqDate 请求日期,格式:yyyyMMdd
|
||||
* @param {String} reqJournalNo 请求流水号
|
||||
* @param {String} logId 可选,流水ID,传入后会回写状态
|
||||
*/
|
||||
main: async (event) => {
|
||||
let { data = {}, util } = event;
|
||||
let { vk } = util;
|
||||
let { accNo, reqDate, reqJournalNo, logId } = data;
|
||||
let { accNo, reqDate, reqJournalNo } = data;
|
||||
|
||||
accNo = String(accNo || '').trim();
|
||||
reqDate = String(reqDate || '').trim();
|
||||
reqJournalNo = String(reqJournalNo || '').trim();
|
||||
logId = String(logId || '').trim();
|
||||
|
||||
if (!accNo) {
|
||||
return {
|
||||
@@ -48,83 +46,43 @@ module.exports = {
|
||||
};
|
||||
}
|
||||
|
||||
let res = await balanceUtil.requestActionLog({
|
||||
vk,
|
||||
accNo,
|
||||
reqDate,
|
||||
reqJournalNo
|
||||
let res = await vk.request({
|
||||
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,
|
||||
reqDate,
|
||||
reqJournalNo
|
||||
},
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
if (res && typeof res === 'object' && typeof res.code === 'undefined') {
|
||||
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 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)) {
|
||||
responseObjects.data.status = status;
|
||||
responseObjects.data.statusText = statusText;
|
||||
responseObjects.data.logId = logId;
|
||||
responseObjects.data.list = list;
|
||||
} else if (res && typeof res === 'object') {
|
||||
res.data = {
|
||||
status,
|
||||
statusText,
|
||||
logId
|
||||
list
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
const { BALANCE_DOC_ID, getBalanceInfoList, getAvailableWithdrawBalance, normalizeAmount } = require('./util.js');
|
||||
const { BALANCE_DOC_ID, getBalanceInfoList } = require('./util.js');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -25,24 +25,6 @@ module.exports = {
|
||||
}
|
||||
|
||||
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 {
|
||||
code: 0,
|
||||
@@ -50,7 +32,7 @@ module.exports = {
|
||||
data: {
|
||||
balance: Number(Number(info.balance || 0).toFixed(2)),
|
||||
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)] || '';
|
||||
}
|
||||
|
||||
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({
|
||||
vk,
|
||||
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 = '' }) {
|
||||
const balanceInfo = await ensureBalanceRecord({ transaction, vk });
|
||||
const nextBalance = Number((Number(balanceInfo.balance || 0) + Number(delta || 0)).toFixed(2));
|
||||
@@ -473,13 +372,10 @@ module.exports = {
|
||||
parseOrderSet,
|
||||
getActionStatus,
|
||||
getActionStatusTextByStatus,
|
||||
applyMerchantBalanceDelta,
|
||||
syncMerchantBalance,
|
||||
ensureBalanceRecord,
|
||||
recalculateBalance,
|
||||
checkBalanceAvailable,
|
||||
getPendingWithdrawAmount,
|
||||
getAvailableWithdrawBalance,
|
||||
updateBalanceWithDelta,
|
||||
rollbackTransaction
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use strict';
|
||||
const dayjs = require('dayjs');
|
||||
const balanceUtil = require('./util.js');
|
||||
const merchantMap = balanceUtil.MERCHANT_MAP;
|
||||
|
||||
@@ -33,28 +34,11 @@ module.exports = {
|
||||
updateTime: ''
|
||||
};
|
||||
const merchantBalance = balanceUtil.normalizeAmount(merchantBalanceInfo.balance || 0);
|
||||
const {
|
||||
pendingAmount,
|
||||
availableBalance
|
||||
} = await balanceUtil.getAvailableWithdrawBalance({
|
||||
transaction,
|
||||
vk,
|
||||
merchantId,
|
||||
merchantBalance
|
||||
});
|
||||
if (availableBalance <= 0) {
|
||||
throw new Error(
|
||||
pendingAmount > 0
|
||||
? `可提现余额不足(处理中提现¥${pendingAmount.toFixed(2)}已占用)`
|
||||
: '当前企业余额小于等于0,无法提现'
|
||||
);
|
||||
if (merchantBalance <= 0) {
|
||||
throw new Error('当前企业余额小于等于0,无法提现');
|
||||
}
|
||||
if (amount > availableBalance) {
|
||||
throw new Error(
|
||||
pendingAmount > 0
|
||||
? `提现金额不能大于可提现余额¥${availableBalance.toFixed(2)}(已排除处理中提现¥${pendingAmount.toFixed(2)})`
|
||||
: '提现金额不能大于当前企业余额'
|
||||
);
|
||||
if (amount > merchantBalance) {
|
||||
throw new Error('提现金额不能大于当前企业余额');
|
||||
}
|
||||
const description = String(data.description || '').trim() || `余额提现-${merchantMap[merchantId]}`;
|
||||
|
||||
@@ -96,28 +80,52 @@ module.exports = {
|
||||
merchantId,
|
||||
merchantName: merchantMap[merchantId],
|
||||
srcReqId: requestRes.srcReqId,
|
||||
status: 1,
|
||||
// 新逻辑:处理中不扣余额,成功后再扣;用于兼容旧数据(旧单默认已扣)
|
||||
balanceDeducted: false
|
||||
status: 1
|
||||
};
|
||||
const changeAmount = balanceUtil.buildBalanceChange(logData);
|
||||
|
||||
await vk.baseDao.add({
|
||||
await balanceUtil.checkBalanceAvailable({
|
||||
transaction,
|
||||
vk,
|
||||
changeAmount
|
||||
});
|
||||
|
||||
const addRes = await vk.baseDao.add({
|
||||
db: transaction,
|
||||
dbName: 'jjx-balance-log',
|
||||
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();
|
||||
return {
|
||||
code: 0,
|
||||
msg: '',
|
||||
data: {
|
||||
balance: merchantBalance,
|
||||
pendingAmount: balanceUtil.normalizeAmount(pendingAmount + amount),
|
||||
availableBalance: balanceUtil.normalizeAmount(Math.max(0, availableBalance - amount)),
|
||||
merchantBalance
|
||||
balance: nextBalance,
|
||||
merchantBalance: nextMerchantBalance
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
@@ -25,7 +25,7 @@ module.exports = {
|
||||
}
|
||||
});
|
||||
|
||||
let answer = '暂时无法回复,请拨打 400-686-5559 咨询';
|
||||
let answer = '';
|
||||
let conversationId = '';
|
||||
const text = res.data || '';
|
||||
const lines = text.split('\n');
|
||||
@@ -37,10 +37,11 @@ module.exports = {
|
||||
const p = JSON.parse(d);
|
||||
if (p.conversation_id) conversationId = p.conversation_id;
|
||||
if (p.type === 'answer' && p.content) {
|
||||
answer = p.content;
|
||||
answer += p.content; // 拼接,不是覆盖
|
||||
}
|
||||
} catch (e) { /* 跳过 */ }
|
||||
}
|
||||
if (!answer) answer = '暂时无法回复,请拨打 400-686-5559 咨询';
|
||||
|
||||
return {
|
||||
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' }
|
||||
};
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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