Docs 用户账号合并(User Account Reconciliation)实战指南:基于 CSV 导入与 Django Admin 的邮箱账号归并方案
【免费下载链接】docsDocs is an open-source text editor: web-native, made for real-time collaboration, cleanly structured documents and sub-documents with full ownership of your data. Built to scale with Django and React.项目地址: https://gitcode.com/GitHub_Trending/docs150/docs
本文聚焦开源协作文档平台 Docs(Django + React 构建)中的用户账号归并能力。Docs 本身不提供自助合并入口,而是通过从外部表单(如 Grist)导出的 CSV 文件,在 Django Admin 后台批量导入合并请求,配合邮件验证与后台批量执行,将多个邮箱账号合并为单一活跃账号。读完本文,你将掌握 CSV 的字段格式与校验规则、导入与处理的全流程、底层合并逻辑(文档权限、收藏、评论等数据迁移),以及USER_RECONCILIATION_FORM_URL等关键环境变量的配置方法。
一、为什么需要账号合并:场景与整体思路
在 Docs 的实际运行中,同一用户常常因为注册渠道不同而拥有多个账号(例如先用企业邮箱注册,后又用个人邮箱登录)。这会导致:
- 文档分散在多个账号下,无法统一管理;
- 文档访问权限、收藏、评论等数据被割裂在不同身份上;
- 同一团队成员的“活跃身份”不明确。
Docs 的解决思路是以邮箱为合并键,执行单向数据迁移:将“非活跃邮箱”账号下的数据并入“活跃邮箱”账号,然后停用非活跃账号,保留活跃账号继续使用。整体流程由四段组成:
- 外部表单收集请求:用户通过第三方表单(如 Grist)提交自己的活跃邮箱与待合并邮箱;
- CSV 导入:管理员将表单导出的 CSV 上传到 Django Admin;
- 邮件验证:系统向两个邮箱发送确认邮件,证明请求人确实控制这些邮箱(也可由表单侧预先勾选验证字段跳过);
- 后台批量执行:管理员在 Admin 中选择待处理条目执行合并,数据完成迁移后停用非活跃账号。
该功能的后端实现分布在 models.py(数据模型与合并逻辑)、tasks/user_reconciliation.py(CSV 解析任务)与 admin.py(管理后台入口)中,下文逐一展开。
二、CSV 文件格式:必填列与可选列
CSV 是导入的唯一数据载体,其格式由 tasks/user_reconciliation.py 中的解析逻辑严格约束。
2.1 必填列
| 列名 | 含义 | 说明 |
|---|---|---|
active_email | 合并后保留的活跃邮箱 | 对应模型字段active_email(models.py),必须是一个真实存在且匹配到用户的邮箱 |
inactive_email | 将被合并进活跃账号的邮箱 | 对应模型字段inactive_email(models.py)。支持一行的 inactive_email 以竖线|分隔多个邮箱,这样用户即使拥有两个以上账号,也只需提交一次请求(见_process_row中row["inactive_email"].split("|")的实现) |
id | 该行数据的唯一标识 | 对应模型字段source_unique_id(models.py),用于幂等去重:已处理过的id再次导入会被跳过,避免重复生成合并请求 |
其中id的去重逻辑非常关键:导入任务在创建条目前会先查询UserReconciliation.objects.filter(source_unique_id=source_unique_id).exists(),若已存在则直接跳过并计入already_processed_source_ids计数(tasks/user_reconciliation.py)。
2.2 可选列
| 列名 | 取值 | 默认值 | 说明 |
|---|---|---|---|
active_email_checked | 0或1 | 0(False) | 置1表示外部表单已验证请求人确实控制活跃邮箱,跳过发送到活跃邮箱的确认邮件 |
inactive_email_checked | 0或1 | 0(False) | 同上,针对非活跃邮箱 |
解析代码中通过row.get("active_email_checked", "0") == "1"读取(tasks/user_reconciliation.py),因此这两列即使缺失也会安全地回退为 False。对应模型字段为active_email_checked/inactive_email_checked,均为BooleanField(default=False)(models.py)。
2.3 一个可用的 CSV 示例
id,active_email,inactive_email,active_email_checked,inactive_email_checked R001,alice@example.com,bob@example.com,0,0 R002,alice@example.com,alice.old@example.com|alice.work@example.com,1,1第一行演示了最常规的双账号合并;第二行演示了|分隔的多邮箱合并,并通过两个_checked列置1跳过邮件验证环节(适用于表单侧已完成强验证的场景)。
三、CSV 导入:Django Admin 中的操作路径
3.1 入口位置
CSV 上传入口位于 Django Admin 的"Core" > "User reconciliation CSV imports" > "Add user reconciliation"。对应的模型是UserReconciliationCsvImport(models.py),其关键字段包括:
file:上传的 CSV 文件,存储在imports/目录下(FileField(upload_to="imports/"));status:任务状态,取值pending/running/done/error,默认pending;logs:处理日志,记录每行结果与错误堆栈。
3.2 上传后发生了什么:异步任务调度
当管理员在 Admin 中新建一条 CSV 导入记录时,UserReconciliationCsvImportAdmin.save_model会在事务提交后立即把解析任务投递到 Celery 队列:
if not change: transaction.on_commit( partial(user_reconciliation_csv_import_job.delay, obj.pk) ) messages.success(request, _("Import job created and queued."))(见 admin.py)。因此导入动作本身是异步的,需要确保 Docs 的 Celery Worker 处于运行状态,否则任务不会被执行。任务启动后状态先变为running,完成或失败后再落定为done或error。
3.3 导入任务的数据校验规则
Celery 任务user_reconciliation_csv_import_job(tasks/user_reconciliation.py)在逐行解析时执行以下校验:
- 必填列检查:若表头缺少
active_email、inactive_email、id三列中的任意一列,直接抛出KeyError("CSV is missing mandatory columns: active_email, inactive_email, id"),整个任务置为error; - 邮箱格式校验:
active_email无效时向inactive_email发送错误通知邮件;inactive_email无效时向active_email发送错误通知邮件(使用 Django 的validate_email); - 相同邮箱检查:若
inactive_email == active_email,报错并跳过该行,防止“自己合并自己”; - 幂等去重:
id已存在则跳过(见上文 2.1)。
关键设计是:某行出错只会被计入rows_with_errors并记录日志,不会让整个任务失败,也不会阻断后续行处理。任务完成后,logs中会汇总统计:
Import completed successfully. N rows processed. X reconciliation entries created. Y rows were already processed. Z rows had errors.四、合并请求的生命周期:状态机与邮件验证
CSV 每行成功校验后,会创建一条UserReconciliation记录(模型定义见 models.py),其状态机如下:
| 状态 | 含义 | 触发时机 |
|---|---|---|
pending | 待处理 | 条目刚由 CSV 创建时 |
ready | 可执行合并 | 两个邮箱均匹配到现有用户,且验证邮件已发送(或已由_checked跳过) |
error | 合并失败 | 活跃或非活跃邮箱未匹配到任何现有用户 |
done | 已合并完成 | 管理员执行批量合并后 |
4.1 创建条目时的自动处理
UserReconciliation.save()方法在状态为pending时执行关键逻辑(models.py):
if self.status == "pending": self.active_user = User.objects.filter(email=self.active_email).first() self.inactive_user = User.objects.filter(email=self.inactive_email).first() if self.active_user and self.inactive_user: if not self.active_email_checked: self.send_reconciliation_confirm_email( self.active_user, "active", self.active_email_confirmation_id ) if not self.inactive_email_checked: self.send_reconciliation_confirm_email( self.inactive_user, "inactive", self.inactive_email_confirmation_id ) self.status = "ready" else: self.status = "error" self.logs = "Error: Both active and inactive users need to exist."即:只要任一邮箱在系统中不存在对应用户,该条目的状态就会直接变为error,不会发送任何确认邮件。这也解释了为什么官方文档强调“出错邮件可以回链到合并表单”——出错通常意味着邮箱拼写错误或账号尚未创建。
4.2 邮件验证的 URL 结构
确认邮件的正文链接指向 Docs 前端的一个确认页,其 URL 由send_reconciliation_confirm_email拼接(models.py):
{domain}/user-reconciliations/{user_type}/{confirmation_id}/其中user_type取active或inactive,confirmation_id是模型中的active_email_confirmation_id/inactive_email_confirmation_id(均为自动生成的唯一UUIDField,见 models.py)。前端通过 useUserReconciliations.tsx 调用user-reconciliations/{type}/{reconciliationId}/接口完成确认动作。domain取自EMAIL_URL_APP设置或当前站点域名。
用户点击确认后,对应邮箱的*_checked字段会被置为True。只有当两个邮箱都被确认(或导入时已通过_checked=1跳过验证)时,该条目才具备执行合并的条件。
4.3 合并完成后的通知
合并执行完毕后,系统会向活跃用户发送一封“合并完成”邮件(send_reconciliation_done_email,见 models.py),文案提示“你的合并请求已处理完成,你的账号下可能关联了新的文档”,并提供跳转到文档首页的链接。
五、执行合并:Admin 批量操作与底层数据迁移
5.1 Admin 操作入口
合并请求全部录入后,管理员进入"Core" > "User reconciliations",勾选需要处理的行,在下拉框中选择"Process selected user reconciliations"操作并执行。
该操作由process_reconciliation后台动作实现(admin.py),其筛选条件非常严格:
processable_entries = queryset.filter( status="ready", active_email_checked=True, inactive_email_checked=True ) for entry in processable_entries: entry.process_reconciliation_request()即:只有状态为ready且两个邮箱都已验证通过的条目才会被处理;未满足条件的勾选项会被静默跳过。这也呼应了官方文档中“Only rows that have the statusreadyand for which both emails have been validated will be processed”的说明。
5.2 合并的原子性
每一条目的合并都在一个数据库事务中执行(@transaction.atomic装饰process_reconciliation_request,见 models.py),保证数据迁移要么全部成功、要么全部回滚。合并完成后条目状态置为done,日志中会记录各类数据的迁移条数。
5.3 合并到底迁移了哪些数据
process_reconciliation_request按以下顺序组织数据迁移,各prepare_*方法位于 models.py:
- 文档访问权限(DocumentAccess):将非活跃用户对文档的访问权限迁移给活跃用户;若活跃用户对同一文档已拥有权限,则取两者中更高的角色(
RoleChoices.max(entry.role, existing_role)),并删除非活跃用户的重复条目; - 文档收藏(DocumentFavorite):非活跃用户的收藏迁移给活跃用户;若活跃用户已收藏同一文档,则删除非活跃用户的重复收藏;
- 链接追踪(LinkTrace):用于“谁访问过该文档”的追踪记录同样迁移,已存在则去重删除;
- 讨论线程(Thread):非活跃用户创建的线程
creator全部改为活跃用户; - 评论(Comment):非活跃用户发表的评论
user全部改为活跃用户; - 表情反应(Reaction):为非活跃用户已反应但活跃用户未反应的帖子补充反应记录,随后删除非活跃用户的全部反应;
- 账号启停:
active_user.is_active = True,inactive_user.is_active = False(models.py),通过一次bulk_update落库。
从数据覆盖范围看,该合并机制完整迁移了 Docs 中与用户身份强相关的文档协作数据,迁移完成后非活跃账号虽然仍保留在系统中(保证历史数据外键完整),但已无法登录使用。
六、环境变量配置:出错邮件回链合并表单
当合并请求因邮箱不匹配等原因出错(状态变为error)时,系统向用户发送的错误邮件需要提供一个“重新发起请求”的入口,即第三方合并表单的 URL。这一行为通过环境变量配置:
USER_RECONCILIATION_FORM_URL=<url used in the email for reconciliation with errors to allow a new requests> # e.g. "https://yourgristinstance.tld/xxxx/UserReconciliationForm"该配置在 settings.py 中以 Django-environ 的values.Value(None, environ_name="USER_RECONCILIATION_FORM_URL", environ_prefix=None)方式读取,默认值为None。它同样出现在 env.md 的环境变量速查表中(描述为“用于用户合并请求的第三方表单 URL”),并在 env.d/production.dist/backend 与 examples/helm/impress.values.yaml 等部署配置样例中预留了位置。在 Docker Compose 或 Helm 部署时,将其指向你自己的 Grist 表单地址即可。
七、端到端流程回顾与运维建议
综合上述实现,一次完整的账号合并运维流程如下:
- 外部表单收集:在 Grist(或任意可导出 CSV 的表单工具)中建立字段为
id、active_email、inactive_email(可含|多值)、active_email_checked、inactive_email_checked的合并请求表,并让用户自助填写; - 导出并上传:将表单数据导出为 CSV,在 Django Admin 的 "Core" > "User reconciliation CSV imports" > "Add user reconciliation" 上传,确保 Celery Worker 在线;
- 等待校验:观察导入任务日志,确认条目创建数、错误数;
pending条目会自动匹配用户并发验证邮件; - 用户确认:请求人点击两封确认邮件中的链接(除非表单已预验证);
error条目对应的用户会收到附有USER_RECONCILIATION_FORM_URL的错误邮件,可重新提交; - 后台批量合并:待所有条目变为
ready且双邮箱已验证后,在 "Core" > "User reconciliations" 勾选并执行 "Process selected user reconciliations",随后核对logs中的迁移统计与done状态。
运维层面值得注意的三点:
- 幂等性保障:CSV 中的
id一旦成功创建过条目即不可重复导入,因此表单侧的id应使用稳定的唯一值(如请求编号),而不是时间戳,否则重复提交会被直接忽略; - 批量处理粒度:Admin 操作一次可勾选多行,但每一行独立在各自事务中执行,单行失败不会影响其他条目;
- 权限前置校验:合并前务必确认两个邮箱都对应真实的 Docs 用户,否则条目将停留在
error状态,这是合并失败最常见的原因。
至此,Docs 的账号合并能力已从 CSV 格式、导入任务、状态机、邮件验证到数据迁移底层实现全链路打通,可直接参照本指南在你的实例上完成部署与运维。
【免费下载链接】docsDocs is an open-source text editor: web-native, made for real-time collaboration, cleanly structured documents and sub-documents with full ownership of your data. Built to scale with Django and React.项目地址: https://gitcode.com/GitHub_Trending/docs150/docs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考