feat(gateway): 多租户2b-A —— Task 加 owner/tenant 隔离 + 系统旁路修 admin 全局口径

- store.WithoutTenant(ctx):显式跨租户旁路,插件即使 ctx 带租户也不过滤。
  修复增量2 引入的回归:admin SystemCounts 数 KB 时被 admin 自己租户误过滤。
  AdminOverview 改用旁路 ctx → 任务/KB/Doc/Eval 恢复全平台口径。
- Task 加 TenantID(插件自动填) + Owner(提交者 user.id) + isTenantScoped()。
  SaveTask 记录 owner;RecentTasks/RecentRuns 按 owner+租户过滤"我的运行"。
  RecentRuns 是 raw Table 查询绕过插件,手动补 owner+tenant WHERE。

live 验证:A 提交任务 → 行 owner/tenant 自动填对 ✓;A 的 /runs 只见己方、
B 空、legacy 无 owner 行被排除 ✓;admin/overview tasks_total=39/kb_count=21
= DB 全局真值(旁路生效,回归已修)✓。

Eval/Doc/DocLink 的 tenant 传播见 2b-B。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-07 09:18:15 +08:00
parent 30d667954b
commit ee1e9cfdca
5 changed files with 46 additions and 19 deletions
+16 -9
View File
@@ -117,11 +117,12 @@ func migrateDocLinkToID(db *gorm.DB) {
func (p *Postgres) Enabled() bool { return p.db != nil }
// SaveTask 持久化一次任务提交(best-effort:降级模式下静默跳过)。
func (p *Postgres) SaveTask(ctx context.Context, id, graph string) error {
func (p *Postgres) SaveTask(ctx context.Context, owner, id, graph string) error {
if p.db == nil {
return nil
}
return p.db.WithContext(ctx).Create(&Task{TaskID: id, Graph: graph, Status: contract.TaskSubmitted}).Error
// TenantID 由 tenant 插件按请求 ctx 自动填;Owner 显式记录提交者(供个人工作台过滤)。
return p.db.WithContext(ctx).Create(&Task{Owner: owner, TaskID: id, Graph: graph, Status: contract.TaskSubmitted}).Error
}
// UpdateTaskStatus 流转任务状态(running/done/failed/timeout),由 dispatcher 经 NATS 回写驱动。
@@ -270,12 +271,14 @@ func (p *Postgres) StatsOverview(ctx context.Context, owner string) *Overview {
}
// RecentTasks 返回最近 n 条任务(工作台「近期运行」feed)。
func (p *Postgres) RecentTasks(ctx context.Context, n int) []Task {
// RecentTasks 返回某用户最近 n 条任务(个人工作台「近期运行」feed)。
// owner 过滤"我的运行";tenant 由插件自动叠加(双保险:跨用户/跨租户都隔离)。
func (p *Postgres) RecentTasks(ctx context.Context, owner string, n int) []Task {
if p.db == nil {
return nil
}
var out []Task
p.db.WithContext(ctx).Order("created_at desc").Limit(n).Find(&out)
p.db.WithContext(ctx).Where("owner = ?", owner).Order("created_at desc").Limit(n).Find(&out)
return out
}
@@ -289,18 +292,22 @@ type RunRow struct {
EvalOverall float64 `json:"eval_overall"`
}
// RecentRuns 返回最近 n 条运行(含评测分级,供「运行历史」列表)。
func (p *Postgres) RecentRuns(ctx context.Context, n int) []RunRow {
// RecentRuns 返回某用户最近 n 条运行(含评测分级,供「运行历史」列表)。
// 注:raw Table 查询绕过 gorm 模型回调 → 租户插件不生效,故此处**手动**按 owner(+ctx 租户) 过滤。
func (p *Postgres) RecentRuns(ctx context.Context, owner string, n int) []RunRow {
if p.db == nil {
return nil
}
var out []RunRow
p.db.WithContext(ctx).Table("sundynix_task as t").
q := p.db.WithContext(ctx).Table("sundynix_task as t").
Select("t.task_id, t.status, t.detail, t.created_at as at, " +
"coalesce(e.level,'') as eval_level, coalesce(e.overall,0) as eval_overall").
Joins("left join sundynix_eval e on e.task_id = t.task_id").
Where("t.deleted_at is null").
Order("t.created_at desc").Limit(n).Scan(&out)
Where("t.deleted_at is null AND t.owner = ?", owner)
if tid := tenantFromCtx(ctx); tid != "" && !isSystemCtx(ctx) {
q = q.Where("t.tenant_id = ?", tid)
}
q.Order("t.created_at desc").Limit(n).Scan(&out)
return out
}