源碼:SpringBoot整合MyBatis-Plus權(quán)限實戰(zhàn))
簡介一套基于Java的智慧園區(qū)管理系統(tǒng)源碼采用SpringBoot加Ant Design Pro技術(shù)棧前后端分離架構(gòu)適合有Java基礎的學習者、畢設選題或園區(qū)數(shù)字化項目參考。系統(tǒng)覆蓋駕駛艙工作臺、租戶管理、園區(qū)列表、樓宇管理、房間管理、入駐申請等核心業(yè)務模塊駕駛艙提供本月賬單、物業(yè)費、租金、水電費等TOP10柱形圖統(tǒng)計房間維度精確到每棟每層每間可配置收租面積、租金和閑置天數(shù)入駐流程銜接小程序申請整體業(yè)務鏈完整。資源包共593個文件其中Java源碼280個、Vue組件105個、JS腳本91個配合XML配置、SQL及YML工程文件壓縮包僅1.82MB便于下載部署與二次開發(fā)。當前已有738人學習源碼結(jié)構(gòu)清晰能幫助快速掌握多租戶園區(qū)中臺的數(shù)據(jù)報表、資產(chǎn)管理及入駐審批等典型實現(xiàn)。1. 基于Java的智慧園區(qū)管理系統(tǒng)源碼先想清楚它到底解決誰的什么問題園區(qū)運營方最頭疼的不是單一系統(tǒng)不好用而是門禁、道閘、訪客登記、設備報修各自為政。一個訪客要進樓保安翻三個后臺才能確認身份和放行權(quán)限?;?Java 的智慧園區(qū)管理系統(tǒng)源碼就是把“人、車、設備、工單”統(tǒng)一收進一套 Spring Boot MyBatis 的后端里讓這些數(shù)據(jù)從一個入口管起來。這套東西適合三類人準備接手園區(qū)類項目的 Java 工程師拿它當 Java 課程設計案例源碼的學生以及想快速搭建信息化底座的中小型園區(qū) IT 負責人。說句反直覺的話這類源碼真正費勁的往往不是業(yè)務代碼而是數(shù)據(jù)權(quán)限和建表這些“地基”活兒。2. 技術(shù)選型與工程骨架為什么 Spring Boot MyBatis-Plus 是復現(xiàn)這套源碼的最短路徑2.1 選型邏輯先定數(shù)據(jù)模型再定 ORM最后才輪到前端智慧園區(qū)管理的本質(zhì)是“人、車、物、事”四個對象的關(guān)系網(wǎng)。人員綁定樓棟訪客關(guān)聯(lián)人員設備掛在樓棟和樓層下工單又圍繞設備流轉(zhuǎn)。這種場景下聯(lián)表查詢、報表統(tǒng)計、權(quán)限過濾會占掉一大半接口邏輯所以 ORM 選型的第一原則是 SQL 可控性。MyBatis-Plus 在這一點上的優(yōu)勢非常直接單表 CRUD 不用手寫 XML復雜聯(lián)查又可以退回去寫原生 SQL進退都有余地。相比之下JPA 的全自動映射在簡單場景省事但一碰到跨園區(qū)匯總、多條件動態(tài) SQL調(diào)試成本會明顯上升。這些判斷說白了是 Java 基礎里的常識不是玄學。前端在標題里不是重點工程上按前后端分離的常規(guī)做法處理即可后端只輸出 JSON做好 REST 風格的接口約定前端用 Vue 或 React 都行。很多源碼包還會附帶一套管理后臺頁面但復現(xiàn)時的核心工作在后端先把數(shù)據(jù)模型和服務層跑通前端頁面只是殼。2.2 可復現(xiàn)的 Maven 工程結(jié)構(gòu)依賴與配置一次到位先把工程骨架搭出來。常見的做法是標準 Maven 多模塊或單模塊結(jié)構(gòu)我一般用單模塊起步包結(jié)構(gòu)固定為 controller、service、mapper、entity、config、common 六層后面按業(yè)務規(guī)模再拆。核心依賴如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies依賴里最關(guān)鍵的是 MyBatis-Plus 的 starter它把分頁插件、MyBatis 自動配置、條件構(gòu)造器全部集成進來了。Lombok 可以省掉實體類的 getter/setter但要注意如果你的團隊不習慣 Lombok也可以去掉它改用手寫方法功能上沒有差別。mysql-connector-j 的 scope 設為 runtime只參與運行時不參與編譯。配套的application.yml是這樣spring: datasource: url: jdbc:mysql://localhost:3306/smart_park?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password jackson: time-zone: Asia/Shanghai mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.smartpark.entity configuration: map-underscore-to-camel-case: true兩個參數(shù)容易踩坑serverTimezoneAsia/Shanghai不配置Java 8 時間類型在 JDBC 驅(qū)動 8.0 以上版本會報時區(qū)異常map-underscore-to-camel-case開啟后數(shù)據(jù)庫的building_id才能自動映射到實體的buildingId字段省去一堆TableField注解。2.3 園區(qū)四層數(shù)據(jù)模型園區(qū)、樓棟、樓層、房間怎么落表智慧園區(qū)的數(shù)據(jù)模型核心是“租戶-園區(qū)-樓棟-樓層-房間”這條層級鏈。租戶不是每個項目都有但只要是多園區(qū)運營tenant_id建議從一開始就加上不然后面數(shù)據(jù)權(quán)限會返工。我常按下面的表結(jié)構(gòu)設計表名用途關(guān)鍵字段park_building樓棟id, tenant_id, name, address, lat, lngpark_floor樓層id, building_id, floor_no, namepark_room房間id, floor_id, room_no, use_type, areapark_person常駐人員id, tenant_id, building_id, name, phone, id_cardpark_visitor訪客記錄id, person_id, visit_time, expire_time, statuspark_device設備id, building_id, device_type, status, install_datepark_work_order工單id, device_id, reporter, assignee, status, create_time樓層和房間單獨建表而不是用一個location字符串字段糊弄這是為了后面做樓層級權(quán)限和能耗統(tǒng)計時能直接按字段過濾不用解析字符串。tenant_id放進人員表和設備表而不是只放園區(qū)表也是同樣的道理行級權(quán)限過濾時直接在業(yè)務表上加條件避免先查園區(qū)再二次過濾。3. 核心業(yè)務落庫用 MyBatis-Plus 實體類反向生成建表 SQL3.1 實體類怎么設計才不返工實體類設計是面向?qū)ο缶幊汤镒罨A但也最容易返工的一環(huán)。很多源碼跑不起來不是代碼邏輯錯而是實體字段和表結(jié)構(gòu)對不上。MyBatis-Plus 里三個注解最關(guān)鍵TableName指定表名TableId標記主鍵及生成策略TableField處理字段映射和存在性。Data TableName(park_person) public class ParkPerson { TableId(type IdType.ASSIGN_ID) private Long id; TableField(tenant_id) private Long tenantId; TableField(building_id) private Long buildingId; private String name; private String phone; private String idCard; private String faceUrl; private LocalDateTime createTime; }IdType.ASSIGN_ID是雪花算法主鍵適合分布式環(huán)境生成的 ID 是 Long 類型不用依賴數(shù)據(jù)庫自增。faceUrl存的是人臉圖片的文件路徑不是 base64 字符串這是常見做法。createTime建議統(tǒng)一放到每個表里做時間范圍查詢和審計時都會用到。一個細節(jié)TableField(exist false)可以標記實體里存在但表里沒有的字段比如臨時用的startTime、endTime查詢參數(shù)這樣既能在代碼里用又不會在建表或查詢時報錯。3.2 寫一個自動生成 CREATE TABLE 的工具MyBatis-Plus 本身不提供從實體類直接生成建表 SQL 的能力但可以通過反射自己寫一個輕量工具。這個工具在項目初始化時特別有用實體類改完了跑一下 main 方法所有表的 DDL 就出來了不用手寫第二遍。public class DdlGenerator { private static final MapClass?, String TYPE_MAP new HashMap(); static { TYPE_MAP.put(String.class, VARCHAR(255)); TYPE_MAP.put(Long.class, BIGINT); TYPE_MAP.put(Integer.class, INT); TYPE_MAP.put(LocalDateTime.class, DATETIME); TYPE_MAP.put(BigDecimal.class, DECIMAL(18,2)); } public static String generate(Class? entityClass) { TableName tableName entityClass.getAnnotation(TableName.class); StringBuilder ddl new StringBuilder(); ddl.append(CREATE TABLE IF NOT EXISTS ).append(tableName.value()).append( (\n); for (Field field : entityClass.getDeclaredFields()) { TableField tf field.getAnnotation(TableField.class); if (tf ! null !tf.exist()) { continue; } String column camelToUnderscore(field.getName()); String type TYPE_MAP.getOrDefault(field.getType(), VARCHAR(255)); ddl.append( ).append(column).append( ).append(type); if (field.isAnnotationPresent(TableId.class)) { ddl.append( PRIMARY KEY); } ddl.append(,\n); } ddl.append( create_time DATETIME DEFAULT CURRENT_TIMESTAMP\n); ddl.append() ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return ddl.toString(); } private static String camelToUnderscore(String name) { return name.replaceAll(([a-z])([A-Z]), $1_$2).toLowerCase(); } }邏輯不復雜反射遍歷實體類的所有聲明字段讀TableName拿表名讀TableField判斷字段是否真實存在用TYPE_MAP做 Java 類型到 MySQL 類型的映射最后拼成 DDL。camelToUnderscore把buildingId轉(zhuǎn)成building_id保證和 MyBatis-Plus 的駝峰映射規(guī)則一致。TYPE_MAP是可以擴展的項目里用到LocalDate、Boolean就補兩行映射Boolean 映射成TINYINT(1)LocalDate 映射成DATE。這個工具不是生產(chǎn)環(huán)境的遷移工具但配合 Flyway 做初始化比手寫 SQL 省力得多。3.3 通用 CRUD 最小閉環(huán)Mapper、Service、Controller 一次跑通實體類建好后Mapper 層幾乎不用寫業(yè)務代碼繼承BaseMapper就有單表 CRUDpublic interface ParkPersonMapper extends BaseMapperParkPerson { }Service 層我習慣繼承ServiceImpl這樣既能拿到通用的save、getById、page方法又能自己擴展方法。Controller 層寫分頁查詢時用LambdaQueryWrapper構(gòu)造條件避免手拼 SQLRestController RequestMapping(/api/person) public class ParkPersonController { Autowired private ParkPersonService personService; GetMapping(/page) public IPageParkPerson page(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) String keyword) { LambdaQueryWrapperParkPerson wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), ParkPerson::getName, keyword) .orderByDesc(ParkPerson::getCreateTime); return personService.page(new Page(page, size), wrapper); } }LambdaQueryWrapper的好處是方法引用直接指向?qū)嶓w字段字段改名時編譯期就能發(fā)現(xiàn)不會像手寫字符串那樣把name拼錯。page和size直接透傳給 MyBatis-Plus 的分頁插件返回的IPage里有 records、total、current 三個關(guān)鍵值前端表格組件直接能用。跑通這個閉環(huán)人員、訪客、設備、工單四套 CRUD 都是同一個模板后面的業(yè)務開發(fā)就快了。4. 權(quán)限與數(shù)據(jù)隔離多園區(qū)場景下行級權(quán)限的實現(xiàn)思路4.1 只做前端菜單控制等于裸奔一個常見誤區(qū)登錄后根據(jù)角色隱藏菜單就以為權(quán)限做完了。實際上后端接口完全沒有數(shù)據(jù)范圍校驗A 園區(qū)的樓棟管理員只要手動調(diào)接口就能把 B 園區(qū)的工單列表拉出來。這個問題在智慧園區(qū)里尤其嚴重因為一個運營方往往管著多個園區(qū)數(shù)據(jù)天然需要按租戶和樓棟隔離。數(shù)據(jù)權(quán)限這塊是 Java 面試題里反復出現(xiàn)的高頻點面試官問的不只是“你怎么實現(xiàn)”而是你有沒有考慮過行級過濾的落點。落點只有一個SQL 層。在 Service 里手動拼where tenant_id ?是能跑但每個接口都要寫一遍漏一個就出大事故。4.2 用 DataPermissionInterceptor 做統(tǒng)一的行級數(shù)據(jù)過濾MyBatis-Plus 從 3.5 開始自帶DataPermissionInterceptor它可以攔截 Mapper 方法生成的 SQL在執(zhí)行前自動拼接上當前用戶的數(shù)據(jù)權(quán)限條件。用它的好處是業(yè)務代碼不用做任何改動權(quán)限規(guī)則收攏到一處配置。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); DataPermissionHandler handler new DataPermissionHandler() { Override public Expression getSqlSegment(Expression where, String mappedStatementId) { Long buildingId UserContext.get().getBuildingId(); if (buildingId null) { return where; } EqualsTo equalsTo new EqualsTo( new Column(building_id), new LongValue(buildingId)); if (where null) { return equalsTo; } return new AndExpression(where, equalsTo); } }; interceptor.addInnerInterceptor(new DataPermissionInterceptor(handler)); return interceptor; } }關(guān)鍵點在getSqlSegment方法where是原始 SQL 的 WHERE 條件如果當前用戶是園區(qū)級管理員buildingId為 null直接返回原條件不做過濾如果用戶是樓棟管理員就把building_id 當前樓棟的條件用AndExpression拼接到原條件后面。UserContext是自定義的 ThreadLocal 工具用戶登錄時把當前用戶信息放進去請求結(jié)束由攔截器清理避免線程復用導致的數(shù)據(jù)串號。這種方案有一個使用前提所有需要行級隔離的表都要有building_id字段。這就是第 2 章設計表結(jié)構(gòu)時把層級字段鋪到每張表的原因。如果有些表實在沒有層級字段可以用mappedStatementId判斷當前執(zhí)行的是哪個 Mapper 方法按白名單跳過過濾。4.3 角色-菜單-數(shù)據(jù)范圍三層模型與授權(quán)落點權(quán)限體系建議拆成三層而不是把角色和范圍混在一個表里層級內(nèi)容示例菜單權(quán)限用戶能看到哪些功能和路由工單管理、設備管理、報表中心操作權(quán)限對功能能做什么動作新增工單、派單、關(guān)閉工單數(shù)據(jù)范圍能看到哪一層的業(yè)務數(shù)據(jù)全部園區(qū)、本園區(qū)、本樓棟、僅本人菜單和操作權(quán)限做成 RBAC即用戶-角色-菜單的三張關(guān)聯(lián)表。數(shù)據(jù)范圍不放進 RBAC而是作為角色上的一個擴展屬性比如data_scope字段取值ALL、TENANT、BUILDING、SELF。授權(quán)時給角色分配數(shù)據(jù)范圍用戶登錄時把范圍解析成具體的過濾條件在攔截器里消費。這樣新增一個“只要本樓層數(shù)據(jù)”的角色只需要新增一個枚舉值再補一個樓層字段的過濾邏輯不用動任何業(yè)務接口。5. 避坑手冊源碼拿回來跑不起來時先查這 5 個地方5.1 Java 啟動失敗端口被占、數(shù)據(jù)庫連不上、依賴沖突現(xiàn)象Application啟動類一運行控制臺先報Web server failed to start或者等了幾秒后報Communications link failure。原因最常見的是 8080 端口被其他進程占用其次是數(shù)據(jù)庫連接串里的庫名和密碼不對本地根本連不上 MySQL。依賴沖突一般出現(xiàn)在 MyBatis-Plus 和原生 MyBatis 同時引入時兩個框架的 SqlSessionFactory 會打架。解決先在命令行確認端口占用情況netstat -ano | grep 8080找到 PID 后按系統(tǒng)結(jié)束進程。數(shù)據(jù)庫連接串、賬號、密碼逐項核對smart_park這個庫要先建出來連接串的庫名是大小寫敏感的。依賴沖突的話檢查 pom 里是否把mybatis和mybatis-spring重復引入了MyBatis-Plus 的 starter 已經(jīng)內(nèi)含了不要手動再加原生 MyBatis 依賴。5.2 自動建表腳本沒生效實體類沒被掃描到類型映射悄悄出錯現(xiàn)象DdlGenerator 跑完沒有任何輸出或者生成的 CREATE TABLE 里字段類型全變成 VARCHAR(255)。原因前者通常是實體類不在type-aliases-package指定的包路徑下反射拿不到TableName注解后者是TYPE_MAP里沒有對應該字段類型的映射走了getOrDefault的兜底分支。解決確認實體類統(tǒng)一放在com.smartpark.entity包下并且application.yml里的type-aliases-package和實際包名完全一致。新增字段類型時同步更新TYPE_MAP特別是Boolean、LocalDate這幾種常見類型提前加好映射還能避免建出來的表字段語義不對。5.3 日期和時區(qū)錯亂JSON 返回的時間比數(shù)據(jù)庫早 8 小時現(xiàn)象數(shù)據(jù)庫里存的是2025-01-15 10:30:00接口返回給前端變成2025-01-15 02:30:00所有時間整體少了 8 小時。原因MySQL 驅(qū)動在連接串里沒有指定時區(qū)時默認按 JVM 時區(qū)換算而 JDBC 驅(qū)動 8.0 以上版本默認使用服務器時區(qū)兩者不一致就產(chǎn)生了偏移。同時 Jackson 序列化 LocalDateTime 時也有自己的時區(qū)設置兩個地方只要有一個沒對齊就會翻車。解決application.yml里spring.datasource.url的serverTimezoneAsia/Shanghai和spring.jackson.time-zone必須同時設置成同一個時區(qū)。只改其中一個治標不治本。數(shù)據(jù)庫連接串里的characterEncodingutf8也順手加上中文亂碼問題一并避免。5.4 多表聯(lián)查變慢N1 查詢和缺失索引現(xiàn)象門禁記錄列表頁打開要 3 秒以上或者同一批數(shù)據(jù)在數(shù)據(jù)量過萬后明顯卡頓。原因最常見的是 Service 層循環(huán)里調(diào)用getById查關(guān)聯(lián)數(shù)據(jù)這就是典型的 N1 查詢。另外park_work_order表按device_id查、park_visitor表按building_id查時沒有建索引全表掃描在數(shù)據(jù)量上來后就是災難。解決循環(huán)查庫改成批量查詢比如一次查出所有工單的device_id再統(tǒng)一listByIds取設備信息內(nèi)存里做關(guān)聯(lián)組裝。索引方面每個表的building_id、tenant_id都要建普通索引工單表的device_id、訪客表的visit_time同樣加上。用EXPLAIN看執(zhí)行計劃確認 type 列不是ALL。5.5 文件上傳路徑Windows 和 Linux 下的路徑分隔符現(xiàn)象人臉圖片上傳后在本地能訪問部署到 Linux 服務器后全部 404。原因代碼里用字符串拼接路徑寫死了D:\\upload\\face\這種 Windows 風格路徑而 Linux 的文件系統(tǒng)結(jié)構(gòu)完全不同。更隱蔽的是用\和/混拼出來的路徑Windows 能容忍Linux 直接報找不到文件。解決上傳路徑做成配置項放到application.yml的custom.upload-path里不同環(huán)境用不同的配置文件覆蓋。路徑拼接統(tǒng)一用Paths.get()或File.separator不要手寫斜杠。另外上傳的文件不應該直接放在項目目錄下否則重新部署就會丟文件建議掛載獨立的數(shù)據(jù)盤或?qū)ο蟠鎯Α?. 源碼值不值得投入先跑通這三步驗收再決定拿到任何一套智慧園區(qū)源碼我都先做三層驗收過了才談改造成本。第一層是空庫啟動把數(shù)據(jù)庫清空配好連接串啟動項目觀察能不能自動建表、內(nèi)置數(shù)據(jù)能不能初始化成功。這一步卡住的多半是依賴版本或配置項問題按第 5 章的順序查。第二層是權(quán)限驗證造兩個賬號一個是園區(qū)管理員一個是樓棟管理員用同一套接口請求數(shù)據(jù)對比兩個賬號返回的列表數(shù)量是否不同如果完全相同說明數(shù)據(jù)權(quán)限沒有真正生效。第三層是性能摸底打開慢 SQL 日志故意查詢一次大范圍數(shù)據(jù)看打印出來的 SQL 有沒有全表掃描。慢 SQL 日志這樣配logging: level: com.smartpark.mapper: debug把 Mapper 包下的日志級別調(diào)到 debugMyBatis 會把每次執(zhí)行的預編譯 SQL 和參數(shù)都打出來。這條配置在排查“接口突然變慢”時價值極高生產(chǎn)環(huán)境不建議長期開啟但驗收階段必須開。我接過一套老園區(qū)系統(tǒng)業(yè)務代碼寫得還算規(guī)整但數(shù)據(jù)權(quán)限全靠 Service 層手動拼條件散落了十幾個地方一次漏改就導致跨園區(qū)數(shù)據(jù)泄露。后來花了整整一周把它收攏到攔截器里副作用是重構(gòu)期間測試量暴漲。從那以后我養(yǎng)成了一個習慣先檢查權(quán)限過濾有沒有統(tǒng)一收口再動業(yè)務代碼。三層驗收法也是那時候逼出來的現(xiàn)在每次接手新源碼都會先跑一遍。這套基于 Java 的智慧園區(qū)管理系統(tǒng)源碼只要過了這三層驗收剩下的就是按你自己的業(yè)務場景做減法把用不到的模塊裁掉把報表和 BI 這類增值功能慢慢補上。希望幫到你。本文還有配套的精品資源點擊獲取