了,一文搞懂助學金申請表系統(tǒng)實戰(zhàn))
別再抄作業(yè)了,一文搞懂助學金申請表系統(tǒng)實戰(zhàn)
看了一堆教程還是不會寫項目?這大概是每個程序員新手最真實的寫照。視頻里跑得通,自己一動手就報錯,需求文檔看不懂,數(shù)據(jù)庫設計一團漿糊。今天咱們不整虛的,直接上手一個【助學金申請表】后端服務。
這不是一個簡單的 CRUD,而是一個包含狀態(tài)流轉、數(shù)據(jù)校驗、權限控制的真實業(yè)務場景。通過這個項目,你能徹底理清從接口設計到數(shù)據(jù)落地的全鏈路邏輯。別再被碎片化知識困住,咱們一文搞懂一個完整系統(tǒng)的搭建思路,讓你從“代碼搬運工”變成“系統(tǒng)構建者”。
項目目標與業(yè)務拆解
在敲代碼之前,先搞清楚我們要做什么。很多初學者上來就建表、寫 Controller,結果發(fā)現(xiàn)業(yè)務邏輯對不上,改起來痛苦不堪。
助學金申請表系統(tǒng)的核心目標有三個:申請?zhí)峤唬簩W生填寫個人信息、家庭情況、申請金額,上傳證明材料。
多級審核:班級初審 - 學院復審 - 學校終審,每一步狀態(tài)都要可追溯。
數(shù)據(jù)歸檔:審核通過的數(shù)據(jù)需生成最終檔案,并支持導出。這里有一個容易踩的坑:狀態(tài)機設計。
申請單不是簡單的“提交”和“通過”兩個狀態(tài),它可能處于“待初審”、“初審駁回”、“待復審”、“終審通過”等狀態(tài)。如果一開始沒想好狀態(tài)流轉,后期加需求時你會發(fā)現(xiàn) SQL 寫得像天書。
業(yè)務痛點直擊:數(shù)據(jù)一致性:如果審核員修改了金額,學生端看到的必須是最新值,不能出現(xiàn)緩存臟數(shù)據(jù)。
防重復提交:網(wǎng)絡抖動導致前端多次點擊,后端必須能識別并拒絕。
權限隔離:班長只能看本班的,院長只能看本學院的,越權訪問必須攔截。目錄結構與環(huán)境準備
為了保持工程化規(guī)范,我們采用標準的 Maven 分層架構。不要把所有代碼堆在一個包里,那是災難的開始。
assistant-apply-system/
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── assistant
│ │ │ ├── config # 配置類(攔截器、Swagger等)
│ │ │ ├── controller # 控制層,處理 HTTP 請求
│ │ │ ├── service # 業(yè)務邏輯層,核心代碼在此
│ │ │ ├── mapper # 數(shù)據(jù)訪問層,MyBatis Plus
│ │ │ ├── entity # 數(shù)據(jù)庫實體類
│ │ │ ├── dto # 數(shù)據(jù)傳輸對象(請求/響應)
│ │ │ ├── exception # 全局異常處理
│ │ │ └── common # 通用常量、工具類
│ │ └── resources
│ │ ├── mapper # XML 映射文件
│ │ ├── application.yml # 配置文件
│ │ └── sql # 初始化腳本
│ └── test
└── pom.xml環(huán)境依賴:JDK 17+
Spring Boot 3.x
MySQL 8.0
MyBatis Plus在 application.yml 中配置數(shù)據(jù)源時,建議開啟連接池監(jiān)控。很多新手本地調試沒問題,一上生產環(huán)境就報 Connection leak,通常是因為事務內手動獲取了連接卻沒關閉,或者 finally 塊里忘了釋放資源。參考 Spring Boot 官方開發(fā)者文檔,合理配置 hikari 連接池參數(shù)是避免此類問題的關鍵。
核心代碼實現(xiàn):從實體到服務
1. 實體設計與數(shù)據(jù)庫映射
不要直接暴露 Entity 給前端。我們需要區(qū)分 Entity(數(shù)據(jù)庫結構)和 DTO(業(yè)務傳輸結構)。
@Data
@TableName(t_assistant_apply)
public class AssistantApply {@TableId(type = IdType.AUTO)private Long id;private String studentId; // 學號private String studentName; // 姓名private Integer applyAmount; // 申請金額(分)private String familySituation;// 家庭情況描述private Integer status; // 狀態(tài):0-待初審, 1-待復審, 2-待終審, 3-通過, 4-駁回private LocalDateTime createTime;private LocalDateTime updateTime;// 邏輯刪除標識@TableLogicprivate Integer deleted;
}注意:金額字段用 Integer 存“分”,避免 Double 帶來的精度丟失。這是金融類或涉及資金業(yè)務的基本常識。
2. Service 層:狀態(tài)流轉與事務控制
這是項目的靈魂。我們重點看“提交申請”和“審核”兩個核心方法。
@Service
@RequiredArgsConstructor
public class AssistantApplyService {private final AssistantApplyMapper applyMapper;// 分布式鎖前綴,防止并發(fā)重復提交private static final String LOCK_PREFIX = assistant:apply:;/*** 提交助學金申請* @param dto 申請信息* @return 申請ID*/@Transactional(rollbackFor = Exception.class)public Long submitApply(ApplyCreateDTO dto) {// 1. 參數(shù)校驗if (dto.getApplyAmount() = 0) {throw new BusinessException(申請金額必須大于0);}// 2. 檢查是否已有未完結的申請(防止重復提交)Long count = applyMapper.countByStudentIdAndStatusNot(dto.getStudentId(), Arrays.asList(3, 4));if (count 0) {throw new BusinessException(您已有正在處理中的申請,請勿重復提交);}// 3. 構建實體AssistantApply entity = new AssistantApply();BeanUtils.copyProperties(dto, entity);entity.setStatus(0); // 初始狀態(tài):待初審entity.setCreateTime(LocalDateTime.now());// 4. 入庫applyMapper.insert(entity);return entity.getId();}/*** 審核操作* @param id 申請ID* @param pass 是否通過* @param remark 備注* @param currentStatus 當前期望的狀態(tài)(樂觀鎖思想)*/@Transactional(rollbackFor = Exception.class)public void audit(Long id, Boolean pass, String remark, Integer currentStatus) {AssistantApply apply = applyMapper.selectById(id);if (apply == null) {throw new BusinessException(申請記錄不存在);}// 狀態(tài)機校驗:只有處于當前狀態(tài)才能操作if (!apply.getStatus().equals(currentStatus)) {throw new BusinessException(申請狀態(tài)已變更,請刷新后重試);}if (pass) {// 根據(jù)當前狀態(tài)決定下一個狀態(tài)int nextStatus = currentStatus + 1;apply.setStatus(nextStatus);} else {// 駁回:直接置為4apply.setStatus(4);}apply.setUpdateTime(LocalDateTime.now());// 更新數(shù)據(jù)庫,利用 MyBatis Plus 的 UpdateWrapper 確保原子性boolean updated = applyMapper.updateById(apply);if (!updated) {throw new BusinessException(審核失敗,可能存在并發(fā)沖突);}// 如果終審通過,觸發(fā)歸檔邏輯if (pass apply.getStatus() == 3) {archiveService.generateArchive(id);}}
}逐行解析關鍵點:@Transactional:必須加 rollbackFor = Exception.class,否則只捕獲 RuntimeException,檢查型異常會導致事務不回滾,數(shù)據(jù)不一致。
狀態(tài)校驗:if (!apply.getStatus().equals(currentStatus)) 這一步看似多余,實則是高并發(fā)下的救命稻草。它實現(xiàn)了簡單的樂觀鎖,防止兩個審核員同時操作同一單。
金額處理:在 DTO 中接收前端傳來的金額,如果是元為單位,建議在 Service 層統(tǒng)一轉換為分,保持數(shù)據(jù)庫存儲的一致性。3. Controller 層:接口規(guī)范
接口設計要符合 RESTful 規(guī)范,但不要為了規(guī)范而規(guī)范。
@RestController
@RequestMapping(/api/v1/assistant)
@RequiredArgsConstructor
public class AssistantApplyController {private final AssistantApplyService applyService;@PostMapping(/apply)public ResultLong createApply(@RequestBody @Valid ApplyCreateDTO dto) {Long id = applyService.submitApply(dto);return Result.success(id);}@PutMapping(/audit/{id})public ResultVoid auditApply(@PathVariable Long id,@RequestParam Boolean pass,@RequestParam String remark,@RequestParam Integer currentStatus) {applyService.audit(id, pass, remark, currentStatus);return Result.success();}
}運行與測試:如何驗證你的代碼
寫完代碼不等于功能正常。很多新手只測 Happy Path(正常路徑),一測異常場景就崩。
測試場景清單:正常流程:學生提交 - 班長通過 - 院長通過 - 狀態(tài)變?yōu)?。
駁回流程:班長駁回 - 狀態(tài)變?yōu)? - 學生能否再次提交?(根據(jù)業(yè)務定,通常允許修改后重新提交,需清除舊記錄或允許新記錄)。
并發(fā)測試:使用 JMeter 或 Postman 腳本,模擬兩個請求同時提交同一學號的申請。
預期結果:一個成功,一個拋出“已有處理中申請”異常。越權測試:用戶 A 嘗試修改用戶 B 的申請單。
預期結果:返回 403 Forbidden 或業(yè)務異?!盁o權操作”。調試技巧:
在 audit 方法中加入日志:
log.info(Audit started: Id={}, CurrentStatus={}, Operator={}, id, currentStatus, SecurityContext.getUsername());通過日志追蹤狀態(tài)流轉,比打斷點快得多。特別是當狀態(tài)不符合預期時,日志能幫你迅速定位是前端傳錯了狀態(tài),還是后端邏輯漏判。
優(yōu)化擴展:從 Demo 到生產級
現(xiàn)在的代碼能跑,但離生產還差得遠。以下是三個必須考慮的優(yōu)化點。
1. 防重復提交(Idempotency)
前端網(wǎng)絡不好,用戶狂點“提交”按鈕怎么辦?
對策:在提交請求頭中加入 Idempotency-Key(UUID),后端使用 Redis 緩存該 Key。
// 偽代碼
String key = request.getHeader(Idempotency-Key);
if (redisTemplate.hasKey(idempotency: + key)) {return Result.error(請勿重復提交);
}
// 執(zhí)行業(yè)務邏輯
// 成功后設置 key 過期時間這是高并發(fā)場景下的標準解法,參考主流電商系統(tǒng)的做法,能極大減輕后端壓力。
2. 敏感數(shù)據(jù)脫敏
助學金申請包含家庭收入、成員情況等敏感隱私。
對策:數(shù)據(jù)庫存儲時,對身份證號、銀行卡號進行 AES 加密。
接口返回時,對手機號、身份證號中間位進行 * 號掩碼處理。
使用 Jackson 注解自定義序列化器,避免在每個字段寫 Getter 邏輯。3. 異步通知
審核通過后,學生需要收到郵件或站內信通知。
對策:不要在主線程中發(fā)送 HTTP 請求或 SMTP 郵件,這會阻塞主流程。
使用 Spring 的 @Async 注解或引入消息隊列(RabbitMQ/Kafka)。
@Async
public void sendNotification(Long applyId) {// 發(fā)送邏輯
}確保通知失敗不影響主業(yè)務狀態(tài)更新,失敗時重試或記錄日志告警。
小結
通過這個【助學金申請表】系統(tǒng),你應該已經掌握了一個完整后端服務的搭建骨架。從需求拆解到狀態(tài)機設計,從事務控制到并發(fā)處理,每一個環(huán)節(jié)都是面試和工作中繞不開的重點。
不要滿足于“代碼能跑”,要思考“為什么這么寫”。比如,為什么用 Integer 存金額?為什么審核要校驗當前狀態(tài)?為什么提交要加防重?這些細節(jié)才是區(qū)分初級和中級開發(fā)者的分水嶺。
這個知識點你面試被問過嗎?留言說說,看看有多少人踩過同樣的坑。