
腾讯云开发者社区 首发 作者:某集团信息中心 技术负责人
我们集团自研的OA系统,上线半年后日活突破1.2万,但随之而来三个痛点:
本文将逐个击破,给出可复用的解决方案,所有代码均已脱敏。
我们要求审批人可配置为“部门负责人”“岗位角色”或任意Groovy表达式。设计SPI解析器:
// 解析器接口
public interface ApproverResolver {
List<String> resolve(Map<String, Object> variables, String expression);
}
// 部门负责人解析器
@Component
public class DeptLeaderResolver implements ApproverResolver {
@Override
public List<String> resolve(Map<String, Object> vars, String expr) {
// 表达式: deptLeader(表单.deptId)
String deptId = (String) SpelExpressionParser.evaluate(expr, vars);
return userDao.listDeptManagers(deptId).stream()
.map(User::getUserId).collect(Collectors.toList());
}
}
// 脚本解析器(支持热加载)
@Component
public class ScriptResolver implements ApproverResolver {
@Override
public List<String> resolve(Map<String, Object> vars, String expr) {
// expr: script:approvers.groovy
String scriptPath = expr.substring("script:".length());
GroovyScriptEngine engine = new GroovyScriptEngine("/scripts");
Binding binding = new Binding(vars);
return (List<String>) engine.run(scriptPath, binding);
}
}启动流程时,通过监听器动态设置 assigneeList 变量:
@EventListener
public void onStart(ProcessStartedEvent event) {
Map<String, Object> vars = event.getVariables();
String expr = (String) vars.get("approverExpr");
List<String> approvers = resolverRegistry.resolve(expr, vars);
vars.put("assigneeList", approvers); // 供UserTask使用
}原代码将业务保存和流程启动放在同一 @Transactional 中,导致长事务和乐观锁冲突。改为事件驱动 + 独立事务:
@Service
public class ApprovalSubmitService {
@Transactional(transactionManager = "bizTxManager")
public void submit(ApprovalForm form) {
approvalDao.insert(form);
// 发布事件,异步处理流程启动
applicationEventPublisher.publishEvent(new ApprovalCreatedEvent(form));
}
}
@EventListener
public void handleApprovalCreated(ApprovalCreatedEvent event) {
// 使用独立事务管理器
TransactionTemplate flowTmpl = new TransactionTemplate(flowTxManager);
flowTmpl.execute(status -> {
try {
runtimeService.startProcessInstanceByKey(...);
} catch (StaleObjectException e) {
// 记录重试消息(发送到CMQ,最多重试3次)
retryProducer.send(new RetryMessage(event.getFormId(), 3));
}
return null;
});
}效果:业务事务平均耗时从 1.2s 降至 180ms,流程启动异常率从 5% 降至 0.1%。
使用 MyBatis-Plus 元数据填充 tenant_id:
@Component
public class TenantMetaHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "tenantId", Long.class,
TenantContext.getCurrentTenantId());
}
}@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", ...)})
@Component
public class TenantInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
MetaObject meta = SystemMetaObject.forObject(handler);
MappedStatement ms = (MappedStatement) meta.getValue("delegate.mappedStatement");
// 忽略系统表或标注 @IgnoreTenant 的方法
if (shouldSkip(ms)) return invocation.proceed();
String sql = (String) meta.getValue("delegate.boundSql.sql");
String tenantId = TenantContext.getCurrentTenantId().toString();
// 智能插入:只在主表WHERE后追加 AND tenant_id = ?
String newSql = SqlInjector.injectTenant(sql, tenantId);
meta.setValue("delegate.boundSql.sql", newSql);
return invocation.proceed();
}
}坑点:对于带别名的表,如 SELECT * FROM org_user u WHERE u.status=1,注入时要识别别名。我们规定所有表必带别名,注入逻辑改为:
-- 原始
SELECT * FROM org_user u WHERE u.status = 1
-- 注入后
SELECT * FROM org_user u WHERE u.status = 1 AND u.tenant_id = '1001'实现方法:正则匹配 FROM\s+(\w+)\s+(\w+) 获取主表别名,然后追加 AND 别名.tenant_id = ?。
再增加一个拦截器,根据 @DataScope 注解追加 dept_id IN (...) 条件,优先级在租户之后。两者组合实现“租户→部门→个人”三级过滤,零业务侵入。
原查询关联 ACT_RU_TASK、ACT_RU_EXECUTION、ACT_RU_IDENTITYLINK、业务表单表,数据量达80万行,执行计划显示全表扫描 + Using temporary。
第一步:冗余字段 + 复合索引
在 ACT_RU_TASK 增加 ASSIGNEE_USER_ID_(bigint)和 TENANT_ID_,建立复合索引:
CREATE INDEX idx_tenant_assignee_time ON ACT_RU_TASK(TENANT_ID_, ASSIGNEE_USER_ID_, CREATE_TIME_);单用户待办查询直接命中索引,但仍有大量回表。
第二步:宽表 + 异步更新
构建 todo_task_summary 宽表,包含所有展示字段。流程完成、转办、驳回时,通过MQ(腾讯云CKafka)异步更新宽表。
查询变成单表分页:
@Cacheable(value = "todo", key = "#userId + '_' + #page")
public PageResult<TodoVO> query(Long userId, int page) {
return todoSummaryMapper.selectPage(
new Page<>(page, 20),
new LambdaQueryWrapper<TodoVO>()
.eq(TodoVO::getAssigneeUserId, userId)
.orderByDesc(TodoVO::getCreateTime)
);
}第三步:Redis缓存 + 主动失效
采用“永久缓存 + 定时刷新”策略,避免雪崩。当任务状态变更时,发送广播消息清除相关用户缓存。
@Component
public class TodoCacheManager {
@CacheEvict(value = "todo", key = "#userId")
public void evictUser(Long userId) { }
}场景 | 优化前 RT (ms) | 优化后 RT (ms) | DB CPU |
|---|---|---|---|
单用户待办 | 4850 | 48 | 85%→12% |
100并发混合 | 超时 (12s) | 102 | - |
我们将服务容器化,使用 TKE(腾讯云容器服务)部署,结合 CLS 日志服务、Prometheus监控。
FROM openjdk:11-jre-slim
COPY target/oa-flow.jar /app/app.jar
ENTRYPOINT ["java", "-Xmx1g", "-XX:+UseG1GC", "-jar", "/app/app.jar"]所有环境配置(数据库、MQ、Redis)通过 Apollo 管理,不同租户可动态调整。
StaleObjectException 不可避免,用 @Retryable 配合指数退避,成功率提升至 99.9%。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。