design.md 3.1 KB

架构要点

租户识别

Request → TenantInterceptor (读 X-Tenant-Id) → TenantContext.set(tenantId) → Controller → Service → Client
                                                                                        ↓
                                                                       Client 从 TenantContext 拿 appKey/appSecret

TenantContextThreadLocal<TenantProfile>TenantProfile 包含:

  • tenantId(宜搭应用表主键)
  • vendorCredentials: Map<String, VendorCredential>(钉钉/aliwork 等)
  • extraJson(tenant 级自定义配置)

租户配置加载

数据源:宜搭"应用表"(formUuid 配置)。字段约定:

宜搭字段 含义
textField_tenantId 租户唯一标识(建议用客户代号如 guangming
textField_vendor 第三方平台 key(dingtalk / aliwork / ...)
textField_appKey 该 vendor 的 appKey
textField_appSecret 该 vendor 的 appSecret(写入时走宜搭加密字段)
textField_corpId 可选(钉钉需要)
textareaField_extraJson JSON 字符串,扩展参数
radioField_enabled on / off

加载策略

  • 启动时全量拉取 → 内存 Map<tenantId, TenantProfile>
  • TTL 10 分钟(可配 tenant.registry.ttlSeconds)过期后按需刷新
  • 手动 /pro/_admin/reloadTenant?tenantId=xxx(仅 dev profile 开放)做热刷

Client 透传机制

基座 DDClient 签名不变(仍接受 appKey/appSecret 参数),mjava-pro 提供一层 DynamicDDService 组合:

// Service 层内部
DDConf conf = TenantContext.current().credential("dingtalk").toDDConf();
ddClient.listDepartmentUserDetail(conf, deptId, cursor, size);

不做 AOP 反射修改签名 — 保持显式、可读、可调试。

Token 缓存隔离

UtilToken 当前 key 是裸 {vendor}:{appKey}。多租户环境下两个客户可能共用同一 appKey(极少,但要防撞)。建议基座支持 namespace:

UtilToken.put(tenantId, "dingtalk:" + appKey, token, expireSec);
UtilToken.get(tenantId, "dingtalk:" + appKey);

若基座改动太大,回退方案mjava-pro 约束 key 为 {tenantId}:{vendor}:{appKey},不改基座。

错误处理

  • tenantId 缺失 → 401 TENANT_REQUIRED
  • tenantId 不存在或 disabled → 403 TENANT_NOT_FOUND
  • vendor 配置缺失 → 500 TENANT_VENDOR_MISCONFIGURED + 审计日志

技术约束

  • 沿用 mjava-baseline.md 全部规范
  • application-prod.yml 只配宜搭应用表 formUuid / systemToken(访问宜搭自身的凭据仍是静态),不配任何客户凭据
  • 请求监听日志必须含 tenantId 字段(在 §3.5 审计字段基础上扩展)

Non-Goals(设计边界)

  • 不考虑租户级数据库隔离(所有租户共享同一 MySQL)
  • 不考虑租户级流量限额(QPS 限流等;有需求再单独提案)
  • 不考虑跨租户工作流(暂时假设每次请求单租户上下文)
  • 不考虑 Redis 分布式缓存(单实例 TimedCache 足够 Phase 1)