作为一个从业15年的Java全栈开发者,我设计过不下20套企业级建站系统。今天要分享的这套万福企业互联平台,采用了经典的SpringBoot+MyBatis技术栈,实现了多租户SaaS架构。与普通CMS不同,这套系统特别强化了企业间的数据互联能力,比如通过统一权限中心实现跨企业内容共享。
系统最核心的创新点在于"企业级RBAC模型"——不仅实现常规的菜单-角色-用户三级权限控制,还增加了企业维度的数据隔离。举个例子:A企业的管理员登录后,只能看到本企业员工创建的新闻和产品,但通过平台级的"互联白名单",可以授权给B企业查看特定内容。这种设计在传统建站系统中很少见。
系统采用共享数据库+独立Schema的混合模式。所有企业共用同一套MySQL实例,但每个企业拥有独立的数据表前缀(如qy123_products)。关键点在于动态表名处理,我们通过自定义MyBatis拦截器实现:
java复制public class TenantInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) {
String sql = ((MappedStatement)invocation.getArgs()[0]).getSql();
String tenantId = TenantContext.getCurrentTenant();
// 将原始SQL中的表名替换为带租户前缀的表名
String newSql = sql.replaceAll("(from|into|update)\\s+(\\w+)",
"$1 " + tenantId + "_$2");
//...执行改写后的SQL
}
}
特别注意:动态表名方案需要配合连接池的validationQuery使用,避免不同租户连接串用导致的数据泄露。
系统的权限控制包含四个关键实体:
权限验证流程采用责任链模式:
mermaid复制graph TD
A[请求拦截] --> B{是否白名单?}
B -->|是| C[放行]
B -->|否| D[解析JWT获取用户角色]
D --> E[查询cdjs表获取授权菜单]
E --> F{当前请求路径是否在授权菜单中?}
F -->|是| G[执行业务逻辑]
F -->|否| H[返回403]
产品管理模块没有采用固定的字段设计,而是通过元数据配置实现动态表单。核心表结构包括:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| md_attributes | attr_id, attr_name, data_type | 存储所有可选字段 |
| qy123_products | 基础字段+动态字段 | 实际业务表 |
动态字段的SQL生成示例:
java复制StringBuilder sql = new StringBuilder("SELECT ");
// 基础字段
sql.append("cpid, cpmc, qy");
// 动态添加扩展字段
for (Attribute attr : getDynamicAttributes()) {
sql.append(",").append(attr.getColumnName());
}
sql.append(" FROM ").append(getTenantTable("products"));
访问量记录模块面临的主要挑战是高并发写入。我们采用三级缓冲策略:
关键Redis命令:
bash复制# 企业当日UV统计
INCR qy:123:uv:20230801
# 全局PV统计
ZINCRBY site:pv:rank 1 123
初期系统在渲染后台菜单时出现3秒以上的延迟。通过Arthas工具分析发现瓶颈在于N+1查询问题:
java复制// 错误示例:循环查询子菜单
List<Menu> parentMenus = menuDao.listParentMenus();
for(Menu parent : parentMenus) {
parent.setChildren(menuDao.listChildren(parent.getId())); // 产生N次查询
}
优化方案:
sql复制WITH RECURSIVE menu_tree AS (
SELECT * FROM cdxx WHERE f IS NULL
UNION ALL
SELECT m.* FROM cdxx m JOIN menu_tree mt ON m.f = mt.cdid
)
SELECT * FROM menu_tree ORDER BY xh;
在压力测试时发现通过修改URL参数可以越权访问其他企业数据。修复方案包括:
java复制@Interceptor
public void beforeQuery(QueryContext context) {
if (!context.getSql().contains("qy=")) {
throw new SecurityException("Missing tenant filter");
}
}
javascript复制router.beforeEach((to, from, next) => {
if (to.params.qy !== store.state.currentTenant) {
next('/403')
}
})
对于生产环境部署,推荐以下拓扑结构:
code复制 +-----------------+
| CDN/OSS |
+--------+--------+
|
+-------------+ +--------+--------+ +-----------------+
| Web | | API Gateway | | Job Server |
| (Nginx) +----+ (Spring Cloud +----+ (Quartz Cluster)|
+-------------+ | Gateway) | +-----------------+
+--------+--------+
|
+--------+--------+
| Redis Cluster |
+--------+--------+
|
+--------+--------+
| MySQL Cluster |
| (主从+分库) |
+-----------------+
关键配置参数:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 30000
idle-timeout: 600000
properties复制spring.redis.cluster.nodes=192.168.1.101:6379,192.168.1.102:6379
spring.redis.timeout=5000
这套系统在实施过程中最大的收获是:企业级系统设计必须从一开始就考虑多租户隔离,后期改造的成本往往是重构级别的。我们在第二版引入了TenantContext的设计,通过ThreadLocal传递租户标识,使得业务代码几乎不需要感知多租户逻辑