[{"data":1,"prerenderedAt":1298},["ShallowReactive",2],{"article-backend\u002Foinsistsa-token":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"tags":11,"body":13,"_type":1292,"_id":1293,"_source":1294,"_file":1295,"_stem":1296,"_extension":1297},"\u002Farticles\u002Fbackend\u002Foinsistsa-token","backend",false,"","精炼版 RuoYi-Vue-Plus 后端：从多模块骨架到数据权限与多租户","用 Java 21 + Spring Boot 3.5 + MyBatis-Plus + Sa-Token 从零搭一套多模块 RBAC 后端，讲清分层、统一响应、持久层基建、声明式鉴权、数据权限 AOP、多租户行级隔离，以及两个值得记下来的排错故事。","2026-06-03",[12],"软件工程",{"type":14,"children":15,"toc":1278},"root",[16,32,38,51,62,68,90,242,304,322,328,348,357,401,407,417,609,621,627,632,670,721,727,739,752,775,788,848,868,874,923,959,965,972,1014,1045,1051,1080,1129,1160,1178,1241,1246,1251,1256,1267,1272],{"type":17,"tag":18,"props":19,"children":20},"element","p",{},[21,24,30],{"type":22,"value":23},"text","这套后端是精炼重构 RuoYi-Vue-Plus 的产物：技术栈全换成现代版本（Java 21、Spring Boot 3.5、Jakarta 命名空间、PostgreSQL、MyBatis-Plus spring-boot3、Sa-Token、Redis），但",{"type":17,"tag":25,"props":26,"children":27},"strong",{},[28],{"type":22,"value":29},"只实现最核心、最通用的主流程",{"type":22,"value":31},"，砍掉那些应对极端企业场景的层层兜底，让每一层的本质都能一眼看清。这篇按\"分层 → 响应 → 持久层 → 鉴权 → 数据权限 → 多租户 → 排错\"的顺序走一遍。",{"type":17,"tag":33,"props":34,"children":36},"h2",{"id":35},"项目地址",[37],{"type":22,"value":35},{"type":17,"tag":18,"props":39,"children":40},{},[41,43],{"type":22,"value":42},"github：",{"type":17,"tag":44,"props":45,"children":49},"a",{"href":46,"rel":47},"https:\u002F\u002Fgithub.com\u002Foinsist\u002Foinsist-backend",[48],"nofollow",[50],{"type":22,"value":46},{"type":17,"tag":18,"props":52,"children":53},{},[54,56],{"type":22,"value":55},"gitee：",{"type":17,"tag":44,"props":57,"children":60},{"href":58,"rel":59},"https:\u002F\u002Fgitee.com\u002Fo_insist\u002Foinsist-backend",[48],[61],{"type":22,"value":58},{"type":17,"tag":33,"props":63,"children":65},{"id":64},"一多模块分层",[66],{"type":22,"value":67},"一、多模块分层",{"type":17,"tag":18,"props":69,"children":70},{},[71,73,80,82,88],{"type":22,"value":72},"不单独拉一个 ",{"type":17,"tag":74,"props":75,"children":77},"code",{"className":76},[],[78],{"type":22,"value":79},"framework",{"type":22,"value":81}," 一级模块，而是把可复用能力拆进 ",{"type":17,"tag":74,"props":83,"children":85},{"className":84},[],[86],{"type":22,"value":87},"common-*",{"type":22,"value":89},"：",{"type":17,"tag":91,"props":92,"children":93},"table",{},[94,113],{"type":17,"tag":95,"props":96,"children":97},"thead",{},[98],{"type":17,"tag":99,"props":100,"children":101},"tr",{},[102,108],{"type":17,"tag":103,"props":104,"children":105},"th",{},[106],{"type":22,"value":107},"模块",{"type":17,"tag":103,"props":109,"children":110},{},[111],{"type":22,"value":112},"职责",{"type":17,"tag":114,"props":115,"children":116},"tbody",{},[117,135,152,174,191,208,225],{"type":17,"tag":99,"props":118,"children":119},{},[120,130],{"type":17,"tag":121,"props":122,"children":123},"td",{},[124],{"type":17,"tag":74,"props":125,"children":127},{"className":126},[],[128],{"type":22,"value":129},"oinsist-admin",{"type":17,"tag":121,"props":131,"children":132},{},[133],{"type":22,"value":134},"启动 + Controller 出口层（只做 HTTP 编排，不含业务逻辑）",{"type":17,"tag":99,"props":136,"children":137},{},[138,147],{"type":17,"tag":121,"props":139,"children":140},{},[141],{"type":17,"tag":74,"props":142,"children":144},{"className":143},[],[145],{"type":22,"value":146},"oinsist-system",{"type":17,"tag":121,"props":148,"children":149},{},[150],{"type":22,"value":151},"RBAC 核心业务（用户\u002F角色\u002F部门\u002F菜单）的 Service\u002FMapper",{"type":17,"tag":99,"props":153,"children":154},{},[155,164],{"type":17,"tag":121,"props":156,"children":157},{},[158],{"type":17,"tag":74,"props":159,"children":161},{"className":160},[],[162],{"type":22,"value":163},"common-core",{"type":17,"tag":121,"props":165,"children":166},{},[167,169],{"type":22,"value":168},"基础常量\u002F枚举\u002F响应模型\u002F异常\u002F纯工具——",{"type":17,"tag":25,"props":170,"children":171},{},[172],{"type":22,"value":173},"不依赖任何技术栈",{"type":17,"tag":99,"props":175,"children":176},{},[177,186],{"type":17,"tag":121,"props":178,"children":179},{},[180],{"type":17,"tag":74,"props":181,"children":183},{"className":182},[],[184],{"type":22,"value":185},"common-web",{"type":17,"tag":121,"props":187,"children":188},{},[189],{"type":22,"value":190},"全局异常、参数校验、WebMvc、Jackson 配置",{"type":17,"tag":99,"props":192,"children":193},{},[194,203],{"type":17,"tag":121,"props":195,"children":196},{},[197],{"type":17,"tag":74,"props":198,"children":200},{"className":199},[],[201],{"type":22,"value":202},"common-mybatis",{"type":17,"tag":121,"props":204,"children":205},{},[206],{"type":22,"value":207},"MyBatis-Plus、分页、数据权限、租户拦截器、字段填充",{"type":17,"tag":99,"props":209,"children":210},{},[211,220],{"type":17,"tag":121,"props":212,"children":213},{},[214],{"type":17,"tag":74,"props":215,"children":217},{"className":216},[],[218],{"type":22,"value":219},"common-satoken",{"type":17,"tag":121,"props":221,"children":222},{},[223],{"type":22,"value":224},"Sa-Token 登录认证、权限校验",{"type":17,"tag":99,"props":226,"children":227},{},[228,237],{"type":17,"tag":121,"props":229,"children":230},{},[231],{"type":17,"tag":74,"props":232,"children":234},{"className":233},[],[235],{"type":22,"value":236},"common-redis",{"type":17,"tag":121,"props":238,"children":239},{},[240],{"type":22,"value":241},"缓存与分布式能力",{"type":17,"tag":18,"props":243,"children":244},{},[245,247,253,255,261,263,269,270,275,277,283,285,302],{"type":22,"value":246},"依赖方向严格自上而下：",{"type":17,"tag":74,"props":248,"children":250},{"className":249},[],[251],{"type":22,"value":252},"admin → system → common-mybatis → common-core",{"type":22,"value":254},"，",{"type":17,"tag":74,"props":256,"children":258},{"className":257},[],[259],{"type":22,"value":260},"system",{"type":22,"value":262}," 不得反向依赖 ",{"type":17,"tag":74,"props":264,"children":266},{"className":265},[],[267],{"type":22,"value":268},"admin",{"type":22,"value":254},{"type":17,"tag":74,"props":271,"children":273},{"className":272},[],[274],{"type":22,"value":163},{"type":22,"value":276}," 不碰 Web\u002FDB\u002FRedis。这条线最容易被破坏的是 ",{"type":17,"tag":74,"props":278,"children":280},{"className":279},[],[281],{"type":22,"value":282},"BaseEntity",{"type":22,"value":284}," 该放哪——它带 MyBatis-Plus 注解，所以",{"type":17,"tag":25,"props":286,"children":287},{},[288,290,295,297],{"type":22,"value":289},"必须放 ",{"type":17,"tag":74,"props":291,"children":293},{"className":292},[],[294],{"type":22,"value":202},{"type":22,"value":296}," 而不是 ",{"type":17,"tag":74,"props":298,"children":300},{"className":299},[],[301],{"type":22,"value":163},{"type":22,"value":303},"，否则核心层就被 ORM 污染了。",{"type":17,"tag":18,"props":305,"children":306},{},[307,309,314,315,320],{"type":22,"value":308},"这种分层的价值是：同一套业务逻辑能被不同入口（REST、RPC、定时任务）复用。一个直接体现：Controller 全放 ",{"type":17,"tag":74,"props":310,"children":312},{"className":311},[],[313],{"type":22,"value":268},{"type":22,"value":254},{"type":17,"tag":74,"props":316,"children":318},{"className":317},[],[319],{"type":22,"value":260},{"type":22,"value":321}," 只有 Service\u002FMapper——业务层完全不感知 HTTP。",{"type":17,"tag":33,"props":323,"children":325},{"id":324},"二统一响应与异常收敛",[326],{"type":22,"value":327},"二、统一响应与异常收敛",{"type":17,"tag":18,"props":329,"children":330},{},[331,333,339,340,346],{"type":22,"value":332},"所有接口返回统一的 ",{"type":17,"tag":74,"props":334,"children":336},{"className":335},[],[337],{"type":22,"value":338},"R\u003CT> = { code, msg, data }",{"type":22,"value":254},{"type":17,"tag":74,"props":341,"children":343},{"className":342},[],[344],{"type":22,"value":345},"code=200",{"type":22,"value":347}," 成功。请求的主流程是一条收敛的管道：",{"type":17,"tag":349,"props":350,"children":352},"pre",{"code":351},"请求 → 参数绑定 → Jakarta Validation 校验\n   ├─ 校验不过 → GlobalExceptionHandler → R.fail(400, 聚合的字段错误)\n   └─ 通过 → Controller\u002FService\n              ├─ 正常 → R.ok(data)\n              ├─ 业务异常(ServiceException) → R.fail(业务码, 业务消息)\n              └─ 未知异常 → R.fail(500, \"操作失败\")（堆栈只记日志，不暴露给前端）\n",[353],{"type":17,"tag":74,"props":354,"children":355},{"__ignoreMap":7},[356],{"type":22,"value":351},{"type":17,"tag":18,"props":358,"children":359},{},[360,362,368,370,376,378,383,385,391,393,399],{"type":22,"value":361},"关键约定：异常全部交给 ",{"type":17,"tag":74,"props":363,"children":365},{"className":364},[],[366],{"type":22,"value":367},"@RestControllerAdvice",{"type":22,"value":369}," 的 ",{"type":17,"tag":74,"props":371,"children":373},{"className":372},[],[374],{"type":22,"value":375},"GlobalExceptionHandler",{"type":22,"value":377}," 统一拦截（参数校验、请求体解析失败、类型转换、方法不支持、业务异常、未知异常各有分支），",{"type":17,"tag":25,"props":379,"children":380},{},[381],{"type":22,"value":382},"严禁在 Service 层盲目 try-catch 把异常吞掉",{"type":22,"value":384},"。",{"type":17,"tag":74,"props":386,"children":388},{"className":387},[],[389],{"type":22,"value":390},"ServiceException",{"type":22,"value":392}," 给业务主动抛，错误码集中在 ",{"type":17,"tag":74,"props":394,"children":396},{"className":395},[],[397],{"type":22,"value":398},"ResultCode",{"type":22,"value":400}," 枚举里维护。这样 Controller 写起来很干净，只管编排，不管错误怎么变成响应。",{"type":17,"tag":33,"props":402,"children":404},{"id":403},"三持久层mybatis-plus-基建",[405],{"type":22,"value":406},"三、持久层：MyBatis-Plus 基建",{"type":17,"tag":18,"props":408,"children":409},{},[410,415],{"type":17,"tag":74,"props":411,"children":413},{"className":412},[],[414],{"type":22,"value":202},{"type":22,"value":416}," 承载所有持久层基础能力，几个要点：",{"type":17,"tag":418,"props":419,"children":420},"ul",{},[421,447,472,525,583],{"type":17,"tag":422,"props":423,"children":424},"li",{},[425,430,432,438,439,445],{"type":17,"tag":25,"props":426,"children":427},{},[428],{"type":22,"value":429},"雪花主键",{"type":22,"value":431},"：全局 ",{"type":17,"tag":74,"props":433,"children":435},{"className":434},[],[436],{"type":22,"value":437},"id-type: assign_id",{"type":22,"value":254},{"type":17,"tag":74,"props":440,"children":442},{"className":441},[],[443],{"type":22,"value":444},"@TableId(type = IdType.ASSIGN_ID)",{"type":22,"value":446},"，不用自增——这是后面\"前端精度坑\"的根源（见第七节）。",{"type":17,"tag":422,"props":448,"children":449},{},[450,455,456,462,464,470],{"type":17,"tag":25,"props":451,"children":452},{},[453],{"type":22,"value":454},"自动填充",{"type":22,"value":89},{"type":17,"tag":74,"props":457,"children":459},{"className":458},[],[460],{"type":22,"value":461},"MetaObjectHandler",{"type":22,"value":463}," 在新增\u002F更新时自动填 ",{"type":17,"tag":74,"props":465,"children":467},{"className":466},[],[468],{"type":22,"value":469},"createTime\u002FupdateTime\u002FcreateBy\u002FupdateBy",{"type":22,"value":471},"，业务代码不用手写。",{"type":17,"tag":422,"props":473,"children":474},{},[475,480,481,486,487,493,495,501,502,508,510,516,518,524],{"type":17,"tag":25,"props":476,"children":477},{},[478],{"type":22,"value":479},"逻辑删除",{"type":22,"value":89},{"type":17,"tag":74,"props":482,"children":484},{"className":483},[],[485],{"type":22,"value":282},{"type":22,"value":369},{"type":17,"tag":74,"props":488,"children":490},{"className":489},[],[491],{"type":22,"value":492},"deleted",{"type":22,"value":494}," 字段标 ",{"type":17,"tag":74,"props":496,"children":498},{"className":497},[],[499],{"type":22,"value":500},"@TableLogic",{"type":22,"value":254},{"type":17,"tag":74,"props":503,"children":505},{"className":504},[],[506],{"type":22,"value":507},"delete",{"type":22,"value":509}," 被改写成 ",{"type":17,"tag":74,"props":511,"children":513},{"className":512},[],[514],{"type":22,"value":515},"update deleted=1",{"type":22,"value":517},"，普通查询自动追加 ",{"type":17,"tag":74,"props":519,"children":521},{"className":520},[],[522],{"type":22,"value":523},"deleted=0",{"type":22,"value":384},{"type":17,"tag":422,"props":526,"children":527},{},[528,533,535,541,543,549,551,557,559,565,567,573,575,581],{"type":17,"tag":25,"props":529,"children":530},{},[531],{"type":22,"value":532},"分页",{"type":22,"value":534},"：注册 ",{"type":17,"tag":74,"props":536,"children":538},{"className":537},[],[539],{"type":22,"value":540},"PaginationInnerInterceptor",{"type":22,"value":542}," 并指定 ",{"type":17,"tag":74,"props":544,"children":546},{"className":545},[],[547],{"type":22,"value":548},"DbType.POSTGRE_SQL",{"type":22,"value":550},"；注意 MyBatis-Plus 3.5.x 起分页依赖 ",{"type":17,"tag":74,"props":552,"children":554},{"className":553},[],[555],{"type":22,"value":556},"mybatis-plus-jsqlparser",{"type":22,"value":558}," 做 SQL 解析，漏引运行期分页会失败。入参 ",{"type":17,"tag":74,"props":560,"children":562},{"className":561},[],[563],{"type":22,"value":564},"PageQuery{pageNum,pageSize}",{"type":22,"value":566},"、出参精简成 ",{"type":17,"tag":74,"props":568,"children":570},{"className":569},[],[571],{"type":22,"value":572},"PageResult\u003CT>{rows,total}",{"type":22,"value":574},"，作为 ",{"type":17,"tag":74,"props":576,"children":578},{"className":577},[],[579],{"type":22,"value":580},"R\u003CPageResult\u003CT>>",{"type":22,"value":582}," 的 data。",{"type":17,"tag":422,"props":584,"children":585},{},[586,591,593,599,601,607],{"type":17,"tag":25,"props":587,"children":588},{},[589],{"type":22,"value":590},"SQL 日志",{"type":22,"value":592},"：开发环境用 mapper 包的 ",{"type":17,"tag":74,"props":594,"children":596},{"className":595},[],[597],{"type":22,"value":598},"debug",{"type":22,"value":600}," 日志看 SQL，不用 ",{"type":17,"tag":74,"props":602,"children":604},{"className":603},[],[605],{"type":22,"value":606},"StdOutImpl",{"type":22,"value":608},"（生产噪声大）。",{"type":17,"tag":18,"props":610,"children":611},{},[612,614,619],{"type":22,"value":613},"为什么这些放公共持久层而不是业务模块？因为它们是",{"type":17,"tag":25,"props":615,"children":616},{},[617],{"type":22,"value":618},"所有表都要的横切能力",{"type":22,"value":620},"，集中一处配置，业务 Mapper 只管写自己的查询。",{"type":17,"tag":33,"props":622,"children":624},{"id":623},"四sa-token-鉴权",[625],{"type":22,"value":626},"四、Sa-Token 鉴权",{"type":17,"tag":18,"props":628,"children":629},{},[630],{"type":22,"value":631},"选 Sa-Token 而非 Spring Security，图的是主流程可读。鉴权通过注解声明式地挂在 Controller 上：",{"type":17,"tag":349,"props":633,"children":637},{"code":634,"language":635,"meta":7,"className":636,"style":7},"@SaCheckPermission(\"system:user:list\")\n@GetMapping(\"\u002Flist\")\npublic R\u003CPageResult\u003CSysUserVo>> list(PageQuery pageQuery) { ... }\n","java","language-java shiki shiki-themes github-dark",[638],{"type":17,"tag":74,"props":639,"children":640},{"__ignoreMap":7},[641,652,661],{"type":17,"tag":642,"props":643,"children":646},"span",{"class":644,"line":645},"line",1,[647],{"type":17,"tag":642,"props":648,"children":649},{},[650],{"type":22,"value":651},"@SaCheckPermission(\"system:user:list\")\n",{"type":17,"tag":642,"props":653,"children":655},{"class":644,"line":654},2,[656],{"type":17,"tag":642,"props":657,"children":658},{},[659],{"type":22,"value":660},"@GetMapping(\"\u002Flist\")\n",{"type":17,"tag":642,"props":662,"children":664},{"class":644,"line":663},3,[665],{"type":17,"tag":642,"props":666,"children":667},{},[668],{"type":22,"value":669},"public R\u003CPageResult\u003CSysUserVo>> list(PageQuery pageQuery) { ... }\n",{"type":17,"tag":18,"props":671,"children":672},{},[673,675,681,682,688,690,695,697,703,705,711,713,719],{"type":22,"value":674},"几个关键点：",{"type":17,"tag":74,"props":676,"children":678},{"className":677},[],[679],{"type":22,"value":680},"token-name: Authorization",{"type":22,"value":254},{"type":17,"tag":74,"props":683,"children":685},{"className":684},[],[686],{"type":22,"value":687},"token-prefix",{"type":22,"value":689}," ",{"type":17,"tag":25,"props":691,"children":692},{},[693],{"type":22,"value":694},"没启用",{"type":22,"value":696},"（学习阶段简化调试，所以前端传裸 token、不带 ",{"type":17,"tag":74,"props":698,"children":700},{"className":699},[],[701],{"type":22,"value":702},"Bearer ",{"type":22,"value":704},"）。登录时把用户的角色标识、权限标识写进 Session 的 ",{"type":17,"tag":74,"props":706,"children":708},{"className":707},[],[709],{"type":22,"value":710},"LoginUser",{"type":22,"value":712},"，后续 ",{"type":17,"tag":74,"props":714,"children":716},{"className":715},[],[717],{"type":22,"value":718},"@SaCheckPermission",{"type":22,"value":720}," 直接从 Session 取，不每次查库。前端那套\"菜单级 + 按钮级\"权限，数据源头就是这里发出去的角色\u002F权限集和菜单树。",{"type":17,"tag":33,"props":722,"children":724},{"id":723},"五数据权限注解-aop-sql-拼接",[725],{"type":22,"value":726},"五、数据权限：注解 + AOP + SQL 拼接",{"type":17,"tag":18,"props":728,"children":729},{},[730,732,737],{"type":22,"value":731},"\"菜单级权限\"管能不能调某个接口；\"数据权限\"管同一接口里",{"type":17,"tag":25,"props":733,"children":734},{},[735],{"type":22,"value":736},"能看到哪几行",{"type":22,"value":738},"——比如同样查用户列表，部门经理只能看本部门。",{"type":17,"tag":18,"props":740,"children":741},{},[742,744,750],{"type":22,"value":743},"实现是 MyBatis-Plus 的 ",{"type":17,"tag":74,"props":745,"children":747},{"className":746},[],[748],{"type":22,"value":749},"DataPermissionHandler",{"type":22,"value":751}," + 一个注解：",{"type":17,"tag":349,"props":753,"children":755},{"code":754,"language":635,"meta":7,"className":636,"style":7},"@DataPermission(deptIdColumn = \"dept_id\", userIdColumn = \"create_by\")\nPage\u003CSysUser> selectUserPage(Page\u003CSysUser> page, @Param(\"ew\") Wrapper\u003CSysUser> ew);\n",[756],{"type":17,"tag":74,"props":757,"children":758},{"__ignoreMap":7},[759,767],{"type":17,"tag":642,"props":760,"children":761},{"class":644,"line":645},[762],{"type":17,"tag":642,"props":763,"children":764},{},[765],{"type":22,"value":766},"@DataPermission(deptIdColumn = \"dept_id\", userIdColumn = \"create_by\")\n",{"type":17,"tag":642,"props":768,"children":769},{"class":644,"line":654},[770],{"type":17,"tag":642,"props":771,"children":772},{},[773],{"type":22,"value":774},"Page\u003CSysUser> selectUserPage(Page\u003CSysUser> page, @Param(\"ew\") Wrapper\u003CSysUser> ew);\n",{"type":17,"tag":18,"props":776,"children":777},{},[778,780,786],{"type":22,"value":779},"拦截器在 SQL 执行前回调 handler，按当前用户角色的数据范围（",{"type":17,"tag":74,"props":781,"children":783},{"className":782},[],[784],{"type":22,"value":785},"DataScopeEnum",{"type":22,"value":787},"：ALL \u002F DEPT \u002F DEPT_AND_CHILD \u002F CUSTOM \u002F SELF）动态拼 WHERE：",{"type":17,"tag":418,"props":789,"children":790},{},[791,796,809,821,833],{"type":17,"tag":422,"props":792,"children":793},{},[794],{"type":22,"value":795},"有 ALL → 直接放行，不加条件；",{"type":17,"tag":422,"props":797,"children":798},{},[799,801,807],{"type":22,"value":800},"有可访问部门 → ",{"type":17,"tag":74,"props":802,"children":804},{"className":803},[],[805],{"type":22,"value":806},"dept_id IN (...)",{"type":22,"value":808},"；",{"type":17,"tag":422,"props":810,"children":811},{},[812,814,820],{"type":22,"value":813},"SELF → ",{"type":17,"tag":74,"props":815,"children":817},{"className":816},[],[818],{"type":22,"value":819},"create_by = 当前用户ID",{"type":22,"value":808},{"type":17,"tag":422,"props":822,"children":823},{},[824,826,832],{"type":22,"value":825},"二者并存 → ",{"type":17,"tag":74,"props":827,"children":829},{"className":828},[],[830],{"type":22,"value":831},"(dept_id IN (...) OR create_by = ?)",{"type":22,"value":808},{"type":17,"tag":422,"props":834,"children":835},{},[836,847],{"type":17,"tag":25,"props":837,"children":838},{},[839,841],{"type":22,"value":840},"拿不到有效用户上下文 → 返回恒假 ",{"type":17,"tag":74,"props":842,"children":844},{"className":843},[],[845],{"type":22,"value":846},"1=0",{"type":22,"value":384},{"type":17,"tag":18,"props":849,"children":850},{},[851,853,858,860,866],{"type":22,"value":852},"最后这条 fail-closed 很关键：宁可什么都查不到，也不能在身份异常时泄露全量数据。多角色时取",{"type":17,"tag":25,"props":854,"children":855},{},[856],{"type":22,"value":857},"并集",{"type":22,"value":859},"（不是交集，避免缩小权限）。Provider 用 ",{"type":17,"tag":74,"props":861,"children":863},{"className":862},[],[864],{"type":22,"value":865},"ObjectProvider",{"type":22,"value":867}," 延迟获取，绕开\"拦截器依赖 SqlSessionFactory、Provider 又依赖 Mapper\"的循环依赖。",{"type":17,"tag":33,"props":869,"children":871},{"id":870},"六多租户行级隔离",[872],{"type":22,"value":873},"六、多租户：行级隔离",{"type":17,"tag":18,"props":875,"children":876},{},[877,879,885,886,892,894,905,907,913,915,921],{"type":22,"value":878},"多租户 = 一套库表给多家公司用，彼此看不见。这里走 MyBatis-Plus 的租户行级隔离：隔离表每条数据带 ",{"type":17,"tag":74,"props":880,"children":882},{"className":881},[],[883],{"type":22,"value":884},"tenant_id",{"type":22,"value":254},{"type":17,"tag":74,"props":887,"children":889},{"className":888},[],[890],{"type":22,"value":891},"TenantLineHandler",{"type":22,"value":893}," 在所有 SQL 上",{"type":17,"tag":25,"props":895,"children":896},{},[897,899],{"type":22,"value":898},"自动追加 ",{"type":17,"tag":74,"props":900,"children":902},{"className":901},[],[903],{"type":22,"value":904},"tenant_id = ?",{"type":22,"value":906},"，业务代码无感。",{"type":17,"tag":74,"props":908,"children":910},{"className":909},[],[911],{"type":22,"value":912},"sys_menu",{"type":22,"value":914},"、",{"type":17,"tag":74,"props":916,"children":918},{"className":917},[],[919],{"type":22,"value":920},"sys_role_menu",{"type":22,"value":922}," 这类全局共享表则不加。",{"type":17,"tag":18,"props":924,"children":925},{},[926,928,934,936,941,943,949,951,957],{"type":22,"value":927},"难点在\"租户上下文从哪来\"。正常请求从 Sa-Token Session 的 ",{"type":17,"tag":74,"props":929,"children":931},{"className":930},[],[932],{"type":22,"value":933},"LoginUser.tenantId",{"type":22,"value":935}," 取；但",{"type":17,"tag":25,"props":937,"children":938},{},[939],{"type":22,"value":940},"登录这一刻 Session 还没建立",{"type":22,"value":942},"，查用户时拿不到租户。解法是一个 ",{"type":17,"tag":74,"props":944,"children":946},{"className":945},[],[947],{"type":22,"value":948},"TenantContextHolder",{"type":22,"value":950},"（ThreadLocal）：登录流程先用请求里的租户 ID 显式 set，查完用户、建 Session 后再清掉；之后 ",{"type":17,"tag":74,"props":952,"children":954},{"className":953},[],[955],{"type":22,"value":956},"SaTokenTenantProvider",{"type":22,"value":958}," 优先读 ThreadLocal、再退回 Session。需要说明：这是\"行级隔离的内核\"，租户管理后台、登录选租户等运营功能还没做——属于\"机制实现了、功能没补全\"。",{"type":17,"tag":33,"props":960,"children":962},{"id":961},"七两个值得记下来的排错故事",[963],{"type":22,"value":964},"七、两个值得记下来的排错故事",{"type":17,"tag":966,"props":967,"children":969},"h3",{"id":968},"_1-雪花主键的精度问题",[970],{"type":22,"value":971},"1. 雪花主键的精度问题",{"type":17,"tag":18,"props":973,"children":974},{},[975,977,983,985,991,993,998,1000,1006,1007,1012],{"type":22,"value":976},"前端 JavaScript 的 ",{"type":17,"tag":74,"props":978,"children":980},{"className":979},[],[981],{"type":22,"value":982},"Number",{"type":22,"value":984}," 安全整数只到 ",{"type":17,"tag":74,"props":986,"children":988},{"className":987},[],[989],{"type":22,"value":990},"2^53-1",{"type":22,"value":992},"，19 位雪花 Long 一传过去就",{"type":17,"tag":25,"props":994,"children":995},{},[996],{"type":22,"value":997},"静默丢精度",{"type":22,"value":999},"，回传命中错误记录。解法是写一个 ",{"type":17,"tag":74,"props":1001,"children":1003},{"className":1002},[],[1004],{"type":22,"value":1005},"BigNumberSerializer",{"type":22,"value":89},{"type":17,"tag":25,"props":1008,"children":1009},{},[1010],{"type":22,"value":1011},"只对超出 JS 安全范围的 Long 转字符串",{"type":22,"value":1013},"，范围内的小整数（status、count）仍按数字输出，把对前端的影响降到最小：",{"type":17,"tag":349,"props":1015,"children":1017},{"code":1016,"language":635,"meta":7,"className":636,"style":7},"long v = value.longValue();\nif (v > MAX_SAFE_INTEGER || v \u003C MIN_SAFE_INTEGER) gen.writeString(value.toString());\nelse gen.writeNumber(v);\n",[1018],{"type":17,"tag":74,"props":1019,"children":1020},{"__ignoreMap":7},[1021,1029,1037],{"type":17,"tag":642,"props":1022,"children":1023},{"class":644,"line":645},[1024],{"type":17,"tag":642,"props":1025,"children":1026},{},[1027],{"type":22,"value":1028},"long v = value.longValue();\n",{"type":17,"tag":642,"props":1030,"children":1031},{"class":644,"line":654},[1032],{"type":17,"tag":642,"props":1033,"children":1034},{},[1035],{"type":22,"value":1036},"if (v > MAX_SAFE_INTEGER || v \u003C MIN_SAFE_INTEGER) gen.writeString(value.toString());\n",{"type":17,"tag":642,"props":1038,"children":1039},{"class":644,"line":663},[1040],{"type":17,"tag":642,"props":1041,"children":1042},{},[1043],{"type":22,"value":1044},"else gen.writeNumber(v);\n",{"type":17,"tag":966,"props":1046,"children":1048},{"id":1047},"_2-一个列表接口-500根因在-jackson-模块注册",[1049],{"type":22,"value":1050},"2. 一个列表接口 500，根因在 Jackson 模块注册",{"type":17,"tag":18,"props":1052,"children":1053},{},[1054,1056,1062,1064,1070,1072,1078],{"type":22,"value":1055},"注册上面那个序列化器时，我在 ",{"type":17,"tag":74,"props":1057,"children":1059},{"className":1058},[],[1060],{"type":22,"value":1061},"JacksonConfig",{"type":22,"value":1063}," 写了 ",{"type":17,"tag":74,"props":1065,"children":1067},{"className":1066},[],[1068],{"type":22,"value":1069},"builder.modules(sensitiveModule, bigNumberModule)",{"type":22,"value":1071},"。当时构建、单测都过，认证、菜单接口也正常。直到做用户列表，",{"type":17,"tag":74,"props":1073,"children":1075},{"className":1074},[],[1076],{"type":22,"value":1077},"GET \u002Fsystem\u002Fuser\u002Flist",{"type":22,"value":1079}," 稳定 500。",{"type":17,"tag":18,"props":1081,"children":1082},{},[1083,1085,1091,1093,1104,1106,1112,1114,1120,1122,1127],{"type":22,"value":1084},"根因相当隐蔽：",{"type":17,"tag":74,"props":1086,"children":1088},{"className":1087},[],[1089],{"type":22,"value":1090},"Jackson2ObjectMapperBuilder.modules(...)",{"type":22,"value":1092}," 一旦被显式调用，就会",{"type":17,"tag":25,"props":1094,"children":1095},{},[1096,1098],{"type":22,"value":1097},"覆盖 Spring Boot 自动注册的那批\"知名模块\"——包括处理 Java 8 时间的 ",{"type":17,"tag":74,"props":1099,"children":1101},{"className":1100},[],[1102],{"type":22,"value":1103},"JavaTimeModule",{"type":22,"value":1105},"。之前的响应都没有 ",{"type":17,"tag":74,"props":1107,"children":1109},{"className":1108},[],[1110],{"type":22,"value":1111},"LocalDateTime",{"type":22,"value":1113}," 所以没暴露；用户列表的 VO 带了 ",{"type":17,"tag":74,"props":1115,"children":1117},{"className":1116},[],[1118],{"type":22,"value":1119},"createTime",{"type":22,"value":1121},"，序列化时找不到 ",{"type":17,"tag":74,"props":1123,"children":1125},{"className":1124},[],[1126],{"type":22,"value":1103},{"type":22,"value":1128}," 直接抛异常 → 全局处理器兜成 500。修复是把它一并显式装回去：",{"type":17,"tag":349,"props":1130,"children":1132},{"code":1131,"language":635,"meta":7,"className":636,"style":7},"builder\n  .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)\n  .modules(new JavaTimeModule(), sensitiveModule, bigNumberModule);\n",[1133],{"type":17,"tag":74,"props":1134,"children":1135},{"__ignoreMap":7},[1136,1144,1152],{"type":17,"tag":642,"props":1137,"children":1138},{"class":644,"line":645},[1139],{"type":17,"tag":642,"props":1140,"children":1141},{},[1142],{"type":22,"value":1143},"builder\n",{"type":17,"tag":642,"props":1145,"children":1146},{"class":644,"line":654},[1147],{"type":17,"tag":642,"props":1148,"children":1149},{},[1150],{"type":22,"value":1151},"  .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)\n",{"type":17,"tag":642,"props":1153,"children":1154},{"class":644,"line":663},[1155],{"type":17,"tag":642,"props":1156,"children":1157},{},[1158],{"type":22,"value":1159},"  .modules(new JavaTimeModule(), sensitiveModule, bigNumberModule);\n",{"type":17,"tag":18,"props":1161,"children":1162},{},[1163,1165,1176],{"type":22,"value":1164},"教训：",{"type":17,"tag":25,"props":1166,"children":1167},{},[1168,1174],{"type":17,"tag":74,"props":1169,"children":1171},{"className":1170},[],[1172],{"type":22,"value":1173},"builder.modules()",{"type":22,"value":1175}," 是\"全量替换\"语义，不是\"追加\"",{"type":22,"value":1177},"。一旦自己接管模块列表，Spring Boot 默认帮你装的东西就得自己负责装回来。",{"type":17,"tag":1179,"props":1180,"children":1181},"blockquote",{},[1182],{"type":17,"tag":18,"props":1183,"children":1184},{},[1185,1187,1193,1195,1201,1203,1209,1211,1217,1218,1224,1226,1232,1233,1239],{"type":22,"value":1186},"相关的小坑：Java 21 起 javac 默认不再隐式扫描 classpath 上的注解处理器，而 ",{"type":17,"tag":74,"props":1188,"children":1190},{"className":1189},[],[1191],{"type":22,"value":1192},"spring-boot-starter-parent",{"type":22,"value":1194}," 没把 Lombok 放进 ",{"type":17,"tag":74,"props":1196,"children":1198},{"className":1197},[],[1199],{"type":22,"value":1200},"annotationProcessorPaths",{"type":22,"value":1202},"。结果 IDE（自带 Lombok 插件）能编译，纯命令行 ",{"type":17,"tag":74,"props":1204,"children":1206},{"className":1205},[],[1207],{"type":22,"value":1208},"mvn compile",{"type":22,"value":1210}," 却报 ",{"type":17,"tag":74,"props":1212,"children":1214},{"className":1213},[],[1215],{"type":22,"value":1216},"@Slf4j",{"type":22,"value":369},{"type":17,"tag":74,"props":1219,"children":1221},{"className":1220},[],[1222],{"type":22,"value":1223},"log",{"type":22,"value":1225}," 找不到符号。解法是在根 ",{"type":17,"tag":74,"props":1227,"children":1229},{"className":1228},[],[1230],{"type":22,"value":1231},"pom",{"type":22,"value":369},{"type":17,"tag":74,"props":1234,"children":1236},{"className":1235},[],[1237],{"type":22,"value":1238},"maven-compiler-plugin",{"type":22,"value":1240}," 里显式登记 Lombok 处理器路径。",{"type":17,"tag":33,"props":1242,"children":1244},{"id":1243},"小结",[1245],{"type":22,"value":1243},{"type":17,"tag":18,"props":1247,"children":1248},{},[1249],{"type":22,"value":1250},"这套后端最终提供了一个干净的 RBAC 底座：多模块分层、统一响应与全局异常、MyBatis-Plus 的雪花主键\u002F逻辑删除\u002F自动填充\u002F分页、Sa-Token 声明式鉴权、数据权限 AOP、多租户行级隔离。",{"type":17,"tag":18,"props":1252,"children":1253},{},[1254],{"type":22,"value":1255},"回头看，最有价值的不是\"又配好一个 starter\"，而是那两个排错故事——它们都指向同一类问题：",{"type":17,"tag":1179,"props":1257,"children":1258},{},[1259],{"type":17,"tag":18,"props":1260,"children":1261},{},[1262],{"type":17,"tag":25,"props":1263,"children":1264},{},[1265],{"type":22,"value":1266},"当你接管框架的某个默认行为时（自定义 Jackson 模块、自定义注解处理器路径），就等于把框架原本默默替你做好的事一起接管了，漏一样就出问题。",{"type":17,"tag":18,"props":1268,"children":1269},{},[1270],{"type":22,"value":1271},"把这些隐式约定显式化、并在真实数据上验证，比堆功能重要得多。",{"type":17,"tag":1273,"props":1274,"children":1275},"style",{},[1276],{"type":22,"value":1277},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":654,"depth":654,"links":1279},[1280,1281,1282,1283,1284,1285,1286,1287,1291],{"id":35,"depth":654,"text":35},{"id":64,"depth":654,"text":67},{"id":324,"depth":654,"text":327},{"id":403,"depth":654,"text":406},{"id":623,"depth":654,"text":626},{"id":723,"depth":654,"text":726},{"id":870,"depth":654,"text":873},{"id":961,"depth":654,"text":964,"children":1288},[1289,1290],{"id":968,"depth":663,"text":971},{"id":1047,"depth":663,"text":1050},{"id":1243,"depth":654,"text":1243},"markdown","content:articles:backend:oinsist后端架构实战：Sa-Token、数据权限与多租户.md","content","articles\u002Fbackend\u002Foinsist后端架构实战：Sa-Token、数据权限与多租户.md","articles\u002Fbackend\u002Foinsist后端架构实战：Sa-Token、数据权限与多租户","md",1780481290974]