ARTICLE DETAIL

资讯详情

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

Unieap Java简单项目:设备借用登记的事务、并发扣减与验收

Unieap Java简单项目:设备借用登记的事务、并发扣减与验收

简介:Unieap简单小项目是一份面向Java初学者和业务流程开发入门者的示例工程,基于Unieap 3.4框架展示企业级应用的构建思路,适合用来理解工作流引擎、表单设计、数据模型与权限控制等核心组件如何协同。压缩包约82.8MB,文件总数与类型明细上游暂未提供,具体工程结构与配置以包内实际内容为准。已有387人浏览学习,说明该示例在入门群体中具有一定参考价值。读者可从中接触基于BPMN 2.0的流程定义、图形化表单定制、实体映射与CRUD处理,以及角色权限分配的基本做法;结合示例代码进行运行调试,还能熟悉Unieap项目的启动、流程部署与业务逻辑编写过程,把分散的框架概念串成完整项目案例,为后续开发企业级应用积累直观经验。对于希望快速了解Unieap主要功能与使用方式的开发者,这份示例也具备一定的参考和练手价值。

1. 从一次演示现场说起:Unieap 上的 Java 简单项目,难点不在页面

前几年帮人看课设演示,遇到过一个很典型的场面:页面做得很齐整,查询、新增、修改一应俱全,讲的时候也顺,结果点了一次「提交借用」,列表里冒出两条一模一样的记录。问怎么回事,回答是「手抖点了两下」。再追一句服务端有没有做幂等、有没有事务,就答不上来了。这类 Unieap 平台上的 Java 简单项目,卡人的地方几乎从来不是页面画不出来,而是页面背后那层业务逻辑没人认真设计过——库存扣没扣对、状态能不能回退、并发下会不会超借、出错之后数据是不是半截。

这篇方案面向两类人:一类是要用 Unieap 交一份 Java 简单项目的学生,希望结构完整、能讲清楚、能跑起来;另一类是从业者,手上有个小系统要快速交付,想找一套能直接改的骨架。下面按需求收缩、数据建模、服务端与页面实现、避坑排查、验收答辩的顺序,把一个小项目从想法推到能交付的状态。

2. 需求界定与范围收缩:把「简单小项目」压到 5 个功能点、一周可交付

小项目失败的原因,八成不是技术不够,而是一开始胃口太大。命名叫「XX 管理系统」,功能列表写了两页纸,结果每个模块都只做了一半,答辩时哪个都讲不深。我一般的做法是先把范围砍到「一个人一周能完整跑通、每一行代码都能解释」的体量,再往上加。

2.1 场景锚定:为什么选设备借用登记,而不是综合管理平台

选场景有三个判断标准:业务闭环短、状态变化明确、有天然的数据一致性要求。设备借用登记刚好三条都占。一次完整的业务就是「申请—审批—借出—归还」,四个动作串成一条线,任何一步都能在页面上看得见;而且它有库存这个概念,扣减和回滚是绕不开的,正好拿来体现服务端设计能力。

对比一下常见的几个选题方向,差距很明显:

选题方向业务闭环状态流转数据一致性考验一周可完成度
综合管理平台长,边界模糊弱无低
图书借阅短中中高,但同质化严重
选课系统中中强(并发抢课)中,并发部分容易讲不清
设备借用登记短强强(库存扣减)高

选课系统的并发问题其实更适合做深,但对「简单项目」来说,高并发场景往往只能靠压测工具硬造,演示时说服力不足。设备借用的库存是真实存在的实物数量,扣减逻辑一行 SQL 就能验证对错,讲起来更有底气。

具体场景设定为某实验场所的仪器设备借用:管理员维护设备台账,普通用户提交借用申请,管理员审批后设备出库,归还时回填实际归还时间。全部虚构,不涉及任何真实机构。

2.2 功能清单与角色边界:把「能演示」翻译成「能验收」

功能清单不能写成愿望清单,每一条都要能被验证。我习惯按「谁、在哪个页面、做什么操作、系统产生什么结果」四段式来写,写完直接就是验收用例。这个项目的清单如下:

编号角色功能点输入系统结果验收方式
F1管理员设备台账维护设备编号、名称、总数量新增/修改成功,编号唯一重复编号提交被拒绝并提示
F2普通用户提交借用申请设备、数量、预计归还日期生成待审批单据,状态为待审批列表可见,库存暂不扣减
F3管理员审批通过/驳回单据号、审批意见通过则扣减可用库存,驳回则单据关闭库存数与台账一致
F4普通用户归还登记单据号、实际归还时间库存回加,状态变为已归还重复归还不重复加库存
F5两种角色单据查询设备名、状态、日期区间分页返回,按角色过滤可见范围普通用户看不到他人单据

五个功能点,两个角色,三条主流程。这个体量写代码大概两到三天,剩下时间全部用来打磨异常分支和写答辩材料——异常分支才是拉开差距的地方。

角色边界必须写死。普通用户只能看到自己提交的单据,管理员能看到全部;普通用户没有任何直接改库存的入口,库存只能由审批和归还这两个动作间接改变。这条规则看着简单,但它是后面所有权限过滤和服务层校验的依据,写进需求文档比写在脑子里可靠。

2.3 三条硬约束与明确的「不做什么」

需求里我通常固定三条硬约束,它们决定了后面的技术选型:

第一条,库存不能为负。任何时刻可用库存小于申请数量,申请就必须被拒绝,不管前端有没有校验。这条约束最终会落成一条带条件的 UPDATE 语句。

第二条,单据状态只能单向流转。待审批可以变成已通过或已驳回,已通过可以变成已归还,除此之外没有别的路径。状态回退的需求一律不做,做了就要处理一堆历史数据问题。

第三条,所有关键操作留痕。谁在什么时间做了什么操作,写一条流水记录,出问题时能倒查。这条是小项目的加分项,实现成本很低,一个字段加一行插入。

明确不做的部分同样要写下来:不做文件附件上传、不做消息通知、不做多级审批、不做数据导入导出、不做移动端适配。这些功能单个看着都不难,但每一个都会牵扯出额外的表结构、额外的异常分支和额外的演示时间,对「简单项目」来说是纯负担。

提示:范围收缩的结论要落在纸面上,最好直接写进设计文档的「范围说明」一节。后面无论谁提出加功能,都拿这份说明对照,避免项目越做越散。

3. 数据建模与总体架构:四张表撑起业务,三层结构划清职责

数据模型定错,后面全是补丁。小项目最容易犯的错是表建得太随意——主键用自增、状态用中文、时间用字符串,跑起来没问题,一涉及统计和排序就露馅。这一章把四张表的设计意图说清楚,再讲三层结构怎么分工。

3.1 四张核心表:字段取舍与索引设计

四张表分别是部门表、用户表、设备台账表、借用单表。部门表在这个项目里不是必需的,但如果要做「按部门统计借用情况」这类展示,有它省事很多,而且它能让关联查询有内容可讲。

建表语句以 MySQL 为例,其他数据库把类型换掉即可:

-- 设备台账:库存的唯一权威来源 CREATE TABLE t_asset ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', asset_no VARCHAR(32) NOT NULL COMMENT '设备编号,业务唯一键', asset_name VARCHAR(64) NOT NULL COMMENT '设备名称', total_qty INT NOT NULL DEFAULT 0 COMMENT '总数量', available_qty INT NOT NULL DEFAULT 0 COMMENT '可用数量', status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME NOT NULL COMMENT '创建时间', update_time DATETIME NOT NULL COMMENT '更新时间', PRIMARY KEY (id), UNIQUE KEY uk_asset_no (asset_no), KEY idx_name (asset_name) ) COMMENT '设备台账'; -- 借用单:一条记录对应一次完整业务 CREATE TABLE t_borrow_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', order_no VARCHAR(32) NOT NULL COMMENT '单据号,业务唯一键', asset_id BIGINT NOT NULL COMMENT '设备主键', apply_qty INT NOT NULL COMMENT '申请数量', applicant_id BIGINT NOT NULL COMMENT '申请人主键', apply_time DATETIME NOT NULL COMMENT '申请时间', expect_return DATETIME NOT NULL COMMENT '预计归还日期', real_return DATETIME DEFAULT NULL COMMENT '实际归还时间', order_status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审批 1已通过 2已驳回 3已归还', approve_by BIGINT DEFAULT NULL COMMENT '审批人', approve_time DATETIME DEFAULT NULL COMMENT '审批时间', approve_remark VARCHAR(200) DEFAULT NULL COMMENT '审批意见', PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_applicant (applicant_id), KEY idx_status_time (order_status, apply_time) ) COMMENT '借用单'; -- 用户表与部门表字段略去冗余项,核心是 id、姓名、部门 id、角色 CREATE TABLE t_user ( id BIGINT NOT NULL AUTO_INCREMENT, login_name VARCHAR(32) NOT NULL COMMENT '登录名,唯一', user_name VARCHAR(32) NOT NULL COMMENT '姓名', dept_id BIGINT NOT NULL COMMENT '部门主键', role_code VARCHAR(16) NOT NULL COMMENT 'ADMIN/USER', PRIMARY KEY (id), UNIQUE KEY uk_login (login_name) ) COMMENT '用户'; CREATE TABLE t_dept ( id BIGINT NOT NULL AUTO_INCREMENT, dept_name VARCHAR(64) NOT NULL COMMENT '部门名称', PRIMARY KEY (id) ) COMMENT '部门';

几个字段的取舍值得单独说。available_qty和total_qty分成两个字段,而不是靠实时统计借用单算出来,原因是统计口径一旦复杂(比如要扣掉维修中的数量),实时查询会变得很难维护,而冗余一个字段只需要保证每次库存变化都在同一个事务里更新。order_no走业务唯一键而不是只用自增主键,是为了在页面上给用户看,同时天然具备幂等能力——同一张单据号重复提交会被唯一索引挡住。时间字段一律用DATETIME而不是字符串,否则按日期区间查询时排序和比较全靠字符串字典序,很容易出玄学问题。

索引方面只建了三类:业务唯一键、外键查询字段、状态加时间的组合索引。小项目数据量不大,索引过多反而拖慢写入,够用就行。外键约束我不建议加,理由很实际——删数据时外键会挡住操作,而这只增加了排查成本,真正的参照完整性靠服务层校验更可控。

3.2 三层结构:页面层、服务层、数据层各管什么

Unieap 这类平台的典型结构是页面建模 + Java 服务层 + 数据访问层,平台负责页面渲染、请求分发和会话管理,业务逻辑得自己写。分工一旦混乱,最常见的结果就是 Java 里塞满了页面参数处理,页面配置里塞满了业务判断。

层次主要职责明确不做什么
页面层表单渲染、字段格式校验、请求参数收集、结果提示不做业务规则判断,不直接执行 SQL
服务层业务规则、状态流转、事务边界、权限校验不拼 HTML,不关心字段在页面上的排列
数据层SQL 执行、结果集映射、分页不做业务判断,不吞异常

页面层只做「格式对不对」的校验,比如数量必须是正整数、归还日期不能早于今天;服务层做「业务允不允许」的校验,比如可用库存够不够、这张单据当前状态能不能归还。两层都校验不是重复劳动,页面校验是为了体验,服务层校验是为了正确性——用户绕开页面直接调接口的情况永远存在。

工程目录我一般这样组织,包名按你本地平台工程的实际结构替换:

src/main/java/com/example/asset/ ├── service/ 业务接口与实现,事务加在这一层 ├── service/impl/ ├── dao/ 数据访问接口 ├── dao/impl/ SQL 与结果映射 ├── entity/ 与表结构对应的实体 ├── dto/ 页面传输对象,与实体分离 └── common/ 统一返回结构、单据号生成器、常量

实体和 DTO 分开这一条,小项目里经常被省略,但省略之后会遇到一个具体麻烦:页面需要展示设备名称,而借用单表里只存了设备 id。要么在实体里加一个不属于表结构的字段,要么另建一个传输对象。前者会让 ORM 映射配置变得别扭,后者干净得多。多写一个类,省下的是后面改字段时的心惊胆战。

3.3 状态机:借用单的四个状态与流转条件

状态设计不是画个图就完事,每个状态迁移都要对应一个明确的前置条件和一组副作用:

当前状态动作目标状态前置条件副作用
无提交申请待审批数量大于 0 且不超过可用库存生成单据号,写流水
待审批审批通过已通过当前操作人为管理员扣减可用库存,记录审批人
待审批审批驳回已驳回审批意见非空不扣库存,写流水
已通过归还登记已归还实际归还时间不早于申请时间回加库存,写入归还时间
待审批撤销已驳回仅申请人本人可撤销写流水

注意审批通过时才扣库存,提交申请时不扣。这个选择有实际考虑:如果提交即扣,用户提交一堆申请但不审批,库存就被占死了,还得做超时释放。先审批后扣减,业务上更贴近真实场景,实现上也少一个定时任务。

副作用必须写在同一个事务里。扣库存和改状态如果分两个事务提交,中间失败就会出现「状态通过了但库存没扣」的黑匣子数据,事后对账非常痛苦。这个坑几乎每个手写事务的人都踩过,后面第 5 章会详细讲。

4. 服务端与页面实现:事务边界、并发扣减和表单校验的落地写法

设计说清楚了,落到代码上还有一堆细节:事务加在哪一层、库存怎么扣才不会被并发穿透、分页查询的条件怎么拼才不出注入、页面配置和 Java 逻辑的边界在哪。这一章给的是可以直接改的骨架代码。

4.1 服务层核心方法:申请借用如何一次写完不返工

先看审批通过这个动作,它同时涉及状态变更和库存扣减,是整条链路里最需要小心的地方:

@Service public class BorrowServiceImpl implements BorrowService { @Resource private BorrowOrderDao borrowOrderDao; @Resource private AssetDao assetDao; /** * 审批通过:改状态 + 扣库存,必须同一事务 * @param orderId 借用单主键 * @param approveBy 审批人主键 * @param remark 审批意见 */ @Override @Transactional(rollbackFor = Exception.class) public void approve(Long orderId, Long approveBy, String remark) { // 1. 带状态条件查询,防止重复审批 BorrowOrder order = borrowOrderDao.selectByIdAndStatus(orderId, 0); if (order == null) { throw new BizException("单据不存在或已被处理"); } // 2. 条件更新扣库存,库存不足时影响行数为 0 int affected = assetDao.deductStock(order.getAssetId(), order.getApplyQty()); if (affected == 0) { throw new BizException("可用库存不足,无法通过"); } // 3. 更新单据状态,同样带原状态条件 int row = borrowOrderDao.updateStatus(orderId, 0, 1, approveBy, remark); if (row == 0) { // 抛出异常触发回滚,第 2 步的扣减一并撤销 throw new BizException("单据状态已变更,请刷新后重试"); } } }

对应的扣减 SQL 是关键,它用一条语句同时完成判断和更新:

-- 只有可用数量足够时才扣减,返回影响行数用于判断 UPDATE t_asset SET available_qty = available_qty - #{qty}, update_time = NOW() WHERE id = #{assetId} AND status = 1 AND available_qty >= #{qty};

这段代码有几个设计点值得说明。@Transactional(rollbackFor = Exception.class)中的rollbackFor不能省,默认只对运行时异常回滚,抛出受检异常时事务不会回滚,这是新手最容易忽略的一行。扣减库存没有采用「先查询再判断再更新」的三步写法,因为那三步之间存在时间窗口,两个请求同时查到库存为 1,就会都判断成功、都执行扣减,最终库存变成负数。改成带available_qty >= #{qty}条件的单条 UPDATE,判断和写入在数据库层面原子完成,影响行数为 0 就代表没扣成功。第三步更新状态时也带上了原状态0作为条件,防止两个管理员同时点审批,一个成功一个失败但库存被扣了两次。

如果平台的运行环境不方便使用注解式事务,就把这三步包在平台的声明式事务模板里,思路完全一致:边界在服务层,不在 DAO 层,更不在页面层。

参数说明:orderId是主键,approveBy从会话中取,不要让页面传,否则可以伪造他人身份审批。remark驳回时要求非空,通过时可选。

4.2 跨表分页查询:条件拼装与防注入的正确姿势

列表页要展示设备名称、申请人姓名,必须跨三张表关联。手写 SQL 时条件拼装是最容易出问题的地方:

public PageResult<BorrowOrderDto> queryPage(BorrowQuery query, Long currentUserId, String roleCode) { StringBuilder sql = new StringBuilder(); List<Object> params = new ArrayList<>(); sql.append("SELECT o.id, o.order_no, a.asset_name, u.user_name, ") .append("o.apply_qty, o.apply_time, o.expect_return, o.order_status ") .append("FROM t_borrow_order o ") .append("LEFT JOIN t_asset a ON a.id = o.asset_id ") .append("LEFT JOIN t_user u ON u.id = o.applicant_id ") .append("WHERE 1 = 1 "); // 普通用户只能看自己的单据,这个条件必须由服务端拼,不能信页面传参 if (!"ADMIN".equals(roleCode)) { sql.append("AND o.applicant_id = ? "); params.add(currentUserId); } // 所有外部输入一律用占位符,绝不字符串拼接 if (StringUtils.isNotBlank(query.getAssetName())) { sql.append("AND a.asset_name LIKE ? "); params.add("%" + query.getAssetName().trim() + "%"); } if (query.getStatus() != null) { sql.append("AND o.order_status = ? "); params.add(query.getStatus()); } if (query.getStartDate() != null) { sql.append("AND o.apply_time >= ? "); params.add(query.getStartDate()); } sql.append("ORDER BY o.apply_time DESC"); return dao.queryPage(sql.toString(), params, query.getPageNo(), query.getPageSize()); }

逻辑说明:用StringBuilder拼的是结构,值全部走?占位符,这样既保留了动态条件的灵活性,又不会因为用户输入' OR '1'='1之类的字符串改变 SQL 语义。权限条件放在最前面并且不受任何页面参数影响,普通用户即使改了前端传的角色标识也看不到别人的单据——角色信息从会话读,不从请求读。

分页部分由 DAO 统一处理:先执行一次COUNT(*)得到总数,再用LIMIT offset, size取当页数据。offset = (pageNo - 1) * pageSize,pageNo从 1 开始,页面传 0 或负数时在入口处统一修正为 1,否则会出现负偏移导致 SQL 报错。

注意:模糊查询的%一定要拼在参数值里,拼在 SQL 字符串里会破坏占位符结构。日期区间用的是>=和<=,两端都要传,只传一端时把另一端补成当前时间或空条件。

4.3 页面层:页面建模负责什么,Java 负责什么

Unieap 的页面建模通常以 XML 描述页面结构和数据绑定,具体节点名各版本有差异,结构大体是这样:

<!-- 借用申请页面:只声明字段、绑定和基础校验 --> <page id="borrowApply"> <form id="mainForm" submitUrl="/borrow/apply"> <field name="assetId" label="设备" type="select" required="true" /> <field name="applyQty" label="借用数量" type="number" required="true" min="1" max="99" /> <field name="expectReturn" label="预计归还" type="date" required="true" /> <field name="remark" label="用途说明" type="textarea" maxLength="200" /> </form> </page>

页面配置只声明「有哪些字段、什么类型、必填与否、取值范围」,不写业务判断。比如「数量不能超过可用库存」这条就不该放在这里,因为可用库存是随审批变化的动态值,写死在页面上要么过期,要么需要额外接口实时拉取,徒增复杂度。把它放在服务层,校验失败返回明确提示,体验一样够用。

页面提交前的格式校验用一小段脚本兜底,主要目的是减少无效请求:

function validateForm(form) { var qty = parseInt(form.applyQty.value, 10); if (isNaN(qty) || qty <= 0) { alert('借用数量必须是大于 0 的整数'); return false; } // 日期比较统一转成时间戳,避免字符串比较的格式问题 var expect = new Date(form.expectReturn.value).getTime(); if (expect < new Date().setHours(0, 0, 0, 0)) { alert('预计归还日期不能早于今天'); return false; } return true; }

这段校验挡的是手滑,不是恶意。真正的规则在服务层再校验一遍,两边判断口径要保持一致,否则会出现「前端说可以、后端报错」的割裂感,用户会觉得系统有毛病。

4.4 配置与部署:数据源、日志、打包的三处必改项

项目从本地跑通到能在目标环境启动,通常卡在这三处:

数据源配置。连接串、用户名、密码一般写在平台的配置文件里,不要硬编码在 Java 中。连接池大小对小项目来说 5 到 10 就够,设置过大会在演示机器上占满资源。字符集要显式指定,避免中文写入变成问号。

日志配置。至少要打出三类日志:请求入口参数、关键业务动作(审批、归还)、异常堆栈。日志级别在开发环境用 DEBUG,部署时切到 INFO,否则一个列表查询就能刷出几十行无关输出,真出问题时反而找不到关键信息。

打包与路径。打出来的包要确认包含页面配置文件、静态资源、以及平台要求的所有描述文件。路径引用尽量用相对路径,绝对路径在换机器部署时必然失效,这是血泪经验。

启动之后先别急着点业务,按顺序验三件事:登录能不能过、空列表能不能正常渲染、提交一条数据看数据库里有没有。三步都通过,再进入功能验证。

5. 避坑与常见问题排查:五类最容易翻车的现场

下面这五类问题,几乎每个做平台小项目的人都会撞上一两个。按「现象、原因、解决」的顺序写,方便对照排查。

5.1 数据层翻车:编码、时间、主键三件套

现象:页面输入中文,数据库里存成问号或者乱码;按日期区间查询时,明明有数据却查不出来;新增记录偶尔报主键冲突。

原因:编码问题通常出在连接串没指定字符集,或者数据库、表、字段三层的字符集不一致,字符在传输过程中被转换了两次。日期查不出来多半是字段类型用了字符串,存的是2024/5/1这种格式,而查询条件传的是2024-05-01,两者字符串比较自然对不上。主键冲突则常见于自增主键被手动赋值,或者用了「查询最大 id 加一」的方式生成主键,并发下必然重复。

解决:连接串显式带上字符集参数,建库建表时统一字符集,字段类型一律用数据库原生日期类型。主键交给数据库自增或者序列,需要业务编号时单独生成一个带日期和随机后缀的单据号,用唯一索引兜底,生成冲突就重试一次。

5.2 服务层翻车:事务不生效与并发超借

现象:审批通过后状态改了但库存没扣,或者库存扣了状态没改;压测或者手快连点两次,可用库存变成负数。

原因:事务不生效最常见的三种情况是——注解加在了 private 方法上、同类内部方法直接调用绕过了代理、异常被 catch 住没有重新抛出。并发超借则是前面说的三步写法导致的判断与写入之间存在时间窗口。

解决:事务注解只加在 public 方法上,跨方法调用走注入的代理对象,catch 之后要么重新抛出要么手动标记回滚。库存扣减改成带条件的单条 UPDATE,用影响行数判断成败,永远不要先查后改。单据状态更新同样带原状态条件,形成乐观锁的效果。这两条改完之后,把审批接口用多线程跑二十次,库存数应该严格等于初始值减去成功审批数量,对不上就说明还有漏洞。

5.3 服务层翻车:权限校验只做在页面上

现象:普通用户在页面上看不到别人的单据,但把请求里的用户标识改一下,就能看到全部数据。

原因:权限过滤依赖页面传参,服务端直接信任了请求中的角色或用户标识。

解决:角色和用户标识一律从会话上下文获取,服务层每个查询方法的第一个动作就是按当前用户拼权限条件。这条规则没有例外,凡是涉及数据可见性的接口,都要在服务层做一次独立校验,页面上的隐藏和置灰只是体验优化。

5.4 页面与部署翻车:缓存、路径与打包

现象:改了页面配置,重新部署后还是旧样子;本地跑得好好的,换台机器启动就报找不到文件。

原因:浏览器和中间件都可能缓存静态资源,页面配置文件有时被打进了缓存目录;路径问题则多来自绝对路径或者依赖了本机特有的目录结构。

解决:调试阶段强制刷新并清理中间件工作目录,发布时给静态资源加版本后缀。所有路径引用改成相对于应用根的写法,打包后在目标机器上重新解压一份完整包,不要靠复制部分文件来更新。

5.5 排查顺序:从日志到数据,别从代码猜

出问题时最容易犯的错是打开 IDE 盯着代码看。我一般按这个顺序走:先看日志里最后一次请求的入参和异常栈,确认问题出在哪一层;再直接连数据库查那条记录,看数据本身对不对;最后才回到代码。经验是七成的问题在第一步就能定位——要么参数根本不对,要么异常栈写得很清楚,只是之前没人看。

6. 验收与答辩:一张自查表加一段五分钟演示脚本

到了交付这一步,很多人还在纠结功能全不全,其实评审看的不是功能数量,而是你对自己做的东西有多清楚。我的做法是准备两样东西:一张自查表,一段固定顺序的演示脚本。

自查表按「能跑、能对、能讲」三层来列,每一条都必须当场能验证:

层次自查项判定标准
能跑冷启动后五个功能点各走一遍无报错,页面正常渲染
能跑换一台机器部署修改数据源配置后可启动
能对库存数与台账一致审批前后差值等于申请数量
能对重复提交同一单据只产生一条记录,库存只扣一次
能对普通用户越权查询返回空结果或拒绝
能讲状态流转图能默画四个状态、五条迁移条件说清
能讲事务边界能指出来明确说出哪几行在同一个事务里

演示脚本控制在五分钟,顺序固定:先讲一句业务背景和角色划分,然后以普通用户身份提交一条申请,切管理员账号审批通过,回到列表看状态和库存变化,再做一次归还,最后展示一条被驳回的单据。整个过程中途不要打开代码,也不要切到数据库客户端——这些留到提问环节。演示的目的是让评审看到一条完整闭环,而不是看你操作有多熟练。

答辩提问里出现频率最高的三个问题,提前想好答案:库存为什么用冗余字段而不是实时统计;审批和扣减为什么放在一个事务里;普通用户的权限过滤为什么不能靠页面。这三个问题答得出来,基本就稳了。

我自己吃过一次亏,早年间交一个课程项目,功能都做完了,答辩时被问到「两个人同时审批同一张单会怎样」,当场愣住,只能含糊说应该不会同时操作。后来回去一测,库存直接扣成负数。从那以后我每次交付前,都会针对每个写操作强制走一遍「重复执行两次」和「两人同时执行」这两个场景,不管项目多小。这套习惯比任何功能都值钱。

上面这套结构,从需求收缩、四张表建模、服务层事务与并发处理,到避坑清单和验收脚本,基本可以直接套到自己的 Unieap Java 简单项目上改。工程文件、建表脚本和页面配置都在同一个包里,按你本地平台的包名和配置项替换一下就能跑起来,剩下的时间留给异常分支和答辩准备。希望帮到你。

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

返回列表