准备把 AI 辅助完成的项目写进 Java 简历时,我最担心的,是面试官顺着一个接口继续追问:设备为什么能借出去?经办人是谁决定的?归还和逾期巡检撞在一起,会不会把状态改乱?
页面能打开,还回答不了这些问题。所以这次,我把「有借」资产借还项目重新检查了一遍,补了身份认证、操作留痕,也修掉了几个正常演示时不容易碰到的问题。下面就按我实际检查和修改的顺序,聊聊这套项目现在能写什么,还有什么不能写。
一、先说清楚,我做了什么项目
「有借」是一个本地交付练习,用来管理设备台账、借出归还和维保工单。我沿用了已有的设备与借还基础,没有从零重做整个系统。后端采用 Java 17、Spring Boot 3.4、Spring Data JPA 和 Flyway,前端是 Vue 3、TypeScript、Pinia 与 Element Plus。
我把后续工作分成了三组:维保与续借、账号身份认证、操作审计。它们分别对应数据库的 V2、V3、V4 迁移。这里的版本号用来说明功能如何逐步补齐,不代表项目已经在线上发布过四个版本。
飞算 JavaAI 用在 IDEA 里的需求梳理与开发辅助。为了说明这次使用的环境,我保留了插件版本页和开发工具版本信息。IDEA 自带的运行时与项目使用的 JDK 不是一回事,后端的这轮验证用的是 JDK 17。

图 1:飞算 JavaAI 插件的本机安装版本与启用状态。

图 2:本次开发环境信息,区分 IDE 运行时与项目使用的 JDK。
接下来,我先把需求拆开。例如「增加维保功能」,至少要说明谁报修、记录哪些费用、什么情况下恢复可用,以及维修失败后怎么处置设备。续借也是一样,不能只有一个「延期」按钮,还得说明次数上限、时间约束和经办人怎么留痕。
因为原始插件对话没能完整找回,所以我重新演示了一次需求拆解。图 3 展示的是这次复演,不是原始生成记录,也不能据此断言后面的代码都由插件一次生成。

图 3:维保、续借与归还健康度需求的拆解复演,非原始开发记录。
二、把借还、续借和维保接成一个流程
我先检查业务规则,因为后面的认证、审计和页面操作都要落在这些规则上。
设备处于「空闲待借」时才能借出。正常归还后恢复可用;如果验收发现受损,就进入维保;如果确认遗失,就转为报废。维保工单记录故障、厂商和费用,完工验收再决定恢复可用,还是确认报废。
续借沿用原来的借还单,更新本次原因和预计归还时间,并累计次数,最多两次。目前原因字段保存的是最近一次续借的内容,还没有单独的续借明细表;次数和期限的变更则写入审计。

图 4:资产看板中的数量统计、分类分布与借还趋势。

图 5:设备台账中的资产信息、当前状态与操作入口。

图 6:维保工单及处理状态,图中记录为演示数据。
初始数据包含 14 台设备、7 笔借还流水和 3 笔维保工单,人员、电话和厂商信息都是虚构的。因为巡检和借还操作会改变状态,所以页面上的逾期数量不能固定要求为一条。
前端我保留了浅色、深色两套主题。看板的分类进度条和趋势柱形图用 HTML/CSS 实现,并没有实际接入 ECharts。依赖里出现了某个库,不等于我就完成了对应功能,这一点在项目介绍里也得写清楚。
三、经办人不能由表单自己说了算
最初检查经办人字段时,我发现一个容易被忽略的问题:前端可以直接提交借出经办人、归还验收人和报修人。后来虽然加了登录,但服务端只在这些字段留空时回填登录姓名。只要客户端显式传值,仍然能把业务经办人写成别人。
那么,既然服务端已经知道当前是谁登录,为什么还要让表单决定经办身份?
我把这里改成了强制读取登录上下文。认证拦截器先校验 Bearer Token,再把当前用户放进请求线程的上下文;业务方法需要经办人时,直接调用下面这个方法:
public static String requireRealName() {
User user = CURRENT_USER.get();
if (user == null) {
throw new BusinessException(ResultCode.UNAUTHORIZED);
}
return user.getRealName();
}
这里的 CURRENT_USER 是保存当前请求用户的 ThreadLocal<User>,请求结束后会清理。缺少身份时直接拒绝,不再用「系统经办人」替用户补一个名字。
借出、归还和报修都按这个规则处理,旧请求里的身份字段保留兼容,但不参与赋值。借用人则仍是业务输入:我可以替别人登记借用,不能把「借用人」和「办理这笔业务的人」混为一谈。

图 7:借出接口返回的单据与经办人信息,经办身份由服务端确定。
续借还有一个细节。我之前让续借操作覆盖了原来的借出经办人,结果同一个字段前后代表了不同的人。现在原 handler 保留首次借出的经办人,当前续借账号写入审计记录。
审计与业务更新放在同一个事务里,业务失败时一并回滚;登录失败时还没有用户上下文,所以按尝试登录的账号另行记录。这能帮助我追查已记录的操作,但目前只是应用层追加日志,还谈不上防篡改存证。
四、真正返工的地方,在正常流程之外
编辑资料,也可能绕过状态规则
我最先给手动状态接口加了限制:只有空闲设备能手动转维保或报废。接着检查普通编辑接口,才发现它仍然可以接收一个新的状态,把维保甚至报废设备改回可用。
因为两个入口都能改同一个字段,所以只修其中一个入口不够。我让 PUT 编辑与 PATCH 状态处置共用校验方法,并在设备行锁内检查。PUT 如果只是改名称、位置、备注,就保留原状态;确实要改状态时,再走白名单。
下面是白名单判断的核心条件,异常提示和审计代码省略:java DeviceStatus currentStatus = device.getStatus(); boolean manualAllowed = currentStatus == DeviceStatus.AVAILABLE && (targetStatus == DeviceStatus.MAINTENANCE || targetStatus == DeviceStatus.SCRAPPED);
if (!manualAllowed) { throw new BusinessException(ResultCode.CONFLICT); }
device.setStatus(targetStatus);
我也补了维保入口:报废设备不能新建送修单,否则还可能借着「送修—验收」这条路恢复可用。前端编辑表单则改为保留设备原状态,不再固定提交 AVAILABLE。
有行锁,还得检查锁住后读到了什么
另一个问题出在逾期巡检。原来先查出一批借还实体,随后再更新;如果中间有人完成了归还或续借,巡检就可能拿着旧实体覆盖新状态。
我把它改成先查候选 ID,再逐笔加锁读取。只有拿到锁后,单据仍在借用中、预计归还时间也确实已过,才标为逾期。核心步骤如下,整段逻辑在同一事务中执行:
LocalDateTime cutoff = LocalDateTime.now();
List<Long> candidates =
borrowRecordRepository.findExpiredRecordIds(
BorrowStatus.BORROWING, cutoff);
for (Long recordId : candidates) {
BorrowRecord record =
borrowRecordRepository.findByIdWithLock(recordId).orElse(null);
if (record == null || record.getStatus() != BorrowStatus.BORROWING
|| !record.getExpectedReturnTime().isBefore(cutoff)) {
continue;
}
record.setStatus(BorrowStatus.OVERDUE);
borrowRecordRepository.save(record);
auditService.record("BORROW_OVERDUE", "BORROW_RECORD",
record.getRecordNo(), "预计归还时间已过,系统自动将借出单流转为逾期");
}
这里先取 ID,是为了避免候选实体提前进入持久化上下文,后面加锁时仍然拿到旧对象。我们要保护的,是「读取、判断、更新」这整个过程,不能只看到一个锁注解就认为问题解决了。
### 列表能打开,导出却不一定能用
加上认证以后,我还发现两处 CSV 导出仍然用 `window.open()` 打开地址。这个请求不会经过 Axios 的认证拦截器,所以列表查询正常,导出却拿不到同样的 Bearer Token。
现在两处导出都复用带认证头的请求客户端,以 Blob 接收文件,再创建临时下载地址。触发下载后移除临时链接,延迟释放地址;代码没有监听浏览器最终是否保存成功。
这里还有一个容易漏掉的失败分支:服务端返回的可能是一段错误 JSON,而不是 CSV。因为请求指定了 Blob,所以错误响应也要解析,不能直接把它存成一个后缀为 `.csv` 的文件。当前实现会校验响应类型,401 时清理登录态,其他错误保留服务端提示。

> 图 8:CSV 导出相关文件与内容;下载结果需要同时核对文件和数据。
这几个问题让我更愿意沿着一次实际操作检查代码:页面发了什么,服务端信了什么,数据库最后改了什么。只看接口有没有返回 200,很容易漏掉前后不一致的地方。
## 五、我验证了哪些,还有哪些没做好
2026 年 9 月 19 日,我在 JDK 17 下完成了一轮回归:后端 26 项测试通过,失败、错误和跳过均为零;前端 5 项下载逻辑测试通过,TypeScript 检查与 Vite 构建也通过。

> 图 9:后端完整回归测试结果。

> 图 10:前端下载逻辑测试与构建结果。
后端新增用例覆盖了编辑状态旁路、报废送修拒绝、伪造经办人、续借保留原经办人、缺少身份时回滚,以及导出认证。逾期问题则做了两种指定交错:先选出候选单据,让归还或续借提交,再让巡检加锁检查,确认它不会覆盖已提交结果。
这些测试使用 H2,候选 ID 查询处由测试桩控制时序。它们没有验证所有并发情况,也不是 MySQL 压力测试。前端下载测试中的请求适配器、DOM 和本地存储同样用了测试替身,不能据此认定浏览器文件保存已经完整验收。页面冒烟只覆盖了登录、列表、编辑后状态保留和部分表单检查,完整端到端流程还没跑完。
我觉得 AI 辅助开发的价值,是让我能围绕一套具体需求继续讨论表结构、接口和页面,而不是每一步都从空白开始。不过,这次没有做同条件的耗时对照,我不会据此写「效率提升几倍」,也不会把修复后的测试通过说成「生成时一次成功」。
用起来不够省心的部分也很具体:状态限制要检查所有写入口,认证加上以后还要回头检查导出,生成的角色字段也不代表权限已经实现。我希望这类工具在需求确认时,就把「谁能操作」「失败怎么处理」「时间到了由谁改变状态」列出来。这样我们接手检查时,至少知道哪些地方还空着。这些是本项目暴露的问题,不能直接推断所有生成结果都一样。
目前我还留着几项没有完成的工作:
- **权限和会话。** 管理员与操作员仍可访问相同的受保护接口,没有真正的 RBAC。Token 会话保存在单机内存,重启会失效,也不能供多个实例共享;浏览器端使用 localStorage,仍需考虑 XSS 风险。
- **账号和部署配置。** 缺少账号管理与登录限流。当前跨域配置仍放行任意来源,本地开发还启用了 API 文档和数据库控制台,上线前需要单独收紧和验收。
- **导出和审计。** CSV 还缺统一的字段转义及公式注入防护。审计没有防篡改保证,登录用户名也缺少长度限制,超长输入可能让失败审计落库异常;这个风险还需要补边界测试。
- **验证范围。** MySQL 容器、数据库升级回滚、并发压测、备份恢复都没完成;前端构建仍有包体积警告,也还没有做完整的浏览器端到端验证。
所以我会把它定位为一个有回归记录的本地交付练习,不写成已经在企业生产环境稳定运行的系统。
## 六、回到简历,我会怎么写
那么,这个项目到底能不能写进 Java 简历?我的答案是可以,但我会把 AI 辅助的范围、自己接手的工作和验证边界放在一起说明。
项目描述可以收成这样:
> 基于已有设备借还功能,使用飞算 JavaAI 辅助梳理增量需求,补充维保、续借、身份认证和操作审计。修复设备状态编辑旁路、经办人伪造、CSV 导出认证及逾期巡检覆盖问题。完成 26 项后端回归、5 项前端下载测试与构建检查;验证环境为 JDK 17 和 H2,尚未完成 MySQL 验收与并发压测。
这段话没有写「从零独立开发」,也没有给项目加上未经验证的生产指标。如果面试官继续追问,我就可以从一次 PUT 编辑、一次续借或一次导出讲起,说明原来的问题、改动的理由,以及测试覆盖到哪里。
回到开头那份担心,我现在更清楚该准备什么了。用了 AI 这件事可以直接讲;哪些判断是我做的,哪些地方仍然没有把握,也应该讲清楚。对我来说,这才是把项目写进简历之前,值得花时间完成的复盘。
<div class="hexo-wechat-follow-card" style="margin:28px 0 0;padding:16px 18px;border:1px solid #dbe7f3;border-radius:14px;background:#f8fbff;"><a href="weixin://profile/gh_1ab72c968bef" style="font-weight:700;color:#0f5b9f;text-decoration:none;">点这里一键关注『计算机魔术师』</a><p style="margin:8px 0 0;font-size:13px;color:#6f8299;line-height:1.7;">如果浏览器无法直接唤起微信,可在微信内打开公众号主页:<a href="https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzkwNjQyOTUwOA==#wechat_redirect" style="color:#0f5b9f;text-decoration:none;">计算机魔术师</a></p></div>