Skip to content

[Bug] Web 全局编辑弹窗对共享账本 Editor 列出个人标签,且只提交标签名不提交 tag_ids #70

Description

@TNT-Likely

问题描述

Web 端「全局编辑弹窗」(frontend/apps/web/src/components/GlobalEditDialogs.tsx) 没有共享账本分支:Editor 编辑共享账本交易时,标签候选列出的是 Editor 自己的个人标签,而不是 Owner 的共享标签。交易页主路径 (TransactionsPage.tsx) 是共享感知的,两条路径行为不一致。

叠加「该弹窗只提交标签名、不提交 tag_ids」,会复现 TNT-Likely/BeeCount#435 的重复标签链。

这是 #435 重复标签的主要产生路径,详见下面「为什么这条是承重的」。

三个具体点

1. 候选集是调用者自己的标签

弹窗通过 fetchWorkspaceTags(token, { ledgerId, limit: 500 }) 取候选。但服务端 src/routers/read/workspace.py::list_workspace_tags 的标签集是:

tag_query = select(UserTagProjection).where(
    UserTagProjection.user_id == target_user_id   # 非 admin 时 = current_user.id
)

ledger_id 只参与统计聚合,不影响返回哪些标签。所以共享账本下 Editor 拿到的是自己的个人标签。

2. 该弹窗完全不发 tag_ids

GlobalEditDialogs.tsx 的 payload 只有:

tags: editTxForm.tags.filter((s) => s.length > 0),

全文件 grep 无 tag_ids。对比 TransactionsPage.tsx 会先按名字反查候选集再发 tag_ids

const txTagIds = txForm.tags
  .map((value) => tagByName.get(value.trim().toLowerCase()))
  .filter((value): value is string => Boolean(value))

3. Editor 确实能用到这个弹窗

GlobalEditDialogs.tsxwritableLedgers 的过滤条件是 (l) => l.role === 'owner' || l.role === 'editor'

对比:主路径已经做对了

TransactionsPage.tsx

const txWriteTags =
  txIsSharedEditor && sharedBundle ? sharedAsRead.tags : txDictionaryTags

共享账本 Editor 走 shared bundle 的 Owner 标签。GlobalEditDialogs 缺这个分支。

为什么这条是承重的

App 侧 _syncTransactionTags 的建标签行为是分支决定的:

  • tagIds 非空 → name fallback 只复用已有本地标签,匹配不到就丢弃关联,从不创建
  • tagIds 完全缺失(name-only)→ 唯一的建标签点,按名字查不到就用新 UUID 建一条

而 App 的 push 只要有标签就一定带 tagIds(App 建的标签都有 syncId)。所以:

重复标签只会由 name-only payload 触发,而 name-only payload 只可能来自 Web / 服务端侧,不会来自 App。

产生 name-only payload 的路径:

来源 状态
Web GlobalEditDialogs 本 issue
Web AI 批量 src/routers/write/transactions_batch.py 只收 tags: list[str],且服务端会 snapshot_mutator.create_tag 自行建实体
CSV 导入 同类,未按 Owner 标签解析
App push ✅ 不产生

因此 App 侧屏蔽创建入口(TNT-Likely/BeeCount#436)解决不了这条 —— 那个 PR 只改 App 的标签选择器,而 Editor 即使不能在 App 里新建标签,仍然持有通过「标签管理页」创建的个人标签(方案1 明确保留这个能力),照样能在这个 Web 弹窗里挂到共享交易上。

影响链

Editor 用全局弹窗给一笔共享交易挂上自己的个人标签后:

  1. payload 是 name-only(有 tags、无 tag_ids
  2. Owner 的 App 拉到这笔交易,按名字在本地 tags 表查不到(那是 Editor 的标签)
  3. 走唯一的建标签分支,用新 syncId 创建一条本地标签
  4. 该标签作为 Owner 的 user-global 标签上传
  5. 服务端出现两条同名、不同 syncId 的标签,分属 Editor 和 Owner

即 #435 的第 3~5 步。不依赖历史脏数据,是任何 Editor 今天就能触发的操作。

建议

  1. GlobalEditDialogs 补共享账本分支,跟 TransactionsPage 对齐:txIsSharedEditor && sharedBundle 时用 sharedAsRead.tags
  2. 同时补发 tag_ids(按名字反查候选集),别只发名字 —— 让服务端拿到权威 ID,不依赖名字匹配。
  3. 可选:/workspace/tags 在传了 ledger_id 且该账本为共享账本时,是否应返回 Owner 的标签集而非调用者的。这样能从源头消除此类不一致,但影响面更大,需评估其它调用方。

AI 批量与 CSV 导入那两条路径会让服务端「间接为 Owner 建标签」,严重性低于本 issue(只建一次、在 Owner 名下,不产生重复 syncId),可另行评估是否处理。

优先级

高。这是 #435 在修掉 App 侧入口之后仍然存在的产生路径,且改动很小(一个分支 + 补发 tag_ids)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions