闪仓全栈架构解析:五个代码库、一套后端、一个安全边界
闪仓WMS由Spring Boot后端、Vue 3 PC前端、uni-app移动端、Next.js营销站和Go CLI/MCP服务器五个代码库组成。本文深入解析各层技术选型、binding_user_id多租户机制,以及CLI如何为后端补全安全边界。
系统全景
闪仓WMS由五个独立代码库组成,它们共享同一个后端API,各自承担不同的职责:
PC管理端 (Vue 3) 移动端 (uni-app) 营销官网 (Next.js 15)
localhost:8081 H5/微信/App Cloudflare Pages
| | |
+---------- Spring Boot 后端 (:10086) -----+
MyBatis Plus -> MySQL
|
Go CLI / MCP 服务器 (fwh)
PC端和移动端是核心业务入口,直接调用后端REST API完成采购、销售、库存等全部操作。营销官网运行在Cloudflare边缘网络上,拥有自己的D1数据库,仅在需要时与后端通信。Go CLI则以命令行和MCP协议两种模式工作,为AI Agent提供受控的数据访问通道。
后端:Spring Boot 2.7 多模块架构
后端采用Spring Boot 2.7.15,运行在JDK 1.8 (Corretto)上,使用Maven多模块组织代码:
- sys -- 主入口模块,端口10086
- warehouse_manager -- 采购、销售、库存、BI、仓库操作
- goods_manager -- 商品管理(goods表包含154个字段)
- finance -- 财务模块
- user -- 认证与用户管理
- ai -- AI功能(依赖user模块)
- staff_manager -- 员工管理
- taobao -- 淘宝集成
- common -- 共享基础设施
common模块是架构的基石。它提供了SelectUtil、DeleteUtil、ModifyUtil等通用工具类,这些工具基于Java反射机制,根据查询条件对象的非空字段自动构建MyBatis Plus的QueryWrapper,并将驼峰命名自动转换为下划线命名匹配数据库列名。这意味着新增一个查询接口时,开发者只需定义DAO对象,查询逻辑由工具类统一处理。
数据访问层使用MyBatis Plus 3.4.3,ORM映射通过@TableField注解完成。数据库为MySQL 8.0+,核心表包括user、goods、warehouse、bill_of_document(支持16种单据类型)、cooperative_partner、staff等。
请求在到达Controller之前会经过一条过滤器链:CachedBodyFilter缓存POST/PUT/PATCH请求体(跳过multipart)-> RSADecryptionFilter检测X-Encryption-Enabled: true头部并解密 -> LogInterceptor记录日志。加密采用RSA 2048-bit密钥交换 + AES-GCM内容加密的混合方案。
三个前端,一套API
PC端(Vue 3 + TDesign) 是功能最全的客户端。使用Vue CLI 5构建,TDesign作为UI框架,ECharts渲染BI图表,JSEncrypt处理RSA加密,vue-i18n支持中/英/俄三语。开发服务器运行在8081端口,通过代理将/flash_warehouse路径转发到后端的10086端口。
移动端(uni-app + uView) 基于Vue 3的uni-app框架,使用Vite 5构建,可以同时输出H5、Android原生和微信小程序三种产物。UI层使用uView 2.x和uni-ui组件库。页面按业务域分包:pages_purchase/、pages_sales/、pages_warehouse/等,保证小程序包体积可控。
营销官网(Next.js 15 + Cloudflare Pages) 是唯一不依赖Java后端运行的前端。它部署在Cloudflare Pages上,使用D1(SQLite边缘数据库)存储博客内容,KV存储缓存数据,Workers AI提供边缘推理能力。ORM选用Drizzle,国际化方案为next-intl,CSS方案为Tailwind v4。官网通过12个数据库迁移文件管理schema演进。
三个前端调用的是同一组REST API,API文档通过SpringDoc OpenAPI 3自动生成,可在/swagger-ui.html访问。
binding_user_id:多租户的唯一隔离机制
闪仓的多租户隔离完全依赖数据库层面的binding_user_id字段。在create_db.sql中,该字段出现在28处表定义中。每个数据表(商品、仓库、单据、员工、合作伙伴等)都包含此字段,所有查询都以它作为过滤条件。
这意味着:当用户A查询商品列表时,后端自动在SQL的WHERE子句中加入binding_user_id = A的ID,从而确保用户A只能看到自己的数据。这套机制简洁有效,但有一个前提——后端信任客户端发送的binding_user_id值。后端没有JWT、没有服务端session、也没有独立的租户校验逻辑。
在PC端和移动端场景下,这不是问题:前端代码固定使用登录时返回的用户ID。但在CLI和AI Agent场景下,如果binding_user_id可以作为参数传入,Agent就能通过猜测ID访问任意租户的数据。
Go CLI:后端缺失的安全边界
fwh CLI工具用Go编写,编译为单一静态二进制文件,无运行时依赖,支持macOS、Linux和Windows。它提供23个CLI命令和超过100个MCP工具,功能覆盖PC端的全部能力。
CLI的安全模型围绕两条硬规则构建:
第一,必须先登录。 除了login、logout、version、help之外的所有命令,在执行前都会通过PersistentPreRunE检查是否存在session文件。没有session则直接报错。
第二,租户ID锁定。 Session结构体中的userID字段被声明为未导出(小写),只有一个getter方法UserID(),没有setter。该值在登录时从后端响应中获取,写入session文件(权限0600),此后不可被任何CLI flag、环境变量或MCP工具参数覆盖。
为了在代码层面强制执行这一点,CLI包含两层防御:
-
API层审计测试(
internal/api/security_test.go):通过源码扫描确认session.UserID()只在client.go的两个固定位置被调用。如果有人新增了第三个调用点,测试会失败。 -
MCP schema验证(
internal/mcp/schema.go):在程序启动时,通过反射扫描每个MCP工具的输入结构体。如果任何字段的json tag包含binding_user_id、user_id、tenant_id等禁止名称,程序会panic并拒绝启动。
var forbiddenInputFields = map[string]struct{}{
"binding_user_id": {},
"user_id": {},
"tenant_id": {},
}
这意味着即使开发者不小心在MCP工具的输入结构体中添加了租户相关字段,程序也无法启动——错误在编译后的第一次运行时就会被发现。
各层技术栈总结
| 层 | 技术 | 语言 |
|---|---|---|
| 后端API | Spring Boot 2.7.15, MyBatis Plus 3.4.3, MySQL 8.0+ | Java 8 |
| PC管理端 | Vue 3, Vue CLI 5, TDesign, ECharts | TypeScript/JS |
| 移动端 | uni-app (Vue 3), Vite 5, uView 2.x | TypeScript/JS |
| 营销官网 | Next.js 15, Tailwind v4, Drizzle, Cloudflare D1/KV | TypeScript |
| CLI/MCP | Go 1.22+, Cobra, 单一二进制文件 | Go |
五个代码库、四种编程语言、三个前端客户端、一套REST API、一个binding_user_id字段完成租户隔离、一个Go CLI补全安全边界。这就是闪仓的全栈架构。