按 TDD 拆分前后端任务,覆盖交集预选、batch-shares 事务与页面接入。 Co-authored-by: Cursor <cursoragent@cursor.com>
12 KiB
文档库多选批量设置分享 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 让 Web 文档库 /#/doclib/enter 支持多选文档后一次设置分享人员(整份覆盖、交集预选、整批事务)。
Architecture: 后端新增 POST /web/doclib/files/batch-shares,从现有 set_shares 抽出无 commit 的 _apply_shares,批量一次 commit;前端放宽 actionsFor.share、扩展 ShareDialog 为多文档并用 shareUserIntersection 预选,统一调用批量 API。
Tech Stack: FastAPI / SQLAlchemy / Pydantic v2;Vue 3 / TypeScript / Element Plus;node:test + tsx(web-admin)。
Spec: docs/superpowers/specs/2026-11-12-doclib-batch-share-design.md(工单 #25)
File map
| File | Responsibility |
|---|---|
backend/app/schemas/doclib.py |
新增 BatchShareBody |
backend/app/services/doclib_service.py |
_apply_shares、set_shares 复用、set_shares_batch |
backend/app/api/doclib.py |
注册 POST .../batch-shares |
web-admin/src/api/doclib.ts |
saveDoclibBatchShares |
web-admin/src/views/doclib/enter/model.ts |
actionsFor.share 多选 + shareUserIntersection |
web-admin/src/views/doclib/enter/model.test.ts |
前端纯函数回归 |
web-admin/src/views/doclib/enter/api.test.ts |
批量 API 适配器断言 |
web-admin/src/views/doclib/enter/ShareDialog.vue |
多文档摘要、交集预选、批量保存 |
web-admin/src/views/doclib/enter/index.vue |
工具栏传入全部 selected |
说明:仓库内暂无 backend doclib pytest;本计划以服务层行为对照现有 set_shares/delete_files 实现,并用前端单测锁定契约。若执行时已有可用测试 DB,可追加服务层用例,但不阻塞前端交付。
Task 1: 前端纯函数 — 多选可分享 + 分享人交集
Files:
-
Modify:
web-admin/src/views/doclib/enter/model.ts -
Modify:
web-admin/src/views/doclib/enter/model.test.ts -
Step 1: 写失败测试(更新 actionsFor + 新增交集)
在 model.test.ts 将首个测试改为多选可分享语义,并新增交集测试:
test('可管理文档可多选分享,混合选择不能分享', () => {
assert.deepEqual(actionsFor([{ canManage: false }]), {
share: false,
download: true,
delete: false
})
assert.deepEqual(actionsFor([{ canManage: true }, { canManage: false }]), {
share: false,
download: true,
delete: false
})
assert.deepEqual(actionsFor([{ canManage: true }]), {
share: true,
download: true,
delete: true
})
assert.deepEqual(actionsFor([{ canManage: true }, { canManage: true }]), {
share: true,
download: true,
delete: true
})
assert.deepEqual(actionsFor([]), { share: false, download: false, delete: false })
})
test('分享人初始勾选取所选文档交集', async () => {
const { shareUserIntersection } = await import('./model')
const docs = [
{ shares: [{ id: 'a' }, { id: 'b' }] },
{ shares: [{ id: 'b' }, { id: 'c' }] }
]
assert.deepEqual(shareUserIntersection(docs), ['b'])
assert.deepEqual(shareUserIntersection([{ shares: [{ id: 'a' }] }, { shares: [] }]), [])
assert.deepEqual(shareUserIntersection([]), [])
})
- Step 2: 跑测试确认失败
Run(在 web-admin/,使用本仓 node_modules):
./node_modules/.bin/tsx --test src/views/doclib/enter/model.test.ts
Expected: FAIL(share 仍要求 length === 1,且无 shareUserIntersection)
- Step 3: 最小实现
model.ts:
export function actionsFor(rows: Array<{ canManage: boolean }>) {
return {
share: rows.length > 0 && rows.every((row) => row.canManage),
download: rows.length > 0,
delete: rows.length > 0 && rows.every((row) => row.canManage)
}
}
export function shareUserIntersection(
documents: Array<{ shares?: Array<{ id: string }> | null }>
): string[] {
if (!documents.length) return []
let ids = new Set(
(documents[0].shares || []).map((person) => String(person.id || '').trim()).filter(Boolean)
)
for (const doc of documents.slice(1)) {
const next = new Set(
(doc.shares || []).map((person) => String(person.id || '').trim()).filter(Boolean)
)
ids = new Set([...ids].filter((id) => next.has(id)))
}
return [...ids]
}
- Step 4: 跑测试确认通过
./node_modules/.bin/tsx --test src/views/doclib/enter/model.test.ts
Expected: PASS
- Step 5: Commit
git add web-admin/src/views/doclib/enter/model.ts web-admin/src/views/doclib/enter/model.test.ts
git commit -m "feat(doclib): 允许多选可管理文档分享并计算分享人交集。"
Task 2: 前端 API — batch-shares 适配器
Files:
-
Modify:
web-admin/src/api/doclib.ts -
Modify:
web-admin/src/views/doclib/enter/api.test.ts -
Step 1: 写失败测试
在 api.test.ts 追加:
test('批量分享提交文档 id 列表与人员 id', async () => {
const { api, calls } = apiAdapter()
await api.saveDoclibBatchShares(['id/a', 'id/b'], ['u1', 'u2'])
assert.equal(calls[0].method, 'post')
assert.equal(calls[0].config.url, '/web/doclib/files/batch-shares')
assert.deepEqual(calls[0].config.data, { ids: ['id/a', 'id/b'], userIds: ['u1', 'u2'] })
})
- Step 2: 跑测试确认失败
./node_modules/.bin/tsx --test src/views/doclib/enter/api.test.ts
Expected: FAIL(无 saveDoclibBatchShares)
- Step 3: 最小实现
在 doclib.ts 的 saveDoclibShares 旁增加:
export const saveDoclibBatchShares = (ids: string[], userIds: string[]) =>
request.post<DoclibDocument[]>({
url: `${base}/files/batch-shares`,
data: { ids, userIds }
})
- Step 4: 跑测试确认通过
./node_modules/.bin/tsx --test src/views/doclib/enter/api.test.ts
Expected: PASS
- Step 5: Commit
git add web-admin/src/api/doclib.ts web-admin/src/views/doclib/enter/api.test.ts
git commit -m "feat(doclib): 增加批量设置分享 API 客户端。"
Task 3: 后端 — BatchShareBody + set_shares_batch
Files:
-
Modify:
backend/app/schemas/doclib.py -
Modify:
backend/app/services/doclib_service.py -
Modify:
backend/app/api/doclib.py -
Step 1: 增加 schema
在 ShareBody 后增加:
class BatchShareBody(_Body):
ids: List[str] = Field(min_length=1, max_length=100)
userIds: List[str] = Field(max_length=500)
- Step 2: 重构服务层
将 set_shares 拆为 _apply_shares(不 commit)与包装方法;新增 set_shares_batch:
def _apply_shares(self, file_id, user_ids):
row = self._file(file_id, manage=True, lock=True)
requested = set(user_ids) - {row.create_by}
if requested:
valid = {u.id for u in self._base(SysUser).filter(
SysUser.id.in_(requested), SysUser.status == '1').all()}
if requested != valid:
raise HTTPException(400, '分享人员不存在、已停用或不属于当前租户')
else:
valid = set()
history = {s.user_id: s for s in self.db.query(DoclibShare).filter(
DoclibShare.tenant_no == self.tenant, DoclibShare.file_id == file_id
).populate_existing().with_for_update().all()}
for uid, share in history.items():
share.del_flag = 0 if uid in requested else 1
share.update_by = self.uid
for uid in requested - set(history):
self.db.add(DoclibShare(file_id=file_id, user_id=uid, **self._public()))
row.update_by, row.update_time = self.uid, datetime.now()
return row
def set_shares(self, file_id, user_ids):
row = self._apply_shares(file_id, user_ids)
self.db.commit()
return self._serialize_files([row])[0]
def set_shares_batch(self, ids, user_ids):
file_ids = sorted({str(i).strip() for i in ids if str(i).strip()})
if not file_ids:
raise HTTPException(400, '请选择要设置分享的文档')
rows = [self._apply_shares(fid, user_ids) for fid in file_ids]
self.db.commit()
return self._serialize_files(rows)
注意:空 requested 时跳过用户表查询(避免 IN ()),与「清空分享」语义一致。原 set_shares 在 requested 为空时 SysUser.id.in_(set()) 行为依赖方言,重构时一并修掉。
- Step 3: 注册路由
doclib.py 增加 import BatchShareBody,并在 batch-delete 旁:
@router.post('/web/doclib/files/batch-shares')
def batch_share_documents(body: BatchShareBody, svc: DoclibService = Depends(service)):
return success_response(svc.set_shares_batch(body.ids, body.userIds))
- Step 4: 静态核对
确认:
- 单文档
PUT .../shares仍指向set_shares - 批量路径仅一次
commit _apply_shares任一步抛错时不调用commit
可选:在已激活 venv 下对改动文件做语法检查
cd backend && source venv/bin/activate && python -c "from app.schemas.doclib import BatchShareBody; from app.services.doclib_service import DoclibService; print('ok')"
- Step 5: Commit
git add backend/app/schemas/doclib.py backend/app/services/doclib_service.py backend/app/api/doclib.py
git commit -m "feat(doclib): 增加批量设置分享接口并复用单文档写入逻辑。"
Task 4: ShareDialog + 列表页接入
Files:
-
Modify:
web-admin/src/views/doclib/enter/ShareDialog.vue -
Modify:
web-admin/src/views/doclib/enter/index.vue -
Step 1: 改 ShareDialog props 与保存
-
Props:
documents: DoclibDocument[](由页面传入;可去掉单document或保留 computed 取documents[0]兼容内部逻辑)。 -
打开时:
userIds = shareUserIntersection(documents);摘要展示名称列表(超过 5 个截断并显示「等 N 个」)。 -
options排除所有documents的ownerId。 -
save:若无文档或存在!canManage则 return;调用saveDoclibBatchShares(ids, userIds);emit('saved', updated)(updated为数组时由父组件刷新)。
示意:
const props = defineProps<{ modelValue: boolean; documents: DoclibDocument[] }>()
// watch open:
userIds.value = shareUserIntersection(props.documents)
// save:
const ids = props.documents.map((d) => d.id)
const updated = await saveDoclibBatchShares(ids, userIds.value)
emit('saved', updated)
-
Step 2: 改 index.vue
-
shareDocuments替代shareDocument(ref<DoclibDocument[]>)。 -
openShare(rows: DoclibDocument | DoclibDocument[])规范化为数组。 -
工具栏:
@click="openShare(selected)"。 -
详情:
@click="openShare(detail)"。 -
<ShareDialog v-model="shareVisible" :documents="shareDocuments" @saved="shareSaved" /> -
shareSaved:若返回数组则按 id 更新详情;始终refresh()。 -
Step 3: 跑前端相关测试
cd web-admin
./node_modules/.bin/tsx --test src/views/doclib/enter/model.test.ts src/views/doclib/enter/api.test.ts
Expected: PASS
- Step 4: Commit
git add web-admin/src/views/doclib/enter/ShareDialog.vue web-admin/src/views/doclib/enter/index.vue
git commit -m "feat(doclib): 文档库支持多选文档后批量设置分享。"
Task 5: 回归核对与交付准备
- Step 1: 再跑前端测试套件
cd web-admin && ./node_modules/.bin/tsx --test src/views/doclib/enter/model.test.ts src/views/doclib/enter/api.test.ts
- Step 2: 对照验收清单(手工或浏览器)
- 多选可管理 → 可开分享;混入不可管理 → 按钮禁用
- 初始勾选为交集
- 保存后各文档分享人一致
- 详情单文档分享仍可用
-
Step 3: 更新工单 #25 评论:分支、提交、测试命令与结果、未跑项(真机)
-
Step 4: 按用户授权 push 任务分支(
push_task_branch已开;create_pr未开则不建 PR)
Spec coverage self-check
| Spec 要求 | Task |
|---|---|
actionsFor 多选可管理可分享 |
Task 1 |
| 交集预选 | Task 1 + 4 |
batch-shares API + 事务 |
Task 3 |
_apply_shares 复用单路径 |
Task 3 |
| 前端批量客户端 | Task 2 |
| ShareDialog / 工具栏 | Task 4 |
| 单测 | Task 1–2、5 |
| 整份覆盖 / 空列表收回 | Task 3–4(沿用 _apply_shares) |
| 非目标:小程序 / 只追加 / 部分成功 | 无任务(刻意不做) |
Placeholder scan
无 TBD /「类似 Task N」/ 空测试步骤。