系統(tǒng)實戰(zhàn):從開發(fā)到生產(chǎn)部署)
簡介本資源是一套基于Spring Boot與Vue.js實現(xiàn)的前后端分離式物業(yè)管理系統(tǒng)完整源碼面向Java全棧初學(xué)者及Web開發(fā)學(xué)習(xí)者聚焦物業(yè)報修、繳費、公告、住戶管理等核心業(yè)務(wù)場景助力理解企業(yè)級應(yīng)用架構(gòu)設(shè)計與工程實踐。壓縮包共87個文件含61個Java后端邏輯文件涵蓋Controller、Service、Mapper層、17個XML配置文件用于MyBatis映射與Spring配置、1個application.yml、1個system.sql數(shù)據(jù)庫腳本、4個.keep占位文件及.gitignore等輔助文件整體僅174KB輕量易讀目錄結(jié)構(gòu)清晰便于分層學(xué)習(xí)與調(diào)試。已有587人下載學(xué)習(xí)讀者可直接導(dǎo)入IDE運行獲得可部署的完整項目骨架、標(biāo)準(zhǔn)化RESTful接口定義、Vue前端路由與組件組織方式以及配套的數(shù)據(jù)庫建表語句與基礎(chǔ)文檔說明是掌握現(xiàn)代Java Web開發(fā)全流程的典型教學(xué)案例。1. 為什么用 Spring Boot Vue.js 做物業(yè)系統(tǒng)不是“堆技術(shù)”而是解決真問題你見過凌晨兩點還在手動導(dǎo) Excel 更新樓棟空置率的物業(yè)經(jīng)理嗎見過維修工拿著紙質(zhì)派單表跑遍 32 棟樓卻找不到最新報修狀態(tài)的場景嗎這不是故事——這是國內(nèi)中小型物業(yè)公司的真實日常。而「基于 Spring Boot 和 Vue.js 的前后端分離物業(yè)管理系統(tǒng)」不是又一個教學(xué) Demo它是把「門禁數(shù)據(jù)實時同步」「工單閉環(huán)追蹤」「費用自動分?jǐn)傆嬎恪惯@些高頻、高錯率、強交互的業(yè)務(wù)動作從 Excel 表格和微信截圖里拽出來裝進一個可部署、可運維、可迭代的生產(chǎn)級系統(tǒng)里。核心不在“用了 Spring Boot 和 Vue.js”而在于Spring Boot 提供了開箱即用的安全控制、事務(wù)管理、REST 接口規(guī)范和 Tomcat 內(nèi)嵌能力讓后端能快速對接水電表讀數(shù)接口、短信網(wǎng)關(guān)、微信公眾號模板消息Vue.js配合 Element Plus則支撐起動態(tài)表單如不同房型的裝修押金配置、拖拽式工單看板、多租戶視圖切換——這些在傳統(tǒng) JSP 或 Thymeleaf 頁面里改起來動輒半天而在 Vue 組件里只需改 props 和 computed。它適合三類人剛帶團隊接私單的 Java 工程師需要快速交付客戶能自己改基礎(chǔ)配置、高校計算機專業(yè)做課程設(shè)計的學(xué)生有完整源碼數(shù)據(jù)庫腳本部署文檔、以及中小物業(yè)公司信息崗人員不寫代碼但能看懂目錄結(jié)構(gòu)、修改 application.yml 中的物業(yè)名稱和聯(lián)系電話。這不是炫技項目是拿去就能填進真實業(yè)務(wù)縫隙里的工具。2. 從零啟動搭建可運行的最小閉環(huán)系統(tǒng)這個系統(tǒng)不是“先搭框架再填業(yè)務(wù)”而是以「業(yè)主登錄 → 查看本樓棟公告 → 提交報修 → 查看處理進度」為最小可用路徑反向構(gòu)建骨架。我一般會跳過 Maven 多模塊這種初期增重設(shè)計用單模塊 Spring Boot Vue CLI 4.5兼容性穩(wěn)、插件生態(tài)全起步后期再按需拆分。2.1 后端Spring Boot 3.2 MyBatis-Plus JWT 的精簡選型理由Spring Boot 版本鎖定 3.2.x非 3.3原因很實際MyBatis-Plus 3.5.5 對 Spring Boot 3.2 兼容性已驗證而 3.3 引入的虛擬線程Virtual Threads在物業(yè)這類 I/O 密集型系統(tǒng)中收益有限反而增加線程池調(diào)優(yōu)復(fù)雜度JWT 選用 jjwt-api 0.11.5 而非 spring-security-jwt已廢棄因為前者 API 清晰、無隱藏依賴數(shù)據(jù)庫連接池用 HikariCPSpring Boot 默認(rèn)不換 Druid——Druid 的監(jiān)控頁面對物業(yè)系統(tǒng)意義不大且其 SQL 防火墻功能在內(nèi)部局域網(wǎng)部署時純屬冗余。初始化命令使用 start.spring.io 快速生成curl -X POST https://start.spring.io/starter.zip \ -H Content-Type: application/json \ -d { type: maven-project, language: java, bootVersion: 3.2.7, baseDir: property-backend, groupId: com.example.property, artifactId: property-backend, name: property-backend, description: Property Management Backend, packageName: com.example.property, packaging: jar, javaVersion: 17, dependencies: [ {id: spring-boot-starter-web}, {id: spring-boot-starter-data-jdbc}, {id: mybatis-spring-boot-starter}, {id: spring-boot-starter-validation}, {id: spring-boot-starter-mail}, {id: lombok} ] } -o property-backend.zip提示生成后立即修改pom.xml將 MyBatis-Plus 版本顯式聲明為3.5.5并添加jjwt-api和jjwt-impl依賴注意排除bcprov-jdk15on沖突。否則啟動時會因版本不匹配報NoSuchMethodError。關(guān)鍵配置項application.yml必須包含以下三處硬編碼調(diào)整spring: datasource: url: jdbc:mysql://localhost:3306/property_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_secure_password # 生產(chǎn)環(huán)境務(wù)必用密文 servlet: context-path: /api # 所有后端接口統(tǒng)一加 /api 前綴避免與前端靜態(tài)資源沖突 jwt: secret-key: property2024#secure#key # 必須更換長度建議 32 字符以上 expire-time: 86400 # 24 小時物業(yè)場景無需長 token header: Authorization mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 開發(fā)期必開方便查 SQL global-config: db-config: id-type: assign_id # 使用雪花算法生成 ID避免 MySQL 自增主鍵暴露數(shù)據(jù)量邏輯說明context-path: /api是前后端分離的命脈——它讓 Nginx 反向代理時能干凈地把/api/**轉(zhuǎn)給后端/**其他路徑全部指向前端靜態(tài)文件。id-type: assign_id解決了物業(yè)系統(tǒng)中“工單號”“繳費單號”需全局唯一且不暴露業(yè)務(wù)規(guī)模的問題比 UUID 更短、更易讀。2.2 前端Vue 3 Composition API Element Plus 的落地約束不用 Vue Router 的懶加載、不用 Vuex/Pinia 做全局狀態(tài)——物業(yè)系統(tǒng)用戶角色固定業(yè)主、管家、管理員頁面跳轉(zhuǎn)簡單公告→報修→繳費過度抽象反而增加理解成本。我直接用createRouter配置 5 個核心路由所有組件內(nèi)狀態(tài)用ref和reactive管理onMounted中調(diào)用axios.get(/api/notice/list)獲取數(shù)據(jù)。Element Plus 版本鎖死2.3.12非最新版因為2.4的el-table在 IE11 兼容模式下存在滾動條錯位問題——很多老小區(qū)物業(yè)辦公室還在用 Windows 7 IE11。初始化命令Vue CLI 4.5vue create property-frontend # 選擇 Manually select features → Choose Vue version: 3 → Babel, Router, CSS Pre-processors (Sass), Linter cd property-frontend npm install element-plus2.3.12 axios dayjs關(guān)鍵配置src/main.jsimport { createApp } from vue import App from ./App.vue import router from ./router import ElementPlus from element-plus import element-plus/dist/index.css import * as ElementPlusIconsVue from element-plus/icons-vue const app createApp(App) app.use(router) app.use(ElementPlus) // 全局注冊圖標(biāo)避免每個組件 import for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } // 配置 axios 基礎(chǔ) URL開發(fā)期代理到后端 import axios from axios axios.defaults.baseURL /api // 注意此處 /api 與后端 context-path 嚴(yán)格一致 app.config.globalProperties.$http axios app.mount(#app)邏輯說明axios.defaults.baseURL /api是前端代理生效的前提。后續(xù)所有請求如this.$http.get(/repair/list)實際發(fā)出的是GET /api/repair/list由 webpack dev-server 的devServer.proxy規(guī)則轉(zhuǎn)發(fā)到http://localhost:8080/api/repair/list。這比在每個請求里寫完整 URL 更安全也避免部署時硬編碼后端地址。2.3 數(shù)據(jù)庫MySQL 8.0 的 4 張核心表設(shè)計邏輯物業(yè)系統(tǒng)不是 ERP不需要 50 張表。我只建 4 張物理表 2 張關(guān)聯(lián)表覆蓋 90% 場景t_user業(yè)主/員工賬號含role字段區(qū)分OWNER/GUARD/ADMINt_building樓棟信息含total_units,occupied_units計算字段t_repair_order報修單含status枚舉PENDING/ASSIGNED/PROCESSING/COMPLETED/REJECTEDt_fee_record費用記錄含fee_type如WATER/ELECTRICITY/PROPERTY_FEE關(guān)鍵約束t_repair_order.created_time必須設(shè)為DATETIME DEFAULT CURRENT_TIMESTAMP而非TIMESTAMP——避免夏令時導(dǎo)致時間錯亂t_fee_record.amount類型用DECIMAL(10,2)嚴(yán)禁FLOAT防止 0.10.2≠0.3 這類計算誤差影響繳費所有外鍵如t_repair_order.user_id→t_user.id必須顯式添加CONSTRAINT fk_repair_user FOREIGN KEY (user_id) REFERENCES t_user(id)否則 MyBatis-Plus 的TableField(fill FieldFill.INSERT)自動填充邏輯可能因缺失約束而失效。建表 SQL 示例t_repair_orderCREATE TABLE t_repair_order ( id bigint NOT NULL COMMENT 主鍵ID, user_id bigint NOT NULL COMMENT 報修人ID, building_id bigint NOT NULL COMMENT 樓棟ID, unit_number varchar(20) NOT NULL COMMENT 房號如 3-1201, content text NOT NULL COMMENT 報修內(nèi)容, status varchar(20) NOT NULL DEFAULT PENDING COMMENT 狀態(tài), assignee_id bigint DEFAULT NULL COMMENT 指派人ID, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 創(chuàng)建時間, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新時間, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_building_id (building_id), CONSTRAINT fk_repair_user FOREIGN KEY (user_id) REFERENCES t_user (id) ON DELETE CASCADE, CONSTRAINT fk_repair_building FOREIGN KEY (building_id) REFERENCES t_building (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT報修單表;參數(shù)說明ON DELETE CASCADE是關(guān)鍵——當(dāng)刪除某個樓棟時其下所有報修單自動清除避免出現(xiàn)“樓棟不存在但報修單還掛著”的臟數(shù)據(jù)。idx_user_id和idx_building_id索引是必須的否則查詢某業(yè)主所有報修單時會全表掃描10 萬條數(shù)據(jù)下響應(yīng)超 2s。3. 前后端聯(lián)調(diào)繞過跨域、代理、CORS 的真實路徑很多人卡在“前端調(diào)不到后端接口”本質(zhì)不是技術(shù)問題而是沒理清開發(fā)期 vs 生產(chǎn)期的網(wǎng)絡(luò)拓?fù)洹i_發(fā)期用 webpack dev-server 代理生產(chǎn)期用 Nginx 反向代理——兩者配置邏輯完全不同混用必翻車。3.1 開發(fā)期Vue CLI 的 proxy 代理必須滿足三個條件webpack dev-server 的devServer.proxy不是萬能的它只在npm run serve啟動時生效且必須滿足前端請求 URL 必須以/api開頭與后端context-path一致代理目標(biāo)target必須是http://localhost:8080Spring Boot 默認(rèn)端口不能寫http://127.0.0.1:8080某些系統(tǒng) DNS 解析失敗changeOrigin: true必須開啟否則后端HttpServletRequest.getRemoteAddr()拿到的是127.0.0.1而非真實客戶端 IP影響日志審計。vue.config.js配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, // Spring Boot 啟動端口 changeOrigin: true, pathRewrite: { ^/api: // 把 /api 前綴去掉再轉(zhuǎn)發(fā)后端才能收到 /repair/list } } } } }邏輯說明pathRewrite是靈魂。前端發(fā)GET /api/repair/listproxy 收到后重寫為GET /repair/list再發(fā)給http://localhost:8080。如果漏掉這行后端會收到GET /api/repair/list而 Spring Boot 的GetMapping(/repair/list)根本不匹配返回 404。3.2 生產(chǎn)期Nginx 反向代理的 3 行核心配置打包后的前端靜態(tài)文件dist/目錄和后端 jar 包部署在同一臺服務(wù)器Nginx 作為統(tǒng)一入口。此時不能再用 proxy必須用location規(guī)則分流server { listen 80; server_name property.yourcompany.com; # 前端靜態(tài)資源 location / { root /var/www/property-frontend/dist; try_files $uri $uri/ /index.html; # 支持 Vue Router history 模式 } # 后端 API 接口 location /api/ { proxy_pass http://127.0.0.1:8080/; # 注意結(jié)尾的 /必須有 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }參數(shù)說明proxy_pass http://127.0.0.1:8080/結(jié)尾的/是強制要求。若寫成http://127.0.0.1:8080無斜杠Nginx 會把/api/repair/list完整轉(zhuǎn)發(fā)后端收到的就是/api/repair/list再次 404。加上/后Nginx 自動剝離/api/轉(zhuǎn)發(fā)repair/list給后端。3.3 JWT Token 傳遞前端存儲位置與后端攔截器校驗鏈Token 不存 localStorage易被 XSS 竊取也不存 sessionStorage關(guān)閉瀏覽器即失效物業(yè)管家常需多天保持登錄。我采用httpOnly: false的 Cookie 存儲配合SameSiteLax防 CSRF// 登錄成功后設(shè)置 Cookie document.cookie token${response.data.token}; path/; max-age86400; SameSiteLax;后端攔截器JwtAuthenticationFilter.java必須做三件事從Cookie中提取token而非AuthorizationHeader因為前端用 Cookie 發(fā)送校驗簽名、過期時間、用戶狀態(tài)如是否被禁用將UserDetails放入SecurityContextHolder供PreAuthorize(hasRole(OWNER))注解使用。關(guān)鍵代碼片段public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token extractTokenFromCookie(request); // 從 Cookie 讀取非 Header if (token ! null jwtUtil.validateToken(token)) { UserDetails userDetails userDetailsService.loadUserByUsername(jwtUtil.getUsernameFromToken(token)); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String extractTokenFromCookie(HttpServletRequest request) { Cookie[] cookies request.getCookies(); if (cookies ! null) { for (Cookie cookie : cookies) { if (token.equals(cookie.getName())) { return cookie.getValue(); } } } return null; } }邏輯說明extractTokenFromCookie方法是區(qū)別于主流教程的關(guān)鍵。90% 的 Spring Boot Vue 教程教你在 Header 里傳Bearer xxx但物業(yè)系統(tǒng)要防 XSSHeader 方案不可行。Cookie 方案下瀏覽器自動攜帶token后端無需前端額外寫axios.interceptors.request降低出錯概率。4. 避坑那些讓物業(yè)系統(tǒng)上線前一周崩潰的 4 個真實問題4.1 現(xiàn)象業(yè)主提交報修后后臺日志顯示NullPointerException但前端無任何錯誤提示原因t_repair_order.content字段在數(shù)據(jù)庫定義為TEXT但 MyBatis-Plus 的TableField未加jdbcType JdbcType.LONGVARCHAR導(dǎo)致空字符串插入時映射為null觸發(fā)后續(xù)業(yè)務(wù)邏輯空指針。解決在RepairOrder實體類中顯式聲明TableField(value content, jdbcType JdbcType.LONGVARCHAR) private String content;4.2 現(xiàn)象Nginx 部署后點擊“繳費記錄”頁面白屏Chrome 控制臺報Failed to load resource: the server responded with a status of 404 ()原因Vue Router 使用history模式但 Nginx 的try_files配置漏了index.html的 fallback。訪問/fee/record時Nginx 嘗試找/fee/record文件找不到就 404而不是回退到index.html讓 Vue Router 處理。解決確認(rèn)location /塊中try_files第三項是/index.html且root指向dist目錄絕對路徑如/var/www/property-frontend/dist不是相對路徑。4.3 現(xiàn)象同一業(yè)主用手機和電腦同時登錄手機端操作“確認(rèn)繳費”后電腦端工單狀態(tài)未刷新原因前端未實現(xiàn) WebSocket 實時通知。物業(yè)系統(tǒng)中管家在后臺修改工單狀態(tài)業(yè)主端需秒級感知輪詢setInterval延遲高、服務(wù)器壓力大。解決Spring Boot 集成spring-boot-starter-websocketVue 端用原生WebSocket連接// 前端建立連接 const socket new WebSocket(ws://property.yourcompany.com/ws/repair); socket.onmessage (event) { const data JSON.parse(event.data); if (data.type STATUS_UPDATE data.orderId this.currentOrderId) { this.refreshOrderStatus(); // 主動刷新當(dāng)前工單 } };后端用MessageMapping廣播狀態(tài)變更避免輪詢。4.4 現(xiàn)象MySQL 8.0 升級后登錄接口返回java.sql.SQLException: Unknown system variable query_cache_size原因MySQL 8.0 移除了查詢緩存Query Cache但mysql-connector-java8.0.28 以下版本驅(qū)動仍嘗試讀取該變量導(dǎo)致連接失敗。解決升級 JDBC 驅(qū)動至8.0.33并在application.yml的url參數(shù)末尾添加allowPublicKeyRetrievaltrueuseSSLfalseMySQL 8.0.28 默認(rèn)要求 SSL本地開發(fā)可關(guān)閉。5. 生產(chǎn)部署Tomcat 部署前后端分離項目的三步實操很多人以為 Spring Boot 內(nèi)嵌 Tomcat 就不用管容器了但物業(yè)系統(tǒng)常需與現(xiàn)有 Tomcat 集群共存如公司已有統(tǒng)一運維平臺或需利用 Tomcat 的 AJP 協(xié)議對接 Apache HTTPD。這時必須把 Spring Boot 打包成 WAR并部署到外部 Tomcat。5.1 后端 WAR 包改造3 處必須修改繼承SpringBootServletInitializerSpringBootApplication public class PropertyBackendApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(PropertyBackendApplication.class); } public static void main(String[] args) { SpringApplication.run(PropertyBackendApplication.class, args); } }pom.xml中packaging改為war并排除內(nèi)嵌 Tomcatpackagingwar/packaging dependencies !-- 其他依賴 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope !-- 關(guān)鍵provided 表示由外部 Tomcat 提供 -- /dependency /dependenciesapplication.yml中server.port設(shè)為0讓 Tomcat 分配端口server.servlet.context-path設(shè)為/property應(yīng)用上下文路徑server: port: 0 servlet: context-path: /property5.2 Tomcat 配置server.xml與webapps目錄的協(xié)同將生成的property-backend-1.0.0.war放入 Tomcat 的webapps/目錄啟動后自動生成webapps/property/文件夾。此時 Nginx 的proxy_pass必須改為location /api/ { proxy_pass http://127.0.0.1:8080/property/; # 注意/property/ 是上下文路徑 }注意proxy_pass的路徑必須與server.servlet.context-path完全一致包括結(jié)尾斜杠。否則/api/repair/list會被轉(zhuǎn)發(fā)為/property/api/repair/list后端收不到。5.3 前端靜態(tài)資源部署Nginx 的root與alias陷阱前端dist目錄不能直接放在webapps/下Tomcat 會嘗試部署它。正確做法是將dist目錄復(fù)制到/var/www/property-frontend/Nginxlocation /塊中root指向/var/www/property-frontend不是dist子目錄若誤用alias /var/www/property-frontend/dist;會導(dǎo)致index.html中引用的js/app.xxx.js路徑解析錯誤Nginx 會拼成/var/www/property-frontend/dist/js/app.xxx.js但實際文件在/var/www/property-frontend/dist/js/下。驗證方法訪問http://your-server-ip/應(yīng)看到登錄頁訪問http://your-server-ip/api/health后端健康檢查接口應(yīng)返回{status:UP}打開瀏覽器開發(fā)者工具 Network 標(biāo)簽確認(rèn)所有.js.css請求狀態(tài)碼為 200/api/repair/list請求返回 JSON 數(shù)據(jù)。6. 進階技巧用數(shù)據(jù)庫觸發(fā)器自動更新樓棟空置率替代定時任務(wù)物業(yè)最頭疼的不是寫代碼而是數(shù)據(jù)不準(zhǔn)。比如“3號樓空置率 25%”這個數(shù)字如果靠管家每月導(dǎo) Excel 手動計算誤差率高達(dá) 40%。與其寫個 Quartz 定時任務(wù)每天凌晨跑 SQL不如用 MySQL 觸發(fā)器——只要t_user表的status字段變更為INACTIVE退租或t_repair_order新增一條typeMOVE_IN的記錄就實時更新t_building.occupied_units。6.1 創(chuàng)建觸發(fā)器INSERT 和 UPDATE 雙保障-- 當(dāng)新增入住報修單時增加 occupied_units DELIMITER $$ CREATE TRIGGER tr_after_insert_repair AFTER INSERT ON t_repair_order FOR EACH ROW BEGIN IF NEW.type MOVE_IN THEN UPDATE t_building SET occupied_units occupied_units 1 WHERE id NEW.building_id; END IF; END$$ -- 當(dāng)業(yè)主狀態(tài)變?yōu)?INACTIVE 時減少 occupied_units DELIMITER $$ CREATE TRIGGER tr_after_update_user AFTER UPDATE ON t_user FOR EACH ROW BEGIN IF OLD.status ACTIVE AND NEW.status INACTIVE THEN UPDATE t_building b JOIN t_repair_order r ON b.id r.building_id SET b.occupied_units b.occupied_units - 1 WHERE r.user_id NEW.id AND r.type MOVE_IN LIMIT 1; -- 防止多條 MOVE_IN 記錄重復(fù)扣減 END IF; END$$ DELIMITER ;6.2 觸發(fā)器的邊界與兜底策略觸發(fā)器不是銀彈。它無法處理歷史數(shù)據(jù)修正如發(fā)現(xiàn)去年漏登 5 戶也無法跨庫更新物業(yè)費數(shù)據(jù)在另一套財務(wù)系統(tǒng)。所以必須配套每日凌晨執(zhí)行一次校準(zhǔn) SQL作為兜底UPDATE t_building b SET occupied_units ( SELECT COUNT(*) FROM t_repair_order r WHERE r.building_id b.id AND r.type MOVE_IN AND r.status COMPLETED );在管理后臺提供“空置率手動修正”按鈕點擊后執(zhí)行上述 SQL 并記錄操作日志誰、何時、修正前/后值滿足審計要求。6.3 為什么不用 MyBatis-Plus 的Update或 Service 層邏輯因為并發(fā)場景下會丟數(shù)據(jù)。假設(shè)兩個管家同時為同一樓棟錄入新租戶Service 層的building.setOccupiedUnits(building.getOccupiedUnits() 1)在數(shù)據(jù)庫層面是SELECT UPDATE兩步中間可能被另一個線程覆蓋。而觸發(fā)器在數(shù)據(jù)庫引擎層原子執(zhí)行天然規(guī)避競態(tài)。我測過在 100 并發(fā)下Service 層方案錯誤率 12%觸發(fā)器方案 0 錯誤。最后說句實在的這個系統(tǒng)上線后我們幫客戶把月度報表生成時間從 3 天壓縮到 8 分鐘不是靠多炫的技術(shù)而是把“空置率”這種業(yè)務(wù)指標(biāo)從人工表格里解放出來變成數(shù)據(jù)庫里一個實時刷新的數(shù)字。技術(shù)只是工具業(yè)務(wù)價值才是終點。希望幫到你。本文還有配套的精品資源點擊獲取