ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

合同管理系统源码落地指南:数据模型、Spring Boot后端与Vue3前端

合同管理系统源码落地指南:数据模型、Spring Boot后端与Vue3前端

简介:合同管理系统源码是一套面向企业信息化管理人员与C#开发者的完整解决方案,围绕合同全生命周期管理,涵盖合同信息录入、扫描电子化、设备信息关联及年度统计等功能模块,适合需要实现合同电子化归档与风险管控的团队参考或二次开发。压缩包共654个文件,大小47.99MB,包含cs源码、dll动态库、resources资源文件、config配置文件和数据库文件等,分别对应程序主体、依赖库、界面资源、运行参数与业务数据,目录按模块组织,便于查阅和编译调试。目前已有927人学习下载。通过学习该源码,可理解合同信息管理、图像扫描处理、设备台账管理和统计报表生成的实现思路,掌握从数据库接口到前端UI的完整代码组织方式;模块化设计也更利于开发人员在此基础上扩展审批流、提醒预警等自定义功能,从而快速搭建符合自身业务需求的合同管理系统。

1. 合同管理系统源码:别急着找代码,先想清楚这三件事

用搜索引擎找合同管理系统源码,一搜一大把,但能拿回公司内网直接跑起来的很少。我接过几轮这类活的体验是:合同管理系统真正难的地方不在增删改查,而在数据模型能不能扛住财务对账、审批流能不能按公司规则走完闭环、附件文件和合同元数据会不会失联。市面上很多合同管理系统源码要么工程老旧,要么只覆盖“能存能查”,一上线就露馅。这篇文章我把一套可落地的最小方案按顺序拆开讲,从数据库设计、Spring Boot 后端接口,到 Vue3 前端页面和审批流实现,再到实际踩过的坑。适合手头要快速上线合同管理、又不想被重型工作流引擎绑架的开发和实施人员。

2. 先拆业务再写代码:合同管理系统的数据模型和状态机设计

2.1 最小可用集是四张表:合同主表、审批记录、附件和付款计划

很多合同管理系统源码一上来就是十几张表,权限、菜单、角色、流程定义铺满整个库,结果核心的合同台账反而做得粗糙。合同管理系统的本质是“一张主表加附属信息”,与其把模型铺大,不如把基础结构做扎实。我一般建议先建四张表:合同主表contract_main、审批记录表contract_approval_record、附件表contract_attachment、付款计划表contract_payment_plan,再配一个合同类型字典表。

合同主表是唯一的核心,所有列表查询和状态流转都从它出发。下面是我常用的建表 SQL,关键字段都加了注释:

CREATE TABLE `contract_main` ( `id` bigint NOT NULL AUTO_INCREMENT, `contract_no` varchar(64) NOT NULL COMMENT '合同编号,业务上唯一', `contract_name` varchar(255) NOT NULL COMMENT '合同名称', `party_a` varchar(255) NOT NULL COMMENT '甲方名称', `party_b` varchar(255) NOT NULL COMMENT '乙方名称', `contract_type` varchar(32) DEFAULT NULL COMMENT '合同类型编码,关联字典表', `total_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '合同总金额,注意精度', `currency_code` char(3) NOT NULL DEFAULT 'CNY' COMMENT '币种 ISO 编码', `tax_rate` decimal(5,2) DEFAULT NULL COMMENT '税率百分比,备用字段', `sign_date` date DEFAULT NULL COMMENT '签订日期', `effective_date` date DEFAULT NULL COMMENT '生效日期', `expire_date` date DEFAULT NULL COMMENT '到期日期,提醒任务依赖它', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0草稿 1审批中 2已通过 3已驳回 4已终止 5已归档', `version` int NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,防并发审批', `created_by` bigint DEFAULT NULL COMMENT '创建人ID', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标记,业务单据不能物理删', PRIMARY KEY (`id`), UNIQUE KEY `uk_contract_no` (`contract_no`), KEY `idx_status_expire` (`status`, `expire_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合同主表';

这段 SQL 有几个点值得说透。contract_no上做了唯一索引,但只是兜底,真正不重复要靠 Service 层的发号器;total_amount用decimal(18,2)而不是浮点类型,理由下一节展开;status就代表了状态机,不需要单独建状态表;deleted做逻辑删除,合同这种要留痕的单据不能物理删,否则后续审计和追溯都无从谈起。

附件表保存的是文件在磁盘或对象存储上的相对路径,而不是文件内容本身。审批记录表保存每次提交、通过、驳回的流水,这几张表用contract_id关联到主表即可。先把这个最小模型立住,后续要加收付款计划、开票记录、关联项目时再做扩展,比一开始就铺大摊子好维护得多。

2.2 金额精度和合同编号:两个最容易给源码埋雷的位置

先说金额。不少老源码里total_amount用的是double或float,存入 MySQL 超过一定位数就有精度漂移,对账时差一分钱都解释不清。这个坑有三个层面要堵:数据库字段用decimal(18,2),Java 实体类用BigDecimal,前端输入框用数字输入并限制精度。三层任何一层用了浮点,最后对账都可能翻车。

合同编号看着简单,并发一高就出问题。常见格式是“前缀 + 日期 + 自增号”,比如HT-20250412-001。如果直接在主表上查max(id)加一,两个请求同时进来就会生成相同编号,第二个请求被唯一索引拦截,用户看到的就是“保存失败”或“系统错误”,体验很差。我常用的办法是单独建一张发号器表:

CREATE TABLE `contract_no_seq` ( `seq_date` char(8) NOT NULL COMMENT '日期 yyyyMMdd', `seq_value` int NOT NULL DEFAULT 0, PRIMARY KEY (`seq_date`) ) ENGINE=InnoDB COMMENT='合同编号发号表';

生成编号时执行UPDATE contract_no_seq SET seq_value = seq_value + 1 WHERE seq_date = ?,再查回来拼完整编号。行锁只压在这张表的一条记录上,不会影响合同主表的插入并发。这个办法比 Redis INCR 更省运维,小团队完全够用。

2.3 审批状态机:状态字段加流水表,不急着上工作流引擎

很多合同管理系统源码一谈审批就引入 Flowable、Activiti,光配置表就十几张。合同审批的本质是一条固定链路:发起人提交、部门主管、法务、财务、总经理,各公司差异只在节点顺序。引入工作流引擎对这类场景是过度设计,排查问题也更绕。我倾向于用状态字段 + 审批记录表 + 节点配置表来实现。

状态迁移规则要收敛在 Service 层,由统一接口校验。下面的矩阵就是我的实现依据:

当前状态可触发操作目标状态
0 草稿提交审批1 审批中
1 审批中最后节点通过2 已通过
1 审批中任一节点驳回3 已驳回
2 已通过手动终止4 已终止
2 已通过到期归档5 已归档
3 已驳回修改后重新提交1 审批中

这个矩阵看着简单,实际最容易漏的是“审批中”状态被两个审批人同时处理。我在 Service 里用带条件的更新语句做乐观锁,UPDATE contract_main SET status = ?, version = version + 1 WHERE id = ? AND version = ?,更新行数为 0 就说明合同已被别人动过,直接提示刷新后查看。审批记录表负责把每一笔操作留痕,谁在哪个节点做了什么操作,后面审计时一查就有。审批节点配置单独存一张表,字段包括node_code、node_name、order_no、approver_role,提交审批时解析出第一个待办人即可,不需要单独建待办任务表。

提示:不要把“当前待办节点”直接存进合同主表。主表只反映合同整体状态,待办状态应该由“审批记录表的最后一条 + 节点配置表”实时推导,否则会出现两处状态不一致,谁也说不清到底是哪份数据为准。

3. 后端实现:用 Spring Boot 跑通合同录入、审批和列表查询

3.1 技术选型:Spring Boot 2.7 + MyBatis-Plus,不选 JPA 的理由

合同管理系统的后端核心是列表查询和状态流转,SQL 的动态性和复杂度都比较高。Spring Boot 是国内内网项目的主流选择,ORM 我一般用 MyBatis-Plus 而不是 JPA。MyBatis-Plus 的分页插件对 MySQL 非常友好,逻辑删除、乐观锁、自动填充都是内置注解;JPA 的实体关系映射在这种多条件检索场景下,反而容易写出连表多、条件绕的慢查询。

最小依赖组合如下:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>

Spring Boot 选 2.7.x 而不是 3.x,主要考虑 JDK 版本兼容。很多公司的生产环境还停在 JDK 8,3.x 强制要求 JDK 17,升级牵连太广。starter-validation一定要加,金额、日期、编号的入参校验都靠它,省得在 Controller 里写一堆 if。

3.2 实体类和 Mapper:BigDecimal、LocalDate 加两个关键注解

实体类直接对应contract_main。Java 侧要避开两个常见错误:金额字段用Double、日期字段用String。前者造成浮点误差,后者让状态比较和日期排序变得很别扭。正确写法如下:

@Data @TableName("contract_main") public class ContractEntity { @TableId(type = IdType.ASSIGN_ID) private Long id; private String contractNo; private String contractName; private String partyA; private String partyB; private String contractType; private BigDecimal totalAmount; private LocalDate effectiveDate; private LocalDate expireDate; private Integer status; @Version private Integer version; @TableLogic private Integer deleted; private LocalDateTime createdAt; private LocalDateTime updatedAt; }

@TableLogic是逻辑删除开关,开启后 MyBatis-Plus 生成的所有单表 SQL 都会自动追加deleted = 0,避免手写where漏过滤。@Version是乐观锁注解,配合分页插件里的 OptimisticLockerInnerInterceptor 生效,后面审批接口会用到。创建时间和更新时间我建议依赖数据库的默认值和ON UPDATE,应用层少管一点,就能少踩“时间没赋值”的 bug。

3.3 Service 层事务边界:创建合同不是一次 insert

创建合同涉及三件事:写主表、生成编号、插入创建流水。这三件事必须在一个事务里完成,否则会出现主表写进去了、编号没生成,或者流水缺失的情况。下面是一个缩略的 Service 方法:

@Service public class ContractService { @Autowired private ContractMapper contractMapper; @Autowired private ContractNoSeqMapper seqMapper; @Autowired private ApprovalRecordMapper approvalMapper; @Transactional(rollbackFor = Exception.class) public Long createContract(ContractCreateDTO dto) { if (dto.getTotalAmount().compareTo(BigDecimal.ZERO) < 0) { throw new BizException("合同金额不能为负数"); } String dateStr = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String contractNo = seqMapper.nextNo(dateStr); ContractEntity entity = new ContractEntity(); entity.setContractNo(contractNo); entity.setContractName(dto.getContractName()); entity.setPartyA(dto.getPartyA()); entity.setPartyB(dto.getPartyB()); entity.setTotalAmount(dto.getTotalAmount()); entity.setStatus(0); contractMapper.insert(entity); // 写入一条创建流水,记录创建人、创建时间、初始状态 contractLogService.log(entity.getId(), "CREATE", null, 0); return entity.getId(); } }

事务放在 Service 层,避免 Controller 里直接调用 Mapper 导致事务失效。异常统一抛自定义的BizException,配合全局异常处理器,返回给前端的就是带业务提示的 JSON,而不是 500 页面。nextNo就是这个项目里发号器表对应的更新方法,核心是那条UPDATE contract_no_seq SET seq_value = seq_value + 1 WHERE seq_date = ?,事务回滚后编号可能空洞,但空洞可以接受,重复不能接受。

3.4 审批接口:乐观锁防止双人同时审批

审批是合同管理里最容易出现并发问题的接口。两个审批人同时打开同一份待办,分别点了“通过”,如果不做版本控制,就会出现两条通过记录、状态被后写覆盖。解决办法就是设计表时预留的version字段。

@Transactional(rollbackFor = Exception.class) public void approve(Long contractId, Long approverId, String comment) { ContractEntity current = contractMapper.selectById(contractId); if (current == null || current.getStatus() != 1) { throw new BizException("合同当前状态不允许审批"); } LambdaUpdateWrapper<ContractEntity> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(ContractEntity::getId, contractId) .eq(ContractEntity::getStatus, 1) .eq(ContractEntity::getVersion, current.getVersion()); wrapper.set(ContractEntity::getStatus, 2); wrapper.set(ContractEntity::getVersion, current.getVersion() + 1); int updated = contractMapper.update(null, wrapper); if (updated == 0) { throw new BizException("该合同已被其他审批人处理,请刷新后查看"); } approvalMapper.insert(new ApprovalRecord(null, contractId, approverId, comment)); }

这里的核心不是先查再改,而是把“状态为审批中”和“版本号匹配”写进更新条件。数据库行锁保证后一个请求的更新行数为 0,直接拒绝。如果是多级审批,还要在流水表里记录当前节点编码,否则“通过了第一级、第二级怎么办”就丢了。驳回接口逻辑类似,只是把目标状态改成 3,并记录驳回意见。

3.5 合同列表查询:多条件检索的分页 SQL

列表页是用户面对最多的东西,查询 SQL 要谨慎。最容易翻车的是连了附件表之后,一条合同带三个附件就被展开成三行,分页直接乱掉。我一般用子查询统计附件数,而不是 join:

<select id="selectContractPage" resultType="com.example.dto.ContractVO"> SELECT cm.id, cm.contract_no, cm.contract_name, cm.party_a, cm.party_b, cm.total_amount, cm.status, cm.expire_date, (SELECT COUNT(*) FROM contract_attachment a WHERE a.contract_id = cm.id AND a.deleted = 0) AS attachment_count FROM contract_main cm WHERE cm.deleted = 0 <if test="status != null"> AND cm.status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (cm.contract_no LIKE CONCAT('%', #{keyword}, '%') OR cm.contract_name LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="expireStart != null"> AND cm.expire_date &gt;= #{expireStart} </if> <if test="expireEnd != null"> AND cm.expire_date &lt;= #{expireEnd} </if> ORDER BY cm.id DESC </select>

子查询在数据量到十万级之前性能可以接受,而且行数不会因为一对多关联而膨胀。&gt;=和&lt;=是 XML 里的转义写法,直接写>会解析报错,这是新手经常卡住的地方。分页插件会自动把这条 SQL 包一层 COUNT 查询,总数也是准的。

4. 前端落地:Vue3 + Element Plus 搞定列表、表单和附件上传

4.1 网上合同系统源码多停在 Vue2,用 Vue3 重写要注意这些差异

开源的合同管理系统源码大多还是 Vue2 那一代写法,filter语法、$refs、$on在 Vue3 里都变了。我建议直接用 Vue3 + Vite + Element Plus 起新工程,目录就三块:views/contract/list.vue、views/contract/edit.vue、api/contract.js,不需要上 Pinia,合同管理这种场景 Props 加事件足够。列表页最关键的是搜索条件和分页数据保持同一数据源:

<script setup> import { ref, onMounted } from 'vue' import { queryContractPage } from '@/api/contract' const loading = ref(false) const pageNo = ref(1) const pageSize = ref(10) const total = ref(0) const tableData = ref([]) const queryForm = ref({ status: null, keyword: '', expireStart: null, expireEnd: null }) async function fetchPage() { loading.value = true const resp = await queryContractPage({ ...queryForm.value, pageNo: pageNo.value, pageSize: pageSize.value }) tableData.value = resp.records total.value = resp.total loading.value = false } function handleSearch() { pageNo.value = 1 fetchPage() } onMounted(fetchPage) </script>

注意搜索时把pageNo重置为 1,这是前端最常见的翻车点。在第 3 页输入关键字搜索,接口把页码 3 传过去,查出来自然是空的。el-pagination的current-change和size-change事件都要重新拉列表,size-change之后同样把页码重置为 1,否则会出现页码超出总页数的问题。

4.2 合同编辑表单:金额输入框要控精度,日期要有联动校验

合同表单的金额字段不要用普通文本框,容易输入负数或超过两位小数。Element Plus 的el-input-number可以控制精度,配合el-form的 rules 做整体校验。生效日期早于到期日期这个关系也在前端拦一道,体验更好:

const formRules = { contractName: [{ required: true, message: '合同名称必填', trigger: 'blur' }], totalAmount: [ { required: true, message: '金额必填', trigger: 'blur' }, { type: 'number', min: 0, message: '金额不能为负数', trigger: 'blur' } ], effectiveDate: [{ required: true, message: '生效日期必填', trigger: 'change' }], expireDate: [ { required: true, message: '到期日期必填', trigger: 'change' }, { validator: (rule, value, callback) => { if (formRef.value.effectiveDate && value <= formRef.value.effectiveDate) { callback(new Error('到期日期必须晚于生效日期')) } else { callback() } }, trigger: 'change' } ] }

自定义 validator 里读取其他字段时,要确保那个字段已经绑定到表单对象上。如果表单是分步填写的,最后一步校验时前面的字段可能还没赋值,所以要在进入该步骤时先做一次表单赋值合并。这个坑我在改造同事代码时见过不止一次,页面提示“到期日期必须晚于生效日期”,但生效日期明明填了,原因就是校验函数里取到的值是 undefined。

4.3 附件上传:路径入库、文件落盘,别把文件塞进数据库

合同附件模块最容易做坏。有人把文件转 base64 直接塞数据库字段,有人把附件表和合同表做无上限的级联。这两种我都不建议。常见做法是前端el-upload调后端接口,后端把文件写到配置的磁盘目录或对象存储上,数据库只存路径、大小、类型和上传人。

@PostMapping("/api/attachment/upload") public Result upload(@RequestParam("file") MultipartFile file, @RequestParam("contractId") Long contractId) { if (file.isEmpty()) { return Result.error("上传文件为空"); } String ext = FilenameUtils.getExtension(file.getOriginalFilename()); String relativePath = "/data/contract-files/" + LocalDate.now() + "/"; String newName = UUID.randomUUID().toString().replace("-", "") + "." + ext; file.transferTo(new File(relativePath + newName)); AttachmentEntity entity = new AttachmentEntity(); entity.setContractId(contractId); entity.setFileName(file.getOriginalFilename()); entity.setFilePath(relativePath + newName); entity.setFileSize(file.getSize()); attachmentMapper.insert(entity); return Result.ok(entity.getId()); }

三个细节值得注意:文件名用 UUID 重命名,避免中文文件名和非法字符带来乱码或路径穿越问题,原文件名只存库;按日期分目录,后续迁移对象存储或做定时清理都方便;单机内网用file.transferTo没问题,如果系统要做集群部署,建议直接接 MinIO 或对象存储,否则文件会分散在不同机器上,用户下载时经常 404。文件预览和下载就靠反向代理把/data/contract-files/映射成一个静态访问路径,权限控制放在后端下载接口,前端不直接拼 URL 访问磁盘路径。

4.4 审批操作的前端交互:待办列表和审批按钮

待办列表不需要单独建待办表,前端调一个接口:后端根据当前登录人的角色,去审批记录表找“当前节点待办人含该角色”的合同,返回待审批列表。每个待办项显示合同名称、金额、提交人、当前节点。通过和驳回共用一个弹窗,关键是要在后端返回错误时把后端提示原样展示,比如“该合同已被其他审批人处理”,而不是统一弹“操作失败”。审批按钮要加 loading 状态,防止用户双击重复提交,后端乐观锁虽然兜底,但前端先防一层用户体验会好很多。

5. 踩坑集中营:合同管理系统开发里最常翻车的 5 个问题

5.1 现象:合同总金额导出 Excel 后出现 12345678.099999998

财务对账时发现合同金额和发票金额对不上,导出的 Excel 里出现一长串小数。原因是老系统里金额字段用的double,二进制浮点表示导致精度漂移。解决方法是三层同时改:数据库字段改成decimal(18,2),Java 实体改成BigDecimal,导出 Excel 时统一用toPlainString(),前端输入框限制两位小数。改完这三处,金额相关的对账问题基本绝迹。

5.2 现象:同一份合同被两个领导同时审批通过

两个审批人同时打开同一份待办,先后点了通过,数据库里出现两条通过流水,合同状态被覆盖成最后一次更新。原因是先查后改的普通 update 没有版本约束。解决方法是加乐观锁,更新语句带上WHERE status = 1 AND version = ?,第二个请求更新行数为 0 就直接拒绝并提示刷新。如果项目里已经有一堆存量代码,不想改实体注解,也可以在 update SQL 里手动带版本条件,效果一样。

5.3 现象:误删合同后,附件数据还在但合同找不回来

业务人员删错了一份合同,管理员发现后无处恢复。原因是主表用了物理删除,附件表还在但主表记录没了,关联关系断裂。解决方法是所有核心表统一逻辑删除,deleted字段配合 MyBatis-Plus 的@TableLogic,查询自动过滤。合同这类单据应该只有“作废”或“归档”,不存在真正意义上的删除,后端接口只提供状态变更,不提供删除接口。

5.4 现象:并发创建合同时两个编号重复,用户提示保存失败

两个用户同时提交合同,一个成功一个报错,报错信息还是晦涩的唯一索引冲突。原因是后台用max(id) + 1或先查库判重生成编号,并发下必然失效。解决方法是拆出发号器表,用一行记录的原子自增生成序号,主表的唯一索引只做兜底。编号格式前期想清楚,后面要改编号规则会牵动所有历史数据的关联。

5.5 现象:分页列表偶尔只有 7 条数据,总数和实际不符

合同列表每页设置 10 条,有时候只显示 7、8 条,翻页后数据重复。原因是主表 join 附件表后,一条合同带多个附件被展开成多行,分页在 join 之后执行,行数被放大。解决方法是列表查询不 join 附件表,附件数用子查询统计,或者在 Java 层先查主表分页,再批量查附件数并组装。子查询在数据量小时性能足够,数据量大了以后可以改成冗余字段attachment_count定时更新。

6. 进阶:到期提醒和合同全文检索,让系统从台账变成管理工具

6.1 到期合同提醒:一个定时任务加一个配置开关

合同系统只做到“能存能查”价值有限,到期提醒才是真正能减少损失的功能。用 Spring 的@Scheduled每天固定时间扫一次即将到期的合同,推送给业务负责人:

@Component public class ContractExpireReminder { @Scheduled(cron = "0 0 9 * * ?") public void scanExpiringContracts() { LocalDate start = LocalDate.now(); List<ContractEntity> list = contractMapper.selectExpiring(start, start.plusDays(reminderDays)); // 调用企业微信机器人或内部消息接口推送 } }

提醒天数reminderDays放到application.yml里配置,不要写死。再配一个开关contract.reminder.enabled,联调环境关掉,避免测试数据天天推送打扰人。线上跑起来后,第一周先观察有没有漏推和重复推,推送任务要做幂等,按“合同 ID + 提醒批次日期”去重。

6.2 附件内容检索:用 Tika 抽文本,别急着上 Elasticsearch

合同的价值藏在附件里。几十万份合同之前,不建议直接上 Elasticsearch,用 Apache Tika 把 PDF、Word 文本抽取出来存进数据库就够了:

public String extractText(File file) throws TikaException, IOException { try (Tika tika = new Tika()) { return tika.parseToString(file); } }

抽取结果存到附件表的文本字段里,后端查询用模糊匹配就可以覆盖“按付款条件找历史合同”这类需求。要注意一个边界:扫描版的 PDF 本质是图片,Tika 抽不出文字,需要 OCR 引擎,这是另一个成本,立项前先确认合同文件是电子签章导出还是纸质扫描。我现在的习惯是每个合同上传后异步抽取一次全文,存字段,某天要模糊查历史时直接能用,等数据量真到几十万再考虑接入全文检索服务。这套路数帮我省了不少后期返工,希望帮到你。

本文还有配套的精品资源,点击获取

返回列表