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