Compare commits

...

15 Commits

Author SHA1 Message Date
b2894lxlx 683a9411fd feat: 签约提示/赠送次数/自动划付提醒,自营支付成功弹窗,隐藏微信登录
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 03:28:31 +08:00
ngele 85cddfbf8d fix: 积分明细云函数联表加username字段(与jjx-admin同步) 2026-10-23 21:41:43 +08:00
ngele 08b2888704 feat: 同步后台积分明细云函数(admin/points/sys/getList) 2026-10-23 21:20:20 +08:00
ngele 3b8f6c5718 fix: 积分充值订单类型名对齐索引20,修复资金流水显示「类型20」 2026-10-23 20:58:34 +08:00
ngele a227912705 fix: 报告页uid获取增加storage兜底(与wap端同步) 2026-10-23 19:23:21 +08:00
ngele 95024bb2a6 feat: AI审查报告支持下载Word + 我的报告支持重新审查
- 云函数jjxReviewTimer完成审查后自动将HTML报告转换为Word文档并上传
- 新增buildDocx.js纯正则解析HTML报告生成A4公文版式docx(三级数据提取兼容结构漂移)
- 兼容智能体插件直接返回docx链接(jjx-docx-link标记协议)
- router/jjxDoReview同步支持Word生成(try降级不影响主流程)
- 前端报告列表新增下载Word/重新审查按钮
- 修复上传/查询取真实uid(vk.getVuex而非页面本地data)
- login.js清理调试日志、.gitignore忽略node_modules与lock文件
2026-10-23 19:07:05 +08:00
ngele d0fb8802e7 chore: 更新 DCloud AppID 为 __UNI__CBB811F 2026-10-23 15:39:26 +08:00
ngele 16290ae5ac feat: 我的报告操作按钮文案调整,在线查看报告补 result 判断 2026-10-23 15:39:25 +08:00
ngele b680246a56 feat: 补齐 AI 智能服务模块(AI客服/合同审查/我的报告/积分管理)
从 jjx-web-sign-only 增量移植,jjx-web 原有逻辑零改动:
- 左侧菜单新增「AI 智能服务」分组
- 四个功能面板:AI 客服聊天、合同审查(上传+积分预估)、我的报告、积分管理
- data 新增 25 个字段、methods 新增 19 个方法、样式新增 467 行
- onShow 按 menuIndex 加载积分余额/报告/明细
- menuSelect 增加 e>=3 分支,AI 菜单不再触发合同列表查询
- 未带入 sign-only 专属 isSignOnly 参数
2026-10-19 20:07:20 +08:00
ngele a7d0e82f61 fix: 修复 AI 客服回复内容重复(区分 SSE delta/completed 事件,completed 仅作兜底) 2026-10-19 19:31:53 +08:00
b2894lxlx c6fea1cc0d fix: 调整支付申请与报价查看权限
去掉货到付款和发货凭证,尾款或全款时提示交易已完结;已购单据报价次数也可查看买家信息。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 14:20:42 +08:00
b2894lxlx 429513ad8d fix: 登录前补齐空的 dcloud_appid,避免被判成用户不存在
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 14:19:50 +08:00
ngele 631e3caa4c feat: 新增合同审查定时任务与积分体系,重构支付模块与余额流水查询 2026-10-14 21:55:23 +08:00
ngele 988e2e8853 Merge branch 'master' of https://git.websoft.top/b2894lxlx/jjx-web 2026-10-12 17:15:59 +08:00
ngele 5039968316 fix: AI客服回复内容格式化(换行+加粗+列表) 2026-10-12 17:12:19 +08:00
116 changed files with 9693 additions and 2026 deletions
+3 -72
View File
@@ -4,76 +4,7 @@
/uni_modules/
/.idea
/uniCloud-aliyun/cloudfunctions/router/node_modules/
/uniCloud-tcb/cloudfunctions/**/node_modules/
/uniCloud-tcb/cloudfunctions/**/package-lock.json
/pnpm-lock.yaml
/金久信源码/金久信数据库/fadada-verify-record.json
/金久信源码/金久信数据库/jjx-account.json
/金久信源码/金久信数据库/jjx-ad.json
/金久信源码/金久信数据库/jjx-album.json
/金久信源码/金久信数据库/jjx-article.json
/金久信源码/金久信数据库/jjx-article-category.json
/金久信源码/金久信数据库/jjx-article-collect.json
/金久信源码/金久信数据库/jjx-article-download.json
/金久信源码/金久信数据库/jjx-balance.json
/金久信源码/金久信数据库/jjx-balance-log.json
/金久信源码/金久信数据库/jjx-business-card.json
/金久信源码/金久信数据库/jjx-business-contact.json
/金久信源码/金久信数据库/jjx-cart.json
/金久信源码/金久信数据库/jjx-certificate.json
/金久信源码/金久信数据库/jjx-company-manage-change-log.json
/金久信源码/金久信数据库/jjx-contract.json
/金久信源码/金久信数据库/jjx-contract-template.json
/金久信源码/金久信数据库/jjx-coop-config.json
/金久信源码/金久信数据库/jjx-dict.json
/金久信源码/金久信数据库/jjx-enquiry-attributes.json
/金久信源码/金久信数据库/jjx-enquiry-category.json
/金久信源码/金久信数据库/jjx-enquiry-package.json
/金久信源码/金久信数据库/jjx-enquiry-quote.json
/金久信源码/金久信数据库/jjx-enquiry-sheet.json
/金久信源码/金久信数据库/jjx-enquiry-template.json
/金久信源码/金久信数据库/jjx-ensure-config.json
/金久信源码/金久信数据库/jjx-factory.json
/金久信源码/金久信数据库/jjx-goods.json
/金久信源码/金久信数据库/jjx-goods-category.json
/金久信源码/金久信数据库/jjx-goods-cooperate.json
/金久信源码/金久信数据库/jjx-industry.json
/金久信源码/金久信数据库/jjx-message.json
/金久信源码/金久信数据库/jjx-message-config.json
/金久信源码/金久信数据库/jjx-old.json
/金久信源码/金久信数据库/jjx-old-category.json
/金久信源码/金久信数据库/jjx-old-quote.json
/金久信源码/金久信数据库/jjx-order.json
/金久信源码/金久信数据库/jjx-order-goods.json
/金久信源码/金久信数据库/jjx-order-pay-list.json
/金久信源码/金久信数据库/jjx-order-selfProduct.json
/金久信源码/金久信数据库/jjx-other-article.json
/金久信源码/金久信数据库/jjx-other-article-category.json
/金久信源码/金久信数据库/jjx-process-category.json
/金久信源码/金久信数据库/jjx-process-quote.json
/金久信源码/金久信数据库/jjx-process-sheet.json
/金久信源码/金久信数据库/jjx-quote-num-config.json
/金久信源码/金久信数据库/jjx-qyb.json
/金久信源码/金久信数据库/jjx-self-support.json
/金久信源码/金久信数据库/jjx-self-support-quote-list.json
/金久信源码/金久信数据库/jjx-sign-pay_list.json
/金久信源码/金久信数据库/jjx-site-config.json
/金久信源码/金久信数据库/jjx-spot-ad.json
/金久信源码/金久信数据库/jjx-spot-amount.json
/金久信源码/金久信数据库/jjx-spot-category.json
/金久信源码/金久信数据库/jjx-spot-goods.json
/金久信源码/金久信数据库/jjx-steal-factory.json
/金久信源码/金久信数据库/jjx-user-address.json
/金久信源码/金久信数据库/jjx-user-article.json
/金久信源码/金久信数据库/jjx-user-contract.json
/金久信源码/金久信数据库/jjx-user-level.json
/金久信源码/金久信数据库/jjx-user-quote-num.json
/金久信源码/金久信数据库/opendb-admin-log.json
/金久信源码/金久信数据库/opendb-admin-menus.json
/金久信源码/金久信数据库/opendb-app-list.json
/金久信源码/金久信数据库/opendb-verify-codes.json
/金久信源码/金久信数据库/uni-id-log.json
/金久信源码/金久信数据库/uni-id-permissions.json
/金久信源码/金久信数据库/uni-id-roles.json
/金久信源码/金久信数据库/uni-id-users.json
/金久信源码/金久信数据库/vk-files.json
/金久信源码/金久信数据库/vk-files-categories.json
/金久信源码/金久信数据库/vk-global-data.json
/金久信源码/
+12 -6
View File
@@ -21,7 +21,7 @@
</view>
<!-- 对话消息 -->
<view v-for="(msg, i) in messages" :key="i" :id="'msg' + i" :class="['msg-item', msg.role]">
<view class="bubble" v-html="msg.content"></view>
<view class="bubble" v-html="formatContent(msg.content)"></view>
</view>
<view v-if="loading" class="msg-item assistant" id="msg-loading">
<view class="bubble typing">思考中...</view>
@@ -50,6 +50,14 @@
};
},
methods: {
formatContent(text) {
if (!text) return '';
let html = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/^[\-\•]\s+(.+)$/gm, '<div style="padding-left:12px;">• $1</div>');
html = html.replace(/^(\d+)\.\s+(.+)$/gm, '<div style="padding-left:12px;">$1. $2</div>');
html = html.replace(/\n/g, '<br>');
return html;
},
toggle() {
this.visible = !this.visible;
if (this.visible) {
@@ -73,12 +81,10 @@
try {
const res = await this.vk.callFunction({
url: 'client/ai/pub/chat',
data: {
message: msg,
conversation_id: this.conversationId
}
url: 'client/ai/pub/chat',
data: { message: msg, bot_id: '7680345813062680618' }
});
if (res.data.conversation_id) this.conversationId = res.data.conversation_id;
this.messages.push({
role: 'assistant',
+9
View File
@@ -160,6 +160,15 @@ export default {
}).then(() => {
vk.userCenter.logout({
success: (data) => {
try {
if (typeof sessionStorage !== 'undefined') {
Object.keys(sessionStorage).forEach((key) => {
if (key.indexOf('notaryAutoTransferTipShown_') === 0) {
sessionStorage.removeItem(key)
}
})
}
} catch (e) {}
// 退出登录成功后的逻辑
uni.reLaunch({
url: '/'
+2 -2
View File
@@ -26,8 +26,8 @@
<meta name="description" content="金久信签约宝专注线上电子签约,金久信签约宝会员服务.致力于为您提供一站式金久信签约宝会员服务.">
<!--preload-links-->
<!--app-context-->
<!-- 配置H5的 web图标static/logo.png -->
<link rel="icon" href="./static/logo.png"/>
<!-- 配置H5的 web图标 -->
<link rel="icon" type="image/png" href="./static/favicon.png"/>
<script type="text/javascript" src="http://qzonestyle.gtimg.cn/qzone/openapi/qc_loader.js" data-appid="101415280"
data-redirecturi="http://127.0.0.1:8080/"
charset="utf-8"></script>
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name" : "jinjiuxin-mobile",
"appid" : "__UNI__8BFD66E",
"appid" : "__UNI__CBB811F",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"sassImplementationName": "node-sass",
"sassImplementationName" : "node-sass",
"app-plus" : {
"usingComponents" : true,
"nvueCompiler" : "uni-app",
+2 -3
View File
@@ -92,13 +92,12 @@
</a>
</div>
<!-- 暂时隐藏微信登录
<div class=" threeBorder"><span>第三方登录</span></div>
<div class="input-groupwpg clear">
<a href="javascript:;" @click="toWechat" class="dsf-wetChat" title="微信"><i></i></a>
<!-- <a href="javascript:;" @click="toQQ" class="dsf-qq" title="QQ"><i></i></a>-->
<!-- <a href="" class="dsf-dx" title="短信"><i></i></a>-->
</div>
-->
</div>
</div>
</div>
+2 -3
View File
@@ -98,13 +98,12 @@
<navigator url="/pages/auth/login/login">已有账号?<span
style="color: #a92924">去登录</span></navigator>
</div>
<!-- 暂时隐藏微信登录
<div class=" threeBorder"><span>第三方登录</span></div>
<div class="input-groupwpg clear">
<a href="javascript:;" @click="toWechat" class="dsf-wetChat" title="微信"><i></i></a>
<!-- <a href="javascript:;" @click="toQQ" class="dsf-qq" title="QQ"><i></i></a>-->
<!-- <a href="" class="dsf-dx" title="短信"><i></i></a>-->
</div>
-->
</div>
</div>
</div>
+1290 -21
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -629,7 +629,7 @@ export default {
},
openQuoteServiceFeeDialog() {
const rate = this.sheet.notaryServiceRate || 0.1
return this.$confirm(`提示着您成功签约交易,平台将收取总交易金额 ${rate}%服务费,如您存在利用系统漏洞恶意逃避支付服务费的情况,平台有将您的失信行为进行公布,情况严重的平台予以劝退。
return this.$confirm(`若您成功签约交易,平台将收取总交易金额 ${rate}%服务费,如您存在利用系统漏洞恶意逃避支付服务费的情况,平台有将您的失信行为进行公布,情况严重的平台予以劝退。
并有权依法追回平台服务费。您承诺此次交易为合法经营,如违反相关法律法规您将承担全部任与平台无关。您已经完全理解并知悉,您不同意请点击'取消',取消报价,您同意以上约定请点击'同意并知悉'。`, '重要提示', {
confirmButtonText: '同意并知悉',
cancelButtonText: '取消',
+26
View File
@@ -111,6 +111,7 @@ export default {
},
enquiryId: null,
reloaded: false,
sheetQuoteNum: 0,
// 表单相关开始-----------------------------------------------------------
form1: {
// 表单请求数据,此处可以设置默认值
@@ -175,6 +176,7 @@ export default {
this.enquiryId = enquiryId
this.form1.data.sheetId = enquiryId
this.userInfo = vk.getVuex('$user.userInfo') || {userLevel: 0}
this.refreshUserQuotePrivilege()
this.getEnquiryDetail()
},
computed: {
@@ -184,6 +186,7 @@ export default {
canViewBuyerInfo() {
const remainQuoteNum = Number(this.userInfo && this.userInfo.remainQuoteNum) || 0
const sheetQuoteNum = Number(this.sheetQuoteNum) || 0
const quoteExpiredAt = this.userInfo && this.userInfo.quoteExpiredAt
const monthlyQuoteActive = !!(quoteExpiredAt && dayjs(quoteExpiredAt).isAfter(dayjs()))
return this.isLoggedIn && (
@@ -191,6 +194,7 @@ export default {
this.sheet.hasQuote ||
Number(this.userInfo.userLevel) > 0 ||
remainQuoteNum > 0 ||
sheetQuoteNum > 0 ||
monthlyQuoteActive
)
},
@@ -245,6 +249,27 @@ export default {
}
},
async refreshUserQuotePrivilege() {
if (!this.isLoggedIn) return
try {
const {userInfo} = await vk.userCenter.getCurrentUserInfo()
this.userInfo = userInfo || {userLevel: 0}
let sheetQuoteNum = 0
if (this.enquiryId) {
const res = await vk.callFunction({
url: 'client/user/kh/getUserQuoteNumList',
data: {
type: 'enquiry',
sheetId: this.enquiryId,
},
})
if (res && res.data && res.data.num) sheetQuoteNum = Number(res.data.num) || 0
}
this.sheetQuoteNum = sheetQuoteNum
} catch (e) {
// ignore
}
},
// 提交报价单
async submit() {
@@ -308,6 +333,7 @@ export default {
}
},
onShow() {
this.refreshUserQuotePrivilege()
this.reloaded = false
setTimeout(() => {
this.reloaded = true
+2 -7
View File
@@ -227,13 +227,8 @@
</el-radio-group>
</view>
</view>
<view class="text-25 text-main mb-20 flex justify-start items-start">
<p>重要提示:</p>
<view class="flex flex-col justify-start items-start">
<p>1.系统不支持个人用户操作,请使用企业或个体户进行交易或选择自主交易;</p>
<p class="my-15">2.企业用户需平台内进行电子签约,您也可线下双方补充纸质合同:</p>
<p>3.购销双方签约后需向平台银联监管账户支付全部货款您选择即为确认并符合以上条件。</p>
</view>
<view class="text-25 text-main mb-20">
重要提示:在自主交易模式下,本平台仅承担信息交互服务职能,仅确保入驻本平台的交易双方已完成实名认证;对于交易的真实性及交易过程中的任一环节,本平台均不承担责任,请用户完成风险评估与风险尽职调查后,审慎开展交易。
</view>
<view class="form-title-wrap">
+41 -15
View File
@@ -116,7 +116,6 @@
<div class="mod-inner2Listcenter clearfix sliper" style="margin-top: 0">
<ul class="clearfix flex justify-start items-start flex-wrap">
<li v-for="(item, index) in enquirySheetData" class="inli-mask" :key="index"
style="height: 200px"
@click="vk.navigateTo('/pages/enquiry/detail?id=' + item._id);">
<a href="javascript:void(0)" style="display: block;" rel="nofollow">
<div class="detail">
@@ -127,7 +126,7 @@
<div class="text-30 py-10 text-black line-1">
<span>{{ item.title.split('-')[0] }}</span>
</div>
<div class="text-30 py-10 text-black line-1">
<div class="text-30 py-10 text-black line-1" v-if="item.title.split('-')[1]">
<span>{{ item.title.split('-')[1] }}</span>
</div>
<div class="text-30 py-10 text-black line-1">
@@ -135,7 +134,7 @@
item.productList.map(product => (product.procureNumber ? product.procureNumber : '') + (product.procureUnit ? product.procureUnit : ''))[0]
}}</span>
</div>
<div class="text-25 mt-25">
<div class="text-25 mt-25 line-1">
<span>{{ item.fullAddress }}</span>
</div>
<div class="detail-num flex flex-col justify-start items-start">
@@ -575,7 +574,7 @@
<div slot="header" class="clearfix pointer">
<span @click="navTo(`/pages/otherArticle/list?id=${item.id}`)">{{ item.title }}</span>
</div>
<div class="news-list" style="height: 140px">
<div class="news-list news-list--zx">
<el-link @click="navTo(`/pages/otherArticle/detail?id=${article._id}`)"
v-for="(article, articleIndex) in item.list" :key="articleIndex"
:underline="false">
@@ -757,21 +756,31 @@ export default {
vk.navigateTo(path);
},
async checkExpiringNotaryPayList() {
if (!vk.getVuex('$user.userInfo._id')) return
const userId = vk.getVuex('$user.userInfo._id')
if (!userId) return
const tipKey = `notaryAutoTransferTipShown_${userId}`
// 同一登录会话只提示一次(退出登录会清除)
try {
if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(tipKey) === '1') return
} catch (e) {}
try {
const res = await vk.callFunction({
url: 'client/order/kh/getExpiringNotaryPayList',
loading: false,
})
const count = res.data && res.data.count ? res.data.count : 0
if (!count) return
this.$confirm(`您有${count}笔付款申请将在24小时内自动划付,请及时处理。`, '付款申请即将到期', {
confirmButtonText: '立即处理',
cancelButtonText: '稍后处理',
showCancelButton: true,
const content = res.data && res.data.content ? res.data.content : ''
if (!count || !content) return
try {
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(tipKey, '1')
} catch (e) {}
this.$alert(content, '重要提示', {
confirmButtonText: '悉知同意',
dangerouslyUseHTMLString: false,
type: 'warning',
}).then(() => {
vk.navigateTo({url: '/pages/order/order?menuIndex=1'})
closeOnClickModal: false,
closeOnPressEscape: false,
showClose: false,
}).catch(() => {})
} catch (err) {
console.error('checkExpiringNotaryPayList error:', err)
@@ -1031,9 +1040,20 @@ export default {
margin: 0 auto;
.inli-mask {
padding: 10px;
height: 400rpx;
flex: 0 0 232px
padding: 10px 10px 16px;
min-height: 200px;
height: auto;
box-sizing: border-box;
flex: 0 0 232px;
overflow: visible;
.detail-num {
margin-top: 8px;
}
.time {
margin-top: 4px;
}
}
}
@@ -1269,6 +1289,12 @@ export default {
flex-direction: column;
align-items: self-start;
line-height: 1.8em;
&--zx {
min-height: 140px;
padding-bottom: 16px;
box-sizing: border-box;
}
}
.news-card {
+5
View File
@@ -325,6 +325,11 @@ export default {
vk.redirectTo({
url: '/pages/contract/index?tab=sign',
})
} else if (this.info.type === 1 || this.info.type === 14) {
// 金久信自营:返回订单页后展示提交成功弹窗
vk.redirectTo({
url: '/pages/order/order?menuIndex=1&selfSupportPaySuccess=1',
})
} else {
vk.redirectTo({
url: '/pages/order/order?menuIndex=1',
+25 -71
View File
@@ -31,33 +31,9 @@
<el-form-item label="备注">
<el-input v-model="payInfo.remark"/>
</el-form-item>
<template v-if="[0, 1].includes(orderInfo.orderStatus)">
<el-row :gutter="10">
<el-col :span="6">
<el-form-item label="是否发货">
<el-radio-group v-model="payInfo.isExpress"
:disabled="['货到付款', '全款'].includes(payInfo.type)">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item label="物流公司" required>-->
<!-- <el-input v-model="payInfo.expressName" placeholder="请输入物流公司"/>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="物流单号" required>-->
<!-- <el-input v-model="payInfo.expressNo" placeholder="请输入物流单号"/>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
</el-row>
<el-form-item label="凭证" v-if="!payInfo.isExpress">
<vk-data-upload v-model="payInfo.photos"></vk-data-upload>
</el-form-item>
<el-form-item label="凭证" v-else required>
<vk-data-upload v-model="payInfo.expressPhoto"></vk-data-upload>
</el-form-item>
</template>
<el-form-item label="凭证">
<vk-data-upload v-model="payInfo.photos"></vk-data-upload>
</el-form-item>
<el-form-item label="附件">
<vk-data-upload v-model="payInfo.ext"></vk-data-upload>
</el-form-item>
@@ -143,51 +119,29 @@ export default {
type: 'error',
message: '请输入支付金额'
})
if (this.payInfo.isExpress && this.payInfo.expressPhoto.length === 0) return this.$message({
type: 'error',
message: '请上传凭证'
await vk.callFunction({
url: 'client/order/kh/makePay',
title: '请求中...',
data: {payInfo: this.payInfo}
})
// if (this.payInfo.isExpress && !this.payInfo.expressName) return this.$message({
// type: 'error',
// message: '请输入物流公司'
// })
// if (this.payInfo.isExpress && !this.payInfo.expressNo) return this.$message({
// type: 'error',
// message: '请输入物流单号'
// })
if (this.payInfo.isExpress) {
this.$confirm('发货状态一旦确定无法修改,请谨慎操作', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await vk.callFunction({
url: 'client/order/kh/makePay',
title: '请求中...',
data: {payInfo: this.payInfo}
})
this.$message({
type: 'success',
message: '支付单创建成功'
})
this.close()
})
} else {
await vk.callFunction({
url: 'client/order/kh/makePay',
title: '请求中...',
data: {payInfo: this.payInfo}
})
this.$message({
type: 'success',
message: '支付单创建成功'
})
this.close()
}
this.$message({
type: 'success',
message: '支付单创建成功'
})
this.close()
},
changeType() {
this.payInfo.isExpress = ['货到付款', '全款'].includes(this.payInfo.type)
console.log(this.payInfo.isExpress, this.payInfo.type)
if (['尾款', '全款'].includes(this.payInfo.type)) {
this.$confirm(
'选择尾款或全款支付申请,均代表本次交易已完结,对应本次交易最终货款的结算完成。提交申请后,系统无法再次发起付款申请;若发生误申请情形,仅可通过交易双方协商线下渠道协商办理支付,请您谨慎选择。',
'重要提示',
{
confirmButtonText: '确认为最终结算',
showCancelButton: false,
type: 'warning'
}
)
}
},
isNotaryPaidOrder() {
return Number(this.orderInfo.signMethod) === 0
@@ -196,8 +150,8 @@ export default {
},
isPayTypeDisabled(item) {
if (this.isNotaryPaidOrder()) return false
return (!['预付款', '货到付款', '全款'].includes(item) && this.orderInfo.payStatus === 0)
|| (['预付款', '货到付款', '全款'].includes(item) && this.orderInfo.payStatus > 0)
return (!['预付款', '全款'].includes(item) && this.orderInfo.payStatus === 0)
|| (['预付款', '全款'].includes(item) && this.orderInfo.payStatus > 0)
}
},
mounted() {
+14 -9
View File
@@ -72,22 +72,27 @@
</div>
</template>
</el-descriptions-item>
<el-descriptions-item label="交货凭证">
<el-image v-for="(photo, photoIndex) in item.expressPhoto" :key="photoIndex" :src="photo"
style="width: 80px; height: 80px" fit="cover"
:preview-src-list="item.expressPhoto"/>
</el-descriptions-item>
<el-descriptions-item label="备注">
<text>{{ item.remark ? item.remark : '无' }}</text>
</el-descriptions-item>
<el-descriptions-item label="买家备注">
<text>{{ item.buyerRemark ? item.buyerRemark : '无' }}</text>
</el-descriptions-item>
<el-descriptions-item label="图片">
<el-descriptions-item label="凭证">
<template v-if="item.photos && item.photos.length > 0">
<el-image v-for="(photo, photoIndex) in item.photos" :key="photoIndex" :src="photo"
style="width: 80px; height: 80px" fit="cover"
:preview-src-list="item.photos"/>
<el-image v-for="(photo, photoIndex) in item.photos" :key="photoIndex"
:src="typeof photo === 'string' ? photo : photo.url"
style="width: 80px; height: 80px; margin-right: 8px;" fit="cover"
:preview-src-list="item.photos.map(p => typeof p === 'string' ? p : p.url)"/>
</template>
<span v-else>无</span>
</el-descriptions-item>
<el-descriptions-item label="附件">
<template v-if="item.ext && item.ext.length > 0">
<el-image v-for="(file, fileIndex) in item.ext" :key="fileIndex"
:src="typeof file === 'string' ? file : file.url"
style="width: 80px; height: 80px; margin-right: 8px;" fit="cover"
:preview-src-list="item.ext.map(p => typeof p === 'string' ? p : p.url)"/>
</template>
<span v-else>无</span>
</el-descriptions-item>
+12 -1
View File
@@ -427,7 +427,7 @@ export default {
keyword: '',
orderPayList: [],
payInfo: {},
payTypeList: ['预付款', '进度款', '尾款', '货到付款', '全款'],
payTypeList: ['预付款', '进度款', '尾款', '全款'],
topExpiredAt: '',
userInfo: null,
rate: 0,
@@ -460,6 +460,17 @@ export default {
this.userInfo = data.userInfo
}
})
if (options.selfSupportPaySuccess === '1' || options.selfSupportPaySuccess === 1) {
this.$nextTick(() => {
this.$alert('您已成功提交订单,请等待信息提示或平台服务人员通知。', '提示', {
confirmButtonText: '确定',
showClose: false,
closeOnClickModal: false,
closeOnPressEscape: false,
type: 'success'
}).catch(() => {})
})
}
},
methods: {
dayjs,
+35 -18
View File
@@ -1,23 +1,23 @@
<template>
<el-card shadow="never" class="balance-log">
<div slot="header" class="flex justify-between items-center">
<span>余额记录</span>
<div class="flex justify-end items-center">
<div class="query-date mr-20">
<span class="query-date-label">查询日期:</span>
<el-date-picker
v-model="queryDate"
type="date"
size="small"
value-format="yyyyMMdd"
format="yyyy-MM-dd"
:clearable="false"
:editable="false"
placeholder="选择日期"
@change="onQueryDateChange">
</el-date-picker>
</div>
<div class="balance-text mr-20">当前余额:<span class="amount">{{ balanceLoading ? '加载中...' : formatBalance(balance) }}</span></div>
<div slot="header" class="header-bar">
<span class="header-title">余额记录</span>
<div class="query-date">
<span class="query-date-label">查询日期:</span>
<el-date-picker
v-model="queryDate"
type="date"
size="small"
value-format="yyyyMMdd"
format="yyyy-MM-dd"
:clearable="false"
:editable="false"
placeholder="选择日期"
@change="onQueryDateChange">
</el-date-picker>
</div>
<div class="balance-text">当前余额:<span class="amount">{{ balanceLoading ? '加载中...' : formatBalance(balance) }}</span></div>
<div class="header-right">
<el-button type="primary" size="small" :loading="withdrawLoading" @click="openWithdrawDialog">提现</el-button>
</div>
</div>
@@ -328,18 +328,35 @@ export default {
<style lang="scss" scoped>
.balance-log {
.header-bar {
display: flex;
align-items: center;
gap: 24px;
}
.header-title {
flex-shrink: 0;
}
.query-date {
display: flex;
align-items: center;
margin-left: 12px;
}
.query-date-label {
color: #666;
white-space: nowrap;
margin-right: 8px;
}
.balance-text {
color: #666;
white-space: nowrap;
}
.header-right {
margin-left: auto;
}
.amount,
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+2 -2
View File
@@ -26,8 +26,8 @@
<%= htmlWebpackPlugin.options.title %>
</title>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
<!-- 配置H5的 web图标static/logo.png -->
<link rel="icon" href="<%= BASE_URL %>static/logo.png">
<!-- 配置H5的 web图标 -->
<link rel="icon" type="image/png" href="<%= BASE_URL %>static/favicon.png">
</head>
<body>
<!-- 该文件为 H5 平台的模板 HTML,并非应用入口。 -->
@@ -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,922 @@
'use strict';
/**
* 合同审查报告 → Word(.docx) 生成器
*
* 数据来源:智能体在 HTML 报告末尾输出的结构化 JSON
* <script type="application/json" id="jjx-report-data">{...}</script>
*
* 排版目标:正式法律文书风格,A4 纵向,可直接打印装订
*/
const {
Document,
Packer,
Paragraph,
TextRun,
Table,
TableRow,
TableCell,
WidthType,
AlignmentType,
BorderStyle,
ShadingType,
TableLayoutType,
Footer,
Header,
PageNumber,
VerticalAlign,
ExternalHyperlink
} = require('docx');
const FONT = 'Microsoft YaHei';
const FONT_SONG = 'SimSun';
const C = {
primary: '1A5276',
danger: 'E74C3C',
success: '27AE60',
warning: 'F39C12',
text: '222222',
gray: '777777',
lightBg: 'F5F7F9',
pinkBg: 'FDF2F2',
border: 'D0D7DE'
};
const PAGE_BORDER = {
top: { style: BorderStyle.SINGLE, size: 4, color: C.border },
bottom: { style: BorderStyle.SINGLE, size: 4, color: C.border },
left: { style: BorderStyle.SINGLE, size: 4, color: C.border },
right: { style: BorderStyle.SINGLE, size: 4, color: C.border },
insideHorizontal: { style: BorderStyle.SINGLE, size: 4, color: C.border },
insideVertical: { style: BorderStyle.SINGLE, size: 4, color: C.border }
};
// ---------- 基础元件 ----------
function run(text, o) {
o = o || {};
const opt = {
text: String(text === null || text === undefined ? '' : text),
font: o.font || FONT,
size: o.size || 21,
bold: !!o.bold,
italics: !!o.italics,
color: o.color || C.text
};
if (o.underline) opt.underline = {};
return new TextRun(opt);
}
function para(children, o) {
o = o || {};
const opt = {
children: Array.isArray(children) ? children : [children],
spacing: {
before: o.before === undefined ? 60 : o.before,
after: o.after === undefined ? 60 : o.after,
line: o.line || 320
}
};
if (o.alignment) opt.alignment = o.alignment;
if (o.indent) opt.indent = o.indent;
if (o.border) opt.border = o.border;
if (o.shading) opt.shading = { type: ShadingType.CLEAR, fill: o.shading, color: 'auto' };
return new Paragraph(opt);
}
function cell(children, o) {
o = o || {};
const opt = {
children: Array.isArray(children) ? children : [children],
verticalAlign: VerticalAlign.CENTER,
margins: { top: 60, bottom: 60, left: 110, right: 110 }
};
if (o.shading) opt.shading = { type: ShadingType.CLEAR, fill: o.shading, color: 'auto' };
if (o.width) opt.width = { size: o.width, type: WidthType.PERCENTAGE };
if (o.columnSpan) opt.columnSpan = o.columnSpan;
return new TableCell(opt);
}
function table(rows, widths) {
return new Table({
rows: rows,
width: { size: 100, type: WidthType.PERCENTAGE },
columnWidths: widths,
layout: TableLayoutType.AUTOFIT,
borders: PAGE_BORDER
});
}
function sectionTitle(no, text) {
return para([run(no + '、' + text, { size: 28, bold: true, color: C.primary })], {
before: 320,
after: 140,
border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: C.primary, space: 4 } }
});
}
function pick(obj, keys, dft) {
if (!obj) return dft;
for (let i = 0; i < keys.length; i++) {
const v = obj[keys[i]];
if (v !== undefined && v !== null && v !== '') return v;
}
return dft;
}
function asArray(v) {
if (!v) return [];
return Array.isArray(v) ? v : [v];
}
// ---------- 风险条目 ----------
function riskBlock(item, color, levelText) {
const clause = pick(item, ['clause', 'no', '条款', 'clauseNo'], '');
const issue = pick(item, ['issue', 'risk', 'problem', '描述', '问题'], '');
const suggest = pick(item, ['suggestion', 'fix', 'advice', '建议', '修改建议'], '');
const basis = pick(item, ['legal_basis', 'legalBasis', 'basis', '法律依据'], '');
const blocks = [];
blocks.push(para([
run('[' + levelText + ']', { bold: true, color: color, size: 21 }),
run(clause ? ' 第 ' + clause + ' 条' : ' 合同条款', { bold: true, color: color, size: 21 })
], { before: 140, after: 40 }));
blocks.push(para([
run('问题:', { bold: true, size: 21 }),
run(issue, { size: 21 })
], { after: 40, indent: { left: 240 } }));
if (suggest) {
blocks.push(para([
run('建议:', { bold: true, size: 21 }),
run(suggest, { size: 21 })
], { after: 40, indent: { left: 240 } }));
}
if (basis) {
blocks.push(para([
run('依据:', { bold: true, size: 21 }),
run(basis, { size: 21, color: C.gray })
], { after: 80, indent: { left: 240 } }));
}
return blocks;
}
function riskSection(items, color, levelText, emptyText) {
const list = asArray(items);
if (!list.length) {
return [para([run(emptyText || '无', { size: 21, color: C.gray })], { after: 80 })];
}
let out = [];
list.forEach(function (it) {
out = out.concat(riskBlock(it, color, levelText));
});
return out;
}
// ---------- 主函数 ----------
async function buildDocx(input) {
const data = input.data || {};
const fileName = input.fileName || '合同审查报告';
const contractName = pick(data, ['contract_name', 'contractName', '协议名称', 'name'], fileName.replace(/\.[^.]+$/, ''));
const reportTitle = pick(data, ['report_title', 'reportTitle', '报告标题'], '合同审查报告');
const reviewDate = pick(data, ['review_date', 'reviewDate', '审核日期'], new Date().toLocaleDateString('zh-CN'));
const summary = pick(data, ['summary', '统计'], {}) || {};
const conclusion = pick(data, ['conclusion', '审核结论', 'summary_text'], '');
const highRisks = pick(data, ['high_risks', 'highRisks', '高风险'], []);
const mediumRisks = pick(data, ['medium_risks', 'mediumRisks', '中风险'], []);
const lowRisks = pick(data, ['low_risks', 'lowRisks', '低风险'], []);
const suggestions = pick(data, ['suggestions', '优化建议', 'advice_table'], []);
const templates = pick(data, ['clause_templates', 'clauseTemplates', '补充条款'], []);
const legalBases = pick(data, ['legal_bases', 'legalBases', '法律依据'], []);
const cases = pick(data, ['cases', '案例'], []);
const disclaimer = pick(data, ['disclaimer', '免责声明'], '');
const nCompliant = pick(summary, ['compliant', '合规'], 0);
const nPartial = pick(summary, ['partial', '部分合规'], 0);
const nHigh = pick(summary, ['high', '高风险'], asArray(highRisks).length);
const nMedium = pick(summary, ['medium', '中风险'], asArray(mediumRisks).length);
const children = [];
// ===== 抬头 =====
children.push(para([run(reportTitle, { size: 44, bold: true, color: C.primary })], {
alignment: AlignmentType.CENTER,
before: 200,
after: 120
}));
children.push(para([run(contractName, { size: 26, color: C.gray })], {
alignment: AlignmentType.CENTER,
after: 60
}));
children.push(para([run('审核日期:' + reviewDate + ' 出具机构:金久信签约宝', { size: 20, color: C.gray })], {
alignment: AlignmentType.CENTER,
after: 240
}));
// ===== 目录 =====
children.push(sectionTitle('一', '报告目录'));
const tocItems = [
'二、审核结论',
'三、需处理问题(高风险 / 中风险)',
'四、优化建议汇总表',
'五、补充条款模板',
'六、法律依据',
'七、相关案例',
'八、免责声明'
];
tocItems.forEach(function (item) {
children.push(para([run(item, { size: 21 })], { after: 30, indent: { left: 240 } }));
});
// ===== 审核结论 =====
children.push(sectionTitle('二', '审核结论'));
const statRow = new TableRow({
children: [
cell([para([run(String(nCompliant), { size: 40, bold: true, color: C.success })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('合规条款', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'F0F9EB', width: 25 }),
cell([para([run(String(nPartial), { size: 40, bold: true, color: C.warning })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('部分合规', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'FDF6EC', width: 25 }),
cell([para([run(String(nHigh), { size: 40, bold: true, color: C.danger })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('高风险', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'FEF0F0', width: 25 }),
cell([para([run(String(nMedium), { size: 40, bold: true, color: C.warning })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('中风险', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'FDF6EC', width: 25 })
]
});
children.push(table([statRow], [2000, 2000, 2000, 2000]));
if (conclusion) {
children.push(para([run('审核结论', { size: 21, bold: true, color: C.primary })], { before: 200, after: 80 }));
String(conclusion).split(/\n+/).forEach(function (line) {
if (line.trim()) {
children.push(para([run(line.trim(), { size: 21 })], { after: 60, indent: { left: 240 } }));
}
});
}
// ===== 需处理问题 =====
children.push(sectionTitle('三', '需处理问题'));
children.push(para([run('(一)高风险问题', { size: 24, bold: true, color: C.danger })], { before: 140, after: 80 }));
riskSection(highRisks, C.danger, '高风险', '本次审查未发现高风险问题。').forEach(function (p) { children.push(p); });
children.push(para([run('(二)中风险问题', { size: 24, bold: true, color: C.warning })], { before: 200, after: 80 }));
riskSection(mediumRisks, C.warning, '中风险', '本次审查未发现中风险问题。').forEach(function (p) { children.push(p); });
if (asArray(lowRisks).length) {
children.push(para([run('(三)低风险问题', { size: 24, bold: true, color: C.gray })], { before: 200, after: 80 }));
riskSection(lowRisks, C.gray, '低风险', '无').forEach(function (p) { children.push(p); });
}
// ===== 优化建议汇总表 =====
children.push(sectionTitle('四', '优化建议汇总表'));
const sugList = asArray(suggestions);
if (!sugList.length) {
children.push(para([run('无', { size: 21, color: C.gray })]));
} else {
const headRow = new TableRow({
tableHeader: true,
children: [
cell([para([run('优先级', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 12 }),
cell([para([run('条款编号', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 15 }),
cell([para([run('问题描述', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 38 }),
cell([para([run('建议方案', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 35 })
]
});
const rows = [headRow];
sugList.forEach(function (s, i) {
const lv = String(pick(s, ['priority', 'level', '优先级'], 'M')).toUpperCase();
const lvBg = lv === 'H' ? C.danger : (lv === 'L' ? '999999' : C.warning);
rows.push(new TableRow({
children: [
cell([para([run(lv, { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: lvBg, width: 12 }),
cell([para([run(pick(s, ['clause', 'no', '条款编号'], '-'), { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 15 }),
cell([para([run(pick(s, ['issue', 'problem', '问题描述', '描述'], '-'), { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 38 }),
cell([para([run(pick(s, ['solution', 'suggestion', '建议方案', '建议'], '-'), { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 35 })
]
}));
});
children.push(table(rows, [1200, 1500, 3800, 3500]));
}
// ===== 补充条款模板 =====
children.push(sectionTitle('五', '补充条款模板'));
const tplList = asArray(templates);
if (!tplList.length) {
children.push(para([run('无', { size: 21, color: C.gray })]));
} else {
tplList.forEach(function (t, i) {
const title = pick(t, ['title', 'name', '条款名称'], '补充条款 ' + (i + 1));
const content = pick(t, ['content', 'text', '条文', '内容'], '');
children.push(para([run(String(i + 1) + '. ' + title, { size: 21, bold: true, color: C.primary })], { before: 140, after: 60 }));
String(content).split(/\n+/).forEach(function (line) {
if (line.trim()) {
children.push(para([run(line.trim(), { size: 20, font: FONT_SONG })], {
after: 50,
indent: { left: 240 },
shading: C.lightBg,
border: { left: { style: BorderStyle.SINGLE, size: 12, color: C.primary, space: 6 } }
}));
}
});
});
}
// ===== 法律依据 =====
children.push(sectionTitle('六', '法律依据'));
const lawList = asArray(legalBases);
if (!lawList.length) {
children.push(para([run('本报告未引用具体法条,相关意见为实务建议,具体法律适用需律师人工核实。', { size: 20, color: C.gray })]));
} else {
lawList.forEach(function (l, i) {
const law = pick(l, ['law', 'name', '法律名称'], '');
const article = pick(l, ['article', 'no', '条款编号'], '');
const content = pick(l, ['content', 'text', '引用内容', '内容'], '');
children.push(para([
run(String(i + 1) + '. ', { size: 20, bold: true }),
run(law + (article ? ' ' + article : ''), { size: 20, bold: true, color: C.primary }),
run(content ? ':' + content : '', { size: 20 })
], { after: 60, indent: { left: 120 } }));
});
}
// ===== 相关案例 =====
children.push(sectionTitle('七', '相关案例'));
const caseList = asArray(cases);
if (!caseList.length) {
children.push(para([run('暂未检索到公开案例,建议人工核实。', { size: 20, color: C.gray })]));
} else {
const caseHead = new TableRow({
tableHeader: true,
children: [
cell([para([run('序号', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 8 }),
cell([para([run('案件名称·案由', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 26 }),
cell([para([run('法院·机构', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 14 }),
cell([para([run('年份', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 8 }),
cell([para([run('判决要点', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 28 }),
cell([para([run('与风险点关联', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 16 })
]
});
const caseRows = [caseHead];
caseList.forEach(function (c, i) {
const name = pick(c, ['name', 'title', '案件名称'], '-');
const url = pick(c, ['url', 'link', '链接'], '');
const court = pick(c, ['court', 'org', '法院'], '-');
const year = pick(c, ['year', '年份'], '-');
const result = pick(c, ['result', 'point', '判决要点', '要点'], '-');
const relation = pick(c, ['relation', '关联', '与本次风险点关联'], '-');
const nameCellChildren = [para([run(name, { size: 19, bold: true })], { after: url ? 20 : 0 })];
if (url) {
const host = url.replace(/^https?:\/\//i, '').split('/')[0];
nameCellChildren.push(para([
new ExternalHyperlink({
link: url,
children: [run('来源:' + host + ' ↗', { size: 16, color: '2E74B5', underline: true })]
})
], {}));
}
caseRows.push(new TableRow({
children: [
cell([para([run(String(i + 1), { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 8 }),
cell(nameCellChildren, { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 26 }),
cell([para([run(court, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 14 }),
cell([para([run(year, { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 8 }),
cell([para([run(result, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 28 }),
cell([para([run(relation, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 16 })
]
}));
});
children.push(table(caseRows, [700, 2300, 1300, 700, 2500, 1500]));
}
// ===== 免责声明 =====
children.push(sectionTitle('八', '免责声明'));
const disText = disclaimer || '本报告由人工智能根据用户上传的合同文本自动生成,仅供内部风险排查参考,不构成正式法律意见,亦不能替代执业律师的专业审查。涉及重大交易、争议解决或诉讼事项,请务必委托执业律师复核。报告中的法律法规引用以官方公布文本为准,案例信息来源于公开检索结果,具体适用请人工核实。';
String(disText).split(/\n+/).forEach(function (line) {
if (line.trim()) {
children.push(para([run(line.trim(), { size: 19, color: C.gray })], {
after: 50,
shading: C.pinkBg,
indent: { left: 120, right: 120 },
border: { left: { style: BorderStyle.SINGLE, size: 12, color: C.danger, space: 6 } }
}));
}
});
// ===== 文档 =====
const doc = new Document({
creator: '金久信签约宝',
title: contractName + ' - 合同审查报告',
description: 'AI 合同审查报告',
styles: {
default: {
document: {
run: { font: FONT, size: 21, color: C.text }
}
}
},
sections: [{
properties: {
page: {
margin: { top: 1134, right: 1134, bottom: 1134, left: 1134 }
}
},
headers: {
default: new Header({
children: [
para([
run('金久信签约宝 · AI 合同审查报告', { size: 18, color: C.gray }),
run(' ' + contractName, { size: 18, color: C.gray })
], { alignment: AlignmentType.RIGHT, after: 0 })
]
})
},
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({ text: '第 ', font: FONT, size: 18, color: C.gray }),
new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18, color: C.gray }),
new TextRun({ text: ' 页 / 共 ', font: FONT, size: 18, color: C.gray }),
new TextRun({ children: [PageNumber.TOTAL_PAGES], font: FONT, size: 18, color: C.gray }),
new TextRun({ text: ' 页', font: FONT, size: 18, color: C.gray })
]
})
]
})
},
children: children
}]
});
return await Packer.toBuffer(doc);
}
// ---------- HTML 解析(生产数据兜底通道) ----------
// 智能体实际返回的是纯 HTML 报告,未必带 jjx-report-data 数据块,
// 这里按区块结构把 HTML 还原成结构化数据,无需修改智能体提示词。
function decodeEntities(s) {
return String(s || '')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;|&apos;/g, "'")
.replace(/&amp;/g, '&')
.replace(/&#(\d+);/g, function (m, d) { return String.fromCharCode(Number(d)); })
.replace(/&#x([0-9a-fA-F]+);/g, function (m, h) { return String.fromCharCode(parseInt(h, 16)); });
}
function stripTags(html) {
return decodeEntities(
String(html || '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|tr|h\d|section)>/gi, '\n')
.replace(/<li[^>]*>/gi, '\n')
.replace(/<[^>]+>/g, '')
)
.replace(/[ \t\u00a0]+/g, ' ')
.replace(/\n[ ]+/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function pickDiv(html, cls) {
const re = new RegExp('<div[^>]*class="[^"]*\\b' + cls + '\\b[^"]*"[^>]*>([\\s\\S]*?)<\\/div>', 'i');
const m = String(html || '').match(re);
return m ? stripTags(m[1]) : '';
}
// 按 <h2> 把报告切成区块,返回 { 区块名: 区块内HTML }
// 兼容:智能体偶尔用 <h3> 代替 <h2> 做区块标题,h2 不足 2 个时降级按 h3 切
function splitByH2(html) {
const map = {};
const h2Count = (html.match(/<h2[^>]*>/gi) || []).length;
const re = h2Count >= 2
? /<h2[^>]*>([\s\S]*?)<\/h2>/gi
: /<h3[^>]*>([\s\S]*?)<\/h3>/gi;
const marks = [];
let m;
while ((m = re.exec(html))) {
marks.push({ name: stripTags(m[1]).replace(/\s+/g, ''), start: m.index, contentStart: re.lastIndex });
}
for (let i = 0; i < marks.length; i++) {
const end = i + 1 < marks.length ? marks[i + 1].start : html.length;
map[marks[i].name] = html.slice(marks[i].contentStart, end);
}
return map;
}
function findSection(map, keywords) {
const keys = Object.keys(map);
for (let i = 0; i < keys.length; i++) {
for (let j = 0; j < keywords.length; j++) {
if (keys[i].indexOf(keywords[j]) >= 0) return map[keys[i]];
}
}
return '';
}
function parseTable(tableHtml) {
const rows = [];
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let m;
while ((m = trRe.exec(tableHtml))) {
const cells = [];
const tdRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
let c;
while ((c = tdRe.exec(m[1]))) cells.push(stripTags(c[1]));
if (cells.length) rows.push(cells);
}
return rows;
}
function extractTables(html) {
const tables = [];
const re = /<table[^>]*>([\s\S]*?)<\/table>/gi;
let m;
while ((m = re.exec(html))) tables.push(parseTable(m[1]));
return tables;
}
// 案例表:需要在行内同时取出单元格文本和 a[href](按行取,避免跨行错位)
function parseRowsWithLinks(tableHtml) {
const rows = [];
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let m;
while ((m = trRe.exec(tableHtml))) {
const tr = m[1];
const cells = [];
let url = '';
const tdRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
let c;
while ((c = tdRe.exec(tr))) {
cells.push(stripTags(c[1]));
if (!url) {
const am = c[1].match(/<a[^>]*href="([^"]+)"/i);
if (am) url = am[1];
}
}
if (cells.length) rows.push({ cells: cells, url: url });
}
return rows;
}
function shortClause(text) {
const t = String(text || '').trim();
if (!t) return '-';
if (/^[0-9.]+$/.test(t)) return t;
return t.length > 16 ? t.slice(0, 16) + '…' : t;
}
function levelOf(text, tagCls) {
if (/高/.test(text)) return 'high';
if (/中/.test(text)) return 'medium';
if (/低/.test(text)) return 'low';
if (tagCls) return tagCls.toLowerCase();
return '';
}
function collectRiskItems(body, level, out) {
const chunks = String(body || '').split(/<div[^>]*class="[^"]*\brisk-item\b[^"]*"[^>]*>/i).slice(1);
chunks.forEach(function (chunk) {
// 级别标签两种写法:<span class="risk-tag high"> 或 <span class="tag h">
const tagMatch = chunk.match(/<span[^>]*class="[^"]*risk-tag\s+(high|medium|low)[^"]*"[^>]*>/i)
|| chunk.match(/<span[^>]*class="[^"]*tag\s+(high|medium|low|h|m|l)\b[^"]*"[^>]*>/i);
const tagRaw = tagMatch ? tagMatch[1].toLowerCase() : '';
const tagLv = tagRaw === 'h' ? 'high' : tagRaw === 'm' ? 'medium' : tagRaw === 'l' ? 'low' : tagRaw;
// 优先级:所在区块标题 > 条目自身标签 > 默认 medium
const lv = level || tagLv || 'medium';
// 字段名两种写法:risk-clause/risk-desc/risk-fix 或 clause/desc/fix
let clauseText = pickDiv(chunk, 'risk-clause') || pickDiv(chunk, 'clause');
clauseText = clauseText.replace(/^[HML]\s*/, '').trim();
const clauseNum = clauseText.match(/条款\s*([0-9.]+)/);
const clause = shortClause(clauseNum ? clauseNum[1] : clauseText);
const issue = pickDiv(chunk, 'risk-desc') || pickDiv(chunk, 'desc');
let fix = (pickDiv(chunk, 'risk-fix') || pickDiv(chunk, 'fix')).replace(/^\s*(修复建议|建议)[::]\s*/, '');
if (!clauseText && !issue) return;
const item = {
level: lv,
clause: clause || '-',
issue: issue || clauseText || '-',
suggestion: fix || ''
};
// 条款标题里的补充信息(如“交付清单、全部附件空白”)拼进问题描述,避免丢信息
if (clauseNum && clauseText) {
const rest = clauseText.replace(/条款\s*[0-9.]+\s*/, '').trim();
if (rest && item.issue.indexOf(rest) < 0) item.issue = rest + ':' + item.issue;
}
if (!out[lv]) out[lv] = [];
out[lv].push(item);
});
}
function parseRisks(sectionHtml, hint) {
const out = { high: [], medium: [], low: [] };
const parts = String(sectionHtml || '').split(/<h3[^>]*>/i);
if (parts.length > 1) {
parts.slice(1).forEach(function (part) {
const headEnd = part.search(/<\/h3>/i);
const headText = headEnd >= 0 ? stripTags(part.slice(0, headEnd)) : '';
const body = headEnd >= 0 ? part.slice(headEnd + 5) : part;
const lv = levelOf(headText, '') || hint || '';
collectRiskItems(body, lv, out);
});
} else {
collectRiskItems(sectionHtml, hint || '', out);
}
return out;
}
/**
* 从智能体生成的真实 HTML 报告里还原结构化数据
*/
function parseHtmlReport(html) {
if (!html || typeof html !== 'string') return null;
if (html.indexOf('<') < 0) return null;
const raw = html
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<script[\s\S]*?<\/script>/gi, '');
const data = {};
// 标题 / 合同名 / 日期
const h1 = raw.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
const reportTitle = h1 ? stripTags(h1[1]) : '';
const nameM = raw.match(/合同名称[::]\s*([^<\n]+)/);
const dateM = raw.match(/审核日期[::]\s*([^<\n]+)/);
const titleM = raw.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
data.contract_name = nameM ? nameM[1].trim()
: (reportTitle || (titleM ? stripTags(titleM[1]) : '')).replace(/审查报告$/, '').trim();
data.report_title = reportTitle || '';
data.review_date = dateM ? dateM[1].trim() : '';
// 统计卡片:按 num + label 配对抓取
const summary = {};
function applyCard(numRaw, labelRaw) {
const num = stripTags(numRaw).trim();
const label = stripTags(labelRaw).replace(/\s+/g, '');
if (!label) return;
const val = /^[0-9]+$/.test(num) ? Number(num) : num;
if (label.indexOf('部分') >= 0) summary.partial = val;
else if (label.indexOf('高') >= 0) summary.high = val;
else if (label.indexOf('中') >= 0) summary.medium = val;
else if (label.indexOf('低') >= 0) summary.low = val;
else if (label.indexOf('合规') >= 0) summary.compliant = val;
}
// 写法一:label 有 class="label"
const cardRe = /<div[^>]*class="[^"]*\bnum\b[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<div[^>]*class="[^"]*\blabel\b[^"]*"[^>]*>([\s\S]*?)<\/div>/gi;
let cm;
while ((cm = cardRe.exec(raw))) applyCard(cm[1], cm[2]);
// 写法二:label 是紧跟的无 class <div>
if (!Object.keys(summary).length) {
const cardRe2 = /<div[^>]*class="[^"]*\bnum\b[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<div[^>]*>([\s\S]*?)<\/div>/gi;
while ((cm = cardRe2.exec(raw))) applyCard(cm[1], cm[2]);
}
data.summary = summary;
const sections = splitByH2(raw);
// 审核结论
const conclusionHtml = findSection(sections, ['审核结论']);
data.conclusion = stripTags(conclusionHtml).replace(/\n{2,}/g, '\n');
// 需处理问题:兼容两种结构
// ① 单一「需处理问题」总区块(内含高风险/中风险小节)
// ② 「高风险问题」「中风险问题」各自独立成区块
const riskKeywords = ['需处理问题', '风险问题', '问题清单', '高风险', '中风险', '低风险'];
const risks = { high: [], medium: [], low: [] };
Object.keys(sections).forEach(function (key) {
const hit = riskKeywords.some(function (k) { return key.indexOf(k) >= 0; });
if (!hit) return;
const part = parseRisks(sections[key], levelOf(key, ''));
['high', 'medium', 'low'].forEach(function (lv) {
risks[lv] = risks[lv].concat(part[lv] || []);
});
});
data.high_risks = risks.high;
data.medium_risks = risks.medium;
data.low_risks = risks.low;
// 优化建议汇总表
const sugHtml = findSection(sections, ['优化建议', '建议汇总']);
const sugTables = extractTables(sugHtml);
const suggestions = [];
if (sugTables.length) {
sugTables[0].forEach(function (row, i) {
if (i === 0) return;
if (/优先级|条款编号/.test(row.join(''))) return;
suggestions.push({
priority: (row[0] || '').replace(/\s+/g, '').slice(0, 1).toUpperCase() || 'M',
clause: row[1] || '-',
issue: row[2] || '-',
solution: row[3] || '-'
});
});
}
data.suggestions = suggestions;
// 补充条款模板(部分报告没有该区块)
// 三种写法:<pre> 代码块 / <div class="clause-template"> 模板块 / 整段纯文本
const tplHtml = findSection(sections, ['补充条款']);
const templates = [];
if (tplHtml) {
const pres = tplHtml.match(/<pre[^>]*>([\s\S]*?)<\/pre>/gi) || [];
pres.forEach(function (p, i) {
templates.push({ title: '补充条款 ' + (i + 1), content: stripTags(p) });
});
if (!templates.length) {
// 模板块:class="clause-template",标题取内部 <strong>【...】</strong>
const tplDivs = tplHtml.match(/<div[^>]*class="[^"]*\bclause-template\b[^"]*"[^>]*>([\s\S]*?)<\/div>/gi) || [];
tplDivs.forEach(function (d, i) {
const text = stripTags(d).trim();
if (!text) return;
const tm = d.match(/<strong[^>]*>([\s\S]*?)<\/strong>/i);
const title = tm ? stripTags(tm[1]).replace(/^【|】$/g, '').trim() : '';
templates.push({ title: title || '补充条款 ' + (i + 1), content: text.replace(/^(【[^】]*】)/, '').trim() });
});
}
if (!templates.length) {
const text = stripTags(tplHtml);
if (text) templates.push({ title: '补充条款', content: text });
}
}
data.clause_templates = templates;
// 法律依据
const lawHtml = findSection(sections, ['法律依据', '法律法规']);
const legalBases = [];
const liRe = /<li[^>]*>([\s\S]*?)<\/li>/gi;
let lm;
while ((lm = liRe.exec(lawHtml))) {
const text = stripTags(lm[1]);
if (!text) continue;
const seg = text.match(/^(.+?第[0-9]+条[之]*[一二三四五六七八九十]*)[::]\s*([\s\S]*)$/);
if (seg) legalBases.push({ law: seg[1], article: '', content: seg[2] });
else legalBases.push({ law: '', article: '', content: text });
}
if (!legalBases.length) {
const text = stripTags(lawHtml);
if (text) {
text.split(/\n+/).forEach(function (line) {
line = line.trim();
if (!line) return;
const seg = line.match(/^(.+?第[0-9]+条[之]*[一二三四五六七八九十]*)[::]\s*([\s\S]*)$/);
if (seg) legalBases.push({ law: seg[1], article: '', content: seg[2] });
else legalBases.push({ law: '', article: '', content: line });
});
}
}
data.legal_bases = legalBases;
// 案例
const caseHtml = findSection(sections, ['案例', '判例']);
const caseTableMatch = caseHtml.match(/<table[^>]*>([\s\S]*?)<\/table>/i);
const cases = [];
if (caseTableMatch) {
const rows = parseRowsWithLinks(caseTableMatch[1]);
rows.forEach(function (r) {
const row = r.cells;
if (!row.length) return;
if (/案件名称/.test(row.join('')) || /^序号$/.test(row[0])) return; // 表头
const hasLinkCol = row.length >= 7;
cases.push({
name: row[hasLinkCol ? 1 : 0] || '-',
url: r.url || '',
court: row[hasLinkCol ? 3 : 2] || '-',
year: row[hasLinkCol ? 4 : 3] || '-',
result: row[hasLinkCol ? 5 : 4] || '-',
relation: row[hasLinkCol ? 6 : 5] || '-'
});
});
}
data.cases = cases;
// 免责声明(优先按 h2 区块取,取不到再从全局找 class="disclaimer")
const disHtml = findSection(sections, ['免责声明', '免责']);
let disclaimer = pickDiv(disHtml, 'disclaimer') || stripTags(disHtml);
if (!disclaimer) {
const dm = raw.match(/<div[^>]*class="[^"]*\bdisclaimer\b[^"]*"[^>]*>([\s\S]*?)<\/div>/i);
if (dm) disclaimer = stripTags(dm[1]);
}
data.disclaimer = disclaimer;
// 生成时间(页脚)
const footM = raw.match(/生成时间[::]\s*([^<\n]+)/);
if (footM && !data.review_date) data.review_date = footM[1].trim();
// 完全没解析出内容则视为失败
if (!data.conclusion && !data.high_risks.length && !data.suggestions.length) return null;
return data;
}
/**
* 从智能体回复里提取「插件生成的 Word 文件链接」
* 只认显式标记,避免误抓案例正文里的 .docx 链接
* 1. <a id="jjx-docx-link" href="https://...">下载 Word</a>
* 2. <!-- jjx-docx: https://... -->
*/
function extractDocxLink(answer) {
if (!answer) return '';
const a = answer.match(/<a[^>]*id=["']jjx-docx-link["'][^>]*href=["']([^"']+)["']/i);
if (a && a[1]) return a[1];
const b = answer.match(/<!--\s*jjx-docx\s*[::]\s*(https?:\/\/[^\s>]+)\s*-->/i);
if (b && b[1]) return b[1];
return '';
}
/**
* 判断智能体回复里是否有一份「可以转成 Word 的报告」
*
* 返回 true 的两种情况:
* 1. 智能体插件已经生成了 Word 并给了链接 → 直接复用,不用本地转
* 2. 回复里有完整的 HTML 报告(有 <html> 开头且有 </html> 结尾)
*
* 返回 false 的典型情况:
* - 「很抱歉,当前系统多次尝试读取您上传的DOC格式合同文件均解析失败…」这类道歉/澄清文本
* - HTML 被 max token 截断,缺 </html> —— 强行转会生成一份残缺的 Word,宁可不转
*/
function isDocxConvertible(answer) {
if (!answer || typeof answer !== 'string') return false;
if (extractDocxLink(answer)) return true;
if (!/<!DOCTYPE\s+html|<html[\s>]/i.test(answer)) return false;
return /<\/html>/i.test(answer);
}
/**
* 从智能体返回的 HTML 中提取结构化报告数据
* 优先级:<script id="jjx-report-data"> → ```json 代码块 → HTML 结构解析
*/
function extractReportData(answer) {
if (!answer) return null;
const m = answer.match(/<script[^>]*id=["']jjx-report-data["'][^>]*>([\s\S]*?)<\/script>/i);
if (m && m[1]) {
try {
return JSON.parse(m[1].trim());
} catch (e) {
console.log('[Docx] jjx-report-data JSON 解析失败,改用 HTML 解析:', e.message);
}
}
const blocks = answer.match(/```json\s*([\s\S]*?)```/g) || [];
for (let i = blocks.length - 1; i >= 0; i--) {
try {
const raw = blocks[i].replace(/```json\s*/, '').replace(/```$/, '').trim();
const obj = JSON.parse(raw);
if (obj && (obj.high_risks || obj.highRisks || obj.summary)) return obj;
} catch (e) { /* 继续找 */ }
}
// 兜底:直接解析 HTML 报告结构
try {
const parsed = parseHtmlReport(answer);
if (parsed) {
console.log('[Docx] 已从 HTML 报告解析结构化数据');
return parsed;
}
// 诊断:打印实际 HTML 结构关键特征,定位解析失败原因
const h2s = (answer.match(/<h2[^>]*>([\s\S]*?)<\/h2>/gi) || [])
.map(function (h) { return h.replace(/<[^>]+>/g, '').replace(/\s+/g, '').slice(0, 20); });
console.log('[Docx] HTML 解析为空。诊断 → h2区块[' + h2s.length + ']:',
h2s.length ? h2s.join(' | ') : '无h2标签',
'| 有table:', /<table/i.test(answer),
'| 有risk-item:', /risk-item/i.test(answer),
'| 有审核结论字样:', /审核结论|审查结论/.test(answer),
'| 长度:', answer.length);
} catch (e) {
console.log('[Docx] HTML 报告解析失败:', e.message);
}
return null;
}
module.exports = {
buildDocx,
extractReportData,
parseHtmlReport,
extractDocxLink,
isDocxConvertible
};
@@ -0,0 +1,282 @@
'use strict';
// 定时任务:每分钟检查 reviewing 状态的记录,触发审查
const db = uniCloud.database();
const _ = db.command;
// 导入审查执行函数(从 router 的 service 目录)
// 注意:定时任务是独立云函数,不能直接 require router 的 service
// 所以把审查逻辑直接写在这里
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
const BOT_ID = '7689821072762011698';
const { buildDocx, extractReportData, extractDocxLink, isDocxConvertible } = require('./buildDocx.js');
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);
}
// 生成 Word 报告:优先用智能体插件生成的链接,否则本地结构化重建
let docxUrl = extractDocxLink(answer);
let docxFileId = '';
if (docxUrl) {
console.log('[Docx] 采用智能体插件生成的 Word 链接:', docxUrl);
} else if (!isDocxConvertible(answer)) {
// 智能体没返回完整报告(如「文件解析失败」的道歉文本,或 HTML 被截断),
// 不生成 Word,避免产出一份残缺文档
console.log('[Docx] 无完整 HTML 报告,跳过 Word 生成');
} else {
try {
const reportData = extractReportData(answer);
if (!reportData) {
console.log('[Docx] 既无插件链接也无结构化数据,跳过 Word 生成');
} else {
const docxBuffer = await buildDocx({
data: reportData,
fileName: file_name || '合同审查报告'
});
const baseName = (file_name || 'report').replace(/\.[^.]+$/, '');
const docxPath = 'review-reports/' + Date.now() + '_' + baseName + '_审查报告.docx';
const docxRes = await uniCloud.uploadFile({
cloudPath: docxPath,
fileContent: docxBuffer
});
if (docxRes.fileID) {
docxFileId = docxRes.fileID;
const docxUrlRes = await uniCloud.getTempFileURL({ fileList: [docxRes.fileID] });
if (docxUrlRes.fileList && docxUrlRes.fileList[0]) {
docxUrl = docxUrlRes.fileList[0].tempFileURL;
}
console.log('[Docx] 本地 Word 报告生成成功:', docxFileId);
}
}
} catch (e) {
console.log('[Docx] Word 报告生成失败(不影响审查结果):', e.message);
}
}
// 更新记录为完成
await db.collection('jjx-review-records').doc(record_id).update({
status: 'completed',
result: answer,
html_url: htmlUrl,
docx_url: docxUrl,
docx_file_id: docxFileId,
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,336 @@
#!/usr/bin/env node
/**
* 本地自测脚本:智能体返回的 HTML 审查报告 → Word 文档
* 纯本地运行,不依赖 uniCloud,不联网,不动数据库。
*
* 用法(在本目录下执行):
* node local-test.js # 用内置样例跑一遍,验证环境
* node local-test.js report.html # 转换指定 HTML(输出到同目录)
* node local-test.js report.html out.docx # 指定输出路径
* node local-test.js dir/ # 批量转换目录下所有 .html/.txt
* node local-test.js report.html --json # 只打印解析结果,不生成 Word
* node local-test.js report.html --open # 生成后自动用 Word 打开
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { exec } = require('child_process');
const { buildDocx, parseHtmlReport, extractDocxLink, isDocxConvertible } = require('./buildDocx.js');
// ==================== 内置样例(覆盖全部区块,用于环境自检) ====================
const SAMPLE_HTML = `<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>样例合同审查报告</title></head>
<body>
<div class="header">
<h1>技术开发与服务合同审查报告</h1>
<p>合同名称:金久信钢城平台AI功能接入开发及运维服务合同</p>
<p>审核日期:2026年10月23日</p>
</div>
<div class="card-wrap">
<div class="stat-card green"><div class="num">0</div><div class="label">合规</div></div>
<div class="stat-card orange"><div class="num">C</div><div class="label">部分合规</div></div>
<div class="stat-card red"><div class="num">2</div><div class="label">高风险</div></div>
<div class="stat-card yellow"><div class="num">1</div><div class="label">中风险</div></div>
</div>
<div class="section">
<h2>审核结论</h2>
<p>本合同核心架构完整,但存在多处核心条款空白,整体风险等级为中高风险,需补充完善后方可签署。</p>
</div>
<div class="section">
<h2>需处理问题</h2>
<h3>🔴 高风险问题</h3>
<div class="risk-item high">
<div class="risk-clause"><span class="risk-tag high">H</span>条款2.3 交付清单、全部附件空白</div>
<div class="risk-desc">合同第2.3条交付清单表格完全空白,未明确交付物名称与交付时间,极易引发履约争议。</div>
<div class="risk-fix">修复建议:详细填写交付清单,明确每项交付物的名称、数量、交付时间。</div>
</div>
<div class="risk-item high">
<div class="risk-clause"><span class="risk-tag high">H</span>合同首部多项核心字段空白</div>
<div class="risk-desc">合同名称、签订地点、合同金额等核心字段均为空白,属于典型的空白合同。</div>
<div class="risk-fix">修复建议:将所有空白字段填写完整,无需填写的标注"无"。</div>
</div>
<h3>🟠 中风险问题</h3>
<div class="risk-item medium">
<div class="risk-clause"><span class="risk-tag medium">M</span>条款9 违约责任约定过于笼统</div>
<div class="risk-desc">违约责任仅约定逾期违约金,未约定交付不合格、侵犯知识产权等情形。</div>
<div class="risk-fix">修复建议:补充交付不合格、侵权、擅自转包的违约责任。</div>
</div>
</div>
<div class="section">
<h2>优化建议汇总表</h2>
<table>
<thead><tr><th>优先级</th><th>条款编号</th><th>问题描述</th><th>建议方案</th></tr></thead>
<tbody>
<tr><td>H</td><td>2.3</td><td>交付清单空白</td><td>详细填写交付清单</td></tr>
<tr><td>H</td><td>合同首部</td><td>核心字段空白</td><td>填写所有空白字段</td></tr>
<tr><td>M</td><td>9</td><td>违约责任笼统</td><td>补充违约情形</td></tr>
</tbody>
</table>
</div>
<div class="section">
<h2>补充条款模板</h2>
<div class="clause-template"><h3>转包限制条款</h3>
<pre>未经甲方书面同意,乙方不得将本合同项下的任何工作转包或分包给第三方,否则视为乙方根本违约。</pre></div>
</div>
<div class="section">
<h2>法律依据</h2>
<ul>
<li>《民法典》第510条:合同生效后,当事人就质量、价款或者报酬等内容没有约定或者约定不明确的,可以协议补充。</li>
<li>《民法典》第577条:当事人一方不履行合同义务或者履行合同义务不符合约定的,应当承担违约责任。</li>
</ul>
</div>
<div class="section">
<h2>相关司法案例</h2>
<table>
<thead><tr><th>序号</th><th>案件名称·案由</th><th>资料链接</th><th>法院·机构</th><th>裁判年份</th><th>判决要点</th><th>关联风险点</th></tr></thead>
<tbody>
<tr><td>1</td><td>空白合同效力认定纠纷</td>
<td><a class="case-link" href="https://example.com/case-1" target="_blank">打开裁判页面 ↗</a></td>
<td>最高人民法院</td><td>2019年</td><td>交付空白合同视为概括授权</td><td>空白合同字段风险</td></tr>
<tr><td>2</td><td>技术开发合同交付约定不明纠纷</td>
<td><a class="case-link" href="https://example.com/case-2" target="_blank">打开裁判页面 ↗</a></td>
<td>某中级人民法院</td><td>2021年</td><td>约定不明按交易习惯确定</td><td>交付方式不明风险</td></tr>
</tbody>
</table>
</div>
<div class="disclaimer">本报告仅基于您提供的合同文本出具,不构成任何法律意见。</div>
<div class="footer">金久信签约宝 · AI合同审查服务 © 2026 | 生成时间:2026年10月23日</div>
</body>
</html>`;
// ==================== 用例:智能体返回「文件读不了」的道歉文本 ====================
// 这种回复里没有报告,线上应当跳过 Word 生成,而不是产出一份空文档
const APOLOGY_TEXT = '很抱歉,当前系统多次尝试读取您上传的DOC格式合同文件均解析失败,暂时无法对该文件开展审查工作。建议您选择以下任一方式解决: 1. 将该DOC文件转换为DOCX、PDF、TXT等兼容性更好的格式后重新上传; 2. 直接粘贴合同的完整文本内容发送给我,我会立即为您生成专业的合同审查报告。';
// ==================== 工具函数 ====================
/** 清洗智能体回复:去掉 markdown 包裹、前后说明文字,只留 HTML 本体 */
function cleanAnswer(text) {
let s = String(text || '');
const fence = s.match(/```(?:html)?\s*([\s\S]*?)```/i);
if (fence && fence[1] && /<(!DOCTYPE|html)/i.test(fence[1])) s = fence[1];
const start = s.search(/<!DOCTYPE\s+html|<html[\s>]/i);
if (start > 0) s = s.slice(start);
const end = s.toLowerCase().lastIndexOf('</html>');
if (end > 0) s = s.slice(0, end + 7);
return s.trim();
}
function pad(s, n) {
const str = String(s);
// 中文按 2 宽度算
let w = 0;
for (const ch of str) w += /[\u4e00-\u9fa5\uff00-\uffef]/.test(ch) ? 2 : 1;
return str + ' '.repeat(Math.max(0, n - w));
}
function line(label, value) {
console.log(' ' + pad(label, 14) + ': ' + value);
}
/** 打印解析结果摘要 */
function summarize(data) {
if (!data) {
console.log(' ✗ 解析失败:未提取到任何有效内容(结论/风险/建议全空)');
return false;
}
const s = data.summary || {};
line('报告标题', data.report_title || '(空)');
line('合同名称', data.contract_name || '(空)');
line('审核日期', data.review_date || '(空)');
line('统计卡片', `合规 ${s.compliant || 0} / 部分合规 ${s.partial || 0} / 高风险 ${s.high || 0} / 中风险 ${s.medium || 0}`);
line('风险项', `高 ${(data.high_risks || []).length} / 中 ${(data.medium_risks || []).length} / 低 ${(data.low_risks || []).length}`);
line('优化建议', (data.suggestions || []).length + ' 条');
line('补充条款', (data.clause_templates || []).length + ' 条');
line('法律依据', (data.legal_bases || []).length + ' 条');
line('案例', (data.cases || []).length + ' 个');
line('免责声明', data.disclaimer ? `已提取(${data.disclaimer.length} 字)` : '(空)');
const cases = data.cases || [];
if (cases.length) {
console.log(' --- 案例链接核对 ---');
cases.forEach((c, i) => {
const url = c.url ? c.url : '(无链接)';
console.log(` ${i + 1}. ${c.name || '-'}`);
console.log(` 法院/年份: ${c.court || '-'} / ${c.year || '-'}`);
console.log(` 链接: ${url}`);
});
}
const risks = [...(data.high_risks || []), ...(data.medium_risks || []), ...(data.low_risks || [])];
if (risks.length) {
console.log(' --- 风险条款号 ---');
risks.forEach((r, i) => {
console.log(` ${i + 1}. [${r.level || '-'}] ${r.clause || '-'} — ${(r.issue || '').slice(0, 40)}`);
});
}
return true;
}
/** 转换单个 HTML 文件/字符串 */
async function convert(label, html, outPath) {
console.log('');
console.log('════════════════════════════════════════');
console.log('▶ ' + label);
console.log('════════════════════════════════════════');
const cleaned = cleanAnswer(html);
if (!cleaned) {
console.log(' ✗ 内容为空');
return 'fail';
}
// 通道判断(与线上逻辑一致)
const pluginLink = extractDocxLink(cleaned);
if (pluginLink) {
console.log(' ⚑ 检测到智能体插件返回的 Word 链接:');
console.log(' ' + pluginLink);
console.log(' (线上会直接使用该链接,不本地生成)');
if (!/<!DOCTYPE\s+html|<html[\s>]/i.test(cleaned)) {
console.log(' ⊘ 回复里没有 HTML 报告,线上直接复用插件链接,不做本地转换');
return 'skip';
}
}
console.log(' 原文长度: ' + cleaned.length + ' 字符');
// 关键拦截:智能体有时不返回报告,只回一段道歉/澄清文字
// (例如「很抱歉,当前系统多次尝试读取您上传的DOC格式合同文件均解析失败…」)
// 线上此时不生成 Word,这里保持一致,避免产出一份残缺文档。
if (!isDocxConvertible(cleaned)) {
console.log(' ⊘ 这不是一份完整报告,线上会跳过 Word 生成(不产出空文档)');
console.log(' 开头内容:' + cleaned.slice(0, 70).replace(/\s+/g, ' ') + '…');
return 'skip';
}
const data = parseHtmlReport(cleaned);
const ok = summarize(data);
if (!ok) return 'fail';
if (FLAGS.jsonOnly) {
console.log(' (--json 模式,未生成 Word)');
return 'skip';
}
const t0 = Date.now();
const buffer = await buildDocx({ data: data, fileName: data.contract_name || '合同审查报告' });
const ms = Date.now() - t0;
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, buffer);
console.log(' ✓ Word 已生成');
line('输出文件', outPath);
line('文件大小', (buffer.length / 1024).toFixed(1) + ' KB');
line('耗时', ms + ' ms');
if (FLAGS.open) {
exec(`start "" "${outPath}"`, { shell: 'cmd.exe' });
console.log(' → 已调用系统默认程序打开');
}
return 'done';
}
// ==================== 主流程 ====================
const argv = process.argv.slice(2);
const FLAGS = {
jsonOnly: argv.includes('--json'),
open: argv.includes('--open'),
help: argv.includes('-h') || argv.includes('--help')
};
const positional = argv.filter((a) => !a.startsWith('-'));
(async function main() {
if (FLAGS.help) {
console.log(`
本地自测:HTML 审查报告 → Word
node local-test.js 用内置样例自检
node local-test.js report.html 转换指定文件(输出到同目录)
node local-test.js report.html out.docx 指定输出路径
node local-test.js someDir/ 批量转换目录下 .html / .txt
node local-test.js report.html --json 只看解析结果
node local-test.js report.html --open 生成后自动打开
`);
return;
}
let ok = 0;
let skip = 0;
let fail = 0;
const tally = (r) => {
if (r === 'done') ok++;
else if (r === 'skip') skip++;
else fail++;
};
try {
if (!positional.length) {
// 三个内置用例,覆盖「能转」和「不该转」两类情况
tally(await convert(
'用例1:正常 HTML 报告(应生成 Word)',
SAMPLE_HTML,
path.join(os.tmpdir(), 'jjx_本地自测_审查报告.docx')
));
tally(await convert(
'用例2:智能体只回「文件解析失败」道歉文本(应跳过)',
APOLOGY_TEXT,
path.join(os.tmpdir(), 'jjx_用例2_不应生成.docx')
));
tally(await convert(
'用例3:HTML 被截断、缺 </html>(应跳过,不产出残缺文档)',
SAMPLE_HTML.slice(0, 1800),
path.join(os.tmpdir(), 'jjx_用例3_不应生成.docx')
));
} else {
const target = positional[0];
const stat = fs.statSync(target);
if (stat.isDirectory()) {
const files = fs.readdirSync(target)
.filter((f) => /\.(html?|txt)$/i.test(f))
.map((f) => path.join(target, f));
if (!files.length) {
console.log('目录下没有 .html / .txt 文件:' + target);
return;
}
console.log(`批量转换:共 ${files.length} 个文件`);
for (const f of files) {
const out = path.join(target, path.basename(f).replace(/\.[^.]+$/, '') + '_审查报告.docx');
try {
tally(await convert(path.basename(f), fs.readFileSync(f, 'utf8'), out));
} catch (e) {
console.log(' ✗ 异常:' + e.message);
fail++;
}
}
} else {
const out = positional[1] || path.join(
path.dirname(target),
path.basename(target).replace(/\.[^.]+$/, '') + '_审查报告.docx'
);
tally(await convert(path.basename(target), fs.readFileSync(target, 'utf8'), out));
}
}
} catch (e) {
console.log('');
console.log('✗ 运行失败:' + e.message);
if (e.code === 'ENOENT') console.log(' 提示:文件路径不存在,注意用引号包裹含空格的路径');
if (e.code === 'MODULE_NOT_FOUND') console.log(' 提示:先执行 npm install 安装 docx 依赖');
process.exitCode = 1;
return;
}
console.log('');
console.log('════════════════════════════════════════');
console.log(`完成:生成 ${ok} 个,跳过 ${skip} 个,失败 ${fail} 个`);
if (fail > 0) console.log('提示:解析失败通常是 HTML 结构与预期不符,把该文件发我看看。');
console.log('════════════════════════════════════════');
})();
@@ -0,0 +1,22 @@
{
"name": "jjxReviewTimer",
"version": "1.0.0",
"description": "合同审查定时任务",
"main": "index.js",
"dependencies": {
"docx": "^8.5.0"
},
"cloudfunction-config": {
"memorySize": 256,
"timeout": 600,
"triggers": [
{
"name": "myTrigger",
"type": "timer",
"config": "0 */1 * * * * *"
}
],
"path": "",
"runtime": "Nodejs16"
}
}
@@ -0,0 +1,49 @@
/**
* 登录前修复 dcloud_appid 为空数组的用户
* uni-id 只放行「字段不存在」或「数组包含当前 appid」的账号,空数组会被当成用户不存在
*/
const LOGIN_APP_IDS = ["__UNI__EB80D26", "__UNI__8BFD66E", "__UNI__0F0703D"];
function normalizeAccount(value) {
if (typeof value === "number") return String(value).trim();
if (typeof value === "string") return value.trim();
return "";
}
module.exports = [{
id: "fixEmptyDcloudAppid",
regExp: [
"^user/pub/login(.*)"
],
description: "登录前把 dcloud_appid 为空数组的用户补上三个应用的 appid",
index: 201,
mode: "onActionExecuting",
enable: true,
main: async function(event) {
let { data = {}, util } = event;
let { db, _ } = util;
const username = normalizeAccount(data.username);
const mobile = normalizeAccount(data.mobile);
const email = normalizeAccount(data.email);
const whereList = [];
if (username) {
whereList.push({ username }, { mobile: username }, { email: username });
}
if (mobile && mobile !== username) whereList.push({ mobile });
if (email && email !== username) whereList.push({ email });
if (whereList.length === 0) return { code: 0 };
const whereJson = whereList.length === 1 ? whereList[0] : _.or(whereList);
const userRes = await db.collection("uni-id-users").where(whereJson).field({
dcloud_appid: true
}).limit(10).get();
const users = (userRes.data || []).filter(user => Array.isArray(user.dcloud_appid) && user.dcloud_appid.length === 0);
for (let i = 0; i < users.length; i++) {
await db.collection("uni-id-users").doc(users[i]._id).update({
dcloud_appid: LOGIN_APP_IDS
});
}
return { code: 0 };
}
}]
+11 -25
View File
@@ -1,7 +1,7 @@
{
"name": "router",
"version": "1.0.0",
"lockfileVersion": 2,
"lockfileVersion": 3,
"requires": true,
"packages": {
"../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center": {
@@ -15,57 +15,46 @@
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
},
"../../../uni_modules/uni-id/uniCloud/cloudfunctions/common/uni-id/node_modules/uni-config-center": {
"resolved": "../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center",
"link": true
},
"../../../uni_modules/vk-unicloud/uniCloud/cloudfunctions/common/vk-unicloud": {
"version": "2.15.3",
"license": "Apache-2.0"
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
"license": "MIT"
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
"license": "MIT"
},
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"license": "BSD-3-Clause",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
@@ -74,18 +63,15 @@
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"license": "MIT"
},
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmmirror.com/node-ensure/-/node-ensure-0.0.0.tgz",
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw=="
"license": "MIT"
},
"node_modules/pdf-parse": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/pdf-parse/-/pdf-parse-1.1.1.tgz",
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
"license": "MIT",
"dependencies": {
"debug": "^3.1.0",
"node-ensure": "^0.0.0"
-1
View File
@@ -1 +0,0 @@
../../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
{
"name": "uni-config-center",
"version": "0.0.3",
"description": "配置中心",
"main": "index.js",
"keywords": [],
"author": "DCloud",
"license": "Apache-2.0"
}
@@ -0,0 +1,169 @@
[
{
"dcloudAppid": "__UNI__8BFD66E",
"isDefaultConfig": true,
"passwordSecret": "passwordSecret-demo",
"tokenSecret": "tokenSecret-demo",
"tokenExpiresIn": 604800,
"tokenExpiresThreshold": 259200,
"tokenMaxLimit": 10,
"passwordErrorLimit": 6,
"bindTokenToDevice": false,
"passwordErrorRetryTime": 3600,
"autoSetInviteCode": true,
"forceInviteCode": false,
"preferedAppPlatform": "app-plus",
"preferedWebPlatform": "h5",
"removeDcloudAppid": false,
"app-plus": {
"tokenExpiresIn": 604800,
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-weixin": {
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"h5-weixin": {
"oauth": {
"weixin": {
"appid": "wxf44c1b32124fe1d5",
"appsecret": "d98e9f42513cca4cc63e7c462886e839"
}
}
},
"h5": {
"oauth": {
"weixin": {
"appid": "wxf44c1b32124fe1d5",
"appsecret": "d98e9f42513cca4cc63e7c462886e839"
},
"qq": {
"appid": "101795826",
"appsecret": "1a41633966fb97cefea512f6c8601638"
}
}
},
"mp-alipay": {
"oauth": {
"alipay": {
"appid": "",
"privateKey": ""
}
}
},
"mp-qq": {
"oauth": {
"qq": {
"appid": "",
"appsecret": ""
}
}
},
"service": {
"sms": {
"name": "重要",
"codeExpiresIn": 180,
"smsKey": "",
"smsSecret": "",
"templateId": ""
},
"univerify": {
"appid": "",
"apiKey": "",
"apiSecret": ""
}
}
},
{
"dcloudAppid": "__UNI__EB80D26",
"passwordSecret": "passwordSecret-demo",
"tokenSecret": "tokenSecret-demo",
"tokenExpiresIn": 604800,
"tokenExpiresThreshold": 259200,
"tokenMaxLimit": 10,
"passwordErrorLimit": 6,
"bindTokenToDevice": false,
"passwordErrorRetryTime": 3600,
"autoSetInviteCode": true,
"forceInviteCode": false,
"preferedAppPlatform": "app-plus",
"preferedWebPlatform": "h5",
"removeDcloudAppid": false,
"app-plus": {
"tokenExpiresIn": 604800,
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"mp-weixin": {
"oauth": {
"weixin": {
"appid": "",
"appsecret": ""
}
}
},
"h5-weixin": {
"oauth": {
"weixin": {
"appid": "wx950c88b951cadd52",
"appsecret": "266aa413275c08d86ab0ffefd79962b7"
}
}
},
"h5": {
"oauth": {
"weixin": {
"appid": "wx950c88b951cadd52",
"appsecret": "266aa413275c08d86ab0ffefd79962b7"
},
"qq": {
"appid": "101795826",
"appsecret": "1a41633966fb97cefea512f6c8601638"
}
}
},
"mp-alipay": {
"oauth": {
"alipay": {
"appid": "",
"privateKey": ""
}
}
},
"mp-qq": {
"oauth": {
"qq": {
"appid": "",
"appsecret": ""
}
}
},
"service": {
"sms": {
"name": "重要",
"codeExpiresIn": 180,
"smsKey": "",
"smsSecret": "",
"templateId": ""
},
"univerify": {
"appid": "",
"apiKey": "",
"apiSecret": ""
}
}
}
]
@@ -0,0 +1,3 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,3 @@
-----BEGIN CERTIFICATE-----
证书内容
-----END CERTIFICATE-----
@@ -0,0 +1,5 @@
### 文件名:
#### 1、`alipayCertPublicKey_RSA2.crt`
#### 2、`alipayRootCert.crt`
#### 3、`appCertPublicKey.crt`
![证书位置](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/61c1582a-2ca7-43d3-92eb-b82136cc583f.png)
@@ -0,0 +1,128 @@
const fs = require('fs');
const path = require('path');
module.exports = {
/**
* 统一支付回调地址,格式为 "服务空间SpaceID":"URL化完整地址"
* 这里的本地开发并不是指 http://localhost:8080/ 的地址,而是另外一个服务空间的ULR化地址(如果你本地开发和线上环境共用同一个服务空间则只需要填线上环境的即可)
* 回调的云函数地址,建议填 /http/vk-pay,因为vk-pay云函数已经写好了回调处理的逻辑,否则你需要自己写全部的回调逻辑。
* 其中vk-pay是可以改名的,只需要修改 uniCloud/cloudfunctions/vk-pay/package.json 文件中的 "path": "/http/vk-pay", 把 /http/vk-pay 改成 /http/xxxx 即可(需要重新上传云函数vk-pay)。
*/
"notifyUrl": {
// 本地开发环境,如果你本地开发和线上环境共用同一个服务空间则只需要填线上环境的即可
"mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5": "https://mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5.bspapp.com/http/vk-pay",
// 线上正式环境
"mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5": "https://fc-mp-0737e52b-fffa-4223-bcdf-d4ad3d9968a5.next.bspapp.com/http/vk-pay"
},
/**
* 微信
* 公共参数说明
* appId 微信后台的appId
* secret 微信后台的secret
* mchId 微信支付的商户id
* key 微信支付的api密钥(V2版本)
* pfx 微信支付的p12证书(退款需要)
*/
"wxpay": {
// 微信 - 小程序支付(微信支付申请JSAPI支付)
"mp-weixin": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - APP支付(微信支付申请APP支付)
"app-plus": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - H5电脑网站二维码支付(微信支付申请Native支付)
"h5": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - 公众号支付(微信支付申请JSAPI支付)
"h5-weixin": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12')
},
// 微信 - 手机外部浏览器H5支付(微信支付申请H5支付)
"mweb": {
"appId": "",
"secret": "",
"mchId": "",
"key": "",
"pfx": fs.readFileSync(__dirname + '/wxpay/wxpay.p12'),
// 场景信息,必填
"sceneInfo": {
"h5_info": {
"type": "Wap", // 此值固定Wap
"wap_url": "https://www.xxxxxx.com", // 你的H5首页地址,必须和你发起支付的页面的域名一致。
"wap_name": "网站名称", // 你的H5网站名称
}
}
}
},
/**
* 支付宝(证书记得选java版本)
* 公共参数说明
* mchId 支付宝商户id 2088开头的那个(此参数可以去除)
* appId 支付宝开放平台的应用appId
* secret 支付宝开放平台的应用secret
* privateKey 应用私钥
* alipayPublicCertPath 支付宝公钥证书路径地址 与之对应的 alipayPublicCertContent 为支付宝公钥证书内容(值可以是字符串也可以是Buffer)
* alipayRootCertPath 支付宝根证书路径地址 与之对应的 alipayRootCertContent 为支付宝根证书内容(值可以是字符串也可以是Buffer)
* appCertPath 应用证书路径地址 与之对应的 appCertPathContent 为应用证书内容(值可以是字符串也可以是Buffer)
* sandbox 是否沙箱模式 true 沙箱模式 false 正常模式
*/
"alipay": {
// 支付宝 - 小程序支付配置
"mp-alipay": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - APP支付配置
"app-plus": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - H5支付配置(包含:网站二维码、手机H5,需申请支付宝当面付接口权限)
"h5": {
"mchId": "",
"appId": "",
"privateKey": "",
"alipayPublicCertPath": path.join(__dirname, 'alipay/alipayCertPublicKey_RSA2.crt'),
"alipayRootCertPath": path.join(__dirname, 'alipay/alipayRootCert.crt'),
"appCertPath": path.join(__dirname, 'alipay/appCertPublicKey.crt'),
"sandbox": false
},
// 支付宝 - 转账到支付宝等资金转出接口,其中 appCertSn 和 alipayRootCertSn 通过工具获取
"transfer": {
"mchId": "",
"appId": "",
"privateKey": "",
"appCertSn": "", // 应用证书的序列号
"alipayRootCertSn": "", // 支付宝根证书的序列号
"sandbox": false
}
}
}
@@ -0,0 +1,3 @@
### 文件名:
#### 1、`wxpay.p12`
![证书位置](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-cf0c5e69-620c-4f3c-84ab-f4619262939f/61c1582a-2ca7-43d3-92eb-b82136cc583f.png)
@@ -0,0 +1,142 @@
var uniIdConfig;
var uniPayConfig;
try {
uniIdConfig = require('../uni-id/config.json');
} catch(err) {
console.error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 编译错误,请检查!↓↓↓请查看下方的错误提示↓↓↓", err.name, err.message, err);
throw new Error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-id/config.json 编译错误,请检查!↑↑↑请查看上方的错误提示↑↑↑");
}
try {
uniPayConfig = require('../uni-pay/config.js');
} catch(err) {
console.error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-pay/config.js 编译错误,请检查!↓↓↓请查看下方的错误提示↓↓↓", err.name, err.message, err);
throw new Error("配置文件:uniCloud/cloudfunctions/common/uni-config-center/uni-pay/config.js 编译错误,请检查!↑↑↑请查看上方的错误提示↑↑↑");
}
module.exports = {
"uni": uniIdConfig,
"uni-pay": uniPayConfig,
"vk": {
"system": {
// 若serviceShutdow:true,则所有云函数无法访问(适用于需要临时关闭后端服务的情况,如迁移数据)
// 注意:本地调试时,需要重新启动本地服务才能生效。
"serviceShutdown": false,
"serviceShutdownDescription": "系统维护中,预计2小时恢复!",
// 云函数默认时区(中国为8)
"targetTimezone": 8
},
// 加密的密钥
"crypto": {
"aes": "dad4c0cb88c7d005bc5e844b8e79a9f0dad4c0cb88c7d005bc5e844b8e79a9f0", // 对称加密的密钥,建议长度在64位-128位(此处建议修改成自己的密钥)
},
// 当 context 内的下面值为空时,赋予默认的值(主要用于解决云函数 URL 后的默认 APPID 问题。
"context": {
"APPID": "", // 默认 dcloud_appid
"PLATFORM": "h5", // 可选项 h5、mp-weixin、app-plus、mp-alipay
"LOCALE": "zh-Hans", // 默认语言
"CLIENTIP":"127.0.0.1", // 默认客户端IP(主要用于解决本地调试无ip的问题)
},
"service": {
// 邮箱发送服务
"email": {
"codeExpiresIn": 180, // 邮件验证码有效期(单位秒)
// qq邮箱参数配置
"qq": {
"host": "smtp.qq.com",
"port": 465,
"secure": true,
"auth": {
"user": "你的邮箱@qq.com",
"pass": "邮箱授权码"
}
},
// 163邮箱配置
"163":{
"host": "smtp.163.com",
"port": 465,
"secure": true,
"auth": {
"user": "",
"pass": ""
}
}
},
// 日志服务
"log": {
// 用户登录日志
"login": {
"status": true // 是否开启用户登录日志
}
},
// 短信服务
"sms": {
// 阿里云短信服务
"aliyun": {
"enable": true, // 是否使用阿里云短信代替unicloud短信发送短信验证码
"accessKeyId": "LTAI4FyY6A493kVYn5ezyAw6", // 短信密钥key
"accessKeySecret": "776MyYZfRb8cQHTe5ji0jBVu1rHfa9", // 短信密钥secret
// "signName": "亿利通", // 默认签名
"signName": "南宁亿利通钢管有限公司", // 默认签名
"templateCode": {
"verifyCode": "SMS_210078693" // 验证码短信模板 - 配合uni-id需要
}
}
},
// 开放平台api
"openapi": {
// 百度开放平台 (主要用于身份证识别,营业执照识别等API)
// API Key申请地址:https://cloud.baidu.com/doc/OCR/s/rk3h7xzck 点击右上角注册
"baidu": {
"appid": "", // 对应的API Key
"appsecret": "" // 对应的Secret Key
}
}
},
"db": {
"unicloud": {
"maxLimit": 1000, // 最大limit限制(目前腾讯云最大1000,阿里云最大1000)
"cancelAddTime": false, // 取消 vk.baseDao.add 时自动生成 _add_time 和 _add_time_str
"cancelAddTimeStr": false, // 仅取消 vk.baseDao.add 时自动生成的 _add_time_str,若已设置 cancelAddTime 为true,则 cancelAddTimeStr 也会强制为true
"getTableData": {
"sortArr": [{ "name": "_add_time", "type": "desc" }], // vk.baseDao.getTableData 默认排序规则 vk.db.unicloud.getTableData.sortArr
}
}
},
// 其他小程序的密钥 当需要多个小程序绑定同一服务空间,并调用小程序服务端API时需要填写 暂只支持微信小程序
"oauth": {
// 微信小程序
"weixin": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
},
// 支付宝
"alipay": {
// 密钥列表
"list": [
{ "appid": "", "privateKey": "" },
{ "appid": "", "privateKey": "" }
]
},
// qq
"qq": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
},
// 抖音
"toutiao": {
// 密钥列表
"list": [
{ "appid": "", "appsecret": "" },
{ "appid": "", "appsecret": "" }
]
}
}
}
};
@@ -0,0 +1,73 @@
## 重要说明
#### 框架目前的配置(不含`uni-id`)在 `cloudfunctions/common/uni-config-center/vk-unicloud/index.js` 中配置
#### `uni-id` 的配置是在 `cloudfunctions/common/uni-config-center/uni-id/config.json` 中配置
#### 每次修改完 配置 中的内容后如果不生效,则尝试以下操作。
```
1. 上传 uni-config-center 公共模块
2. 重新上传 router 云函数
如果是连接本地云函数,则需要重启本地服务才会生效。(hbx的原因)
```
### 由于`uni-id`配置无法打注释,故下方为`uni-id`配置的介绍
```js
{
"passwordSecret": "passwordSecret-demo",// 加密密码所用的密钥,修改会导致所用户之前的密码失效。如一定要修改,请查看https://uniapp.dcloud.io/uniCloud/uni-id?id=modifysecret
"tokenSecret": "tokenSecret-demo", // 生成token所用的密钥,修改会导致所有用户之前的token失效。
"tokenExpiresIn": 604800, // 全平台token过期时间,未指定过期时间的平台会使用此值,604800代表7天
"tokenExpiresThreshold": 3600, // 新增于uni-id 1.1.7版本,checkToken时如果token有效期小于此值则自动获取新token,如果不配置此参数则不开启自动获取新token功能
"passwordErrorLimit": 6, // 密码错误最大重试次数
"bindTokenToDevice": false, // 是否将token和设备绑定,设置为true会进行ua校验
"passwordErrorRetryTime": 3600, // 密码错误重试次数超限之后的冻结时间
"autoSetInviteCode": true, // 是否在用户注册时自动设置邀请码
"forceInviteCode": false, // 是否强制用户注册时必填邀请码,默认为false(需要注意的是目前只有短信验证码注册才可以填写邀请码)
"app-plus": {
"tokenExpiresIn": 604800, // app端 token过期时间
"oauth" : {
// App微信登录所用到的appid、appsecret需要在微信开放平台获取,注意:不是公众平台而是开放平台
"weixin" : {
"appid" : "",
"appsecret" : ""
}
}
},
"mp-weixin": {
"oauth" : {
// 微信小程序登录所用的appid、appsecret需要在对应的小程序管理控制台获取
"weixin" : {
"appid" : "",
"appsecret" : ""
}
}
},
"mp-alipay": {
"oauth" : {
// 支付宝小程序登录用到的appid、privateKey请参考支付宝小程序的文档进行设置或者获取,https://opendocs.alipay.com/open/291/105971#LDsXr
"alipay" : {
"appid" : "",
"privateKey" : ""
}
}
},
"service": {
// unicloud短信
"sms": {
"name": "重要", // 应用名称,对应短信模版的name
"codeExpiresIn": 180, // 验证码过期时间,单位为秒,注意一定要是60的整数倍
"smsKey": "你的smsKey", // 短信密钥key,开通短信服务处可以看到 https://dev.dcloud.net.cn/uniSms
"smsSecret": "你的smsSecret", // 短信密钥secret,开通短信服务处可以看到 https://dev.dcloud.net.cn/uniSms
"templateId":"你的短信模板ID" // 短信模板ID
},
// 一键登录
"univerify": {
"appid":"your appid", // uniapp的appid
"apiKey": "your apiKey", // 一键登录的apiKey
"apiSecret": "your apiSecret" // 一键登录的apiSecret
}
}
}
```
-1
View File
@@ -1 +0,0 @@
../../../../uni_modules/uni-id/uniCloud/cloudfunctions/common/uni-id
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
../../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center
+16
View File
@@ -0,0 +1,16 @@
{
"name": "uni-id",
"version": "3.3.28",
"description": "uni-id for uniCloud",
"main": "index.js",
"homepage": "https://uniapp.dcloud.io/uniCloud/uni-id",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uni-id.git"
},
"author": "",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}
-1
View File
@@ -1 +0,0 @@
../../../../uni_modules/vk-unicloud/uniCloud/cloudfunctions/common/vk-unicloud
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,16 @@
{
"name": "vk-unicloud",
"version": "2.15.3",
"description": "【云函数端SDK】VK云函数路由模式uniCloud开发框架,在router目录下执行 npm i vk-unicloud 进行安装和升级",
"main": "index.js",
"homepage": "https://gitee.com/vk-uni/vk-uni-cloud-router.git",
"repository": {
"type": "git",
"url": "git+https://gitee.com/vk-uni/vk-uni-cloud-router.git"
},
"files": [
"dist"
],
"author": "VK",
"license": "Apache-2.0"
}
+18 -112
View File
@@ -1,7 +1,7 @@
{
"name": "router",
"version": "1.0.0",
"lockfileVersion": 2,
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
@@ -29,17 +29,13 @@
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
},
"../../../uni_modules/uni-id/uniCloud/cloudfunctions/common/uni-id/node_modules/uni-config-center": {
"resolved": "../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center",
"link": true
},
"../../../uni_modules/vk-unicloud/uniCloud/cloudfunctions/common/vk-unicloud": {
"version": "2.15.3",
"license": "Apache-2.0"
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"engines": {
"node": "*"
@@ -47,7 +43,7 @@
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"engines": {
"node": "*"
@@ -56,29 +52,22 @@
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
"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",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"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",
"resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz",
"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",
"resolved": "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dependencies": {
"charenc": "0.0.2",
@@ -86,26 +75,24 @@
"is-buffer": "~1.1.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmmirror.com/node-ensure/-/node-ensure-0.0.0.tgz",
"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",
"resolved": "https://registry.npmmirror.com/pdf-parse/-/pdf-parse-1.1.1.tgz",
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
"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": {
@@ -120,86 +107,5 @@
"resolved": "../../../uni_modules/vk-unicloud/uniCloud/cloudfunctions/common/vk-unicloud",
"link": true
}
},
"dependencies": {
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="
},
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"md5": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"requires": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmmirror.com/node-ensure/-/node-ensure-0.0.0.tgz",
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw=="
},
"pdf-parse": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/pdf-parse/-/pdf-parse-1.1.1.tgz",
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
"requires": {
"debug": "^3.1.0",
"node-ensure": "^0.0.0"
}
},
"uni-config-center": {
"version": "file:../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
},
"uni-id": {
"version": "file:../../../uni_modules/uni-id/uniCloud/cloudfunctions/common/uni-id",
"requires": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
},
"dependencies": {
"uni-config-center": {
"version": "file:../../../uni_modules/uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}
},
"vk-unicloud": {
"version": "file:../../../uni_modules/vk-unicloud/uniCloud/cloudfunctions/common/vk-unicloud"
}
}
}
@@ -23,7 +23,7 @@
"concurrency": 1,
"memorySize": 512,
"path": "/http/router",
"timeout": 60,
"timeout": 600,
"triggers": [],
"keepRunningAfterReturn": false
},
@@ -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) {
@@ -0,0 +1,38 @@
'use strict';
module.exports = {
/**
* 积分流水列表(所有用户)
* @url admin/points/sys/getList 前端调用的url参数地址
* data 请求参数说明
* @param {Number} pageIndex 当前页码
* @param {Number} pageSize 每页显示数量
* @param {Object} formData 查询条件数据源
* @param {Array} columns 查询条件规则
*/
main: async (event) => {
let { data = {}, util } = event;
let { vk, _ } = util;
let res = { code: 0, msg: '' };
// 业务逻辑开始-----------------------------------------------------------
res = await vk.baseDao.getTableData({
dbName: "jjx-points-log",
data,
whereJson: {},
sortArr: [{ name: "_add_time", type: "desc" }],
foreignDB: [{
dbName: "uni-id-users",
localKey: "user_id",
foreignKey: "_id",
as: "userInfo",
limit: 1,
fieldJson: {
nickname: true,
username: true,
mobile: true
}
}]
});
// 业务逻辑结束-----------------------------------------------------------
return res;
}
};
@@ -95,14 +95,14 @@ module.exports = {
const num = Number(value);
return Number.isNaN(num) ? defaultValue : num;
};
if (data.formData && data.formData.isSignOnly && res.rows && res.rows.length > 0) {
if (res.rows && res.rows.length > 0) {
const userIds = res.rows.map(item => item._id).filter(Boolean);
const qybStatMap = {};
userIds.forEach(userId => {
qybStatMap[userId] = {
qybBuyNum: 0,
qybUsedNum: 0,
qybGiftNum: 0,
qybPackageGiftNum: 0,
qybTotalGetNum: 0
};
});
@@ -174,14 +174,18 @@ module.exports = {
}
Object.keys(qybStatMap).forEach(userId => {
qybStatMap[userId].qybGiftNum = Math.max(qybStatMap[userId].qybTotalGetNum - qybStatMap[userId].qybBuyNum, 0);
qybStatMap[userId].qybPackageGiftNum = Math.max(qybStatMap[userId].qybTotalGetNum - qybStatMap[userId].qybBuyNum, 0);
});
res.rows.forEach(item => {
const qybStat = qybStatMap[item._id] || {};
const adminGiftNum = toNum(item.qybTotalGiftNum, 0);
// 用户表上的 qybGiftNum 为后台赠送累加值;套餐优惠政策赠送从订单统计
const storedGiftNum = toNum(item.qybGiftNum, 0);
item.qybBuyNum = qybStat.qybBuyNum || 0;
item.qybUsedNum = qybStat.qybUsedNum || 0;
item.qybNum = toNum(item.qybNum, 0);
item.qybGiftNum = qybStat.qybGiftNum || 0;
item.qybGiftNum = (qybStat.qybPackageGiftNum || 0) + storedGiftNum;
item.qybTotalGiftNum = adminGiftNum || storedGiftNum;
});
}
}
@@ -0,0 +1,46 @@
'use strict';
module.exports = {
/**
* 后台赠送签约宝次数
* @url admin/system/user/sys/giftQybNum
* @param {String} user_id 用户ID
* @param {Number} giftNum 赠送次数
*/
main: async (event) => {
let { data = {}, util } = event;
let { vk, _ } = util;
let res = { code: 0, msg: '' };
const { user_id, giftNum } = data;
const num = Number(giftNum);
if (!user_id) {
return { code: -1, msg: '用户ID不能为空' };
}
if (!Number.isInteger(num) || num <= 0) {
return { code: -1, msg: '赠送次数必须为正整数' };
}
const user = await vk.baseDao.findById({
dbName: 'uni-id-users',
id: user_id
});
if (!user) {
return { code: -1, msg: '用户不存在' };
}
// 剩余签约次数、优惠政策赠送次数、累计赠送次数同步累加
await vk.baseDao.updateById({
dbName: 'uni-id-users',
id: user_id,
dataJson: {
qybNum: _.inc(num),
qybGiftNum: _.inc(num),
qybTotalGiftNum: _.inc(num)
}
});
res.msg = '赠送成功';
return res;
}
};
@@ -0,0 +1,922 @@
'use strict';
/**
* 合同审查报告 → Word(.docx) 生成器
*
* 数据来源:智能体在 HTML 报告末尾输出的结构化 JSON
* <script type="application/json" id="jjx-report-data">{...}</script>
*
* 排版目标:正式法律文书风格,A4 纵向,可直接打印装订
*/
const {
Document,
Packer,
Paragraph,
TextRun,
Table,
TableRow,
TableCell,
WidthType,
AlignmentType,
BorderStyle,
ShadingType,
TableLayoutType,
Footer,
Header,
PageNumber,
VerticalAlign,
ExternalHyperlink
} = require('docx');
const FONT = 'Microsoft YaHei';
const FONT_SONG = 'SimSun';
const C = {
primary: '1A5276',
danger: 'E74C3C',
success: '27AE60',
warning: 'F39C12',
text: '222222',
gray: '777777',
lightBg: 'F5F7F9',
pinkBg: 'FDF2F2',
border: 'D0D7DE'
};
const PAGE_BORDER = {
top: { style: BorderStyle.SINGLE, size: 4, color: C.border },
bottom: { style: BorderStyle.SINGLE, size: 4, color: C.border },
left: { style: BorderStyle.SINGLE, size: 4, color: C.border },
right: { style: BorderStyle.SINGLE, size: 4, color: C.border },
insideHorizontal: { style: BorderStyle.SINGLE, size: 4, color: C.border },
insideVertical: { style: BorderStyle.SINGLE, size: 4, color: C.border }
};
// ---------- 基础元件 ----------
function run(text, o) {
o = o || {};
const opt = {
text: String(text === null || text === undefined ? '' : text),
font: o.font || FONT,
size: o.size || 21,
bold: !!o.bold,
italics: !!o.italics,
color: o.color || C.text
};
if (o.underline) opt.underline = {};
return new TextRun(opt);
}
function para(children, o) {
o = o || {};
const opt = {
children: Array.isArray(children) ? children : [children],
spacing: {
before: o.before === undefined ? 60 : o.before,
after: o.after === undefined ? 60 : o.after,
line: o.line || 320
}
};
if (o.alignment) opt.alignment = o.alignment;
if (o.indent) opt.indent = o.indent;
if (o.border) opt.border = o.border;
if (o.shading) opt.shading = { type: ShadingType.CLEAR, fill: o.shading, color: 'auto' };
return new Paragraph(opt);
}
function cell(children, o) {
o = o || {};
const opt = {
children: Array.isArray(children) ? children : [children],
verticalAlign: VerticalAlign.CENTER,
margins: { top: 60, bottom: 60, left: 110, right: 110 }
};
if (o.shading) opt.shading = { type: ShadingType.CLEAR, fill: o.shading, color: 'auto' };
if (o.width) opt.width = { size: o.width, type: WidthType.PERCENTAGE };
if (o.columnSpan) opt.columnSpan = o.columnSpan;
return new TableCell(opt);
}
function table(rows, widths) {
return new Table({
rows: rows,
width: { size: 100, type: WidthType.PERCENTAGE },
columnWidths: widths,
layout: TableLayoutType.AUTOFIT,
borders: PAGE_BORDER
});
}
function sectionTitle(no, text) {
return para([run(no + '、' + text, { size: 28, bold: true, color: C.primary })], {
before: 320,
after: 140,
border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: C.primary, space: 4 } }
});
}
function pick(obj, keys, dft) {
if (!obj) return dft;
for (let i = 0; i < keys.length; i++) {
const v = obj[keys[i]];
if (v !== undefined && v !== null && v !== '') return v;
}
return dft;
}
function asArray(v) {
if (!v) return [];
return Array.isArray(v) ? v : [v];
}
// ---------- 风险条目 ----------
function riskBlock(item, color, levelText) {
const clause = pick(item, ['clause', 'no', '条款', 'clauseNo'], '');
const issue = pick(item, ['issue', 'risk', 'problem', '描述', '问题'], '');
const suggest = pick(item, ['suggestion', 'fix', 'advice', '建议', '修改建议'], '');
const basis = pick(item, ['legal_basis', 'legalBasis', 'basis', '法律依据'], '');
const blocks = [];
blocks.push(para([
run('[' + levelText + ']', { bold: true, color: color, size: 21 }),
run(clause ? ' 第 ' + clause + ' 条' : ' 合同条款', { bold: true, color: color, size: 21 })
], { before: 140, after: 40 }));
blocks.push(para([
run('问题:', { bold: true, size: 21 }),
run(issue, { size: 21 })
], { after: 40, indent: { left: 240 } }));
if (suggest) {
blocks.push(para([
run('建议:', { bold: true, size: 21 }),
run(suggest, { size: 21 })
], { after: 40, indent: { left: 240 } }));
}
if (basis) {
blocks.push(para([
run('依据:', { bold: true, size: 21 }),
run(basis, { size: 21, color: C.gray })
], { after: 80, indent: { left: 240 } }));
}
return blocks;
}
function riskSection(items, color, levelText, emptyText) {
const list = asArray(items);
if (!list.length) {
return [para([run(emptyText || '无', { size: 21, color: C.gray })], { after: 80 })];
}
let out = [];
list.forEach(function (it) {
out = out.concat(riskBlock(it, color, levelText));
});
return out;
}
// ---------- 主函数 ----------
async function buildDocx(input) {
const data = input.data || {};
const fileName = input.fileName || '合同审查报告';
const contractName = pick(data, ['contract_name', 'contractName', '协议名称', 'name'], fileName.replace(/\.[^.]+$/, ''));
const reportTitle = pick(data, ['report_title', 'reportTitle', '报告标题'], '合同审查报告');
const reviewDate = pick(data, ['review_date', 'reviewDate', '审核日期'], new Date().toLocaleDateString('zh-CN'));
const summary = pick(data, ['summary', '统计'], {}) || {};
const conclusion = pick(data, ['conclusion', '审核结论', 'summary_text'], '');
const highRisks = pick(data, ['high_risks', 'highRisks', '高风险'], []);
const mediumRisks = pick(data, ['medium_risks', 'mediumRisks', '中风险'], []);
const lowRisks = pick(data, ['low_risks', 'lowRisks', '低风险'], []);
const suggestions = pick(data, ['suggestions', '优化建议', 'advice_table'], []);
const templates = pick(data, ['clause_templates', 'clauseTemplates', '补充条款'], []);
const legalBases = pick(data, ['legal_bases', 'legalBases', '法律依据'], []);
const cases = pick(data, ['cases', '案例'], []);
const disclaimer = pick(data, ['disclaimer', '免责声明'], '');
const nCompliant = pick(summary, ['compliant', '合规'], 0);
const nPartial = pick(summary, ['partial', '部分合规'], 0);
const nHigh = pick(summary, ['high', '高风险'], asArray(highRisks).length);
const nMedium = pick(summary, ['medium', '中风险'], asArray(mediumRisks).length);
const children = [];
// ===== 抬头 =====
children.push(para([run(reportTitle, { size: 44, bold: true, color: C.primary })], {
alignment: AlignmentType.CENTER,
before: 200,
after: 120
}));
children.push(para([run(contractName, { size: 26, color: C.gray })], {
alignment: AlignmentType.CENTER,
after: 60
}));
children.push(para([run('审核日期:' + reviewDate + ' 出具机构:金久信签约宝', { size: 20, color: C.gray })], {
alignment: AlignmentType.CENTER,
after: 240
}));
// ===== 目录 =====
children.push(sectionTitle('一', '报告目录'));
const tocItems = [
'二、审核结论',
'三、需处理问题(高风险 / 中风险)',
'四、优化建议汇总表',
'五、补充条款模板',
'六、法律依据',
'七、相关案例',
'八、免责声明'
];
tocItems.forEach(function (item) {
children.push(para([run(item, { size: 21 })], { after: 30, indent: { left: 240 } }));
});
// ===== 审核结论 =====
children.push(sectionTitle('二', '审核结论'));
const statRow = new TableRow({
children: [
cell([para([run(String(nCompliant), { size: 40, bold: true, color: C.success })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('合规条款', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'F0F9EB', width: 25 }),
cell([para([run(String(nPartial), { size: 40, bold: true, color: C.warning })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('部分合规', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'FDF6EC', width: 25 }),
cell([para([run(String(nHigh), { size: 40, bold: true, color: C.danger })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('高风险', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'FEF0F0', width: 25 }),
cell([para([run(String(nMedium), { size: 40, bold: true, color: C.warning })], { alignment: AlignmentType.CENTER, before: 40, after: 20 }),
para([run('中风险', { size: 18, color: C.gray })], { alignment: AlignmentType.CENTER, after: 40 })],
{ shading: 'FDF6EC', width: 25 })
]
});
children.push(table([statRow], [2000, 2000, 2000, 2000]));
if (conclusion) {
children.push(para([run('审核结论', { size: 21, bold: true, color: C.primary })], { before: 200, after: 80 }));
String(conclusion).split(/\n+/).forEach(function (line) {
if (line.trim()) {
children.push(para([run(line.trim(), { size: 21 })], { after: 60, indent: { left: 240 } }));
}
});
}
// ===== 需处理问题 =====
children.push(sectionTitle('三', '需处理问题'));
children.push(para([run('(一)高风险问题', { size: 24, bold: true, color: C.danger })], { before: 140, after: 80 }));
riskSection(highRisks, C.danger, '高风险', '本次审查未发现高风险问题。').forEach(function (p) { children.push(p); });
children.push(para([run('(二)中风险问题', { size: 24, bold: true, color: C.warning })], { before: 200, after: 80 }));
riskSection(mediumRisks, C.warning, '中风险', '本次审查未发现中风险问题。').forEach(function (p) { children.push(p); });
if (asArray(lowRisks).length) {
children.push(para([run('(三)低风险问题', { size: 24, bold: true, color: C.gray })], { before: 200, after: 80 }));
riskSection(lowRisks, C.gray, '低风险', '无').forEach(function (p) { children.push(p); });
}
// ===== 优化建议汇总表 =====
children.push(sectionTitle('四', '优化建议汇总表'));
const sugList = asArray(suggestions);
if (!sugList.length) {
children.push(para([run('无', { size: 21, color: C.gray })]));
} else {
const headRow = new TableRow({
tableHeader: true,
children: [
cell([para([run('优先级', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 12 }),
cell([para([run('条款编号', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 15 }),
cell([para([run('问题描述', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 38 }),
cell([para([run('建议方案', { bold: true, color: 'FFFFFF', size: 20 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 35 })
]
});
const rows = [headRow];
sugList.forEach(function (s, i) {
const lv = String(pick(s, ['priority', 'level', '优先级'], 'M')).toUpperCase();
const lvBg = lv === 'H' ? C.danger : (lv === 'L' ? '999999' : C.warning);
rows.push(new TableRow({
children: [
cell([para([run(lv, { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: lvBg, width: 12 }),
cell([para([run(pick(s, ['clause', 'no', '条款编号'], '-'), { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 15 }),
cell([para([run(pick(s, ['issue', 'problem', '问题描述', '描述'], '-'), { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 38 }),
cell([para([run(pick(s, ['solution', 'suggestion', '建议方案', '建议'], '-'), { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 35 })
]
}));
});
children.push(table(rows, [1200, 1500, 3800, 3500]));
}
// ===== 补充条款模板 =====
children.push(sectionTitle('五', '补充条款模板'));
const tplList = asArray(templates);
if (!tplList.length) {
children.push(para([run('无', { size: 21, color: C.gray })]));
} else {
tplList.forEach(function (t, i) {
const title = pick(t, ['title', 'name', '条款名称'], '补充条款 ' + (i + 1));
const content = pick(t, ['content', 'text', '条文', '内容'], '');
children.push(para([run(String(i + 1) + '. ' + title, { size: 21, bold: true, color: C.primary })], { before: 140, after: 60 }));
String(content).split(/\n+/).forEach(function (line) {
if (line.trim()) {
children.push(para([run(line.trim(), { size: 20, font: FONT_SONG })], {
after: 50,
indent: { left: 240 },
shading: C.lightBg,
border: { left: { style: BorderStyle.SINGLE, size: 12, color: C.primary, space: 6 } }
}));
}
});
});
}
// ===== 法律依据 =====
children.push(sectionTitle('六', '法律依据'));
const lawList = asArray(legalBases);
if (!lawList.length) {
children.push(para([run('本报告未引用具体法条,相关意见为实务建议,具体法律适用需律师人工核实。', { size: 20, color: C.gray })]));
} else {
lawList.forEach(function (l, i) {
const law = pick(l, ['law', 'name', '法律名称'], '');
const article = pick(l, ['article', 'no', '条款编号'], '');
const content = pick(l, ['content', 'text', '引用内容', '内容'], '');
children.push(para([
run(String(i + 1) + '. ', { size: 20, bold: true }),
run(law + (article ? ' ' + article : ''), { size: 20, bold: true, color: C.primary }),
run(content ? ':' + content : '', { size: 20 })
], { after: 60, indent: { left: 120 } }));
});
}
// ===== 相关案例 =====
children.push(sectionTitle('七', '相关案例'));
const caseList = asArray(cases);
if (!caseList.length) {
children.push(para([run('暂未检索到公开案例,建议人工核实。', { size: 20, color: C.gray })]));
} else {
const caseHead = new TableRow({
tableHeader: true,
children: [
cell([para([run('序号', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 8 }),
cell([para([run('案件名称·案由', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 26 }),
cell([para([run('法院·机构', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 14 }),
cell([para([run('年份', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 8 }),
cell([para([run('判决要点', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 28 }),
cell([para([run('与风险点关联', { bold: true, color: 'FFFFFF', size: 19 })], { alignment: AlignmentType.CENTER })], { shading: C.primary, width: 16 })
]
});
const caseRows = [caseHead];
caseList.forEach(function (c, i) {
const name = pick(c, ['name', 'title', '案件名称'], '-');
const url = pick(c, ['url', 'link', '链接'], '');
const court = pick(c, ['court', 'org', '法院'], '-');
const year = pick(c, ['year', '年份'], '-');
const result = pick(c, ['result', 'point', '判决要点', '要点'], '-');
const relation = pick(c, ['relation', '关联', '与本次风险点关联'], '-');
const nameCellChildren = [para([run(name, { size: 19, bold: true })], { after: url ? 20 : 0 })];
if (url) {
const host = url.replace(/^https?:\/\//i, '').split('/')[0];
nameCellChildren.push(para([
new ExternalHyperlink({
link: url,
children: [run('来源:' + host + ' ↗', { size: 16, color: '2E74B5', underline: true })]
})
], {}));
}
caseRows.push(new TableRow({
children: [
cell([para([run(String(i + 1), { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 8 }),
cell(nameCellChildren, { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 26 }),
cell([para([run(court, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 14 }),
cell([para([run(year, { size: 19 })], { alignment: AlignmentType.CENTER })], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 8 }),
cell([para([run(result, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 28 }),
cell([para([run(relation, { size: 19 })])], { shading: i % 2 ? 'F7F9FA' : 'FFFFFF', width: 16 })
]
}));
});
children.push(table(caseRows, [700, 2300, 1300, 700, 2500, 1500]));
}
// ===== 免责声明 =====
children.push(sectionTitle('八', '免责声明'));
const disText = disclaimer || '本报告由人工智能根据用户上传的合同文本自动生成,仅供内部风险排查参考,不构成正式法律意见,亦不能替代执业律师的专业审查。涉及重大交易、争议解决或诉讼事项,请务必委托执业律师复核。报告中的法律法规引用以官方公布文本为准,案例信息来源于公开检索结果,具体适用请人工核实。';
String(disText).split(/\n+/).forEach(function (line) {
if (line.trim()) {
children.push(para([run(line.trim(), { size: 19, color: C.gray })], {
after: 50,
shading: C.pinkBg,
indent: { left: 120, right: 120 },
border: { left: { style: BorderStyle.SINGLE, size: 12, color: C.danger, space: 6 } }
}));
}
});
// ===== 文档 =====
const doc = new Document({
creator: '金久信签约宝',
title: contractName + ' - 合同审查报告',
description: 'AI 合同审查报告',
styles: {
default: {
document: {
run: { font: FONT, size: 21, color: C.text }
}
}
},
sections: [{
properties: {
page: {
margin: { top: 1134, right: 1134, bottom: 1134, left: 1134 }
}
},
headers: {
default: new Header({
children: [
para([
run('金久信签约宝 · AI 合同审查报告', { size: 18, color: C.gray }),
run(' ' + contractName, { size: 18, color: C.gray })
], { alignment: AlignmentType.RIGHT, after: 0 })
]
})
},
footers: {
default: new Footer({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({ text: '第 ', font: FONT, size: 18, color: C.gray }),
new TextRun({ children: [PageNumber.CURRENT], font: FONT, size: 18, color: C.gray }),
new TextRun({ text: ' 页 / 共 ', font: FONT, size: 18, color: C.gray }),
new TextRun({ children: [PageNumber.TOTAL_PAGES], font: FONT, size: 18, color: C.gray }),
new TextRun({ text: ' 页', font: FONT, size: 18, color: C.gray })
]
})
]
})
},
children: children
}]
});
return await Packer.toBuffer(doc);
}
// ---------- HTML 解析(生产数据兜底通道) ----------
// 智能体实际返回的是纯 HTML 报告,未必带 jjx-report-data 数据块,
// 这里按区块结构把 HTML 还原成结构化数据,无需修改智能体提示词。
function decodeEntities(s) {
return String(s || '')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;|&apos;/g, "'")
.replace(/&amp;/g, '&')
.replace(/&#(\d+);/g, function (m, d) { return String.fromCharCode(Number(d)); })
.replace(/&#x([0-9a-fA-F]+);/g, function (m, h) { return String.fromCharCode(parseInt(h, 16)); });
}
function stripTags(html) {
return decodeEntities(
String(html || '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|tr|h\d|section)>/gi, '\n')
.replace(/<li[^>]*>/gi, '\n')
.replace(/<[^>]+>/g, '')
)
.replace(/[ \t\u00a0]+/g, ' ')
.replace(/\n[ ]+/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function pickDiv(html, cls) {
const re = new RegExp('<div[^>]*class="[^"]*\\b' + cls + '\\b[^"]*"[^>]*>([\\s\\S]*?)<\\/div>', 'i');
const m = String(html || '').match(re);
return m ? stripTags(m[1]) : '';
}
// 按 <h2> 把报告切成区块,返回 { 区块名: 区块内HTML }
// 兼容:智能体偶尔用 <h3> 代替 <h2> 做区块标题,h2 不足 2 个时降级按 h3 切
function splitByH2(html) {
const map = {};
const h2Count = (html.match(/<h2[^>]*>/gi) || []).length;
const re = h2Count >= 2
? /<h2[^>]*>([\s\S]*?)<\/h2>/gi
: /<h3[^>]*>([\s\S]*?)<\/h3>/gi;
const marks = [];
let m;
while ((m = re.exec(html))) {
marks.push({ name: stripTags(m[1]).replace(/\s+/g, ''), start: m.index, contentStart: re.lastIndex });
}
for (let i = 0; i < marks.length; i++) {
const end = i + 1 < marks.length ? marks[i + 1].start : html.length;
map[marks[i].name] = html.slice(marks[i].contentStart, end);
}
return map;
}
function findSection(map, keywords) {
const keys = Object.keys(map);
for (let i = 0; i < keys.length; i++) {
for (let j = 0; j < keywords.length; j++) {
if (keys[i].indexOf(keywords[j]) >= 0) return map[keys[i]];
}
}
return '';
}
function parseTable(tableHtml) {
const rows = [];
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let m;
while ((m = trRe.exec(tableHtml))) {
const cells = [];
const tdRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
let c;
while ((c = tdRe.exec(m[1]))) cells.push(stripTags(c[1]));
if (cells.length) rows.push(cells);
}
return rows;
}
function extractTables(html) {
const tables = [];
const re = /<table[^>]*>([\s\S]*?)<\/table>/gi;
let m;
while ((m = re.exec(html))) tables.push(parseTable(m[1]));
return tables;
}
// 案例表:需要在行内同时取出单元格文本和 a[href](按行取,避免跨行错位)
function parseRowsWithLinks(tableHtml) {
const rows = [];
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let m;
while ((m = trRe.exec(tableHtml))) {
const tr = m[1];
const cells = [];
let url = '';
const tdRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
let c;
while ((c = tdRe.exec(tr))) {
cells.push(stripTags(c[1]));
if (!url) {
const am = c[1].match(/<a[^>]*href="([^"]+)"/i);
if (am) url = am[1];
}
}
if (cells.length) rows.push({ cells: cells, url: url });
}
return rows;
}
function shortClause(text) {
const t = String(text || '').trim();
if (!t) return '-';
if (/^[0-9.]+$/.test(t)) return t;
return t.length > 16 ? t.slice(0, 16) + '…' : t;
}
function levelOf(text, tagCls) {
if (/高/.test(text)) return 'high';
if (/中/.test(text)) return 'medium';
if (/低/.test(text)) return 'low';
if (tagCls) return tagCls.toLowerCase();
return '';
}
function collectRiskItems(body, level, out) {
const chunks = String(body || '').split(/<div[^>]*class="[^"]*\brisk-item\b[^"]*"[^>]*>/i).slice(1);
chunks.forEach(function (chunk) {
// 级别标签两种写法:<span class="risk-tag high"> 或 <span class="tag h">
const tagMatch = chunk.match(/<span[^>]*class="[^"]*risk-tag\s+(high|medium|low)[^"]*"[^>]*>/i)
|| chunk.match(/<span[^>]*class="[^"]*tag\s+(high|medium|low|h|m|l)\b[^"]*"[^>]*>/i);
const tagRaw = tagMatch ? tagMatch[1].toLowerCase() : '';
const tagLv = tagRaw === 'h' ? 'high' : tagRaw === 'm' ? 'medium' : tagRaw === 'l' ? 'low' : tagRaw;
// 优先级:所在区块标题 > 条目自身标签 > 默认 medium
const lv = level || tagLv || 'medium';
// 字段名两种写法:risk-clause/risk-desc/risk-fix 或 clause/desc/fix
let clauseText = pickDiv(chunk, 'risk-clause') || pickDiv(chunk, 'clause');
clauseText = clauseText.replace(/^[HML]\s*/, '').trim();
const clauseNum = clauseText.match(/条款\s*([0-9.]+)/);
const clause = shortClause(clauseNum ? clauseNum[1] : clauseText);
const issue = pickDiv(chunk, 'risk-desc') || pickDiv(chunk, 'desc');
let fix = (pickDiv(chunk, 'risk-fix') || pickDiv(chunk, 'fix')).replace(/^\s*(修复建议|建议)[::]\s*/, '');
if (!clauseText && !issue) return;
const item = {
level: lv,
clause: clause || '-',
issue: issue || clauseText || '-',
suggestion: fix || ''
};
// 条款标题里的补充信息(如“交付清单、全部附件空白”)拼进问题描述,避免丢信息
if (clauseNum && clauseText) {
const rest = clauseText.replace(/条款\s*[0-9.]+\s*/, '').trim();
if (rest && item.issue.indexOf(rest) < 0) item.issue = rest + ':' + item.issue;
}
if (!out[lv]) out[lv] = [];
out[lv].push(item);
});
}
function parseRisks(sectionHtml, hint) {
const out = { high: [], medium: [], low: [] };
const parts = String(sectionHtml || '').split(/<h3[^>]*>/i);
if (parts.length > 1) {
parts.slice(1).forEach(function (part) {
const headEnd = part.search(/<\/h3>/i);
const headText = headEnd >= 0 ? stripTags(part.slice(0, headEnd)) : '';
const body = headEnd >= 0 ? part.slice(headEnd + 5) : part;
const lv = levelOf(headText, '') || hint || '';
collectRiskItems(body, lv, out);
});
} else {
collectRiskItems(sectionHtml, hint || '', out);
}
return out;
}
/**
* 从智能体生成的真实 HTML 报告里还原结构化数据
*/
function parseHtmlReport(html) {
if (!html || typeof html !== 'string') return null;
if (html.indexOf('<') < 0) return null;
const raw = html
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<script[\s\S]*?<\/script>/gi, '');
const data = {};
// 标题 / 合同名 / 日期
const h1 = raw.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
const reportTitle = h1 ? stripTags(h1[1]) : '';
const nameM = raw.match(/合同名称[::]\s*([^<\n]+)/);
const dateM = raw.match(/审核日期[::]\s*([^<\n]+)/);
const titleM = raw.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
data.contract_name = nameM ? nameM[1].trim()
: (reportTitle || (titleM ? stripTags(titleM[1]) : '')).replace(/审查报告$/, '').trim();
data.report_title = reportTitle || '';
data.review_date = dateM ? dateM[1].trim() : '';
// 统计卡片:按 num + label 配对抓取
const summary = {};
function applyCard(numRaw, labelRaw) {
const num = stripTags(numRaw).trim();
const label = stripTags(labelRaw).replace(/\s+/g, '');
if (!label) return;
const val = /^[0-9]+$/.test(num) ? Number(num) : num;
if (label.indexOf('部分') >= 0) summary.partial = val;
else if (label.indexOf('高') >= 0) summary.high = val;
else if (label.indexOf('中') >= 0) summary.medium = val;
else if (label.indexOf('低') >= 0) summary.low = val;
else if (label.indexOf('合规') >= 0) summary.compliant = val;
}
// 写法一:label 有 class="label"
const cardRe = /<div[^>]*class="[^"]*\bnum\b[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<div[^>]*class="[^"]*\blabel\b[^"]*"[^>]*>([\s\S]*?)<\/div>/gi;
let cm;
while ((cm = cardRe.exec(raw))) applyCard(cm[1], cm[2]);
// 写法二:label 是紧跟的无 class <div>
if (!Object.keys(summary).length) {
const cardRe2 = /<div[^>]*class="[^"]*\bnum\b[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<div[^>]*>([\s\S]*?)<\/div>/gi;
while ((cm = cardRe2.exec(raw))) applyCard(cm[1], cm[2]);
}
data.summary = summary;
const sections = splitByH2(raw);
// 审核结论
const conclusionHtml = findSection(sections, ['审核结论']);
data.conclusion = stripTags(conclusionHtml).replace(/\n{2,}/g, '\n');
// 需处理问题:兼容两种结构
// ① 单一「需处理问题」总区块(内含高风险/中风险小节)
// ② 「高风险问题」「中风险问题」各自独立成区块
const riskKeywords = ['需处理问题', '风险问题', '问题清单', '高风险', '中风险', '低风险'];
const risks = { high: [], medium: [], low: [] };
Object.keys(sections).forEach(function (key) {
const hit = riskKeywords.some(function (k) { return key.indexOf(k) >= 0; });
if (!hit) return;
const part = parseRisks(sections[key], levelOf(key, ''));
['high', 'medium', 'low'].forEach(function (lv) {
risks[lv] = risks[lv].concat(part[lv] || []);
});
});
data.high_risks = risks.high;
data.medium_risks = risks.medium;
data.low_risks = risks.low;
// 优化建议汇总表
const sugHtml = findSection(sections, ['优化建议', '建议汇总']);
const sugTables = extractTables(sugHtml);
const suggestions = [];
if (sugTables.length) {
sugTables[0].forEach(function (row, i) {
if (i === 0) return;
if (/优先级|条款编号/.test(row.join(''))) return;
suggestions.push({
priority: (row[0] || '').replace(/\s+/g, '').slice(0, 1).toUpperCase() || 'M',
clause: row[1] || '-',
issue: row[2] || '-',
solution: row[3] || '-'
});
});
}
data.suggestions = suggestions;
// 补充条款模板(部分报告没有该区块)
// 三种写法:<pre> 代码块 / <div class="clause-template"> 模板块 / 整段纯文本
const tplHtml = findSection(sections, ['补充条款']);
const templates = [];
if (tplHtml) {
const pres = tplHtml.match(/<pre[^>]*>([\s\S]*?)<\/pre>/gi) || [];
pres.forEach(function (p, i) {
templates.push({ title: '补充条款 ' + (i + 1), content: stripTags(p) });
});
if (!templates.length) {
// 模板块:class="clause-template",标题取内部 <strong>【...】</strong>
const tplDivs = tplHtml.match(/<div[^>]*class="[^"]*\bclause-template\b[^"]*"[^>]*>([\s\S]*?)<\/div>/gi) || [];
tplDivs.forEach(function (d, i) {
const text = stripTags(d).trim();
if (!text) return;
const tm = d.match(/<strong[^>]*>([\s\S]*?)<\/strong>/i);
const title = tm ? stripTags(tm[1]).replace(/^【|】$/g, '').trim() : '';
templates.push({ title: title || '补充条款 ' + (i + 1), content: text.replace(/^(【[^】]*】)/, '').trim() });
});
}
if (!templates.length) {
const text = stripTags(tplHtml);
if (text) templates.push({ title: '补充条款', content: text });
}
}
data.clause_templates = templates;
// 法律依据
const lawHtml = findSection(sections, ['法律依据', '法律法规']);
const legalBases = [];
const liRe = /<li[^>]*>([\s\S]*?)<\/li>/gi;
let lm;
while ((lm = liRe.exec(lawHtml))) {
const text = stripTags(lm[1]);
if (!text) continue;
const seg = text.match(/^(.+?第[0-9]+条[之]*[一二三四五六七八九十]*)[::]\s*([\s\S]*)$/);
if (seg) legalBases.push({ law: seg[1], article: '', content: seg[2] });
else legalBases.push({ law: '', article: '', content: text });
}
if (!legalBases.length) {
const text = stripTags(lawHtml);
if (text) {
text.split(/\n+/).forEach(function (line) {
line = line.trim();
if (!line) return;
const seg = line.match(/^(.+?第[0-9]+条[之]*[一二三四五六七八九十]*)[::]\s*([\s\S]*)$/);
if (seg) legalBases.push({ law: seg[1], article: '', content: seg[2] });
else legalBases.push({ law: '', article: '', content: line });
});
}
}
data.legal_bases = legalBases;
// 案例
const caseHtml = findSection(sections, ['案例', '判例']);
const caseTableMatch = caseHtml.match(/<table[^>]*>([\s\S]*?)<\/table>/i);
const cases = [];
if (caseTableMatch) {
const rows = parseRowsWithLinks(caseTableMatch[1]);
rows.forEach(function (r) {
const row = r.cells;
if (!row.length) return;
if (/案件名称/.test(row.join('')) || /^序号$/.test(row[0])) return; // 表头
const hasLinkCol = row.length >= 7;
cases.push({
name: row[hasLinkCol ? 1 : 0] || '-',
url: r.url || '',
court: row[hasLinkCol ? 3 : 2] || '-',
year: row[hasLinkCol ? 4 : 3] || '-',
result: row[hasLinkCol ? 5 : 4] || '-',
relation: row[hasLinkCol ? 6 : 5] || '-'
});
});
}
data.cases = cases;
// 免责声明(优先按 h2 区块取,取不到再从全局找 class="disclaimer")
const disHtml = findSection(sections, ['免责声明', '免责']);
let disclaimer = pickDiv(disHtml, 'disclaimer') || stripTags(disHtml);
if (!disclaimer) {
const dm = raw.match(/<div[^>]*class="[^"]*\bdisclaimer\b[^"]*"[^>]*>([\s\S]*?)<\/div>/i);
if (dm) disclaimer = stripTags(dm[1]);
}
data.disclaimer = disclaimer;
// 生成时间(页脚)
const footM = raw.match(/生成时间[::]\s*([^<\n]+)/);
if (footM && !data.review_date) data.review_date = footM[1].trim();
// 完全没解析出内容则视为失败
if (!data.conclusion && !data.high_risks.length && !data.suggestions.length) return null;
return data;
}
/**
* 从智能体回复里提取「插件生成的 Word 文件链接」
* 只认显式标记,避免误抓案例正文里的 .docx 链接
* 1. <a id="jjx-docx-link" href="https://...">下载 Word</a>
* 2. <!-- jjx-docx: https://... -->
*/
function extractDocxLink(answer) {
if (!answer) return '';
const a = answer.match(/<a[^>]*id=["']jjx-docx-link["'][^>]*href=["']([^"']+)["']/i);
if (a && a[1]) return a[1];
const b = answer.match(/<!--\s*jjx-docx\s*[::]\s*(https?:\/\/[^\s>]+)\s*-->/i);
if (b && b[1]) return b[1];
return '';
}
/**
* 判断智能体回复里是否有一份「可以转成 Word 的报告」
*
* 返回 true 的两种情况:
* 1. 智能体插件已经生成了 Word 并给了链接 → 直接复用,不用本地转
* 2. 回复里有完整的 HTML 报告(有 <html> 开头且有 </html> 结尾)
*
* 返回 false 的典型情况:
* - 「很抱歉,当前系统多次尝试读取您上传的DOC格式合同文件均解析失败…」这类道歉/澄清文本
* - HTML 被 max token 截断,缺 </html> —— 强行转会生成一份残缺的 Word,宁可不转
*/
function isDocxConvertible(answer) {
if (!answer || typeof answer !== 'string') return false;
if (extractDocxLink(answer)) return true;
if (!/<!DOCTYPE\s+html|<html[\s>]/i.test(answer)) return false;
return /<\/html>/i.test(answer);
}
/**
* 从智能体返回的 HTML 中提取结构化报告数据
* 优先级:<script id="jjx-report-data"> → ```json 代码块 → HTML 结构解析
*/
function extractReportData(answer) {
if (!answer) return null;
const m = answer.match(/<script[^>]*id=["']jjx-report-data["'][^>]*>([\s\S]*?)<\/script>/i);
if (m && m[1]) {
try {
return JSON.parse(m[1].trim());
} catch (e) {
console.log('[Docx] jjx-report-data JSON 解析失败,改用 HTML 解析:', e.message);
}
}
const blocks = answer.match(/```json\s*([\s\S]*?)```/g) || [];
for (let i = blocks.length - 1; i >= 0; i--) {
try {
const raw = blocks[i].replace(/```json\s*/, '').replace(/```$/, '').trim();
const obj = JSON.parse(raw);
if (obj && (obj.high_risks || obj.highRisks || obj.summary)) return obj;
} catch (e) { /* 继续找 */ }
}
// 兜底:直接解析 HTML 报告结构
try {
const parsed = parseHtmlReport(answer);
if (parsed) {
console.log('[Docx] 已从 HTML 报告解析结构化数据');
return parsed;
}
// 诊断:打印实际 HTML 结构关键特征,定位解析失败原因
const h2s = (answer.match(/<h2[^>]*>([\s\S]*?)<\/h2>/gi) || [])
.map(function (h) { return h.replace(/<[^>]+>/g, '').replace(/\s+/g, '').slice(0, 20); });
console.log('[Docx] HTML 解析为空。诊断 → h2区块[' + h2s.length + ']:',
h2s.length ? h2s.join(' | ') : '无h2标签',
'| 有table:', /<table/i.test(answer),
'| 有risk-item:', /risk-item/i.test(answer),
'| 有审核结论字样:', /审核结论|审查结论/.test(answer),
'| 长度:', answer.length);
} catch (e) {
console.log('[Docx] HTML 报告解析失败:', e.message);
}
return null;
}
module.exports = {
buildDocx,
extractReportData,
parseHtmlReport,
extractDocxLink,
isDocxConvertible
};
@@ -3,9 +3,9 @@ module.exports = {
let { data = {}, userInfo } = event;
const COZE_TOKEN = 'sat_OiXlI4Da5Q99fxmul6pNfExmA6hR898UbxBDwrvjZ638APxRSlFnXbz6zyFedKNG';
const BOT_ID = '7680345813062680618';
// 从前端传入 bot_id,不同项目用不同智能体
const BOT_ID = data.bot_id || '7680345813062680618';
// 使用流式接口,直接从 SSE 事件中提取回答
const res = await uniCloud.httpclient.request('https://api.coze.cn/v3/chat', {
method: 'POST',
dataType: 'text',
@@ -25,25 +25,54 @@ module.exports = {
}
});
// 从 SSE 文本中提取 type=answer 的内容
let answer = '暂时无法回复,请拨打 400-686-5559 咨询';
// 扣子 v3 流式事件说明:
// conversation.message.delta —— 增量片段(应当拼接)
// conversation.message.completed —— 该条消息的「完整内容」(不能再次拼接,否则重复)
// 因此必须按事件类型区分,只累加 delta;completed 仅用于兜底。
let conversationId = '';
const msgMap = new Map(); // messageId -> { text }
let curEvent = '';
const text = res.data || '';
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('event:')) {
curEvent = line.slice(6).trim();
continue;
}
if (!line.startsWith('data:')) continue;
const d = line.slice(5).trim();
if (d === '[DONE]' || d === '"[DONE]"') continue;
try {
const p = JSON.parse(d);
if (p.conversation_id) conversationId = p.conversation_id;
if (p.type === 'answer' && p.content) {
answer = p.content;
// 只取正文,忽略 verbose(工具调用日志)等其他类型
if (p.type !== 'answer' || typeof p.content !== 'string') continue;
const key = p.id || '__default__';
const rec = msgMap.get(key) || { text: '' };
if (curEvent === 'conversation.message.delta') {
// 防御:万一上游发的是累计内容而非增量,用长度+前缀判断避免重复
if (p.content.length > rec.text.length && p.content.indexOf(rec.text) === 0) {
rec.text = p.content;
} else {
rec.text += p.content;
}
msgMap.set(key, rec);
} else if (curEvent === 'conversation.message.completed') {
// 已有 delta 拼出的内容就丢掉 completed,避免重复;没有则用它兜底
if (!rec.text && p.content) {
rec.text = p.content;
msgMap.set(key, rec);
}
}
} catch (e) { /* 跳过非 JSON 行 */ }
} catch (e) { /* 跳过 */ }
}
// 按消息首次出现顺序拼接(多段消息之间自带换行,直接相连)
let answer = '';
for (const rec of msgMap.values()) answer += rec.text;
if (!answer) answer = '暂时无法回复,请拨打 400-686-5559 咨询';
return {
code: 0,
msg: 'ok',
@@ -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,277 @@
// client/ai/pub/jjxDoReview.js
// Word 报告生成器(依赖 docx,未安装时降级跳过,不影响审查)
let buildDocx = null;
let extractReportData = null;
let extractDocxLink = null;
let isDocxConvertible = null;
try {
const docxMod = require('./buildDocx.js');
buildDocx = docxMod.buildDocx;
extractReportData = docxMod.extractReportData;
extractDocxLink = docxMod.extractDocxLink;
isDocxConvertible = docxMod.isDocxConvertible;
} catch (e) {
console.log('[Docx] docx 依赖未安装,跳过 Word 生成:', e.message);
}
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;
// 处理回答内容
// 注意:conversation.message.completed 携带的是该条消息的完整内容,
// 若前面已经通过 delta 拼过,再拼一次就会重复,这里做去重
if (p.type === 'answer' && p.content) {
const isDup = eventName === 'conversation.message.completed' && answer.indexOf(p.content) >= 0;
if (!isDup) {
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) {}
// 生成 Word 报告:优先用智能体插件生成的链接,否则本地结构化重建
let docxUrl = extractDocxLink ? extractDocxLink(answer) : '';
let docxFileId = '';
if (docxUrl) {
console.log('[Docx] 采用智能体插件生成的 Word 链接:', docxUrl);
} else if (isDocxConvertible && !isDocxConvertible(answer)) {
// 智能体没返回完整报告(道歉/澄清文本,或 HTML 被截断),不生成残缺 Word
console.log('[Docx] 无完整 HTML 报告,跳过 Word 生成');
} else if (buildDocx && extractReportData) {
try {
const reportData = extractReportData(answer);
if (reportData) {
const docxBuffer = await buildDocx({
data: reportData,
fileName: file_name || '合同审查报告'
});
const baseName = (file_name || 'report').replace(/\.[^.]+$/, '');
const docxPath = 'review-reports/' + Date.now() + '_' + baseName + '_审查报告.docx';
const docxRes = await uniCloud.uploadFile({
cloudPath: docxPath,
fileContent: docxBuffer
});
if (docxRes.fileID) {
docxFileId = docxRes.fileID;
const docxUrlRes = await uniCloud.getTempFileURL({ fileList: [docxRes.fileID] });
if (docxUrlRes.fileList && docxUrlRes.fileList[0]) {
docxUrl = docxUrlRes.fileList[0].tempFileURL;
}
}
} else {
console.log('[Docx] 既无插件链接也无结构化数据,跳过 Word 生成');
}
} catch (e) {
console.log('[Docx] Word 报告生成失败(不影响审查结果):', e.message);
}
}
// 更新审查记录为完成
await db.collection('jjx-review-records').doc(record_id).update({
status: 'completed',
result: answer,
html_url: htmlUrl,
docx_url: docxUrl,
docx_file_id: docxFileId,
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' }
};
}
};
@@ -260,7 +260,7 @@ module.exports = {
if (item.pricingForm) detail += ` ,${item.pricingForm}`
if (item.loadPaidBy) detail += ` ,${item.loadPaidBy}`
item.model.forEach((model) => {
detail += `,${model.label}:${model.value}`
if (model.label && model.value && model.value !== 'null') detail += `,${model.label}:${model.value}`
})
if (index < sheet.productList.length - 1) detail += '\r\n'
})
@@ -1,48 +1,71 @@
'use strict';
function formatZhDateTime(vk, timestamp, fallbackText) {
if (fallbackText) {
const matched = String(fallbackText).match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/);
if (matched) {
return `${matched[1]}年${matched[2]}月${matched[3]}日${matched[4]}:${matched[5]}`;
}
}
const date = new Date(Number(timestamp) || Date.now());
return vk.pubfn.timeFormat(date, 'yyyy年MM月dd日hh:mm');
}
module.exports = {
/**
* 查询当前买家 24 小时内即将自动划付的公证付款申请。
*/
main: async (event) => {
const {data = {}, util} = event;
const {uid} = data;
const {vk, _} = util;
const now = Date.now();
const deadline = now + 24 * 60 * 60 * 1000;
/**
* 查询当前买家 24 小时内即将自动划付的公证付款申请。
* @url client/order/kh/getExpiringNotaryPayList
*/
main: async (event) => {
const { data = {}, util } = event;
const { uid } = data;
const { vk, _ } = util;
const now = Date.now();
const deadline = now + 24 * 60 * 60 * 1000;
const result = await vk.baseDao.select({
dbName: 'jjx-order-pay-list',
whereJson: _.and([
{
buyerUid: uid,
isNotaryPay: true,
isNotaryApply: true,
hasPaid: true,
buyerRefuse: false,
sellerConfirm: false,
},
{autoTransferTimestamp: _.gte(now)},
{autoTransferTimestamp: _.lte(deadline)},
]),
sortArr: [{name: 'autoTransferTimestamp', type: 'asc'}],
pageIndex: 1,
pageSize: 20,
});
const result = await vk.baseDao.select({
dbName: 'jjx-order-pay-list',
whereJson: _.and([
{
buyerUid: uid,
isNotaryPay: true,
isNotaryApply: true,
hasPaid: true,
buyerRefuse: false,
sellerConfirm: false,
},
{ autoTransferTimestamp: _.gte(now) },
{ autoTransferTimestamp: _.lte(deadline) },
]),
sortArr: [{ name: 'autoTransferTimestamp', type: 'asc' }],
pageIndex: 1,
pageSize: 20,
});
return {
code: 0,
msg: '',
data: {
count: (result.rows || []).length,
rows: (result.rows || []).map(item => ({
orderId: item.orderId,
type: item.type,
amount: item.amount,
autoTransferAt: item.autoTransferAt,
autoTransferTimestamp: item.autoTransferTimestamp,
})),
},
};
},
const rows = (result.rows || []).map(item => ({
orderId: item.orderId,
type: item.type,
amount: item.amount,
autoTransferAt: item.autoTransferAt,
autoTransferTimestamp: item.autoTransferTimestamp,
autoTransferAtText: formatZhDateTime(vk, item.autoTransferTimestamp, item.autoTransferAt),
}));
const nearest = rows[0] || null;
const autoConfirmTimeText = nearest ? nearest.autoTransferAtText : '';
const content = autoConfirmTimeText
? `重要提示:您在金久信公正交易项下待确认业务的系统自动确认时间为${autoConfirmTimeText},该期限即将届满;若到期未完成相关处理,系统将依据本平台交易规则自动确认收货或处理申请金额,请您及时登录平台完成对应操作,避免产生利益损失。若您已完成相关处理,可忽略本提示,如有疑问请联系客服咨询。`
: '';
return {
code: 0,
msg: '',
data: {
count: rows.length,
rows,
autoConfirmTimeText,
content,
},
};
},
};
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,143 @@
'use strict';
const notaryPay = require('../../order/kh/util/notaryPay');
/** 短信模板字典 ID,content 填阿里云模板号,如 SMS_xxxxx;模板变量为 time */
const AUTO_TRANSFER_REMIND_SMS_DICT_ID = 'auto_transfer_remind_sms_template';
/** 兜底短信模板 ID(模板变量:time),上线前请替换为已审核通过的阿里云模板 */
const AUTO_TRANSFER_REMIND_SMS_TEMPLATE_ID = '';
function formatZhDateTime(vk, timestamp) {
const date = new Date(Number(timestamp) || Date.now());
return vk.pubfn.timeFormat(date, 'yyyy年MM月dd日hh:mm');
}
function buildRemindContent(autoConfirmTimeText) {
return `重要提示:您在金久信公正交易项下待确认业务的系统自动确认时间为${autoConfirmTimeText},该期限即将届满;若到期未完成相关处理,系统将依据本平台交易规则自动确认收货或处理申请金额,请您及时登录平台完成对应操作,避免产生利益损失。若您已完成相关处理,可忽略本提示,如有疑问请联系客服咨询。`;
}
async function getRemindSmsTemplateId(vk) {
try {
const config = await vk.baseDao.findByWhereJson({
dbName: 'jjx-dict',
whereJson: {
_id: AUTO_TRANSFER_REMIND_SMS_DICT_ID
}
});
if (config && config.content) return String(config.content).trim();
const configByKey = await vk.baseDao.findByWhereJson({
dbName: 'jjx-dict',
whereJson: {
key: AUTO_TRANSFER_REMIND_SMS_DICT_ID
}
});
if (configByKey && configByKey.content) return String(configByKey.content).trim();
} catch (err) {
console.error('getRemindSmsTemplateId error:', err);
}
return AUTO_TRANSFER_REMIND_SMS_TEMPLATE_ID;
}
module.exports = {
/**
* 距离自动划付不足 24 小时时,发送短信 + 站内信提醒(每笔申请仅提醒一次)
* @url client/timedTask/pub/remindAutoTransferPayList
*/
main: async (event) => {
let { data = {}, util } = event;
let { vk, _, pubFun } = util;
let res = { code: 0, msg: '', data: [] };
if (data.key !== 'lsgjoksdjf') return { code: 1, msg: '非法请求' };
const now = Date.now();
const deadline = now + 24 * 60 * 60 * 1000;
const smsTemplateId = await getRemindSmsTemplateId(vk);
const payList = await vk.baseDao.select({
dbName: 'jjx-order-pay-list',
whereJson: _.and([
{
isNotaryPay: true,
isNotaryApply: true,
hasPaid: true,
buyerRefuse: false,
sellerConfirm: false,
autoTransferRemindSent: _.neq(true)
},
{ autoTransferTimestamp: _.gt(now) },
{ autoTransferTimestamp: _.lte(deadline) }
]),
sortArr: [{ name: 'autoTransferTimestamp', type: 'asc' }],
pageIndex: 1,
pageSize: 200
});
for (const payRecord of payList.rows || []) {
const itemRes = {
_id: payRecord._id,
orderId: payRecord.orderId,
buyerUid: payRecord.buyerUid,
message: false,
sms: false,
msg: ''
};
try {
if (!payRecord.buyerUid) {
itemRes.msg = '缺少买家用户';
res.data.push(itemRes);
continue;
}
const autoConfirmTimeText = payRecord.autoTransferAt
? String(payRecord.autoTransferAt).replace(
/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}).*$/,
'$1年$2月$3日$4:$5'
)
: formatZhDateTime(vk, payRecord.autoTransferTimestamp);
const content = buildRemindContent(autoConfirmTimeText);
await notaryPay.addMessage(vk, {
uid: payRecord.buyerUid,
title: '重要提示',
content,
type: 'notary-auto-transfer-remind',
pk: payRecord.orderId
});
itemRes.message = true;
if (smsTemplateId) {
try {
if (typeof pubFun.sendNotifySms === 'function') {
await pubFun.sendNotifySms(payRecord.buyerUid, smsTemplateId, {
time: autoConfirmTimeText
});
itemRes.sms = true;
}
} catch (smsErr) {
console.error('remindAutoTransferPayList sms error:', payRecord._id, smsErr);
itemRes.msg = smsErr.message || '短信发送失败';
}
} else {
itemRes.msg = '未配置短信模板';
}
await vk.baseDao.updateById({
dbName: 'jjx-order-pay-list',
id: payRecord._id,
dataJson: {
autoTransferRemindSent: true,
autoTransferRemindAt: vk.pubfn.timeFormat(new Date(), 'yyyy-MM-dd hh:mm:ss'),
autoTransferRemindTimestamp: Date.now()
}
});
} catch (err) {
console.error('remindAutoTransferPayList error:', payRecord._id, err);
itemRes.msg = err.message || '提醒失败';
}
res.data.push(itemRes);
}
res.msg = `已处理${res.data.length}笔即将自动划付提醒`;
return res;
}
};
@@ -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;
}
}
@@ -55,6 +55,15 @@ exports.main = async (event, context) => {
}
}
});
await uniCloud.callFunction({
name: "router",
data: {
$url: "client/timedTask/pub/remindAutoTransferPayList",
data: {
key: "lsgjoksdjf"
}
}
});
await uniCloud.callFunction({
name: "router",
data: {
@@ -0,0 +1,50 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "logTime",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "logTime",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "type",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "type",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "operatorId",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "operatorId",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,14 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,6 @@
[
{
"balance": 0,
"lastLogId": ""
}
]
@@ -0,0 +1,26 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "userId",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "user_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,26 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "userId",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "user_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -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,38 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "parentId",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "parent_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "level",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "level",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,26 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "userId",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "user_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,26 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "userId",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "user_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -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":"最后更新时间"}}}
@@ -0,0 +1,38 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "user_id",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "user_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "title",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "title",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,62 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "menu_id",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "menu_id",
"Direction": "1"
}
],
"MgoIsUnique": true
}
},
{
"IndexName": "parent_id",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "parent_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "url",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "url",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "sort",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "sort",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,183 @@
[
{
"_id": "sys-admin",
"_add_time": 1596416400000,
"menu_id": "sys-admin",
"name": "用户角色权限",
"icon": "el-icon-s-tools",
"url": "",
"comment": "系统内置",
"sort": 100,
"enable": true
},
{
"_id": "sys-user-manage",
"_add_time": 1596416400000,
"menu_id": "sys-user-manage",
"name": "用户管理",
"icon": "el-icon-s-custom",
"url": "/pages_plugs/system/user/list",
"comment": "1个角色可以分配多个角色",
"sort": 1,
"parent_id": "sys-admin",
"enable": true
},
{
"_id": "sys-role-manage",
"_add_time": 1596416400000,
"menu_id": "sys-role-manage",
"name": "角色管理",
"icon": "el-icon-user",
"url": "/pages_plugs/system/role/list",
"comment": "1个角色可以分配多个权限和菜单",
"sort": 2,
"parent_id": "sys-admin",
"enable": true
},
{
"_id": "sys-permission-manage",
"_add_time": 1596416400000,
"menu_id": "sys-permission-manage",
"name": "权限管理",
"icon": "vk-icon-lock",
"url": "/pages_plugs/system/permission/list",
"comment": "1个权限可以匹配多个云函数",
"sort": 3,
"parent_id": "sys-admin",
"enable": true
},
{
"_id": "sys-menus-manage",
"_add_time": 1596416400000,
"menu_id": "sys-menus-manage",
"name": "菜单管理",
"icon": "el-icon-tickets",
"url": "/pages_plugs/system/menu/list",
"comment": "控制admin左侧菜单的显示和隐藏",
"sort": 4,
"parent_id": "sys-admin",
"enable": true
},
{
"_id": "sys-app-manage",
"_add_time": 1596416400000,
"menu_id": "sys-app-manage",
"name": "应用管理",
"icon": "el-icon-cloudy",
"url": "/pages_plugs/system/app/list",
"comment": "应用管理",
"sort": 5,
"parent_id": "sys-admin",
"enable": true
},
{
"_id": "sys-app-upgrade-center",
"_add_time": 1596416400000,
"menu_id": "sys-app-upgrade-center",
"name": "App升级中心",
"icon": "vk-icon-shengji3-xianxing",
"url": "/pages_plugs/system/app-upgrade-center/list",
"comment": "管理和发布新的app版本",
"sort": 6,
"parent_id": "sys-admin",
"enable": true
},
{
"_id": "system-uni",
"_add_time": 1596416400000,
"enable": true,
"icon": "el-icon-s-tools",
"menu_id": "system-uni",
"name": "系统设置",
"sort": 110
},
{
"_id": "system-uni-uni-id-files",
"sort": 1,
"url": "/pages_plugs/system_uni/uni-id-files/list",
"_add_time": 1596416400000,
"enable": true,
"icon": "el-icon-folder-opened",
"menu_id": "system-uni-uni-id-files",
"name": "素材管理",
"parent_id": "system-uni"
},
{
"_id": "system-uni-vk-global-data",
"enable": true,
"icon": "",
"menu_id": "system-uni-vk-global-data",
"name": "系统缓存管理",
"parent_id": "system-uni",
"sort": 2,
"url": "/pages_plugs/system_uni/vk-global-data",
"_add_time": 1596416400000
},
{
"_id": "system-uni-vk-components-dynamic",
"sort": 3,
"url": "/pages_plugs/system_uni/vk-components-dynamic",
"_add_time": 1596416400000,
"enable": true,
"icon": "",
"menu_id": "system-uni-vk-components-dynamic",
"name": "动态组件数据",
"parent_id": "system-uni"
},
{
"_id": "system-uni-vk-pay-orders",
"sort": 4,
"url": "/pages_plugs/system_uni/vk-pay-orders",
"_add_time": 1596416400000,
"enable": true,
"icon": "",
"menu_id": "system-uni-vk-pay-orders",
"name": "支付订单明细",
"parent_id": "system-uni"
},
{
"_id": "system-uni-uni-id-log",
"sort": 8,
"url": "/pages_plugs/system_uni/uni-id-log",
"_add_time": 1596416400000,
"enable": true,
"icon": "",
"menu_id": "system-uni-uni-id-log",
"name": "用户登录日志",
"parent_id": "system-uni"
},
{
"_id": "system-uni-opendb-admin-log",
"sort": 9,
"url": "/pages_plugs/system_uni/opendb-admin-log",
"_add_time": 1596416400000,
"enable": true,
"icon": "",
"menu_id": "system-uni-opendb-admin-log",
"name": "系统操作日志",
"parent_id": "system-uni"
},
{
"_id": "jjx-finance",
"_add_time": 1596416400000,
"menu_id": "jjx-finance",
"name": "资金管理",
"icon": "el-icon-wallet",
"url": "",
"comment": "资金管理",
"sort": 120,
"enable": true
},
{
"_id": "jjx-finance-balance",
"_add_time": 1596416400000,
"menu_id": "jjx-finance-balance",
"name": "资金明细",
"icon": "el-icon-money",
"url": "/pages/finance/balance/list",
"comment": "资金流水和余额管理",
"sort": 1,
"parent_id": "jjx-finance",
"enable": true
}
]
@@ -0,0 +1,74 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "appid",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "appid",
"Direction": "1"
}
],
"MgoIsUnique": true
}
},
{
"IndexName": "name",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "name",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "type",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "type",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "create_date",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "create_date",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "enable_upgrade_center",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "enable_upgrade_center",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,22 @@
[
{
"_id": "001",
"appid": "__UNI__01F080F",
"type": "client",
"name": "用户端",
"description": "此为用户端应用",
"create_date": 1596416400000,
"_add_time": 1596416400000,
"_add_time_str": "2020-08-03 09:00:00"
},
{
"_id": "002",
"appid": "__UNI__570A7FB",
"type": "admin",
"name": "管理端",
"description": "此为管理端应用",
"create_date": 1596416400000,
"_add_time": 1596416400000,
"_add_time_str": "2020-08-03 09:00:00"
}
]
@@ -0,0 +1,98 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "appid",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "appid",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "name",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "name",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "platform",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "platform",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "type",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "type",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "uni_platform",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "uni_platform",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "create_env",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "create_env",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "stable_publish",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "stable_publish",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,26 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "expired",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "expired",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,26 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "expired",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "expired",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,58 @@
[
{
"IndexName": "mobile",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "mobile",
"Direction": "1"
},
{
"Name": "code",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "email",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "email",
"Direction": "1"
},
{
"Name": "code",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "type",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "type",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "state",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "state",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]
@@ -0,0 +1,26 @@
[
{
"IndexName": "_add_time",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "_add_time",
"Direction": "1"
}
],
"MgoIsUnique": false
}
},
{
"IndexName": "user_id",
"MgoKeySchema": {
"MgoIndexKeys": [
{
"Name": "user_id",
"Direction": "1"
}
],
"MgoIsUnique": false
}
}
]

Some files were not shown because too many files have changed in this diff Show More