Compare commits

..

2 Commits

90 changed files with 1356 additions and 410 deletions
+4 -4
View File
@@ -12,10 +12,10 @@
<view class="bubble">
您好!我是金久信签约宝AI智能客服,可以帮您解答金久信签约宝使用过程中注册、操作、适用场景等问题。请问有什么可以帮您?
<view class="quick-btns">
<view class="quick-btn" @click="chatQuickSend('注册准备资料事项')">注册准备资料事项</view>
<view class="quick-btn" @click="chatQuickSend('关键字无法识别')">关键字无法识别</view>
<view class="quick-btn" @click="chatQuickSend('如何下询价单')">金久信签约宝法律效力</view>
<view class="quick-btn" @click="chatQuickSend('电子签约不适用场景')">电子签约不适用场景</view>
<view class="quick-btn" @click="quickSend('注册准备资料事项')">注册准备资料事项</view>
<view class="quick-btn" @click="quickSend('关键字无法识别')">关键字无法识别</view>
<view class="quick-btn" @click="quickSend('金久信签约宝法律效力')">金久信签约宝法律效力</view>
<view class="quick-btn" @click="quickSend('电子签约不适用场景')">电子签约不适用场景</view>
</view>
</view>
</view>
+248 -171
View File
@@ -502,22 +502,27 @@
<!-- 合同审查 -->
<view v-if="menuIndex === '4'" class="review-container">
<view class="review-title">📋 AI 合同审查</view>
<!-- 积分提示 -->
<view class="points-notice">
<span class="pn-icon">💎</span>
<view class="pn-text">
<view class="pn-title">合同审查消耗积分</view>
<view class="pn-desc">每页消耗 60 积分,上传后自动计算。</view>
</view>
<view class="pn-balance">余额:{{ pointsBalance }} 分</view>
</view>
<!-- 上传区 -->
<view v-if="!reviewFile" class="upload-area" @click="selectReviewFile">
<view class="upload-icon">📤</view>
<view class="upload-title">上传合同文件</view>
<view class="upload-desc">支持 PDF / Word / 图片,最大 20MB</view>
<view class="upload-btn">选择文件上传</view>
<!-- 积分提示 -->
<view class="points-notice">
<span class="pn-icon">💎</span>
<view class="pn-text">
<view class="pn-title">合同审查消耗积分</view>
<view class="pn-desc">根据文件大小自动计算,每 100KB 约 150 积分。审查后余额需保留至少 200 积分。</view>
</view>
<view class="pn-balance">余额:{{ pointsBalance }} 分</view>
</view>
<!-- 免责声明 -->
<view class="disclaimer-notice">
<span class="dn-icon">⚠️</span>
<view class="dn-text">请确保合同内容清晰可见。如因合同不清晰导致分析失败,平台不承担后果,积分正常消耗。</view>
</view>
<!-- 上传区 -->
<view v-if="!reviewFile" class="upload-area" @click="selectReviewFile">
<view class="upload-icon">📤</view>
<view class="upload-title">上传合同文件</view>
<view class="upload-desc">仅支持 PDF / Word(.pdf .doc .docx),最大 20MB</view>
<view class="upload-btn">选择文件上传</view>
</view>
<!-- 上传中提示 -->
<view v-if="reviewUploading" style="text-align:center;padding:30px 0;">
<i class="el-icon-loading" style="font-size:32px;color:#a92924;"></i>
@@ -538,7 +543,7 @@
<view class="opt-icon">🔍</view>
<view class="opt-title">标准审查</view>
<view class="opt-desc">风险条款识别 · 合规性检查</view>
<view class="opt-points">{{ reviewPoints }} 积分</view>
<!-- <view class="opt-points">{{ reviewPoints }} 积分</view> -->
</view>
<!-- <view :class="['review-opt', reviewMode === 1 && 'selected']" @click="reviewMode = 1">
<view class="opt-icon">📊</view>
@@ -547,11 +552,17 @@
<view class="opt-points">{{ reviewPoints * 2 }} 积分</view>
</view> -->
</view>
<view style="text-align:center;margin-top:16px;">
<el-button type="primary" :loading="reviewLoading" @click="startReview">
{{ reviewLoading ? '审查中...' : '开始审查(' + (reviewMode === 0 ? reviewPoints : reviewPoints * 2) + ' 积分)' }}
</el-button>
</view>
<view style="text-align:center;margin-top:16px;">
<el-button type="primary" :loading="reviewLoading"
:disabled="pointsBalance < reviewPoints + 200"
@click="startReview">
{{ reviewLoading ? '审查中...' : '开始审查' }}
</el-button>
<p v-if="pointsBalance < reviewPoints + 200" style="font-size:12px;color:#f56c6c;margin-top:6px;">
⚠️ 积分不足,当前余额 {{ pointsBalance }},需要 {{ reviewPoints }} 积分且审查后余额需保留至少 200 积分。
<span style="color:#a92924;cursor:pointer;text-decoration:underline;" @click="$refs.aiChat ? null : null">去充值</span>
</p>
</view>
</view>
<!-- 审查中 -->
<view v-if="reviewLoading" style="text-align:center;padding:40px 0;">
@@ -560,34 +571,49 @@
<el-progress :percentage="reviewProgress" :stroke-width="8"
style="max-width:400px;margin:0 auto;"></el-progress>
</view>
<!-- 审查结果 -->
<view v-if="reviewResult">
<!-- 审查结果 -->
<view v-if="reviewResult">
<view class="result-header">
<h3>📋 审查结果 — {{ reviewFile.name }}</h3>
<span class="result-status">✅ 审查完成</span>
<h3>📋 {{ reviewStreaming ? 'AI 正在分析...' : '审查结果' }} — {{ reviewFile.name }}</h3>
<span class="result-status">{{ reviewStreaming ? '⏳ 分析中...' : '✅ 审查完成' }}</span>
</view>
<view class="result-body">
<view class="result-body" :style="reviewStreaming ? 'max-height:400px;overflow-y:auto;' : ''">
<div v-html="reviewResult"></div>
<div v-if="reviewStreaming" style="margin-top:8px;">
<span style="display:inline-block;width:8px;height:8px;background:#a92924;border-radius:50%;animation:pulse 1s infinite;"></span>
<span style="font-size:12px;color:#999;margin-left:6px;">AI 正在输出分析结果...</span>
</div>
</view>
<view class="result-footer">
<el-button type="primary" size="small" @click="resetReview">重新审查</el-button>
</view>
<view class="result-footer">
<el-button v-if="reviewHtmlUrl" type="success" size="small" @click="openUrl(reviewHtmlUrl)">📄 查看完整 HTML 报告</el-button>
<el-button size="small" @click="resetReview">重新审查</el-button>
</view>
</view>
</view>
<!-- 我的报告 -->
<view v-if="menuIndex === '5'" class="review-container">
<view class="review-title">📈 我的报告</view>
<view class="review-title">
📈 我的报告
<el-button size="mini" type="primary" style="margin-left:12px;" @click="queryReports()">🔍 查询报告</el-button>
</view>
<view v-if="reportList.length === 0" style="text-align:center;padding:60px 0;color:#999;">
<div style="font-size:48px;margin-bottom:12px;">📋</div>
暂无审查报告
暂无审查报告,点击上方「查询报告」从服务器加载
</view>
<view v-for="(r, i) in reportList" :key="i" class="report-item">
<view class="report-info">
<view class="report-name">{{ r.fileName }}</view>
<view class="report-time">{{ r.time }}</view>
<view class="report-name">📄 {{ r.fileName }}</view>
<view class="report-time">{{ r.time }} · 消耗 {{ Math.abs(r.amount) }} 积分</view>
</view>
<el-button size="mini" type="text" @click="viewReport(r)">查看</el-button>
<el-tag v-if="r.status === 'uploaded'" size="mini" type="info">待审查</el-tag>
<el-tag v-else-if="r.status === 'reviewing'" size="mini" type="warning">排队中</el-tag>
<el-tag v-else-if="r.status === 'processing'" size="mini" type="warning">审查中</el-tag>
<el-tag v-else-if="r.status === 'completed'" size="mini" type="success">已完成</el-tag>
<el-tag v-else-if="r.status === 'failed'" size="mini" type="danger">失败</el-tag>
<el-button v-if="r.htmlUrl" size="mini" type="text" @click="openUrl(r.htmlUrl)">查看报告</el-button>
<el-button v-if="r.fileUrl" size="mini" type="text" @click="previewFile(r)">查看文件</el-button>
<el-button v-if="r.status === 'completed'" size="mini" type="text" @click="viewReport(r)">查看摘要</el-button>
</view>
</view>
@@ -625,12 +651,15 @@
<!-- 使用说明 -->
<view class="review-title" style="font-size:14px;margin-top:16px;">📖 积分使用说明</view>
<view style="font-size:13px;color:#666;line-height:2;margin-bottom:20px;">
<p>• <strong>标准审查</strong>:每页 60 积分 — 风险条款识别、合规性检查</p>
<p>• <strong>深度分析</strong>:标准审查 × 2 — 标准审查 + 估价参考 + 修改建议</p>
<p>• <strong>合同审查</strong>:每 100KB 约 150 积分(根据文件大小自动计算)</p>
<p>• <strong>AI 客服</strong>:免费 — 签约宝专属问答</p>
<p>• 新用户注册自动赠送 500 体验积分</p>
<p>• 积分长期有效,不过期</p>
</view>
<view class="review-title" style="font-size:14px;">
📜 积分明细
<el-button size="mini" type="text" style="margin-left:12px;" @click="getPointsLog()">🔍 查看</el-button>
</view>
<view class="review-title" style="font-size:14px;">📜 积分明细</view>
<el-table :data="pointsLogList" style="width:100%">
<el-table-column prop="_add_time_str" label="时间" width="180"></el-table-column>
<el-table-column label="类型" width="80">
@@ -844,6 +873,11 @@
// 合同审查数据
reviewFile: null,
reviewFileId: null,
reviewRecordId: null,
reviewHtmlUrl: '',
reviewPdfUrl: '',
reviewContractInfo: {},
reviewStreaming: false,
reviewUploading: false,
reviewMode: 0,
reviewPoints: 100,
@@ -854,34 +888,34 @@
// 积分充值
selectedRecharge: 1,
rechargePackages: [{
points: 500,
price: '30元',
points: 10000,
price: '10元',
bonus: ''
},
{
points: 1000,
points: 50000,
price: '50元',
bonus: '送100积分'
bonus: ''
},
{
points: 2000,
price: '98元',
bonus: '送300积分'
points: 100000,
price: '100元',
bonus: ''
},
{
points: 5000,
price: '198元',
bonus: '送1000积分'
points: 450000,
price: '450元',
bonus: ''
},
{
points: 10000,
price: '368元',
bonus: '送3000积分'
points: 1000000,
price: '1000元',
bonus: ''
},
{
points: 20000,
price: '688元',
bonus: '送8000积分'
points: 2000000,
price: '2000元',
bonus: ''
}
],
// 我的报告
@@ -1474,6 +1508,8 @@
this.userInfo = vk.getVuex('$user.userInfo')
this.getUserInfo()
this.getPointsBalance()
if (this.menuIndex === '6') this.getPointsLog();
if (this.menuIndex === '5') this.getReportList();
},
methods: {
dayjs,
@@ -1487,8 +1523,6 @@
this.isSignTips = false
// AI 智能服务菜单(3/4/5/6)不调 query,加载对应数据
if (e >= 3) {
if (e === 6) this.getPointsLog();
if (e === 5) this.getReportList();
if (e === 4 || e === 6) this.getPointsBalance();
return;
}
@@ -1640,6 +1674,43 @@
html = html.replace(/\n/g, '<br>');
return html;
},
openUrl(url) {
if (url) window.open(url, '_blank');
},
fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
},
formatReviewResult(text) {
if (!text) return '';
let html = text;
// 处理 markdown 标题 ## / ###
html = html.replace(/^###\s+(.+)$/gm, '<h4 style="margin:16px 0 8px;color:#333;">$1</h4>');
html = html.replace(/^##\s+(.+)$/gm, '<h3 style="margin:20px 0 10px;color:#222;">$1</h3>');
// 处理加粗
html = html.replace(/\*\*(.*?)\*\*/g, '<strong style="color:#a92924;">$1</strong>');
// 处理链接(PDF下载链接等)— 自动识别 http/https 链接
html = html.replace(/(https?:\/\/[^\s\)\]\"\<]+)/g, '<a href="$1" target="_blank" style="color:#a92924;text-decoration:underline;word-break:break-all;">$1</a>');
// 处理列表
html = html.replace(/^[\-\•]\s+(.+)$/gm, '<div style="padding-left:16px;margin:4px 0;">• $1</div>');
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="padding-left:16px;margin:4px 0;">$1. $2</div>');
// 处理表格行(简单的 | 分隔表格)
html = html.replace(/\|(.+)\|/g, function(match) {
const cells = match.split('|').filter(c => c.trim());
if (cells.length > 1) {
return '<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid #eee;">' +
cells.map(c => '<span style="flex:1;font-size:13px;">' + c.trim() + '</span>').join('') + '</div>';
}
return match;
});
// 换行
html = html.replace(/\n/g, '<br>');
return html;
},
// ===== 积分相关 =====
async getPointsBalance() {
try {
@@ -1663,36 +1734,64 @@
}
});
if (res.code === 0) {
this.pointsLogList = (res.data.rows || []).map(r => ({
...r,
_add_time_str: dayjs(r._add_time).format('YYYY-MM-DD HH:mm:ss')
this.pointsLogList = (res.data.rows || []).map(r => ({
...r,
_add_time_str: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm:ss')
}));
}
} catch (e) {
/* 静默 */ }
},
async getReportList() {
try {
const reports = JSON.parse(localStorage.getItem('jjx_review_reports') || '[]');
this.reportList = reports.map(r => ({
fileName: r.fileName,
fileSize: r.fileSize,
time: r.time,
amount: r.points,
result: r.result,
status: 'completed'
}));
} catch (e) { /* 静默 */ }
},
async queryReports() {
try {
const res = await vk.callFunction({
url: 'client/points/kh/getLog',
data: {
pageIndex: 1,
pageSize: 50,
biz_type: 'contract_review'
}
url: 'client/ai/pub/getMyReports',
data: { pageIndex: 1, pageSize: 50 }
});
if (res.code === 0) {
this.reportList = (res.data.rows || []).map(r => ({
fileName: r.reason ? r.reason.replace('合同审查 — ', '') : '未知文件',
time: dayjs(r._add_time).format('YYYY-MM-DD HH:mm:ss'),
bizId: r.biz_id
if (res.code === 0 && res.data && res.data.rows) {
this.reportList = res.data.rows.map(r => ({
fileName: r.file_name,
fileSize: r.file_size,
fileUrl: r.file_url || '',
htmlUrl: r.html_url || '',
time: dayjs(r._add_time && r._add_time['$date'] ? r._add_time['$date'] : r._add_time).format('YYYY-MM-DD HH:mm:ss'),
amount: r.points_used || 0,
result: r.result,
status: r.status,
recordId: r._id
}));
this.$message.success('加载了 ' + this.reportList.length + ' 条报告');
}
} catch (e) {
/* 静默 */ }
this.$message.error('查询失败');
}
},
viewReport(r) {
this.$message.info('查看报告:' + r.fileName);
// 跳转到合同审查页并显示该报告的结果
this.menuIndex = '4';
this.reviewResult = r.result || '暂无审查结果';
this.reviewFile = { name: r.fileName, size: r.fileSize || 0 };
},
previewFile(r) {
if (!r.fileUrl) {
this.$message.warning('文件链接不存在');
return;
}
// 新窗口打开文件(PDF直接预览,Word下载)
window.open(r.fileUrl, '_blank');
},
formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
@@ -1703,7 +1802,7 @@
selectReviewFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx,.jpg,.png';
input.accept = '.pdf,.doc,.docx';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
@@ -1725,17 +1824,19 @@
reader.readAsDataURL(file);
});
const res = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
file_base64: fileBase64,
file_name: file.name,
upload_only: true
}
});
const res = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
file_base64: fileBase64,
file_name: file.name,
file_size: file.size,
upload_only: true
}
});
if (res.code === 0 && res.data && res.data.file_id) {
this.reviewFileId = res.data.file_id;
this.reviewRecordId = res.data.record_id; // 保存待审查记录ID
this.$message.success('文件上传成功');
} else {
this.$message.error(res.msg || '文件上传失败');
@@ -1749,97 +1850,62 @@
};
input.click();
},
// 前端 startReview() 完整改造
async startReview() {
if (!this.reviewFileId) {
this.$message.warning('文件尚未上传完成,请稍候');
return;
}
const pointsNeeded = this.reviewMode === 0 ? this.reviewPoints : this.reviewPoints * 2;
if (this.pointsBalance < pointsNeeded) {
this.$message.warning('积分不足,当前余额 ' + this.pointsBalance + ',需要 ' + pointsNeeded);
return;
}
this.reviewLoading = true;
this.reviewProgress = 0;
// 模拟进度
const interval = setInterval(() => {
this.reviewProgress = Math.min(this.reviewProgress + Math.random() * 10, 95);
}, 800);
try {
// 1. 扣积分
const deductRes = await vk.callFunction({
url: 'client/points/kh/deduct',
data: {
amount: pointsNeeded,
reason: '合同审查 — ' + this.reviewFile.name,
biz_type: 'contract_review'
}
});
if (deductRes.code !== 0) {
this.$message.error(deductRes.msg || '扣积分失败');
this.reviewLoading = false;
clearInterval(interval);
return;
}
this.pointsBalance = deductRes.data.balance;
// 2. 调用合同审查(异步,立即返回)
const res = await vk.callFunction({
url: 'client/ai/pub/review',
data: { file_id: this.reviewFileId, file_name: this.reviewFile.name }
});
if (res.code !== 0) {
this.$message.error(res.msg || '审查发起失败');
this.reviewLoading = false;
clearInterval(interval);
return;
}
const chatId = res.data.chat_id;
const conversationId = res.data.conversation_id;
// 3. 轮询结果(每 3 秒查一次,最多 5 分钟)
let answer = '';
for (let i = 0; i < 100; i++) {
await new Promise(r => setTimeout(r, 3000));
this.reviewProgress = Math.min(this.reviewProgress + 1, 95);
const pollRes = await vk.callFunction({
url: 'client/ai/pub/getReviewResult',
data: { chat_id: chatId, conversation_id: conversationId }
});
if (pollRes.code === 0 && pollRes.data.status === 'completed') {
answer = pollRes.data.answer;
break;
}
}
clearInterval(interval);
this.reviewProgress = 100;
if (answer) {
this.reviewResult = answer;
this.getReportList();
this.$message.success('合同审查完成');
} else {
this.$message.error('审查超时,请稍后在「我的报告」中查看');
}
} catch (e) {
clearInterval(interval);
this.$message.error('审查异常,请稍后重试');
}
this.reviewLoading = false;
if (!this.reviewFileId) {
this.$message.warning('请先选择合同文件并等待上传完成');
return;
}
// 1. 检查积分
const balanceRes = await vk.callFunction({
url: 'client/points/kh/getBalance'
});
if (balanceRes.code !== 0 || !balanceRes.data || balanceRes.data.balance < 150) {
this.$message.warning('积分不足,请先充值');
return;
}
this.reviewLoading = true;
// 2. 发起审查(更新已有记录状态为 reviewing,用 selectReviewFile 时保存的 file_id)
const reviewRes = await vk.callFunction({
url: 'client/ai/pub/review',
data: {
record_id: this.reviewRecordId,
file_id: this.reviewFileId,
file_name: this.reviewFile.name,
file_size: this.reviewFile.size,
uid: this.userInfo.uid
}
});
this.reviewLoading = false;
if (reviewRes.code !== 0) {
this.$message.error(reviewRes.msg || '发起审查失败');
return;
}
// 3. 提示用户 + 跳转到我的报告
this.$alert(
'合同已提交审查,AI 正在分析中,预计 3-5 分钟完成。您可以在「我的报告」中查看审查结果。',
'审查已提交',
{
confirmButtonText: '查看我的报告',
callback: () => {
this.menuSelect('5');
}
}
);
// 重置上传区
this.resetReview();
},
resetReview() {
this.reviewFile = null;
this.reviewFileId = null;
this.reviewRecordId = null;
this.reviewUploading = false;
this.reviewResult = null;
this.reviewProgress = 0;
@@ -1848,12 +1914,12 @@
async doRecharge() {
const pkg = this.rechargePackages[this.selectedRecharge];
const priceMap = {
'30元': 3000,
'10元': 1000,
'50元': 5000,
'98元': 9800,
'198元': 19800,
'368元': 36800,
'688元': 1
'100元': 10000,
'450元': 45000,
'1000元': 100000,
'2000元': 200000
};
const price = priceMap[pkg.price] || 5000;
const pointsMatch = pkg.points;
@@ -2739,6 +2805,17 @@
gap: 8px;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
.disclaimer-notice {
display: flex; align-items: flex-start; gap: 8px;
background: #fff8e1; border: 1px solid #ffe082; border-radius: 8px;
padding: 12px 16px; margin-bottom: 20px; font-size: 12px; color: #e6a23c;
}
.dn-icon { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
.dn-text { line-height: 1.6; }
.points-notice {
display: flex;
align-items: center;
@@ -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,7 +23,7 @@
"concurrency": 1,
"memorySize": 512,
"path": "/http/router",
"timeout": 60,
"timeout": 600,
"triggers": [],
"keepRunningAfterReturn": false
},
@@ -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
}
};
}
};
@@ -1,47 +1,27 @@
// 云函数:client/ai/pub/getReviewResult
module.exports = {
main: async (event) => {
let { data = {} } = event;
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
const { chat_id, conversation_id } = data;
if (!chat_id || !conversation_id) return { code: -1, msg: '参数不完整' };
// 查询对话状态
const statusRes = await uniCloud.httpclient.request(
'https://api.coze.cn/v3/chat?chat_id=' + chat_id + '&conversation_id=' + conversation_id + '&bot_id=7689821072762011698',
{ method: 'GET', dataType: 'text', headers: { 'Authorization': 'Bearer ' + COZE_TOKEN }, timeout: 15000 }
);
let status = 'unknown';
try {
const parsed = JSON.parse(statusRes.data);
status = parsed.data && parsed.data.status;
} catch (e) {}
if (status !== 'completed') {
return { code: 0, data: { status } };
}
// 获取消息
const msgRes = await uniCloud.httpclient.request(
'https://api.coze.cn/v3/chat/message/list?chat_id=' + chat_id + '&conversation_id=' + conversation_id,
{ method: 'GET', dataType: 'text', headers: { 'Authorization': 'Bearer ' + COZE_TOKEN }, timeout: 15000 }
);
let answer = '';
try {
const parsed = JSON.parse(msgRes.data);
const messages = parsed.data;
if (Array.isArray(messages)) {
for (const m of messages) {
if (m.type === 'answer' && m.role === 'assistant') {
answer = m.content;
break;
}
}
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 || ''
}
} catch (e) {}
return { code: 0, data: { status: 'completed', answer } };
};
}
};
};
@@ -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 };
}
}
};
@@ -1,12 +1,131 @@
// 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:只上传文件(选文件时调用)
// ===== 模式1:上传文件 + 创建待审查记录 =====
if (data.upload_only && data.file_base64) {
const fileBuffer = Buffer.from(data.file_base64, 'base64');
const boundary = '----FormBoundary' + Date.now();
@@ -28,58 +147,106 @@ module.exports = {
'Content-Length': uploadBody.length.toString()
},
content: uploadBody,
timeout: 60000
timeout: 600000
});
if (uploadRes.data.code !== 0) {
return { code: -1, msg: '文件上传失败: ' + uploadRes.data.msg };
}
fileId = uploadRes.data.data.id;
return { code: 0, msg: 'ok', data: { file_id: fileId } };
// 同时上传到 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:发起审查(不等结果,立即返回 chat_id)
if (!fileId) {
return { code: -1, msg: '缺少 file_id' };
// ===== 模式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 || '审查失败' } };
}
}
const objectString = JSON.stringify([
{ type: 'file', file_id: fileId },
{ type: 'text', text: '请审查这份合同' }
]);
// 更新状态为审查中
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()
});
try {
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: (userInfo && userInfo.uid) || 'anonymous',
stream: false,
auto_save_history: true,
additional_messages: [{
role: 'user',
content: objectString,
content_type: 'object_string'
}]
}
});
const chatData = chatRes.data && chatRes.data.data;
return {
code: 0, msg: 'ok',
data: {
chat_id: chatData && chatData.id,
conversation_id: chatData && chatData.conversation_id
}
};
} catch (e) {
return { code: -1, msg: '审查发起失败: ' + e.message };
}
// ===== 立即返回,审查由定时任务触发 =====
return {
code: 0, msg: 'ok',
data: { record_id: recordId, status: 'reviewing' }
};
}
};
@@ -2,13 +2,23 @@
module.exports = {
main: async (event) => {
let { data = {}, userInfo, util } = event;
let { db, _ } = util;
const { pageIndex = 1, pageSize = 20, type } = data;
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 (type) where.type = type; // 可选筛选 earn/spend
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)
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
+1
View File
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
+1 -24
View File
@@ -1,24 +1 @@
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"status": {
"bsonType": "int",
"description": "状态 0待创建 1待签署 2已签署 -1已作废"
},
"partyList": {
"bsonType": "array",
"description": "当事人"
}
}
}
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -1,20 +1 @@
// 文档教程: https://uniapp.dcloud.net.cn/uniCloud/schema
{
"bsonType": "object",
"required": [],
"permission": {
"read": false,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID,系统自动生成"
},
"status": {
"bsonType": "int",
"description": "状态 0默认 1已被选择"
}
}
}
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -1,63 +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": "记录创建时间,系统自动填充"
}
}
}
{"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":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1,99 @@
{
"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":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -1,40 +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": "最后更新时间"
}
}
}
{"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":"最后更新时间"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}
@@ -0,0 +1 @@
{"bsonType":"object","required":[],"permission":{"read":false,"create":false,"update":false,"delete":false},"properties":{"_id":{"description":"ID,系统自动生成"}}}