交付出件箱

← 返回出件箱

近视防控 SaaS「家长子席位—礼品卡模式」终版需求立项文档与技术方案

第一部分:立项文档

一、需求背景与目标

1.1 项目背景

品牌方需要将家长子席位账号和密码预先批量生成,并印刷到刮刮卡中,由家长购买刮刮卡后完成以下操作:

  1. 刮开卡片获取账号和密码;
  2. 在微信内打开品牌方提供的登录二维码或链接;
  3. 完成微信授权;
  4. 输入账号和密码登录;
  5. 系统记录微信绑定时间;
  6. 强制修改为新的 6 位数字密码并填写手机号;
  7. 开始使用近视防控服务。

现有系统存在以下问题:

1.2 建设目标

本期建设目标如下:

  1. 新增礼品卡模式的家长子席位批量开号能力;
  2. 新家长账号采用全平台按月递增的 10 位新格式;
  3. 支持一次最多生成 5 万个账号;
  4. 批量开号采用异步任务和分块批量插入,避免请求超时;
  5. 生成后导出包含明文密码的 Excel,供品牌方制作刮刮卡;
  6. 密码仅在生成流程中以明文存在,数据库仅保存密码哈希;
  7. 支持短期临时文件补下载,过期自动删除;

⚠️ 更正:本节临时文件/N小时自动删除方案已被文末「补充定案」的【Excel 永久保存 + 仅 admin 下载】取代,以补充定案为准。

  1. 明细导出改为全量导出,并补充绑定时间;
  2. 支持按开通时间、绑定时间、到期时间进行范围筛选;
  3. 支持状态多选;
  4. 保持旧家长账号正常使用,老号不迁移;
  5. 不建设登录入口和二维码生成功能。

二、功能范围

本期功能分为两块:


A. 批量开号 / 礼品卡

A.1 新增批量开号入口

在后台家长子席位管理页面新增“批量开号”功能。

管理员填写:

提交后:

本期不新增批次号字段,也不在 Excel 中增加批次号列。

A.2 新账号格式

新生成的家长账号格式固定为:

6位全局月序号 + YYMM

例如:

2026年10月第1个新号:0000012610
2026年10月第2个新号:0000022610

账号总长度固定为 10 位。

具体规则:

旧格式示例:

0001202610

旧号与新号均为 10 位,因此不能通过简单解析账号字符串判断新旧格式。

A.3 密码规则

A.4 礼品卡激活流程

礼品卡激活沿用现有家长激活逻辑:

  1. 家长必须在微信内打开登录页面;
  2. 非微信环境访问直接返回 403;
  3. 家长完成微信授权;
  4. 输入卡片上的账号和密码;
  5. 登录成功;
  6. 系统写入 seat_bound_at;
  7. 系统设置或保持 must_change_password=true;
  8. 强制修改为新的 6 位数字密码;
  9. 强制填写手机号;
  10. 完成激活后进入正常使用流程。

有效期起算规则:

A.5 生成后 Excel 下载

批量开号完成后,系统生成 Excel 文件,列固定为:

列名
账号
密码
机构名称
开通日期

说明:

A.6 临时文件补下载

⚠️ 更正:本节临时文件/N小时自动删除方案已被文末「补充定案」的【Excel 永久保存 + 仅 admin 下载】取代,以补充定案为准。

为防止管理员首次下载失败,系统保留一份短期临时文件:

A.7 部分失败处理

批量任务可能出现以下情况:

处理规则:

  1. 已成功插入的账号保留,不回滚;
  2. 已分配的账号序号不回收;
  3. 任务状态标记为“部分成功”或“失败待补文件”;
  4. 管理员可以基于已生成账号重新生成 Excel;
  5. 重新生成 Excel 不重复开号;
  6. 对没有成功插入的记录,不应出现在本次 Excel 中;
  7. 对已插入但密码明文临时数据丢失的情况,必须提供明确错误提示,不能凭哈希反推出密码;
  8. 若需重新补充缺失密码,只能重新创建新账号,不允许为已有账号“重置初始密码并继续使用原卡片”。

B. 明细导出增强

B.1 明细导出改为全量

现有按机构明细导出存在固定:

page=1
size=200

的限制,导致超过 200 条数据后静默丢失。

本期要求:

B.2 明细导出新增绑定时间列

导出文件新增:

绑定时间

字段值来源:

seat_bound_at

含义:

yyyy-MM-dd HH:mm:ss

B.3 时间范围筛选

明细页新增时间范围筛选,并允许用户选择时间类型:

  1. 开通时间;
  2. 绑定时间;
  3. 到期时间。

前端请求参数必须明确传递:

time_type
start_time
end_time

例如:

time_type=opened
start_time=2026-10-01 00:00:00
end_time=2026-10-31 23:59:59

后端根据 time_type 映射查询字段:

time_type 查询字段
opened 账号开通/生成时间
bound seat_bound_at
expired 到期时间

要求:

B.4 状态筛选改为多选

状态筛选支持多选。

状态保持三态:

  1. 待绑定;
  2. 使用中;
  3. 已到期。

绑定状态不单独增加为第四种状态。

状态规则:

前端请求示例:

status=pending,using

后端使用集合查询:

status IN ('pending', 'using')

若未选择任何状态,则不增加状态过滤条件。

B.5 明细页面和导出条件一致

明细页面查询和 Excel 导出必须复用同一套筛选条件,包括:

避免出现页面展示结果和导出结果不一致。


三、关键设计决策及理由

3.1 账号采用“6 位序号 + YYMM”

该方案已确定,原因如下:

  1. 月度容量由旧版 9,999 提升到 999,999;
  2. 账号总长度仍为 10 位;
  3. 前端输入框无需调整;
  4. 礼品卡卡面模板无需调整;
  5. 保留年月信息,便于定位账号生成月份;
  6. 全平台统一按月分配,避免机构维度的号段管理复杂度;
  7. 新老账号可以通过数据库字段明确区分,不依赖字符串解析。

3.2 新旧账号不能通过账号字符串解析区分

新旧账号长度相同:

因此不能使用以下方式作为唯一判断依据:

必须通过以下方式区分:

3.3 全局按月分配,不按机构拆分

新账号序号是全平台共享资源:

202610 月所有机构共用 000001~999999

原因:

3.4 密码采用简单可靠方案

本期不采用以下方案:

原因:

  1. 密码为刮刮卡上的 6 位数字;
  2. 首次激活后必须强制修改;
  3. 密码本身不是长期使用密码;
  4. 本期重点是可靠生成和交付,不是构建高敏密钥托管平台;
  5. 过度设计会增加实施复杂度和运维成本。

采用的方案是:

3.5 账号生成采用异步分块批量插入

不能在单个 HTTP 请求中逐条创建 5 万个账号。

采用:

原因:

3.6 不增加批次号

本期明确不增加批次号:


四、边界与非目标

以下内容明确不在本期范围内:

  1. 不新增品牌方登录入口;
  2. 不建设二维码生成能力;
  3. 不建设二维码投放和渠道追踪;
  4. 不迁移老家长账号;
  5. 不修改旧账号格式;
  6. 不按机构分配独立账号序号;
  7. 不增加批次号;
  8. 不在 Excel 中输出有效期;
  9. 不在 Excel 中输出绑定时间;
  10. 不改变初始密码为其他长度;
  11. 不允许管理员查看数据库中的密码明文;
  12. 不允许从密码哈希恢复密码;
  13. 不建设 KMS、AES-GCM、专用对象存储等银行级密码保管体系;
  14. 不将“已绑定”增加为独立状态;
  15. 不改变现有微信授权和强制改密流程;
  16. 不为老账号补发新格式账号;
  17. 不回收已分配但尚未使用的月序号;
  18. 不因 Excel 生成失败回滚已经成功创建的账号。

五、验收标准

5.1 账号格式与容量

5.2 批量开号

5.3 激活和有效期

5.4 Excel 交付

⚠️ 更正:本节临时文件/N小时自动删除方案已被文末「补充定案」的【Excel 永久保存 + 仅 admin 下载】取代,以补充定案为准。
- [ ] 下载接口具备管理员权限校验;
- [ ] 首次下载失败时支持再次下载;
- [ ] Excel 生成失败时不删除已生成账号;
- [ ] 明文密码临时数据丢失时,不允许伪造或恢复密码。

5.5 明细导出


第二部分:技术实现要点

1. 号码生成与号段分配

1.1 数据模型

建议在家长子席位表中明确保留账号类型字段:

seat_type

示例值:

legacy    -- 旧格式账号
gift_card -- 礼品卡新格式账号

同时使用独立的全局月度序号表:

CREATE TABLE parent_seat_month_counter (
    id BIGINT PRIMARY KEY,
    year_month CHAR(4) NOT NULL,          -- YYMM,例如 2610
    next_sequence INT NOT NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_year_month (year_month)
);

推荐将 year_month 实际按业务年月唯一管理。若系统跨世纪运行,需要额外保留完整年月字段,例如:

business_month CHAR(6) NOT NULL  -- YYYYMM
display_month CHAR(4) NOT NULL   -- YYMM

账号展示仍使用 YYMM,计数唯一性使用完整业务年月,避免不同世纪发生计数器冲突。

1.2 原子分配序号

批量任务不应逐条调用“获取下一个序号”,而应一次申请一个连续号段。

例如本批需要 1,000 个账号:

  1. 锁定对应月份计数器;
  2. 读取当前 next_sequence;
  3. 校验:
next_sequence + 1000 - 1 <= 999999
  1. 分配:
start_sequence = next_sequence
end_sequence = next_sequence + 999
  1. 更新:
next_sequence = next_sequence + 1000
  1. 提交事务;
  2. 释放锁;
  3. 应用层按连续序号生成账号。

伪 SQL:

BEGIN;

SELECT next_sequence
FROM parent_seat_month_counter
WHERE business_month = '202610'
FOR UPDATE;

-- 校验 next_sequence + batch_size - 1 <= 999999

UPDATE parent_seat_month_counter
SET next_sequence = next_sequence + :batch_size,
    updated_at = NOW()
WHERE business_month = '202610';

COMMIT;

首次使用某月份时,通过唯一索引和事务创建计数器,避免并发初始化产生重复月份记录。

1.3 账号生成

应用层生成账号:

account = LPAD(sequence, 6, '0') + YYMM

示例:

sequence = 1
YYMM = 2610
account = 0000012610

生成时必须同时写入:

1.4 并发和唯一性

账号表应存在唯一索引:

UNIQUE KEY uk_parent_account (account)

月度序号表负责分配连续号段,账号唯一索引负责最终兜底。

注意:

1.5 月度容量校验

批量任务开始前校验:

本月已分配数量 + 本次申请数量 <= 999999

如果不足:


2. 一次 5 万账号的异步分块批量 INSERT 流程

2.1 任务创建

接口示例:

POST /admin/parent-seats/batch-create

请求参数:

{
  "organization_id": 10001,
  "count": 50000
}

接口处理:

  1. 校验管理员权限;
  2. 校验机构权限;
  3. 校验数量范围 1 <= count <= 50000;
  4. 校验当月剩余容量;
  5. 创建批量任务记录;
  6. 投递异步任务;
  7. 立即返回任务 ID。

任务表建议包含:

batch_task_id
organization_id
requested_count
success_count
failed_count
status
current_chunk
total_chunk
excel_status
error_message
created_by
created_at
finished_at

本表的 batch_task_id 仅用于异步任务技术管理,不作为业务批次号,也不输出到 Excel。

2.2 分块策略

推荐:

单批 1,000 条

允许在配置中设置范围:

500~2,000 条

5 万条账号约分为:

50000 / 1000 = 50 批

每批处理流程:

  1. 申请一个月度连续号段;
  2. 在内存中生成本批账号;
  3. 为每个账号生成随机 6 位数字密码;
  4. 计算密码哈希;
  5. 构造批量 INSERT 参数;
  6. 批量写入数据库;
  7. 记录本批明文密码映射;
  8. 更新任务进度;
  9. 进入下一批。

2.3 密码生成

密码规则:

000000~999999 的随机 6 位数字

要求:

建议密码哈希使用系统现有密码哈希方案,例如 bcrypt、Argon2 或现有统一实现,不在本期更换密码哈希体系。

2.4 数据库事务

每个分块建议一个独立事务:

事务开始
  申请月度号段
  批量 INSERT 家长子席位
事务提交
记录本批明文密码
更新任务进度

需要明确一个关键点:

如果数据库和任务系统采用独立事务,必须依赖账号唯一索引和任务幂等标识,避免任务重试重复建号。

2.5 任务幂等

任务执行节点重启后,不能从头无条件重新开号。

应记录:

任务重试时:

2.6 进度展示

前端可展示:

任务状态:排队中 / 生成中 / Excel生成中 / 成功 / 部分成功 / 失败
已完成:12000 / 50000
成功:12000
失败:0

进度更新不应每插入一条更新一次,建议每批更新一次,降低数据库写压力。


3. 密码明文一次性落 Excel 的可靠交付流程

3.1 明文数据组织

在任务执行期间维护本次成功账号的密码记录:

account -> plaintext_password

由于一次最多 5 万条,短时间内保存 5 万组账号密码在任务进程内是可接受的,但应注意:

如果任务执行过程中需要跨节点,应将明文密码放入受访问控制的短期任务工作区,不能依赖普通日志或永久数据库表。

3.2 Excel 生成

全部账号成功写入或任务确定为部分成功后,生成 Excel:

列顺序固定:

账号 | 密码 | 机构名称 | 开通日期

生成内容必须满足:

建议采用流式 Excel 写入,避免一次性构造超大内存对象。

3.3 文件保存

文件可以保存到现有文件服务或应用文件目录,但需要满足:

例如文件记录:

provision_file_id
task_id
file_path
file_size
expires_at
status
created_at
deleted_at

3.4 下载接口

示例:

GET /admin/parent-seats/batch-tasks/{task_id}/download

服务端校验:

  1. 当前用户是否为管理员;
  2. 当前用户是否有对应机构权限;
  3. 任务是否已生成文件;
  4. 文件是否未过期;
  5. 文件是否实际存在;
  6. 文件状态是否正常。

校验通过后再返回文件流。

3.5 失败处理

场景一:数据库部分成功,Excel 尚未生成

处理:

场景二:Excel 生成成功,但首次下载失败

处理:

场景三:文件生成失败且明文密码仍在任务内存

处理:

场景四:进程异常导致明文密码丢失

处理:

3.6 明文清理

以下内容必须清理:

注意:


4. 明细导出和筛选改造

4.1 去除 200 条导出上限

后端改造

排查并移除以下逻辑:

page = 1
size = 200

以及类似逻辑:

size = Math.min(request.size, 200)

导出接口不应复用只用于页面展示的固定分页参数。

建议新增独立导出接口或明确增加导出模式:

GET /admin/organizations/{org_id}/parent-seats/export

导出实现:

可能的坑

  1. 直接去掉 LIMIT 可能导致数据库和应用内存压力过大;
  2. 直接使用大 OFFSET 会越翻越慢;
  3. 页面查询接口和导出接口字段不一致;
  4. 导出查询条件漏传状态或时间参数;
  5. 数据导出过程中新增数据导致结果不稳定。

建议导出开始时确定查询边界,例如记录导出开始时间或使用主键游标,保证一次导出的结果相对稳定。


4.2 新增绑定时间字段

后端查询增加:

seat_bound_at

返回字段建议统一命名:

{
  "account": "0000012610",
  "seat_bound_at": "2026-10-20 10:30:00"
}

Excel 映射为:

绑定时间

注意:


4.3 时间筛选参数链路

前端

前端明细弹窗或明细页必须将筛选条件传入接口:

{
  "time_type": "bound",
  "start_time": "2026-10-01 00:00:00",
  "end_time": "2026-10-31 23:59:59"
}

不能只在前端修改显示条件,而不修改请求参数。

后端路由

修改 org_detail 路由或对应明细接口,使其能够接收:

time_type
start_time
end_time

并将参数传递到 Service、Repository、SQL 查询层。

SQL 条件映射

不能直接把前端传入的字段名拼接进 SQL。应使用白名单映射:

switch (timeType) {
    case OPENED:
        column = "opened_at";
        break;
    case BOUND:
        column = "seat_bound_at";
        break;
    case EXPIRED:
        column = "expired_at";
        break;
    default:
        throw new InvalidParameterException();
}

然后使用参数化查询传入时间值。

时间边界

推荐统一转换为:

[start_time, end_time]

或使用:

>= start_time
< end_time + 1 秒

避免前端传入日期 2026-10-31 时遗漏当天数据。


4.4 状态多选

前端参数

将原单值参数改为数组或逗号分隔值,例如:

{
  "status": ["pending", "using"]
}

或:

status=pending,using

项目内部应统一一种格式,推荐数组。

后端处理

先校验状态值只能来自固定集合:

pending
using
expired

再构造集合条件:

status IN (:status_list)

不要将原始字符串直接拼接到 SQL。

状态计算规则

建议在查询层统一计算或使用已有状态字段:

若 expired_at <= now:
    expired
否则若 seat_bound_at IS NULL:
    pending
否则:
    using

如果数据库已有状态字段,也必须确认状态更新逻辑与上述规则一致,特别是:

页面与导出一致性

状态计算逻辑必须由公共查询服务复用,避免:


4.5 关联索引建议

为保证新筛选和导出性能,应根据实际数据库和数据规模检查索引,至少评估:

(organization_id, opened_at)
(organization_id, seat_bound_at)
(organization_id, expired_at)
(organization_id, status)
(account)

不建议盲目建立所有组合索引,应结合现有查询计划确认。

重点关注:


4.6 监控与日志

应增加以下监控指标:

日志中必须记录:

日志中禁止记录:


4.7 发布前重点回归

上线前必须重点验证:

  1. 老格式账号登录不受影响;
  2. 新格式账号登录不受影响;
  3. 10 位账号不会被前端截断;
  4. 月度序号并发生成不重复;
  5. 月末和跨月生成逻辑正确;
  6. 单次 50,000 条任务不超时;
  7. 中途失败后不重复开号;
  8. Excel 账号与密码一一对应;
  9. Excel 过期后不可下载;
  10. 有效期从激活日计算;
  11. seat_bound_at 正确写入并展示;
  12. 时间筛选实际影响后端查询;
  13. 开通、绑定、到期三种时间筛选均有效;
  14. 状态多选结果正确;
  15. 已绑定账号显示为使用中;
  16. 超过 200 条导出不丢失;
  17. 大数据量导出不导致应用内存溢出;
  18. 失败日志不泄露密码;
  19. 老账号不被错误识别为新账号;
  20. 当月超过 999,999 个账号时能够明确阻断并提示。

补充定案(2026-09-18 沟通确认)

以下为立项文档初稿之后、与长官逐条沟通确认的补充决策,与前文一并生效:

1. 开号门槛

2. 批量开号记录(下载记录)

3. Excel 永久保存 + 仅 admin 下载

4. Excel 格式

5. 效果图定稿