YAOTU INSIGHTS

典当业务管理系统实战:六状态建模与息费计算解析

典当业务管理系统实战:六状态建模与息费计算解析
简介这是一份《典当业务管理信息系统设计与实现》硕士学位论文面向软件开发、金融信息化领域的工程师与研究者。文档以典当行为背景完整记录了系统从需求分析到测试交付的全过程梳理典当行的抵押、贷款、赎回等业务流程划分功能性需求与非功能性需求采用基于Java的三层架构实现表示层、业务层和数据层并基于SQL Server完成E-R图、核心表结构及数据字典设计。测试通过黑盒与白盒方法验证了系统稳定性和安全性。资源包共1个doc文件大小437KB便于下载后直接阅读。目前已有113人学习浏览适合作为毕业设计、课程设计或典当类MIS项目开发的参考范本也可帮助读者掌握UML建模、Java分层架构和SQL Server数据库设计的综合实践方法。1. 一份 2009 年的典当业务论文值钱的不是技术栈是六个业务状态典当业务管理信息系统这套资源第一眼会让人觉得技术栈偏老Java 配 SQL Server 2000、B/S 结构放到今天甚至要费点劲才能跑起来。但真正值钱的不是技术栈而是它对典当业务的建模——建当、赎当、续当、绝当、冻结、挂失六个核心状态加上息费计算、权限控制和统计报表这套业务骨架拿到现在依然是典当行信息化的标准打法。适合三类人来读正在做管理信息系统类课程设计或毕业设计的从业者要接手典当、小额融资类项目的实施工程师以及想把手工作业流程梳理成系统化产品逻辑的设计人员。下面按文档结构做完整拆解。2. 需求分析把六个典当动作拆成可编程的业务规则典当这门生意表面上是「收物放款」本质上是一套有严格时间约束和利率约束的短期融资流程。需求分析做不好后面建出来的系统就是一张皮——能录数据但算不清利息、管不住状态。文档第 2 章花了大量篇幅在梳理业务模型和业务流程这一章反而是全篇最值得反复看的。2.1 角色与业务模型谁在用这个系统做什么典当行的日常操作角色大致分四类柜员、业务主办、业务主管、系统管理员财务角色可以挂在业务主管或单独的财务权限下。柜员负责民品典当的收当、赎当操作业务主办处理大宗业务房产、机动车、股权质押的初审业务主管做超权限审批和绝当确认系统管理员管用户、角色、菜单权限和系统参数。文档里对基本业务程序的描述很典型顾客持当物和身份证到店柜员先识别证件、登记当户信息然后验当估值确定当金和费率开具当票最后放款。整个流程看起来简单但放到系统里要拆成至少五个事务边界当户建档、当物登记、估值录入、当票生成、放款确认。每个边界都可能中途放弃所以数据表设计时不能把这几步塞进一条 SQL 里。大宗业务和民品业务的流程差异也要在需求阶段想清楚。文档里专门提到业务部门制定的分支大宗业务操作房产、机动车这类当物需要实地查验、评估和更高一级的审批流程比民品长角色参与更多。做系统设计时我的习惯是民品走「快速通道」大宗走「审批通道」两类流程共用当票主表但状态流转的控制点不同。2.2 用例图与数据流需求怎么变成模块清单文档用 UML 用例图梳理了系统功能这是把业务需求转成系统模块的关键一步。整理后的角色与用例大致如下角色核心用例说明柜员建当、赎当、续当、冻结、挂失、当物盘存日常民品业务全部操作业务主办大宗业务初审、验当、估值录入房产、机动车等大额当物业务主管超权限审批、绝当确认、费率调整高风险业务控制点财务营业统计、报表查看、对账只读为主部分冲正权限系统管理员用户管理、角色管理、菜单权限、系统参数不含业务数据操作数据流图分三层看更清晰。顶层图就是当户与系统之间的交互当户拿当物进来系统输出当金当户来赎当系统收回本金和息费。0 层图把系统拆成业务处理、统计查询、权限管理三大子系统。1 层图再看「建当」这个动作数据流依次经过当户登记、当物估值、审批、放款、开票五个处理节点。做需求分析时能把这五步的数据流向画明白模块划分就不会跑偏。2.3 业务规则怎么固化成约束当期、息费、绝当三条主线典当系统的业务规则可以浓缩成三条主线。第一条是当期计算当期由当户和柜员约定通常按自然月计算典当期满前当户可以续当续当次数有没有上限由企业制度决定。第二条是息费计算典当行的收入来自综合费加利息综合费率和利率在当期开始时锁定续当时要先结清前一段息费才能续下一期。第三条是绝当判定当户逾期不赎当也不续当超过行业惯例的宽限日后当物归典当行所有这一条最容易在实现时漏掉自动判定。文档在业务需求里把这几个动作拆成了完整的业务流程说明。落到代码时我一般先把状态机定义出来再写业务逻辑。下面这段是六个业务状态的典型定义方式// 典当票据状态机六个状态与允许的转移方向 enum PawnState { ACTIVE, // 建当完成正常当期内 RENEWED, // 已续当当期重新起算 REDEEMED, // 已赎当票据关闭 DEAD, // 已绝当当物归典当行 FROZEN, // 已冻结业务暂停 LOST; // 已挂失走补票流程 // 从当前状态能否执行某个业务动作 boolean canTransit(PawnAction action) { switch (this) { case ACTIVE: case RENEWED: // 当期和续当状态都可以赎当、再续当、冻结、挂失 return action PawnAction.REDEEM || action PawnAction.RENEW || action PawnAction.FREEZE || action PawnAction.LOST; case FROZEN: // 冻结后只能解冻或赎当 return action PawnAction.UNFREEZE || action PawnAction.REDEEM; case LOST: // 挂失后补票补票后回到原状态 return action PawnAction.REPLACE_TICKET; case REDEEMED: case DEAD: // 已关闭的票据不允许再做任何业务操作 return false; } return false; } }把状态机固化成枚举后界面上每个按钮的可用性都由canTransit统一决定而不是散落在各个 JSP 页面的 if 判断里。初学阶段很容易把状态逻辑写进页面结果同一个按钮在不同入口能走通两种不同的业务路径后续对账就会发现问题。状态机是这套系统需求分析阶段最该抄的作业。3. 架构与数据库设计B/S 三层加 pawn 库一张当票怎么建模需求分析定了「做什么」架构设计解决「怎么做」。这套系统的技术选型是典型的 2009 年企业级方案B/S 结构、Java Web 作为表示层、SQL Server 2000 存数据。放在当时看这个选型最大的好处是客户端零安装典当行几十个终端只需要维护一台服务器。3.1 技术选型为什么当时选 B/S 而不是 C/S典当行的营业网点通常不大但终端数量不少柜员、主管、财务各有一台电脑。如果用 C/S 结构每次升级都要逐台机器更新客户端实施成本太高。B/S 结构下应用部署在一台应用服务器上客户端只需要浏览器加一个身份证读卡器的 ActiveX 插件升级时只动服务器就行。这套系统的部署架构是数据库服务器单独一台跑 SQL Server 2000应用服务器跑 Tomcat 或类似容器业务终端通过局域网访问。文档里明确提到「用户只要在公司的局域网内部就可以对系统的信息进行共享和功能的调用」这就是 B/S 选型的核心场景。对比来看C/S 方案在报表展示和复杂交互上确实更流畅但典当业务本身以表单录入和流程审批为主没有高密度图形交互需求B/S 完全够用。选 B/S 还有一个隐性收益统计报表可以做成网页管理层在办公室直接打开浏览器就能看营业数据不用装任何客户端。3.2 核心实体关系ER 图里的五张必建表文档的数据库设计部分给出了完整的 E-R 图核心实体可以归纳为五类当户、当票、当物、续当记录、赎当记录。关系上需要注意两点。一个当户可以有多张当票当户与当票是一对多。一张当票可以包含多件当物当票与当物是一对多。这里有一个细节同一位顾客拿一件金饰和一台数码相机一起来典当可以开在一张当票里也可以分开开票系统设计时要允许柜员在界面里勾选当物按票分组而不是强制一张票只对应一件物品。续当记录和赎当记录都是当票的交易流水一张当票可以多次续当但赎当只会发生一次绝当确认后票据关闭。这个模型的关键在于当票主表是业务核心所有交易流水都指向当票号而不是直接挂在当户名下。这样设计的好处是统计口径清晰按票查流水、按户查汇总、按日查营业都从当票主表出发去关联子表不会出现数据口径不一致的问题。3.3 pawn 库的表结构与 DDL照着建就能用文档的数据字典部分定义了主数据库 pawn 的核心表结构。按数据字典整理出可以直接执行的建表语句字段命名和类型都遵循 SQL Server 2000 的规范-- 当户表存放客户身份信息身份证号为业务索引 CREATE TABLE t_customer ( cust_id INT IDENTITY(1,1) PRIMARY KEY, cust_name NVARCHAR(50) NOT NULL, id_no NVARCHAR(20) NOT NULL, -- 二代身份证号建当必填 phone NVARCHAR(20), address NVARCHAR(200), reg_date DATETIME DEFAULT GETDATE() ); -- 当票主表一张当票对应一次质押借贷 CREATE TABLE t_pawn_ticket ( ticket_no NVARCHAR(20) PRIMARY KEY, -- 当票号格式如 P20091015001 cust_id INT NOT NULL REFERENCES t_customer(cust_id), goods_desc NVARCHAR(400) NOT NULL, -- 当物描述登记时文本录入 appraise_amt DECIMAL(18,2) NOT NULL, -- 验当估价 loan_amt DECIMAL(18,2) NOT NULL, -- 实发当金一般低于估价 monthly_fee DECIMAL(8,4) NOT NULL, -- 月综合费率 interest_rate DECIMAL(8,4) NOT NULL, -- 月利率 pawn_date DATETIME NOT NULL, -- 建当日期 due_date DATETIME NOT NULL, -- 到期日期按当期计算 state TINYINT NOT NULL -- 状态1建当 2续当 3赎当 4绝当 5冻结 6挂失 );字段设计有几个细节值得说明。费率字段用DECIMAL(8,4)而不是DECIMAL(8,2)是因为综合费率可能算到小数点后三位留足精度防止四舍五入累积误差。当票号用NVARCHAR(20)字符串做主键而不是自增整数因为业务上要支持「P20091015001」这种带业务含义的票号打印到纸质当票上也更可读。due_date不在建当时人工录入而是由程序根据pawn_date加约定当期自动计算避免手工填错日期导致息费计算全盘出错。再补一张当物明细表和续当记录表-- 当物明细表一张当票下可登记多件当物 CREATE TABLE t_pawn_goods ( goods_id INT IDENTITY(1,1) PRIMARY KEY, ticket_no NVARCHAR(20) NOT NULL REFERENCES t_pawn_ticket(ticket_no), goods_name NVARCHAR(100) NOT NULL, -- 物品名称 quantity INT DEFAULT 1, weight DECIMAL(10,3), -- 贵金属重量可选 goods_rating TINYINT, -- 成色/品相评级 remark NVARCHAR(200) -- 备注可描述瑕疵 ); -- 续当记录表每续当一次生成一条流水 CREATE TABLE t_renew_record ( renew_id INT IDENTITY(1,1) PRIMARY KEY, ticket_no NVARCHAR(20) NOT NULL REFERENCES t_pawn_ticket(ticket_no), renew_date DATETIME NOT NULL, new_due_date DATETIME NOT NULL, -- 本次续当后的新到期日 fee_paid DECIMAL(18,2) NOT NULL -- 本期结清的息费金额 );这里把当物单独拆表的用意当物可能有多件、需要记录重量和成色如果全部拼在当票主表的goods_desc文本字段里后面做当物盘存、绝当后处置变卖时根本没法统计。拆表后盘存报表可以直接按当物明细的品类、状态、存放位置分组汇总。3.4 数据字典与库表关联没这步后面报表全对不上文档专门给出了一节数据字典这个细节在同类论文里很少见但实际项目里极其重要。数据字典的作用是统一每个字段的业务口径比如state字段的每个取值代表什么状态、fee_paid是当期结清费用还是累计费用、due_date是否含当日这些如果不定义清楚开发时各写各的三张关联表同一含义的字段取名都不一样统计报表时就会出现金额对不上的情况。库表关联方面t_pawn_goods和t_renew_record都通过外键指向t_pawn_ticket形成以当票为中心的星型结构。实际开发中外键约束在 SQL Server 2000 里可以建但很多项目为了插入性能选择不建物理外键只保留逻辑关联。我的建议是金额相关的核心关联必须建外键比如续当记录、赎当记录指向当票防止程序 bug 写入了不存在当票号的脏数据纯描述性的关联可以只保留逻辑关系比如当物表避免过度约束影响录入灵活性。4. 民品典当模块的代码骨架建当、计息、续当重算的实现细节第 5 章是文档里代码最集中的地方核心是民品典当业务的界面设计和部分源代码。这一章能看出整个系统的实现风格界面驱动业务业务逻辑集中在服务层数据库操作走 DAO。下面按实际开发时的落地顺序拆解。4.1 界面流程登记、验当、制票、放款四步走民品典当界面设计上主要分三个区域当户信息区、当物明细区、当金费率区。当户信息区支持二代身份证读卡器直接读取芯片里的姓名和身份证号不用手工敲一方面快另一方面减少录入错误。当物明细区是一个可增删行的表格柜员把顾客带来的每一件物品登记进去金饰填重量和成色数码产品填型号和备注。当金费率区显示本次估价、实发当金、月综合费率和月利率保存时系统自动计算到期日。界面的操作流程是先查当户是否已建档新客户先建档已建档的直接调出历史信息。之后录入当物、录入估值和当金、确认费率提交后系统生成当票号并落库。大宗业务的页面在提交后会多一个「送审」按钮进入主管审批队列审批通过后才生成正式当票。4.2 建当核心方法票号生成与业务数据落库建当是系统最核心的方法几乎所有业务模块都会调用。票号生成这一步要特别小心常见做法是单独建一张号段表存当日流水号生成新票号时用事务把流水号取出来加一再落当票数据两步放在同一个事务里。// 创建当票票号生成与当票落库必须在一个事务里 public PawnTicket createPawn(Customer cust, ListPawnGoods goods, BigDecimal appraiseAmt, BigDecimal loanAmt, BigDecimal monthlyFee, BigDecimal interestRate, int termMonths) { // 1. 从号段表取当日流水号日切后流水归零 String ticketNo genTicketNo(new Date()); // 如 P20241015001 // 2. 按自然月计算到期日 Calendar cal Calendar.getInstance(); cal.add(Calendar.MONTH, termMonths); Date dueDate cal.getTime(); // 3. 组装当票对象并落库 PawnTicket ticket new PawnTicket(); ticket.setTicketNo(ticketNo); ticket.setCustId(cust.getId()); ticket.setGoodsDesc(buildGoodsDesc(goods)); ticket.setAppraiseAmt(appraiseAmt); ticket.setLoanAmt(loanAmt); ticket.setMonthlyFee(monthlyFee); ticket.setInterestRate(interestRate); ticket.setPawnDate(new Date()); ticket.setDueDate(dueDate); ticket.setState(ACTIVE); pawnTicketDao.insert(ticket); return ticket; }genTicketNo的实现逻辑是先按当前日期查号段表没有记录就插入一条流水号 1有记录就执行UPDATE t_ticket_seq SET seq_no seq_no 1 WHERE seq_date ?并取回新值然后拼成PyyyyMMdd三位流水。这里的关键是加锁方式SQL Server 2000 下用UPDATE语句自带的行锁就能保证并发时两个请求不会拿到同一个流水号。如果放在应用层用SELECT再UPDATE两个请求同时读到一个号码必然出现票号重复。4.3 续当与息费重算最容易算错的三个点续当业务是息费计算最容易翻车的地方。业务流程上当户在到期日或宽限期内来续当必须先结清上一期的综合费和利息然后按新的到期日重新计算下一期。系统里常见的做法是统一按「每月 30 天」折算日费率而不是按自然月的实际天数逐月计算这样系统内所有票据的计息口径一致不会出现大月小月带来的争议。// 计算续当时应结清的息费本金 * 月费率 * 已当天数 / 30 public BigDecimal calcRenewPayable(PawnTicket ticket, Date renewDate) { long days daysBetween(ticket.getPawnDate(), renewDate); if (days 0) { throw new BusinessException(续当日期不能早于建当日期); } BigDecimal monthlyRate ticket.getMonthlyFee() .add(ticket.getInterestRate()); BigDecimal fee ticket.getLoanAmt() .multiply(monthlyRate) .multiply(new BigDecimal(days)) .divide(new BigDecimal(30), 2, RoundingMode.HALF_UP); return fee; }这段代码里有三个容易踩坑的点。第一daysBetween计算的是自然日差实际典当业务里有的公司按「对日」计算即 10 月 10 日建的当11 月 9 日来赎按 30 天算11 月 10 日按 31 天算这里的口径必须由业务部门确认不能开发自己拍板。第二fee只算了综合费和利息没收逾期违约金实际业务里逾期未赎通常还有滞纳金需要单独配置。第三除法的舍入方式用HALF_UP这是金融系统的基本要求不能用向下取整否则每笔业务都会少收一分钱月底对账必出问题。续当动作本身更新的是当票主表的due_date和state同时往t_renew_record插一条流水这两步也必须在一个事务里。如果只更新主表忘记插流水后续查续当历史就断了绝当判定也会因为缺失记录而算错时间。4.4 到期当票提示与营业统计两个高频 SQL文档提到系统提供到期当票提示功能这是典当行日常风控最依赖的一个功能。实现思路很简单一个定时任务或首页查询把未来七天到期且状态为建当或续当的当票列出来提醒柜员联系当户赎当或续当。-- 未来七天内到期的有效当票 SELECT t.ticket_no, c.cust_name, t.due_date, t.loan_amt, DATEDIFF(day, GETDATE(), t.due_date) AS remain_days FROM t_pawn_ticket t JOIN t_customer c ON t.cust_id c.cust_id WHERE t.state IN (1, 2) -- 建当或续当状态 AND t.due_date BETWEEN GETDATE() AND DATEADD(day, 7, GETDATE()) ORDER BY t.due_date;营业统计按日汇总的 SQL 逻辑也类似只是把维度换成日期和业务类型。做统计报表时有个经验所有金额类统计都以主表状态字段为准先过滤掉已绝当或已挂失的无效数据再按日期分组这样报表数字才不会和柜台实际流水对不上。5. 部署、测试与避坑指南五个让典当系统翻车的常见问题系统实现部分看完了下面进入文档第 5 章的测试与实际问题环节。这一章内容不多但把测试用例的表格结构和部署环境的注意事项讲清楚了。结合我在类似项目里的实施经验补充几条真实场景下最容易踩的坑。5.1 测试方案黑盒为主白盒补关键路径文档的测试思路是黑盒测试覆盖功能流程白盒测试覆盖关键计算逻辑。测试用例的设计重点放在业务规则验证上而不是单纯的界面点检。实际能直接参考的测试用例大致如下测试项前置条件输入数据预期结果新客户建当客户未建档身份证、金饰一件、估价 5000生成当票 P日期流水状态为建当同日两笔建当已有一笔当票第二笔提交票号连续递增无重复续当息费计算当票建当满 15 天发起续当结清金额 当金 × 月费率 × 15 / 30绝当自动判定当票逾期超过宽限期触发定时任务状态转为绝当当物转入待处置权限控制柜员角色无调价权限修改当金并保存界面报无权限数据不落库表格里的绝当自动判定用例是文档功能需求里提到但最容易在实现时被忽略的一条。很多系统做完建当、赎当、续当就觉得完事了绝当靠人工在界面点结果月底库里有几十张已经过期一个多月还挂在「建当」状态的票当物盘存账实不符。绝当一定要有自动任务兜底。5.2 部署要点SQL Server 2000 的兼容性处理这套系统的数据库是 SQL Server 2000在现在的 Windows 版本上部署会有些障碍。常见做法是装 SQL Server 2000 时选「兼容模式」运行安装程序装完打 SP4 补丁数据库文件用附加方式挂载。附加语句如下-- 附加典当业务数据库mdf 和 ldf 文件要放在数据目录 EXEC sp_attach_db pawn, D:\Data\pawn.mdf, D:\Data\pawn_log.ldf;JDBC 连接串用的还是老式驱动写法是jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseNamepawn驱动类名是com.microsoft.jdbc.sqlserver.SQLServerDriver。如果换成 SQL Server 2005 以后的版本连接串要改成jdbc:sqlserver://。这类问题在文档里没写但实际复现时一定会遇到。提示SQL Server 2000 的老版 JDBC 驱动在较新的 JDK 上可能无法加载报错信息指向类找不到。这不是代码问题是驱动太老需要换用兼容的 JDK 版本或升级到新驱动。5.3 五大踩坑记录现象、原因、解决测试和部署过程中下面五类问题几乎每个接手这套系统的人都会撞上。按现象、原因、解决的顺序列出来能省不少排查时间。坑一当票号重复或跳号。现象是同一张号段表下出现了两张票号一模一样的当票或者后台服务器重启后票号从 1 重新开始。原因是票号流水存在应用内存里两台应用服务器之间的计数没有同步重启后内存清零。解决方法是把流水号落到数据库号段表用事务更新取号绝不在应用层维护计数器。坑二续当后赎当金额不对。现象是顾客续当一次后第五天来赎当系统算出的息费比手算多出一截。原因是续当后的起息日没有改成「原到期日次日」系统按续当操作当天重新起算再把前段息费又算了一遍。解决方案是续当接口里强制重置计息起点用new_due_date作为下一期唯一基准不读取界面传入的日期。坑三统计报表金额大于业务流水。现象是营业日报汇总出来的放款总额比当日所有当票的loan_amt加起来多了好几千。原因是部分已作废的当票没有被过滤或者作废操作只改了界面状态没改数据表。解决方法是统计 SQL 统一加状态过滤条件作废动作必须走业务接口不能直接改库里字段。坑四数据库文件附加失败。现象是拿到 mdf 文件在另一台机器上附加时报「文件头部信息不一致」。原因是原数据库文件被高版本 SQL Server 打开过文件头版本号已经升级低版本实例拒绝加载。解决方法是准备原环境一致的 SQL Server 2000 实例和 SP4 补丁或者在交付时同时提供数据库脚本和初始化数据避免直接传 mdf 文件。坑五改了角色权限用户刷新后依然没权限。现象是管理员给柜员角色勾选了「调价」权限柜员重新登录后台还是提示无权。原因是登录时权限列表被一次性放入 Session后续权限变更没有同步到已登录会话。解决方法是权限变更时主动使对应角色的会话失效或者每个高权限操作实时从数据库校验权限而不是无条件信任 Session 里的旧数据。6. 移植到现代环境表结构迁移与六流程验证清单这套文档要真正用起来最后一步是把它从 SQL Server 2000 的老环境搬到现代技术栈。迁移时优先保留的是业务模型和表结构需要改动的是数据库方言和连接层。6.1 迁移的核心改法数据类型映射上NVARCHAR换成 MySQL 的VARCHAR时注意字符集统一用utf8mb4DATETIME换成TIMESTAMP或DATETIMEIDENTITY对应 MySQL 的AUTO_INCREMENTDECIMAL保持不动。统计报表里的GETDATE()换成NOW()DATEADD(day, 7, GETDATE())换成DATE_ADD(NOW(), INTERVAL 7 DAY)。业务逻辑层如果当年是 Servlet 加 JDBC迁移到 Spring Boot 后 DAO 换 MyBatisService 层实现基本可以原样保留因为息费计算、状态流转、票号生成的代码和框架无关。6.2 复现后的核心验证清单迁移完成后业务正确性验证按下面清单走一遍再跑报表就有底气了验证项操作通过标准建当新客户建当一笔票号格式正确到期日计算正确赎当当期未到直接赎当实收金额等于当金加当期息费续当到期日前续当结清前段息费新到期日正确挂失登记挂失后补票原票作废补票后可赎当绝当构造逾期票据触发任务状态自动转绝当报表可见统计跑当日营业汇总金额与当票明细逐笔可对这套文档我看完最大的收获是它把典当业务里最容易被做成黑匣子的「息费计算」和「状态流转」讲清楚了。复现这套系统时不要先纠结界面好不好看先把建当、赎当、续当、绝当、冻结、挂失六条主流程走通再回过来加权限和报表。从那以后我每次接手管理信息系统项目第一件事就是先找数据字典和状态流转图把状态之间所有可能的跳转路径列出来再决定从哪块代码开始读。先看业务再看代码能省下好几个晚上的排查时间。希望帮到你。本文还有配套的精品资源点击获取