Skip to content

feat(mybatis): fill audit fields and scope statements by tenant from the operator context - #69

Merged
poppycoderr merged 5 commits into
mainfrom
feat/operator-context-tenant-audit
Oct 2, 2026
Merged

poppycoderr merged 5 commits into
mainfrom
feat/operator-context-tenant-audit

Conversation

@poppycoderr

@poppycoderr poppycoderr commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Background

Audit fields and tenant isolation are needed by most business systems and are usually wired by hand: a thread-local somewhere, a MetaObjectHandler that reads it, a tenant plugin that reads something else. The roadmap asks for these MyBatis-Plus features to be driven by one operator context. The risk worth designing around is multi-tenancy: a statement that runs without a tenant condition reads every tenant's data, and nothing fails.

Changes

  • ddk-core: Operator (id, name, tenant) and OperatorContext. runAs / callAs restore the previous operator when they finish, so a pooled thread does not carry one request's operator into the next; wrap carries the operator to another thread.
  • Web starter: an application OperatorResolver bean turns a request into an Operator. When it is present, a filter sets the context for the request and clears it afterwards. DDK does not assume how authentication works.
  • MyBatis starter:
    • the default MetaObjectHandler also fills createBy and updateBy (String or Long) from the operator; without an operator they stay empty;
    • ddk.mybatis.tenant.enabled=true adds MyBatis-Plus's TenantLineInnerInterceptor, first in the chain, with the tenant taken from the operator.
flowchart LR
    R[HTTP request] --> F[OperatorContextFilter<br/>OperatorResolver → Operator]
    J[Job / listener] -->|OperatorContext.runAs| C
    F --> C[(OperatorContext)]
    C --> A[AuditMetaObjectHandler<br/>createBy / updateBy]
    C --> T[TenantLineInnerInterceptor<br/>WHERE tenant_id = ?]
    A --> SQL[SQL]
    T --> SQL
Loading

Multi-tenancy fails closed:

statement on a tenant table
  tenant = OperatorContext.current().tenantId
  if no operator, or the operator has no tenant
    throw IllegalStateException          # not "run without the condition"
  append tenant_id = <tenant>            # select / update / delete; inserts set the column
exemptions, both explicit:
  ddk.mybatis.tenant.ignore-tables       # tables that are not tenant-scoped
  @InterceptorIgnore(tenantLine = "true") on a mapper method

Not included: data permissions, which depend on each application's organization model. Soft delete is left to MyBatis-Plus's own @TableLogic; DDK adds nothing on top of it, and this PR does not test the combination with the generic repository.


背景

审计字段和租户隔离是大多数业务系统都需要的能力,通常靠手工拼接:某处放一个 ThreadLocal,一个 MetaObjectHandler 去读它,租户插件又读另一个来源。路线图要求这些 MyBatis-Plus 能力由同一个操作者上下文驱动。设计时最需要防的是多租户的风险:一条没带租户条件的语句会读到所有租户的数据,而且不会有任何报错。

改动

  • ddk-core:新增 Operator(标识、名称、租户)与 OperatorContext。runAs / callAs 结束时恢复之前的操作者,线程池里的线程不会把上一个请求的操作者带给下一个请求;wrap 用来把操作者带到另一个线程。
  • Web starter:应用声明一个 OperatorResolver Bean,把请求解析成 Operator。有这个 Bean 时,过滤器在请求期间设置上下文、结束时清除。DDK 不假设认证方式。
  • MyBatis starter:
    • 默认的 MetaObjectHandler 同时填充 createBy、updateBy(String 或 Long),取自操作者;没有操作者时保持为空;
    • ddk.mybatis.tenant.enabled=true 时加入 MyBatis-Plus 的 TenantLineInnerInterceptor,排在插件链最前面,租户取自操作者。

数据流见上文流程图。多租户取不到租户时拒绝执行而不是放行,两种豁免都必须显式声明,见上文伪代码。

未包含:数据权限,它取决于各个应用自己的组织模型。逻辑删除交给 MyBatis-Plus 自带的 @TableLogic,DDK 没有在它之上再做什么;它与通用仓储的配合本 PR 没有测试。

@poppycoderr
poppycoderr merged commit 643da75 into main Oct 2, 2026
4 checks passed
@poppycoderr
poppycoderr deleted the feat/operator-context-tenant-audit branch October 2, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant