架构总览
架构总览
整体分层
┌──────────────────────────────────────────────────────────┐
│ 前端 frontend (Vue3 + TS + Vite, :3000) │
│ views → components(设计器) → api(axios, baseURL=/api) │
└───────────────────────────┬──────────────────────────────┘
│ /api/** (Vite proxy 透传)
▼
┌──────────────────────────────────────────────────────────┐
│ 后端 backend (Spring Boot 3, :52856) │
│ │
│ 过滤器链: TenantFilter → JwtAuthenticationFilter → ... │
│ 拦截器: 限流 / IP白名单 / 防抖 / 日志 / 慢SQL │
│ AOP: @AuditLog 审计 / 参数校验 │
│ │
│ Controller (@RequestMapping("/api/xxx")) │
│ ↓ │
│ Service (ServiceImpl<Mapper, Entity>) │
│ ↓ ↓ │
│ MyBatis-Plus Mapper SchemaService(动态表 JdbcTemplate)
│ ↓ ↓ │
│ 租户拦截器(自动追加 tenant_id) │
└───────────────────────────┬──────────────────────────────┘
▼
MySQL 8 / H2 (+ Redis 缓存)平台同时提供两条数据通路:
- 静态实体通路:常规业务模块(系统管理、ERP、HR 等)走
Controller → Service → MyBatis-Plus Mapper → 实体表。 - 动态表通路:低代码引擎设计的表走
Controller → SchemaService → JdbcTemplate,表结构运行时才存在,SQL 由元数据动态拼装。
请求链路
以「动态查询某张设计器建的表数据」为例:
- 前端
api/tableDesigner.ts调用request.get('/database/table/' + id + '/data/list')(注意:不带/api前缀)。 - Vite dev server 把
/api/database/...代理到http://127.0.0.1:52856。 - 后端
TenantFilter从 JWT 解析tenantId存入TenantContext(ThreadLocal)。 JwtAuthenticationFilter校验 JWT 并填充SecurityContextHolder。- 拦截器链依次执行(限流、防抖、日志等)。
DatabaseController命中@GetMapping("/table/{id}/data/list"),委托SchemaService.queryDataList。SchemaService读取lc_column_meta元数据 → 拼装带tenant_id/deleted=0过滤的参数化 SQL →JdbcTemplate执行 → 蛇形行转驼峰对象返回。- 结果经
Result<T>包装后回写前端。
API 路径约定
双重 /api 前缀陷阱
- 前端 axios
baseURL = '/api' - 后端控制器
@RequestMapping("/api/xxx") - 因此前端 api 模块的请求路径里不要再写
/api
| 写法 | 实际请求 | 结果 |
|---|---|---|
request.get('/database/...') | /api/database/... | ✅ 正确 |
request.get('/api/database/...') | /api/api/database/... | ❌ 404 |
历史上 BPM 的多个前端接口文件因此踩坑(已修复)。新增接口务必遵守此约定。
统一响应 Result
所有接口返回 com.lowcode.common.Result<T>:
public class Result<T> {
private int code; // 200 成功
private String msg;
private T data;
public static <T> Result<T> success(T data) { ... }
public static <T> Result<T> error(int code, String msg) { ... }
}前端 axios 响应拦截器统一解包:code === 200 取 data,否则抛错提示 msg。
分页 PageHelper
平台没有使用 MyBatis-Plus 的 PaginationInnerInterceptor(本地缺少 mybatis-plus-jsqlparser 依赖),而是自研 common.PageHelper.doPage(...):
- 接收
pageNum/pageSize。 - 执行
LIMIT/OFFSET查询 + 一条COUNT查询。 - 返回统一的分页结构(
list/total/pageNum/pageSize)。
动态表的分页由 SchemaService.queryDataList 内部按相同方式实现。
MyBatis-Plus 配置要点
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.lowcode.entity
configuration:
map-underscore-to-camel-case: true # 蛇形 ↔ 驼峰
global-config:
db-config:
logic-delete-field: deleted # 1=已删除 0=正常
logic-delete-value: 1
logic-not-delete-value: 0- 无 XML Mapper:所有 Mapper 都是注解 / 方法式 Java 接口,
resources/mapper/下没有 XML。 - 自动填充:
MyMetaObjectHandler在插入/更新时自动写createTime/updateTime/createBy/updateBy(注:当前createBy/updateBy硬编码为"1",未取真实用户,见 )。 - 租户:自定义
TenantInterceptor(实现InnerInterceptor,反射改写 SQL)给查询追加tenant_id条件(见下)。
多租户链路
请求 → TenantFilter(从 JWT 取 tenantId) → TenantContext(ThreadLocal)
↓
自定义 TenantInterceptor(实现 InnerInterceptor, 反射改写)
仅拦截 SELECT, 追加 WHERE tenant_id = ?
↓
SchemaService 动态 SQL 也手动拼 tenant_id 条件TenantContext是 ThreadLocal,请求结束在finally中clear(),避免线程复用串号。- 白名单硬编码为
{sys_tenant, sys_dict_type, sys_dict_data, sys_menu, sys_role, sys_user_role, sys_role_menu}(不含sys_user)。注意:application-h2.yml里的tenant.ignore-tables实际未被读取,以代码硬编码为准。 TenantQuotaService设计上校验租户配额(用户数、应用数等);当前为占位实现(checkUserQuota/checkTableQuota恒返回true),配额未真正生效。
配置 Profile
| Profile | 数据源 | 用途 | 自动建表 |
|---|---|---|---|
h2(dev) | H2 内存库 jdbc:h2:mem:lowcode | 快速体验、调试 | ✅ 启动执行 schema-h2.sql + data-h2.sql |
mysql(默认) | MySQL jdbc:mysql://localhost:3306/lowcode | 生产 / 类生产 | ❌ mode: never,需手动导入 |
切换方式:mvn spring-boot:run -Dspring-boot.run.profiles=h2。
安全模型
- 登录
POST /api/auth/login校验账号密码 → 签发 JWT(jjwt,24h 有效,HeaderAuthorization)。 JwtAuthenticationFilter每次请求校验签名与过期。SecurityConfig白名单(细粒度):/api/auth/login、/captcha、/register、/api/auth/sso/*、/api/open/**、/api/mobile/**、/uploads/**;其余anyRequest().authenticated()。- 开放平台用
ApiKeyAuthFilter做 API Key 鉴权。
已校正旧文档
platform-status.md 中「安全过滤链采用 permitAll」的描述已过时。真实 SecurityConfig 采用 anyRequest().authenticated(),未带有效 token 的请求会被拒绝。残留风险:@PreAuthorize 覆盖不全、部分角色权限树硬编码、jwt.secret 用默认值。详见 。
