
简介这份操作手册面向小额贷款公司信贷业务人员与系统管理员用于快速掌握信贷管理系统的日常操作与后台维护。文档共 1 个 docx 文件压缩包约 10.07MB适合在阅读或培训时对照使用。内容从系统概要、运行环境、页面布局和登录方式讲起再按功能模块拆解操作步骤其中重点包括机构管理、用户管理、利率维护、口令更改也涉及客户注册与权限转移等维护场景客户管理、贷款产品、风险管理、还款计划、报表分析等模块可帮助用户理解从贷款申请到放款、还款监控的完整流程。已有84人学习适合作为新员工培训资料、系统上线参考手册或日常查询的操作指南。1. 从一份 docx 操作手册反推信贷管理系统的最小闭环任何一个在小贷公司待过的人都见过那种几十页的 Word 操作手册开头是系统登录中间是客户管理、进件审批、合同打印、放款还款最后是催收和报表。点开这份 docx里面对应的是一套已经跑了好几年的老系统字段老、流程硬、报表要人工对。但如果你反过来想这份操作手册其实是一份绝佳的需求说明书。每个菜单项都是一张表每个操作按钮背后都是一次状态流转每个校验提示都是一条可以落进数据库的约束规则。拿着手册逆向画 ER 图再用现成的框架搭一套最小闭环系统是理解信贷业务最快的方式也是给公司做系统替换或二次开发时最靠谱的起步路径。这篇文章就围绕“信贷管理系统”这个核心场景展开先用领域模型说清楚贷款业务的状态机再用 Spring Boot 3 MySQL Vue 3 把进件、审批、放款、还款这条主链路跑起来最后落到两个高频坑——跨级审批的流转控制以及 docx 合同和操作手册的自动生成。读完你能直接拿里面的表结构和代码改造成自己的版本而不是对着手册抄按钮。2. 小额贷款信贷管理系统的额度模型与账务状态机2.1 信贷系统里最容易被忽略的三张表客户、额度、合同做信贷管理系统新手最容易犯的错是上来就建一堆业务表比如“借款申请表”“审批记录表”“放款表”。这些表确实存在但它们都是围绕一个核心展开的客户在某段时间内可用的额度以及这笔额度一旦被占用就要通过合同和借据来跟踪。所以第一版建模我建议先落三张基础表。客户表建议独立出来别把自然人的信息塞进借款单。customer表设计时除了身份证、手机号、姓名一定要预留customer_no作为业务编号。这个编号对外展示不在 URL 里传数据库自增主键避免撞库和越权。额度表credit_limit要记录总额度、已用额度、剩余额度、生效时间和失效时间。小贷公司常见的额度模型有两种一种是授信额度审批通过后给一个定额客户在额度内循环借另一种是单笔审批制每笔借款单独审批。第二种其实不需要额度表但大多数做现金贷和抵押贷的公司都会先走授信。额度表里每次放款要做已用额度的累加还款时做释放这个动作必须放在同一个事务里否则账对不上。合同表contract相对复杂但核心字段只有几个合同编号、客户 ID、额度 ID、借款金额、年化利率、还款方式、期数、起息日、到期日。还款方式这里推荐用枚举存储不建议把“等额本息”“先息后本”直接存字符串因为后面计算还款计划时要按枚举做分支。用REPAAY_TYPE_EQUAL_PMT和REPAY_TYPE_INTEREST_FIRST这种编码看着啰嗦但算账时不会写错。CREATE TABLE credit_limit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_no VARCHAR(32) NOT NULL COMMENT 客户业务编号, total_amount DECIMAL(15,2) NOT NULL COMMENT 总额度, used_amount DECIMAL(15,2) NOT NULL DEFAULT 0 COMMENT 已用额度, available_amount DECIMAL(15,2) NOT NULL COMMENT 剩余额度, status VARCHAR(16) NOT NULL COMMENT ACTIVE/FROZEN/CLOSED, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT授信额度表;这段建表 SQL 里有个细节available_amount没有用计算列而是单独存。原因是信贷系统里额度会被冻结比如客户在审批中需要先冻结一部分额度审批拒绝再释放。如果每次都把总金额减已用金额冻结状态就没法表达。所以额度表里一般还要加一个frozen_amount字段真正可借额度是total_amount - used_amount - frozen_amount。计算口径要写清楚免得后面对账时跟财务吵。2.2 借据与还款计划系统唯一的账务真相合同签完紧接着要生成借据和还款计划。借据loan_receipt代表一笔具体的放款一笔合同可以分多笔放款所以它必须挂在合同 ID 下。借据上要记录放款金额、放款日期、起息日期、实际执行利率。这些字段跟合同可以不一致因为客户可能提前还款、部分还款导致后续借据利率发生变化。还款计划repayment_plan是每期应还的记录一个借据对应多期。每期记录里要有period_no、due_date、principal、interest、status。状态从 UNPAID 到 PARTIAL 到 PAID逾期了要单独标记。这里要注意罚息不要直接更新到还款计划里而是单独建一张penalty_detail表按照逾期天数实时计算。因为罚息是每天变的如果写死在计划表里每天要跑定时任务去更新成本高而且容易出错。生成还款计划是信贷系统里最容易出 bug 的地方尤其是等额本息。月供计算公式是M P * r * (1r)^n / ((1r)^n - 1)但浮点运算在 Java 里直接用 double 会丢精度必须用 BigDecimal并且除法时要指定精度和舍入模式。下面这段代码是等额本息生成计划的核心逻辑public ListRepaymentPlan generateEqualPmtPlan(BigDecimal principal, BigDecimal annualRate, int months, LocalDate startDate) { ListRepaymentPlan plans new ArrayList(); BigDecimal monthlyRate annualRate.divide(BigDecimal.valueOf(12), 10, RoundingMode.HALF_UP); BigDecimal power monthlyRate.add(BigDecimal.ONE).pow(months); BigDecimal monthPay principal.multiply(monthlyRate).multiply(power) .divide(power.subtract(BigDecimal.ONE), 2, RoundingMode.HALF_UP); BigDecimal balance principal; for (int i 1; i months; i) { BigDecimal interest balance.multiply(monthlyRate).setScale(2, RoundingMode.HALF_UP); BigDecimal principalPart monthPay.subtract(interest); if (i months) { principalPart balance; } balance balance.subtract(principalPart); plans.add(new RepaymentPlan(i, startDate.plusMonths(i), principalPart, interest, UNPAID)); } return plans; }这段代码有三处必须注意。第一monthlyRate计算时除法的精度给了 10 位不能直接用 2 位否则每期利息算完再乘回去误差会累积。第二最后一期的本金部分直接用剩余本金赋值而不是用月供减利息这是为了消除前面每期四舍五入造成的差额。第三起息日如果是放款日而不是自然月第一天plusMonths之后要判断日期是否溢出比如 1 月 31 日加一个月会变成 2 月 28 日财务上对这种边界日期要有明确约定。2.3 审批流状态机从进件到放款的每一步都很具体信贷审批不是一条直线从客户提交申请到放款中间有退回、有补充材料、有拒绝。这个流程用状态机来描述比用一堆 if else 清晰得多。状态用字符串枚举流转记录单独用一张approval_record表每次操作插入一条记录后台可以看到完整的审批轨迹。状态编码状态含义可流转到触发动作SUBMITTED客户已提交申请UNDER_REVIEW客户经理初审UNDER_REVIEW初审中APPROVED / REJECTED / PENDING_INFO风控审核PENDING_INFO待补充材料UNDER_REVIEW / CANCELLED客户补充后重新提交APPROVED审批通过CONTRACT_PENDING生成合同CONTRACT_PENDING待签约SIGNED / REJECTED客户签署SIGNED已签约PENDING_DISBURSEMENT确认放款PENDING_DISBURSEMENT待放款DISBURSED / FAILED财务放款DISBURSED已放款REPAYING / OVERDUE进入贷后这个状态机在代码里不要用 if 散落着写建议用一个枚举加一个 Map 来维护。每次状态流转前校验当前状态在不在可流转集合里不在就抛异常。这里的关键不是状态定义得有多全而是每个状态变更时额度冻结、合同生成、借据生成的联动动作必须在同一个事务里。比如从 APPROVED 流转到 CONTRACT_PENDING 时要把客户额度里的授信金额冻结掉如果合同最终没签状态回退时要解冻。这个回退动作经常被遗漏最后导致客户额度被莫名其妙占用客户投诉说“我明明没借钱为什么额度没了”。3. 用 Spring Boot 3 搭一套可扩展的信贷管理脚手架3.1 分层设计为什么 Controller 里不该写业务信贷系统的代码结构我习惯按controller / service / mapper / domain四层来拆。Controller 只做参数接收和结果包装Service 里放业务规则Mapper 用 MyBatis-Plus 做数据访问。domain 里放实体和枚举。很多公司会多拆一层manager用来做跨 Service 的编排比如创建借据要同时更新额度和生成还款计划这种操作放在一个LoanFacade里会更清晰避免 Service 之间互相注入造成循环依赖。选型上Spring Boot 3 配合 MyBatis-Plus 是目前比较稳的组合。有人用 Spring Data JPA但信贷系统里报表查询多、动态 SQL 多MyBatis 的 XML 写复杂查询更顺手。数据库用 MySQL 8事务隔离级别默认的REPEATABLE_READ够用但要注意额度扣减这种高并发操作不能用先查后改的方式要用UPDATE ... SET used_amount used_amount #{amount} WHERE id #{id} AND available_amount #{amount}这种带条件的原子更新。下面是一个典型的放款接口实现注意事务边界放在最外层Transactional(rollbackFor Exception.class) public void disburse(Long contractId, BigDecimal amount) { // 1. 更新额度原子操作条件里带可用额度校验 int updated creditLimitMapper.freezeAmount(contractId, amount); if (updated 0) { throw new BusinessException(额度不足或已冻结); } // 2. 创建借据 LoanReceipt receipt new LoanReceipt(); receipt.setContractId(contractId); receipt.setAmount(amount); receipt.setStatus(DISBURSED); receiptMapper.insert(receipt); // 3. 生成还款计划 ListRepaymentPlan plans generatePlans(receipt); repaymentPlanMapper.batchInsert(plans); }放款接口的真实场景比这段复杂得多比如要对接资金存管银行、要同步核心账务系统、要生成代收授权协议。但无论外面接了多少个系统数据库层的动作就这三步。事务注解放在disburse方法上只要任何一步失败前面扣减的额度和插入的借据都会回滚。这个逻辑是信贷系统的安全底线也是代码评审时最先看的地方。3.2 用脚手架把基础功能跑通拿 Excel 模板导入导出代替手工录单信贷管理系统里最烦人的工作是录单。客户经理拿到纸质申请表要把身份证、收入证明、联系人信息一项一项打进系统。这一环节如果不用工具辅助一天最多录几十单而且错误率高。常见的做法是做一个 Excel 批量导入客户经理在模板里填好系统校验完导入数据库。导出则反向操作把存量客户的合同、还款计划导成 Excel 给业务部门看。用阿里巴巴的 EasyExcel 做导入导出是 Java 生态里的标配。下面是一个导入客户信息的监听器示例重点是校验错误要收集到每一行每一列的级别不能中间一遇到错误就中断否则客户经理改了 20 个错误要跑 20 次导入会被骂死。public class CustomerDataListener extends AnalysisEventListenerCustomerImportRow { private final ListCustomerImportRow validRows new ArrayList(); private final ListImportErrorRow errors new ArrayList(); Override public void invoke(CustomerImportRow row, AnalysisContext context) { Integer rowIndex context.readRowHolder().getRowIndex(); if (StringUtils.isBlank(row.getIdCard())) { errors.add(new ImportErrorRow(rowIndex, 身份证不能为空)); } if (row.getIdCard() ! null !row.getIdCard().matches(\\d{17}[\\dXx])) { errors.add(new ImportErrorRow(rowIndex, 身份证格式不正确)); } if (errors.stream().noneMatch(e - e.getRowIndex().equals(rowIndex))) { validRows.add(row); } } Override public void doAfterAllAnalysed(AnalysisContext context) { if (!errors.isEmpty() validRows.isEmpty()) { throw new BusinessException(导入失败请先修正错误数据); } } }这个监听的逻辑里invoke是每解析一行触发一次doAfterAllAnalysed是全部解析完成后触发。错误收集用validRows和errors两个列表分开最后如果错误列表有内容就把错误信息返回给前端展示同时把有效行写入库。实际生产环境中导入往往是分批的一传就传几千行需要把ReadSheet的headRowNumber配置好并且设置批处理大小避免一次性加载到内存导致 OOM。3.3 权限模型要能管按钮菜单权限、数据权限、操作日志信贷系统跟普通管理系统最大的不同在于它的按钮权限必须细到“谁能点放款”“谁能点审批通过”。这不是产品经理强迫症是合规要求。监管回头看的时候要问这个客户是谁审批的、谁放款的、中间有没有越权操作。所以权限模型至少要有五张表用户、角色、菜单、用户角色关联、角色菜单关联。如果再细一点还要有数据权限比如客户经理只能看自己名下的客户团队经理能看整个团队的。操作日志是另一个容易被忽视的点。每次状态流转、合同导出、放款操作都要记录操作人、操作时间、操作前状态、操作后状态、IP 地址。这个日志不能用 AOP 注解草草记录因为信贷系统的日志需要支持事后审计查询必须单独落表。下面是一个operation_log表的简化结构CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, user_name VARCHAR(64) NOT NULL, operation_type VARCHAR(32) NOT NULL COMMENT APPROVE/DISBURSE/REJECT/EXPORT, target_type VARCHAR(32) NOT NULL COMMENT APPLICATION/CONTRACT/RECEIPT, target_id BIGINT NOT NULL, before_status VARCHAR(16), after_status VARCHAR(16), remark VARCHAR(512), ip_address VARCHAR(64), created_at DATETIME NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT操作日志表;这个表的数据量增长很快一个中型小贷公司一年可能有几十万条日志。所以查询时一定要按created_at建索引归档时按月分表。日志记录代码用 AOP 注解是常见做法定义一个AuditLog(type APPROVE)在切面里解析方法参数把业务 ID、操作人塞进日志。这里要注意事务切面和日志切面的顺序要保证日志在业务提交后才写入否则业务回滚了日志却留下了审计时会发现“操作失败但留痕成功”的怪象。4. 信贷管理系统的核心流程从进件审批到放款出账的完整链路4.1 进件与征信核验外部数据接口怎么接才不闹心小贷公司的进件流程一般先过征信核验。征信分两类一类是央行征信需要走专线或第三方服务商另一类是百行征信、芝麻信用、税务数据、社保数据。这些外部接口的响应速度都很慢动辄 3 到 5 秒而且不是每次都稳定。前端经验不足的人会把征信查询做成同步调用用户点“提交申请”之后浏览器转圈 8 秒。这个体验非常差实际系统一般做成两步提交进件时先落库状态为PENDING_CREDIT_CHECK后台异步调用征信接口回调后更新状态并通知前端。异步调用用 Spring 的Async注解加消息队列都可以。小贷系统体量不大直接用CompletableFuture加线程池就够。征信查询结果要存原始报文因为后续如果有异议需要回溯原始数据。下面是一个简化版的征信推送处理逻辑Async(creditCheckExecutor) public void checkCredit(Long applicationId) { CreditCheckRequest req new CreditCheckRequest(applicationId); CreditCheckResponse resp creditReportClient.query(req); if (resp.isSuccess()) { creditReportMapper.saveRawResponse(applicationId, resp.getRawData()); applicationService.updateStatus(applicationId, UNDER_REVIEW); } else { applicationService.updateStatus(applicationId, CREDIT_CHECK_FAILED); } }这段代码的核心逻辑是把征信查询结果存原始报文不直接解析字段后丢弃原始数据。很多小机构图省事只存解析后的得分和几个关键字段后来客户说“你们查到的收入不对”系统里根本找不到当时收到的报文说不清楚。加一个saveRawResponse存 JSON 或 XML 原文成本极低但能省掉很多麻烦。外部接口的另一个问题是超时和重试。征信接口超时时间一般设 10 秒重试一次。超过重试次数后申请状态要置为CREDIT_CHECK_TIMEOUT由客户经理决定是人工处理还是重新发起。这里不要自动重试太多次因为征信接口是按次收费的重试多了成本上去了不说万一接口方重复扣费还要对账。4.2 合同生成与电子签章docx 模板引擎是这里的核心合同生成是信贷系统里最有意思的部分。一份借款合同可能有十几页里面的借款人信息、金额、利率、还款计划表都是动态的。常见做法是准备一份 Word 模板里面用${customerName}这种占位符后台用 Apache POI 或者芋道框架里的 docx 处理工具去填充数据。但用 POI 直接操作 docx 的段落和表格非常繁琐尤其是合同里有两张以上的表格时替换逻辑容易断层。我常用的方案是使用 poi-tl一个基于 Apache POI 的轻量 Word 模板引擎。它在模板里用{{custmerName}}这种语法代码里只需要构建一个MapString, Object数据模型然后调用XWPFTemplate.render就能生成完整的合同 docx。数据模型里支持表格循环还款计划表可以直接用 List 渲染。XWPFTemplate template XWPFTemplate.compile(contract_template.docx).render(new HashMap() {{ put(contractNo, HT20250101001); put(customerName, 张伟); put(idCard, 110101199001011234); put(principal, 100000.00); put(annualRate, 7.2%); put(repayPlans, Arrays.asList( new RepayPlanRow(1, 2025-02-01, 8333.33, 600.00), new RepayPlanRow(2, 2025-03-01, 8333.33, 550.00) )); }}); template.writeToFile(contract_HT20250101001.docx);生成合同后不能直接让客户拿着 Word 去找银行必须转 PDF 并加盖电子签章。合同转 PDF 用 LibreOffice 的无头模式执行命令行把 docx 转 pdfJava 里用ProcessBuilder调用即可。电子签章涉及 CA 证书和加密一般对接第三方电子签约平台。这里注意一个点**让客户在线签署的 H5 页面不要内嵌在系统里用平台提供的链接跳转。**因为电子签约平台要求实名认证、短信验证、人脸识别自己做一套成本极高而且监管不认。4.3 放款出账与还款代扣对账文件是最后的兜底放款动作完成后资金从公司账户转到客户银行账户。小贷公司一般通过银行存管或第三方支付渠道代发。放款指令发出后状态不是立即变成 “放款成功”而是先变成DISBURSING处理中。渠道方通过回调或者每日对账文件确认实际结果。对账文件是最可靠的回调可能丢文件不会。常见做法是每天定时拉取银行的对账文件解析后跟系统里的借据比对状态不一致的放进异常列表等财务人工处理。还款代扣的流程类似每个还款日之前先跑批把当天的应扣客户列表拉出来生成代扣文件传给渠道。渠道扣款成功后回调系统更新还款计划状态并释放额度。代扣失败的要自动进入逾期流程。下面是还款试算的一个关键接口前端展示给客户的剩余应还金额、罚息都由它计算public RepayPreviewVO previewRepay(Long receiptId, LocalDate repayDate) { LoanReceipt receipt receiptMapper.selectById(receiptId); ListRepaymentPlan plans repaymentPlanMapper.selectByReceiptId(receiptId); BigDecimal totalUnpaid BigDecimal.ZERO; BigDecimal penalty BigDecimal.ZERO; for (RepaymentPlan plan : plans) { if (PAID.equals(plan.getStatus())) continue; if (repayDate.isAfter(plan.getDueDate())) { long overdueDays ChronoUnit.DAYS.between(plan.getDueDate(), repayDate); penalty penalty.add(plan.getPrincipal().add(plan.getInterest()) .multiply(penaltyRate).multiply(BigDecimal.valueOf(overdueDays))); } totalUnpaid totalUnpaid.add(plan.getPrincipal().add(plan.getInterest())); } return new RepayPreviewVO(totalUnpaid, penalty); }previewRepay接口里固定的罚息利率penaltyRate是从系统参数表里读出来的没写死。罚息利率通常按日万分之五但不同产品可能不一样。每次代扣失败后重新试算把最新的逾期天数和罚息展示给客户看。这段代码里用ChronoUnit.DAYS.between计算逾期天数注意dueDate和repayDate都是LocalDate不含时分秒。这跟项目中实际使用的LocalDateTime不同接口设计时两者要约定清楚否则在跨天边界会差一天。4.4 贷后管理借据状态对不上时先查这两张表贷后管理包括还款提醒、催收记录、逾期管理、贷后检查。这一阶段的系统逻辑不复杂但数据必须跟财务账对得上。如果财务说“客户还了钱系统里还显示逾期”这问题基本出在两张表上还款流水表和借据还款计划表。每次还款动作执行时要先插还款流水再更新还款计划两个动作同一个事务。绝对不能出现插了流水但没更新计划那样账就平不了。催收记录表要单独建字段包括客户 ID、借据 ID、催收方式电话/上门/短信、催收结果、下次跟进时间、催收员。这张表的价值在于管理催收过程更是应对监管检查的重要材料。设置自动提醒时用next_follow_time做索引每天定时任务扫描一次把到期的催收任务推到客户经理待办里。5. 信贷管理系统上线前必须做的一键式核验技巧5.1 用数据库脚本验证额度、合同、借据的勾稽关系信贷系统没有业务数据的时候一切正常数据量一上来就开始乱。上线前做个数据核验用一段 SQL 把客户维度、合同维度、借据维度的数据拉平看总账能不能对上。下面这个 SQL 是核验额度与借据余额是否一致的常见写法把已经放款的借据求和跟额度表里的已用金额比对。如果两边不一致说明某个放款或还款事务没走完要查operation_log里对应的操作记录。SELECT cl.customer_no, cl.total_amount, cl.used_amount AS limit_used, IFNULL(SUM(lr.principal), 0) - IFNULL(SUM( CASE WHEN rp.status PAID THEN rp.principal ELSE 0 END ), 0) AS receipt_balance FROM credit_limit cl LEFT JOIN loan_receipt lr ON lr.customer_no cl.customer_no AND lr.status DISBURSED LEFT JOIN repayment_plan rp ON rp.receipt_id lr.id GROUP BY cl.customer_no HAVING ABS(cl.used_amount - receipt_balance) 0.01;这个 SQL 的逻辑并不复杂但它把三类数据全部拉到一起了额度表 借据 还款计划。ABS(...) 0.01这里留了一个小的容差因为 BigDecimal 的精度在迁移老数据时可能有几分钱的差异只要不超过一分钱就不算账不平。余额对不上时优先去翻approval_record和operation_log看是不是审批通过后合同生成失败、额度冻结没解冻或者退票回滚时只更新了表业务状态没更新额度。用字段名能明确定位问题。5.2 审批流程的修改要留一个后门支持管理员手工调状态信贷系统投产之后的日常维护里最高频的需求是“帮我把这笔单子状态改一下”。原因五花八门客户填写错误、审批人离职交接、第三方征信接口故障导致状态卡住。这种需求如果每次都要开发改代码运维会疯掉。所以系统里要做一个隐藏的管理员工具支持有 SUPER_ADMIN 权限的人直接调整申请单、借据的状态。这个功能的实现很简单一个独立接口传入业务 ID 和目标状态系统自动记录操作日志。但它必须有两个限制不能跳过必经步骤比如不能把 SUBMITTED 直接改成 DISBURSED凡是用这个工具改过的单子必须在详情页醒目地显示一个“人工干预”标签。这既是风控要求也是万一出现纠纷时保护系统的取证手段。5.3 把操作手册 docx 变成系统内部的在线帮助回到一篇文档本身。系统开发完了操作手册 docx 依然要交付。与其交付一份孤立的文档不如在手册系统里直接引入一个help模块把 docx 里每个菜单项的说明按菜单编码比如menu_codecustomer/add拆成小片段存进数据库前端页面右下角加一个帮助按钮点开直接显示当前页面操作说明。文档更新时重跑一次导入脚本不用发版。这个思路成本极低、对业务人员很友好也是运营维护里性价比很高的事。实现方式是解析手头的 Word 文档。用 POI 按标题层级拆分标题用Heading1、Heading2区分章节把正文段落挂到对应的menu_code下。导入后做一个模糊搜索用户在帮助中心输入关键词能搜到对应操作步骤。这个功能不炫技但它把“系统操作手册.docx”从一个静态文件变成了系统的一部分后续也可以承接 AI 问答式的操作指引做到这里这份手册才真正发挥了它该有的价值。本文还有配套的精品资源点击获取