Open edX 用户退休(User Retirement)脚本实战:scripts/user_retirement 的部署、配置与运行指南
【免费下载链接】openedx-platformThe Open edX LMS & Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform
本文基于 openedx-platform 仓库中 scripts/user_retirement/README.rst 展开,讲解 Open edX 用户退休(User Retirement)驱动脚本的完整使用流程:如何以稀疏克隆方式获取脚本、用 uv 搭建运行环境、编写 YAML 驱动配置、执行get_learners_to_retire.py与retire_one_learner.py完成 PII 数据擦除流水线,并配合仓库内 docs 目录下的设计文档与源码,理解其状态机模型、LMS 侧 Django 设置与失败恢复机制。
一、用户退休功能与脚本目录定位
随着 GDPR 等隐私法规的演进,Open edX 平台需要提供"遗忘"用户个人身份信息(PII)的能力。学习者可以在 LMS 账户页面点击Delete My Account确认删除请求,其账户被停用并进入PENDING状态;随后由集中的driver(驱动)脚本编排对 LMS、论坛、ecommerce、credentials 等内部系统以及第三方营销服务的删除/解绑请求。这一设计目标在 docs/index.rst 与 docs/implementation_overview.rst 中有完整阐述。
README.rst 说明,scripts/user_retirement目录中的 Python 脚本是从 edX 早期的tubular仓库迁移而来的,用于驱动用户退休工作流,处理账户的停用与移除。这些脚本设计为可被任意自动化/CI 框架调用。
从源码结构看,该目录的组织方式如下:
- 顶层驱动脚本:retire_one_learner.py、get_learners_to_retire.py、replace_usernames.py、retirement_archive_and_cleanup.py、retirement_bulk_status_update.py、retirement_partner_report.py
- 共享工具库
utils/:LMS/IDA API 客户端 utils/edx_api.py、Jenkins 属性文件导出 utils/jenkins.py,以及第三方营销 API 封装(utils/thirdparty_apis/ 下的 amplitude、braze、hubspot、salesforce、segment 等) - 依赖与配置:pyproject.toml、requirements/base.txt、requirements/testing.txt、uv.lock、pytest.ini
- 测试:tests/ 目录,包含对每个顶层脚本及第三方 API 客户端的测试用例
二、退休工作流与状态机模型
在理解脚本行为之前,需要先理解 docs/implementation_overview.rst 描述的工作流模型:
- 工作流是一条由"积木式 API"组成的可配置流水线,用于"遗忘"用户 PII、阻止用户重新登录、防止用户名/邮箱被复用;
- 流水线是线性的、可重跑的(rerunnable),某一阶段失败后可以恢复继续,多个用户可以同时处于不同状态;
- LMS 是状态的权威来源:
UserRetirementStatus模型记录每个用户当前所处的状态,RetirementState模型则存储状态列表本身(而非硬编码),因为 Open edX 社区需要的状态无法预先穷举。
典型的示例状态流转路径为:
PENDING -> RETIRING_ENROLLMENTS -> ENROLLMENTS_COMPLETE -> RETIRING_FORUMS -> FORUMS_COMPLETE -> ... -> COMPLETE其中任意RETIRING_*状态在 API 调用失败(4xx/5xx)时都会进入终端状态ERRORED;PENDING也可进入ABORTED。除工具内部错误外,用户最终必然落在COMPLETE/ERRORED/ABORTED三个终端状态之一。
从 retire_one_learner.py 的源码可以印证这一模型——脚本用常量定义了这些"魔法状态":
# "Magic" states with special meaning, these are required to be in LMS START_STATE = 'PENDING' ERROR_STATE = 'ERRORED' COMPLETE_STATE = 'COMPLETE' ABORTED_STATE = 'ABORTED' END_STATES = (ERROR_STATE, ABORTED_STATE, COMPLETE_STATE)学习者视角的体验
根据 docs/index.rst,学习者点击Delete My Account按钮并输入密码确认后,其所有浏览器会话会被登出并锁定账户;系统随即发送确认邮件,学习者在此后有限时间内(由驱动脚本的--cool_off_days参数定义)可联系管理员撤销请求。此时账户被停用(deactivated)但尚未退休(retired),UserRetirementStatus表中新增一条记录并置为PENDING。
LMS 设置中的FEATURES['ENABLE_ACCOUNT_DELETION'](默认True)控制Delete My Account区块的可见性。另外,通过第三方认证(社交登录)注册的学习者必须先解除 LMS 账户与第三方账户的绑定,否则删除按钮保持禁用状态。
三、获取脚本:稀疏克隆(Sparse Checkout)
README.rst 给出了通过部分克隆(partial clone)只获取脚本目录的方法,避免下载整个 edx-platform 仓库。原文档给出的完整流程如下(repo_url替换为 Open edX 平台仓库的克隆地址):
repo_url=<Open edX 平台仓库地址> branch=master directory=scripts/user_retirement git clone --branch $branch --single-branch --depth=1 --filter=tree:0 $repo_url cd edx-platform git sparse-checkout init --cone git sparse-checkout set $directory要点说明:
--single-branch --depth=1只拉取目标分支的最新一次提交,减少下载量;--filter=tree:0是不带 tree 对象的克隆,配合sparse-checkout实现按目录过滤;git sparse-checkout set $directory之后,工作区中只会出现scripts/user_retirement下的文件。
README 同时指出,也可以选用其他工具或库来完成部分克隆,上述步骤只是演示。
四、安装依赖:uv(推荐)与传统 venv 两种方式
方式一:使用 uv
README.rst 推荐用 uv 安装依赖:
uv sync --project scripts/user_retirement --frozen--frozen表示直接使用锁定的依赖集合(对应 uv.lock),保证环境可复现。随后所有命令都加上uv run --project scripts/user_retirement前缀,在该虚拟环境中执行;也可以直接激活环境:
source scripts/user_retirement/.venv/bin/activate方式二:不使用 uv
pyproject.toml 声明了requires-python = ">=3.12",因此不借助 uv 时需要用 Python 3.12 创建虚拟环境,并从兼容性导出文件requirements/base.txt安装:
python3.12 -m venv ../venv source ../venv/bin/activate pip install -r scripts/user_retirement/requirements/base.txt从源码看,该脚本集的核心依赖(pyproject.toml)包括click(命令行解析)、pyyaml(驱动配置解析)、requests/backoff(API 调用与重试)、edx-rest-api-client(IDP API 客户端)、jenkinsapi、unicodecsv、simplejson、simple-salesforce、google-api-python-client、boto3等,与其"编排 LMS + 第三方服务"的定位一致。
五、建立入口点(Entry Points)
README.rst 中说明,先给脚本目录中的入口点脚本加执行权限并 source 它:
chmod +x scripts/user_retirement/entry_points.sh source scripts/user_retirement/entry_points.shentry_points.sh 的实际内容是一组 shell alias,把顶层脚本映射为短命令:
alias get_learners_to_retire.py='python scripts/user_retirement/get_learners_to_retire.py' alias replace_usernames.py='python scripts/user_retirement/replace_usernames.py' alias retire_one_learner.py='python scripts/user_retirement/retire_one_learner.py' alias retirement_archive_and_cleanup.py='python scripts/user_retirement/retirement_archive_and_cleanup.py' alias retirement_bulk_status_update.py='python scripts/user_retirement/retirement_bulk_status_update.py' alias retirement_partner_report.py='python scripts/user_retirement/retirement_partner_report.py'因此,retire_one_learner.py这样的"命令"实际上是走 alias 再调用python 文件路径的执行方式。当然,也可以不依赖 alias,直接用文件路径执行 Python 脚本。
六、驱动配置与两个核心驱动脚本
docs/driver_setup.rst 指出,scripts/user_retirement中包含两个用于驱动退休工作流的脚本,它们共享一个必需的--config_file参数,指向环境的驱动配置文件:
get_learners_to_retire.py:生成"已准备好立即退休"的用户列表——即处于PENDING状态且已满足--cool_off_days冷却天数的用户。输出格式为 Jenkins 消费而设计,为每个用户生成一个下游构建。retire_one_learner.py:退休--username参数指定的用户。
6.1 驱动配置文件(YAML)
该配置是一个 YAML 文件,包含 LMS 认证凭据(auth secrets)、API URL 映射和该环境特有的退休流水线阶段。docs/driver_setup.rst 给出的示例:
client_id: <client ID for the retirement service user> client_secret: <client secret for the retirement service user> base_urls: lms: https://courses.example.com/ ecommerce: https://ecommerce.example.com/ credentials: https://credentials.example.com/ retirement_pipeline: - ['RETIRING_EMAIL_LISTS', 'EMAIL_LISTS_COMPLETE', 'LMS', 'retirement_retire_mailings'] - ['RETIRING_ENROLLMENTS', 'ENROLLMENTS_COMPLETE', 'LMS', 'retirement_unenroll'] - ['RETIRING_LMS_MISC', 'LMS_MISC_COMPLETE', 'LMS', 'retirement_lms_retire_misc'] - ['RETIRING_LMS', 'LMS_COMPLETE', 'LMS', 'retirement_lms_retire']各字段含义:
client_id/client_secret:OAuth 凭据,直接复制自create_dot_application管理命令的输出(见下文"退休服务用户"一节);base_urls:IDA 名称到基础 URL 的映射,脚本据此拼接 API 地址。LMS 是必需的;若流水线中还有对其它服务的 API 调用(ecommerce、credentials 等),这些服务也必须出现在base_urls中;retirement_pipeline:定义每个环境的执行步骤、状态名与顺序。每一项是一个四元素列表:- 起始状态名(start state)
- 结束状态名(end state)
- 要调用的服务 key(如
LMS、LICENSE_MANAGER) - 在 utils/edx_api.py 中调用的方法名
例如['RETIRING_ENROLLMENTS', 'ENROLLMENTS_COMPLETE', 'LMS', 'retirement_unenroll']的含义是:先把用户状态置为RETIRING_ENROLLMENTS,调用已实例化的LmsApi.retirement_unenroll方法,成功后把状态置为ENROLLMENTS_COMPLETE。
从源码看,这一解释与 retire_one_learner.py 的主循环完全吻合:
for start_state, end_state, service, method in config['retirement_pipeline']: # Skip anything that has already been done if config['all_states'].index(start_state) < learner_state_index: LOG('State {} completed in previous run, skipping'.format(start_state)) continue ... config['LMS'].update_learner_retirement_state(username, start_state, 'Starting: {}'.format(start_state)) # This does the actual API call response = getattr(config[service], method)(learner) ... config['LMS'].update_learner_retirement_state(username, end_state, 'Ending: {} with response:\n{}'.format(end_state, response))这里能看到"可重跑"设计的关键实现:每次执行都先向 LMS 查询用户当前状态并映射到流水线中的索引,凡是索引早于当前状态的阶段直接跳过;每个阶段开始/结束时都会调用update_learner_retirement_state回写 LMS,把响应内容写入状态记录。若中途抛异常,脚本会把用户置为ERRORED并带上异常信息(见下文"从 ERRORED 恢复")。
retire_one_learner.py 顶部还定义了各失败场景的返回码,便于 CI 框架区分失败原因:
ERR_SETUP_FAILED = -1 ERR_USER_AT_END_STATE = -2 ERR_USER_IN_WORKING_STATE = -3 ERR_WHILE_RETIRING = -4 ERR_BAD_LEARNER = -5 ERR_UNKNOWN_STATE = -6 ERR_BAD_CONFIG = -7其中ERR_USER_AT_END_STATE(用户已在终端状态)与ERR_USER_IN_WORKING_STATE(用户正处于RETIRING_*工作态,可能有另一进程在处理)都来自 retire_one_learner.py 的状态校验逻辑——它要求学习者不在END_STATES中、也不在config['working_states'](即流水线各项的第一个状态)中,否则会直接退出,防止并发重复执行同一用户。
6.2 获取待退休学习者列表
docs/driver_setup.rst 给出的示例:
mkdir learners_to_retire get_learners_to_retire.py \ --config_file=path/to/config.yml \ --output_dir=learners_to_retire \ --cool_off_days=5从 get_learners_to_retire.py 源码可以补全参数与默认值的完整说明(脚本用click解析参数,且支持RETIREMENT_前缀的环境变量传参):
| 参数 | 默认值 | 说明 |
|---|---|---|
--config_file | 无(必填) | YAML 配置文件路径 |
--cool_off_days | 7 | 学习者在退休队列(PENDING)中需停留的天数,达到后才会被实际退休 |
--output_dir | ./jenkins_props | 输出 Jenkins 属性文件的目录 |
--user_count_error_threshold | 300 | 安全阀:若返回的用户数超过该值,直接报错退出而不退休,防止攻击者向退休队列批量注入用户 |
--max_user_batch_size | 200 | 单次最多获取 X 个用户;若低于user_count_error_threshold则只截断不报错 |
脚本的执行逻辑(get_learners_to_retire.py):读取 YAML 配置后,构造states_to_request = ['PENDING'] + end_states(即PENDING加上流水线各阶段的结束状态),调用LmsApi.learners_to_retire(...)拉取列表,按max_user_batch_size截断、按user_count_error_threshold检查,最后通过 utils/jenkins.py 的export_learner_job_properties把结果写成每用户一个的 INI 属性文件。
6.3 对单个学习者执行退休
README.rst 与 docs/driver_setup.rst 都给出了对单个用户执行退休的命令。运行完get_learners_to_retire.py后,输出目录中会有若干 INI 文件,每个文件包含一行USERNAME=<username-of-learner>;遍历这些文件,对每个用户执行:
retire_one_learner.py \ --config_file=path/to/config.yml \ --username=<username-of-learner-to-retire>也可以直接用文件路径执行 Python 脚本:
python scripts/user_retirement/retire_one_learner.py \ --config_file=src/config.yml \ --username=user1记得把src/config.yml替换为你的实际配置文件路径、user1替换为实际用户名。retire_one_learner.py 文件头部的 docstring 中还给出了本地环境(devstack 风格的端口)的配置示例,其中流水线示例使用了LICENSE_MANAGER服务:
client_id: <client id from LMS DOT> client_secret: <client secret from LMS DOT> base_urls: lms: http://localhost:18000/ ecommerce: http://localhost:18130/ credentials: http://localhost:18150/ retirement_pipeline: - ['RETIRING_LICENSE_MANAGER', 'LICENSE_MANAGER_COMPLETE', 'LICENSE_MANAGER', 'retire_learner'] - ['RETIRING_FORUMS', 'FORUMS_COMPLETE', 'LMS', 'retirement_retire_forum'] - ['RETIRING_EMAIL_LISTS', 'EMAIL_LISTS_COMPLETE', 'LMS', 'retirement_retire_mailings'] - ['RETIRING_ENROLLMENTS', 'ENROLLMENTS_COMPLETE', 'LMS', 'retirement_unenroll'] - ['RETIRING_LMS', 'LMS_COMPLETE', 'LMS', 'retirement_lms_retire']另外,从 entry_points.sh 与目录结构看,该目录还提供了一批配套运维脚本,用于流水线的收尾与报告:replace_usernames.py(退休后的用户名处理)、retirement_archive_and_cleanup.py(归档与清理退休队列数据)、retirement_bulk_status_update.py(批量更新状态)、retirement_partner_report.py(面向合作方/第三方的退休报告)。这些同样通过 alias 暴露为短命令,测试覆盖见 tests/test_retirement_archive_and_cleanup.py、tests/test_retirement_bulk_status_update.py 等文件。
七、LMS 侧设置:Django 设置、状态表与服务用户
docs/service_setup.rst 描述了 LMS 侧需要完成的设置,这是驱动脚本能够工作的另一半前提。
7.1 控制用户退休行为的 Django 设置
| 设置名 | 默认值 | 说明 |
|---|---|---|
RETIRED_USERNAME_PREFIX | 'retired__user_' | 哈希用户名所用的前缀,供RETIRED_USERNAME_FMT使用 |
RETIRED_EMAIL_PREFIX | 'retired__user_' | 哈希邮箱所用的前缀,供RETIRED_EMAIL_FMT使用 |
RETIRED_EMAIL_DOMAIN | 'retired.invalid' | 哈希邮箱的域名部分,供RETIRED_EMAIL_FMT使用 |
RETIRED_USERNAME_FMT | lambda settings: settings.RETIRED_USERNAME_PREFIX + '{}' | 退休后用户名转换成的格式,{}被用户名的哈希值替换 |
RETIRED_EMAIL_FMT | lambda settings: settings.RETIRED_EMAIL_PREFIX + '{}@' + settings.RETIRED_EMAIL_DOMAIN | 退休后邮箱转换成的格式,{}被邮箱哈希值替换 |
RETIRED_USER_SALTS | None | 用户名/邮箱哈希使用的盐值列表。只有列表最后一项用于所有新的退休;历史盐值保留以保证所有历史哈希值仍可校验。默认值必须覆盖! |
RETIREMENT_SERVICE_WORKER_USERNAME | 'RETIREMENT_SERVICE_USER' | 退休服务 worker 的用户名 |
RETIREMENT_STATES | 见lms/envs/common.py中的RETIREMENT_STATES设置 | 定义退休工作流状态名与顺序的列表 |
FEATURES['ENABLE_ACCOUNT_DELETION'] | True | 是否在账户设置页面显示 "Delete My Account" 区块 |
文档特别指出,部分设置的值是 lambda 函数而非普通字符串字面量,这是 Open edX 特有的"派生设置(derived settings)"模式。
7.2 退休状态表与 populate_retirement_states
每个用户的退休状态存储在 LMS 数据库中,状态列表本身也单独存储在数据库中(RetirementState模型)。由于状态列表会随时间、随不同安装而变化,填充状态列表是管理员的责任。文档对状态列表提出了硬性约束:
- 至少要有开头的
PENDING状态,以及结尾的COMPLETED、ERRORED、ABORTED状态; - 每一个
RETIRING_foo状态都必须有对应的foo_COMPLETE状态。
如需自定义状态,通常在lms.yml中覆盖RETIREMENT_STATES,然后用管理命令把状态表与设置同步:
$ ./manage.py lms --settings=<your-settings> populate_retirement_states All states removed and new states added. Differences: Added: set([u'RETIRING_ENROLLMENTS', u'RETIRING_LMS', u'LMS_MISC_COMPLETE', u'RETIRING_LMS_MISC', u'ENROLLMENTS_COMPLETE', u'LMS_COMPLETE']) Removed: set([]) Remaining: set([u'ERRORED', u'PENDING', u'ABORTED', u'COMPLETE']) States updated successfully. Current states: PENDING (step 1) RETIRING_ENROLLMENTS (step 11) ENROLLMENTS_COMPLETE (step 21) ... COMPLETE (step 91)该命令是幂等的:始终让状态表与设置中的RETIREMENT_STATES列表保持一致;已存在的状态列在Remaining下不会重复添加。
7.3 创建退休服务用户
驱动脚本以"退休服务用户"的身份、通过 OAuth 客户端凭据与 LMS/IDA 认证,因此必须创建该用户并生成 DOT 应用与客户端凭据:
app_name=retirement user_name=retirement_service_worker ./manage.py lms --settings=<your-settings> manage_user $user_name $user_name@example.com --staff --superuser ./manage.py lms --settings=<your-settings> create_dot_application $app_name $user_name注意:客户端凭据(client ID 与 client secret)只会打印到终端一次,需要及时复制保存,之后填入驱动脚本的 YAML 配置。
退休服务用户还需要具备执行退休任务的权限,通过在 Django 设置中指定RETIREMENT_SERVICE_WORKER_USERNAME完成:
RETIREMENT_SERVICE_WORKER_USERNAME = 'retirement_service_worker'7.4 Django 管理后台入口
Django Admin 的USER_API分组下有三个与用户退休相关的模型:
| 名称 | URI | 说明 |
|---|---|---|
| Retirement States | /admin/user_api/retirementstate/ | 状态表,由RETIREMENT_STATES定义、populate_retirement_states填充 |
| User Retirement Requests | /admin/user_api/userretirementrequest/ | 记录所有曾请求删除账户的用户 ID,主要用于内部审计记账 |
| User Retirement Statuses | /admin/user_api/userretirementstatus/ | 管理每个学习者退休状态的核心模型,特殊情况下可在此手动改状态 |
八、处理特殊情况:失败恢复、批量重跑与取消退休
docs/special_cases.rst 覆盖了运维中最重要的三类场景。
8.1 从 ERRORED 状态恢复
当某个退休 API 返回失败(4xx 或 5xx),驱动脚本会立即把用户状态置为ERRORED。排查方法:查看user_api_userretirementstatus表中该用户行的responses字段(脚本每次状态切换都会把 API 响应写入该字段,见 retire_one_learner.py 中的update_learner_retirement_state调用)。问题修复后,手动把current_state重置为"应重试状态的前一个状态"(例如在 forums 退休阶段出错,则从ERRORED改回ENROLLMENTS_COMPLETE)。改完之后,驱动脚本下次执行时会自动从断点继续——这正是 retire_one_learner.py 中"跳过已完成阶段"逻辑带来的可重跑能力。
8.2 重跑部分或全部状态
如果想从最初开始重跑所有退休,把所有current_state == COMPLETE的记录改回PENDING即可。典型场景:全体退休跑完之后(但清理退休队列之前)新加了一个流水线阶段,或某个阶段/API 开发时实际未生效却返回了成功。退休 API 被设计为幂等的,对已经跑完的阶段重跑应该是无副作用的(no-op)。
8.3 取消退休请求
刚提交删除请求、仍处于PENDING状态的用户,可以通过联系管理员撤销请求。平台提供管理命令,给定用户邮箱即可恢复其登录能力并使其退出所有退休队列:
$ ./manage.py lms --settings=<your-settings> cancel_user_retirement_request <email-of-user-to-cancel-retirement>限制:该命令只对状态尚未越过PENDING的用户有效;且用户恢复访问前需要重置密码。
九、扩展退休流水线
docs/extending_retirement.rst 说明,当前退休代码只支持有限的一组服务(LMS、Ecommerce、Course Discovery 以及部分历史第三方提供方),要接入新系统(例如学校的邮件服务商),有两种主要方式:
- LMS 内监听 Django Signal:用户退休时 LMS 会发射名为
USER_RETIRE_LMS_MISC的信号(定义于openedx.core.djangoapps.user_api.accounts.signals),可以编写 edx-platform 插件监听该信号,在 LMS 内直接执行清理动作; - 带外(out of band)进程:利用退休 API 查询"近期退休"的用户,在用户退休与退休数据被完全清除之间的时间窗口内,把用户传播到外部系统并删除。具体可参照 utils/edx_api.py 的
LmsApi,例如用get_learners_by_date_and_status方法查找COMPLETE状态且最近 24 小时内更新的用户。该方式没有第一种的状态保证,但能执行更灵活的动作。
十、运行测试用例
README.rst 最后给出了测试的两种运行方式。
使用 uv:
uv run --project scripts/user_retirement --group test pytest scripts/user_retirement--group test对应 pyproject.toml 中的test依赖组(pytest、requests_mock、responses、moto、mock、ddt等)。
不使用 uv:先安装测试依赖再直接运行 pytest:
pip install -r scripts/user_retirement/requirements/testing.txt pytest scripts/user_retirement测试用例分布在 tests/ 目录,按脚本与 API 客户端组织,例如 tests/test_get_learners_to_retire.py、tests/test_retire_one_learner.py、tests/utils/test_edx_api.py,以及 tests/utils/thirdparty_apis/ 下对 amplitude、braze、hubspot、salesforce、segment 客户端的测试。
十一、小结:一条可复现的退休流水线
把 README 与 docs 串起来,一个完整的部署闭环是:
- 稀疏克隆仓库,只取
scripts/user_retirement; uv sync --project scripts/user_retirement --frozen建好环境;- LMS 侧配置
RETIREMENT_STATES、RETIRED_USER_SALTS等 Django 设置,执行populate_retirement_states同步状态表,创建退休服务用户并记录 DOT 客户端凭据; - 编写含
client_id/client_secret/base_urls/retirement_pipeline四段 YAML 的驱动配置; source scripts/user_retirement/entry_points.sh后,按"拉名单 → 逐人退休"的节奏运行get_learners_to_retire.py(注意cool_off_days、user_count_error_threshold安全阀)与retire_one_learner.py;- 失败时借助
responses字段定位,在 Django Admin 中把ERRORED重置回断点前的状态,重新执行即可自动续跑; - 需要接第三方系统时,优先监听
USER_RETIRE_LMS_MISC信号,或用LmsApi.get_learners_by_date_and_status做带外同步。
整套工具的可重跑性、幂等 API 设计、LMS 作为状态权威源这三点,是它与一次性数据擦除脚本的本质区别,也是运维时最重要的心智模型。
【免费下载链接】openedx-platformThe Open edX LMS & Studio, powering education sites around the world!项目地址: https://gitcode.com/GitHub_Trending/ed/openedx-platform
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考