## Context M1、M4~M6 原写在 `fix-august-iteration-follow-up-gaps`,改写该 Change 为第 1 批缺陷时移出;2026-10-11 确定并入第 1 批,单独成本 Change。现状已核对: - `ChangePasswordRequest`(`internal/model/dto/auth_dto.go:50`)校验为 `required` 与 `min=8,max=32`,Handler 校验失败只返回“参数验证失败”,字段与规则被丢弃。 - 设置登录密码的入口规则不一且无强度要求:本人改密码、后台账号创建/修改/改密码、建店铺初始密码为 8~32;企业客户新增与改密码为 6~20;代理注册为 6~64(`internal/domain/distribution/distribution.go:67`)。错误码 `1016`(`CodePasswordTooWeak`)已定义但未使用。 - `customer_account_dto.go`、`shop_account_dto.go` 中的建账号与改密码 DTO 没有任何调用方,不在本次范围。 - 订单导出表头有“店铺”无“操作人”(`internal/exporter/order_scene.go`),订单列表已有冻结的 `operator_name`。 - IoT 卡导出无到期列;卡列表最终到期由 `packageexpiry.Query.ResolveBatch`(`internal/query/packageexpiry/query.go:41`)推算。 - 设备导出表头为 `ICCIDn使用的流量`、“设备合计”(`internal/exporter/device_scene.go:378`),数值实为 MB,未标单位。 - 导出任务创建时冻结 `ResolvedHeaders`,Worker 按冻结表头生成行。 ## Goals / Non-Goals **Goals:** 设置登录密码的入口统一长度与强度规则;修改密码失败消息直接说明字段与规则;三类导出补齐核对字段与单位;历史任务列不错位。 **Non-Goals:** 不强制存量弱密码修改,不改登录校验;不改超级管理员操作密码与 WiFi 密码;不改订单与支付写入、到期推算口径;不改前端;无迁移。 ## Decisions 1. **修改密码复用 `internal/handler/validation.Message`。** 为请求字段补中文 `description`,Handler 返回“修改密码参数不合法:”加字段级消息;错误码、HTTP 状态和校验顺序不变。与项目已有中文校验提示约定一致,不在 Handler 内硬编码 validator 分支。 2. **订单“操作人”取冻结的 `operator_name`,追加在最后一列。** 不回查当前账号名称;是否输出以任务 `ResolvedHeaders` 是否含“操作人”为准。 3. **卡“套餐到期时间”按分页批量推算,追加在最后一列。** 每页调用一次 `ResolveBatch`,禁止逐卡查询;只有 `exact` 输出 `estimated_final_expires_at`,其余留空。 3a. **卡导出 GB 列追加在“套餐到期时间”之后(M5 补充,2026-10-14 确认)。** 新任务在最后追加“使用流量(GB)”“剩余流量(GB)”,值为同行 `使用流量(MB)`、`剩余流量(MB)` ÷ 1024 保留两位小数,与设备导出 `formatMB` 口径一致;原 MB 列名与数值不变。冻结表头按末列识别三个版本:末列“剩余流量(GB)”→ 输出到期列与两列 GB;末列“套餐到期时间”→ 只输出到期列;其余 → 两者都不输出(替换现有 `iotCardHeadersIncludeFinalExpiry` 的单一判断)。 4. **设备流量数值不变,只改表头并加 GB 列。** 新任务表头改为 `ICCIDn使用的流量(MB)`、“设备合计(MB)”,在最后一列(“关联手机号”之后)加“设备合计(GB)”,值为 MB ÷ 1024 保留两位小数;历史任务按冻结表头输出旧列名与旧列数。 5. **密码规则集中在一个函数,在服务层哈希前调用。** `pkg/validator` 新增登录密码规则函数,依次判断:长度 8~12(`1001`)→ 只含 ASCII 可见字符且无空格(`1016`)→ 大写、小写、数字、特殊符号四类至少三类(`1016`)。各入口在 `bcrypt.GenerateFromPassword` 前调用;DTO 标签同步改为 `min=8,max=12` 供文档与早期拦截。不只靠 DTO 标签,因为项目存在多个独立 `validator.New()` 实例,且代理注册在领域层自行校验,集中函数能保证口径一致。 6. **启动时的系统管理员不校验。** `CreateSystemAccount`(`internal/bootstrap/admin.go:51`)按配置创建,校验会让配置弱密码的环境无法启动;该账号上线后改密码走本人改密码入口,受新规则约束。 7. **长度上限 8~12 位为业务确认值。** 现有 8~32 位的密码超过 12 位的可继续登录,只在下次修改时受限。 5. **新列一律追加在最后(2026-10-11 确认)。** 与项目既有做法一致(IoT 卡导出注释:“「关联手机号」固定追加在尾部”)。原因:分片按位置把行截齐到冻结表头(`internal/task/export_common.go` 的 `alignRowsToHeaders`),新代码上线前派发、上线后执行的任务只会少最后一列,插在中间则后面各列整体错位。订单与设备导出的卡列是动态的,组数靠表头反推:订单为(总列数 − 固定列数)÷ 2(`orderCardGroupCountFromHeaders`),设备为按尾部固定列数(现为 6,旧为 5)反推(`cardGroupCountFromHeaders`)。追加末列后须先按末列表头是否为“操作人”/“设备合计(GB)”识别新旧表头再反推,否则卡列组数算错或为 0。 ## Risks / Trade-offs - [前端提示与后端规则不一致] → 任务中单列前端同步,测试环境联调确认。 - [12 位上限压低可用强度] → 业务已确认;规则集中在一个函数,后续放宽只改一处。 - [批量推算增加导出耗时] → 每页一次批量查询,无 N+1。 - [新列导致历史任务列错位] → 行构造一律以冻结 `ResolvedHeaders` 判定是否输出新列。 - [调用方按旧消息“参数验证失败”匹配] → 错误码 `1001` 不变,调用方应按错误码分支。 ## Migration Plan 无迁移。测试环境部署 API 与 Worker 后,新建导出任务使用新表头,已有任务沿用冻结表头。 ## Open Questions 无。