## Context 项目为 Vue 3 / uni-app H5。`openspec/specs` 当前没有已归档基线,因此本提案在现有相关 capability 名称下使用 `ADDED Requirements` 建立本次需求,不对不存在的基线使用 `MODIFIED`。`phone-change-migration` 保留历史 capability 名称以便追踪,但行为明确改为“只更换客户手机号”,不表示仍支持迁移。 接口行为以 2026-10-12 的第 1 批对接说明和用户 2026-10-13 的答复为准。仓库未提供说明中引用的 `docs/admin-openapi.yaml`,不因此回退旧的换号或全部已读约定。 | 代码证据 | 问题 | 处理方向 | |---|---|---| | `pages/login/login.vue` 只读取 `need_bind_phone` | 登录的资产绑定事实未保存 | 保存三个布尔字段,按当前登录资产隔离 | | `pages/index/index.vue` 用本地手机号优先覆盖 `bound_phone`,并据此计算 `alreadyBindPhone` | 客户手机号与资产关联混为一体,旧缓存可覆盖服务端 | 拆分客户手机号、资产绑定/归属、是否需要提示 | | `pages/bind/bind.vue` 成功后无条件 `setPhoneBindStatus` | 只给客户绑号也可能被记为资产换号 | 按本次绑定对象刷新,不推断资产号码 | | `pages/change-phone/change-phone.vue` 把 `newPhone` 写入资产缓存 | 与新文档“只换客户手机号”冲突 | 仅更新客户手机号,保留资产关联 | | 首页、TrafficCard、绑定页等分别请求 `asset/info` | 一次性提醒可能先被非展示调用方取走 | 在 API 返回入口集中接收,再由资产信息区展示 | | 首页 `loadUnreadNotifications` 展示所有未读项 | 仍以旧临期通知触发弹窗 | 从旧弹窗来源排除临期项,通知中心不删除 | | `api/modules/notification.js` 的 `markAllRead` 方法约定曾冲突 | 用户最新确认应为 PUT | 保持 PUT,不采用此前 POST 约定 | ## Goals / Non-Goals - Goals:正确区分客户与资产绑定、支持文档四种流程、客户换号不改变资产关联、一次性临期提醒不丢失/不重复、通知全部已读方法正确。 - Non-Goals:代理注册及密码规则、后台接口、全局通知重构、新增登录方式、重新设计支付或实名流程。 ## Decisions ### 1. 分离客户与资产手机号状态 `asset_phone_bound` 只表示当前资产存在有效手机号关联,`asset_phone_is_own` 只表示该关联是否属于当前客户;二者不提供号码。`need_bind_phone` 是后端计算的提示开关,不能由前端用手机号为空、归属为 false 或本地缓存重新计算。 现有八月份接口定义 `docs/产品迭代8月份/11.md` 将 `asset/info.bound_phone` 描述为“当前登录用户绑定的手机号”。新对接说明声明字段为兼容性新增、未改变该字段,故保留其客户手机号用途:可作为绑定预填和更换客户手机号的原号来源,但不能作为资产关联是否存在的证据。若实施联调的实际响应与这一未变字段约定冲突,先向用户说明并确认,不猜测新增字段或号码。 在用户状态中记录当前登录资产的三个布尔字段;在退出、切换账号或资产时清理/更换。原 `phoneBindStatus` 不再作为服务端资产事实或客户原手机号的权威来源,不能让历史的 `bound: true` 绕过当前状态,也不能跨客户复用。未获取新字段时保持“未知”,不把缺失默认为已绑定,也不新增 OAuth 自动重登来刷新事实。 ### 2. 按四种场景处理绑定 | 客户手机号 | 资产手机号 | 表单与成功后的处理 | |---|---|---| | 无 | 无 | 验证一次输入号码,由后端同时写入客户与资产 | | 有 | 无 | 提示“给当前资产绑定手机号”;客户号码可预填且可编辑,允许提交不同号码,不据此改客户手机号 | | 无 | 有 | 提示“绑定客户手机号”;仍使用既有 bind-phone 请求,成功后只更新客户状态,不覆盖资产关联 | | 有 | 有 | 不要求绑定;资产号码属于他人也不要求改绑或暴露号码 | 是否触发登录后的绑定提示仍取决于 `need_bind_phone`;关闭强制开关时不产生自动跳转。首页功能入口分别展示资产绑定状态与客户换号能力,不能因客户已有手机号就禁用资产首次绑号。 对于 `1192`,展示联系管理员的明确提示,停止再次提交当前资产改绑请求;不自动重试、不换接口解除、不显示资产号码。`1184` 等其他错误按返回消息提示,失败不更新状态。请求仍使用 `{ phone, code }`,不凭空新增绑定对象参数。 用户最新确认绑定手机号不会让 token 失效。强制绑定成功后保留 token、identifier 及客户信息,直接进入首页;不清除登录态、不进行自动 OAuth 或要求手动重登。普通绑定成功返回上一页。成功后的状态更新必须对应本次实际绑定对象,不能用“请求成功”统一代表客户号和资产号均已修改。 强制与普通绑定页均可使用 asset/info 预填客户手机号:本次会话不变,共享接收的一次性提醒保留到首页展示。`asset_phone_bound=true` 且 `need_bind_phone=true` 明确对应“只给客户绑号”。预填失败时不阻塞输入;已知资产原本无号时,绑定成功更新资产已绑号,但客户号码和归属未知时不猜测,首页后续 asset/info 读取客户手机号。 ### 3. 客户换号不迁移资产 保留原/新手机号和验证码的现有表单以及 `change_phone_old`、`change_phone_new` 场景。成功后只更新服务端确认的客户手机号,不调用 `setPhoneBindStatus(identifier, newPhone)`,不显示“全部资产换绑成功”或旧事务迁移文案。 原资产仍可保持已绑定,但其号码可能不再属于客户;例如原 `asset_phone_is_own=true` 在客户改成不同号码后可能为 false。若响应不提供更新后的归属事实,不能把旧 true 留作最新事实,应失效该归属状态,等下一次真实登录响应确认;不借此要求资产改绑。 ### 4. 一次性临期提醒使用弹窗及共享接收 首页就是当前 H5 的资产信息展示页,本次不新增资产详情路由。按用户最新要求,移除横幅,使用 `ExpiryReminderPopup` 居中弹窗:等级配色、小型时钟图形、突出显示剩余天数和预计到期时间;不展示 level_name 标签。提供“立即续费”、右上关闭及“我知道了”操作。 首页按现有风险换货/运营候选 → 临期提醒 → 普通通知的顺序展示,一次只显示一个。asset/info 和候选请求未完成时不消费待展示提醒;通知已显示时才收到临期提醒,则等通知关闭后显示。候选按钮立即导航时保持提醒待展示;候选组件卸载后的异步响应不得回调新页面。临期关闭不再占首页布局,也不因此创建新的业务请求。 原样保留 `expiry_reminder.remaining_days`、`expires_at`、`level`、`level_name`;使用服务端等级映射 red / purple / pink 的样式,保留文本提示,不能只靠颜色传达紧急程度。`remaining_days=0` 必须正常展示。日期展示遵循 Asia/Shanghai,不能用客户端当前时区重算剩余自然日或等级。 “立即续费”先关闭临期弹窗且不推进其他通知,再调用与首页按钮相同的 startHomepageRenewal,使用当前套餐 ID、名称和续费价格。续费窗口准备中/可见时暂停其他提醒,关闭后继续排队。无可续费套餐或无支付方式时沿用原提示,不新增下单接口,也不在点击提醒按钮时自动创建订单。 `assetApi.getInfo` 成功返回处集中接收非空提醒并交给共享状态,同次资产进入期间的并发响应不允许用“未返回字段”的响应覆盖已经接收的提醒。共享状态隔离当前客户上下文与 identifier,避免切换资产/退出后显示旧资产或他人的提醒。提醒到达时若资产信息区尚不可见,应先暂存,待对应资产可见时再展示,不能静默丢弃。 后端负责“客户 + 资产 + 上海自然日”的每日一次;前端只防止同一次接收被多个组件重复展示,不增加每日定时推送、补发请求或长期缓存复播。关闭/已展示的提醒在当天后续无字段响应时不得重新出现。审核所有 `asset/info` 调用方,特别是绑定前预填及首页 TrafficCard 的请求顺序;绑定成功使用同一 token,提醒无需跨登录传递。真实退出、登录过期或切换上下文仍清理旧提醒。 临期弹窗展示/关闭不自动标记通知已读:提醒不含 notification_id,不能按标题、日期或列表位置猜测 ID,也不能调用 read-all 消掉新通知。临期通知继续由通知中心管理阅读状态。 ### 5. 通知仅调整临期来源与全部已读方法 从首页旧通知弹窗来源排除临期项,沿用既有 `category=expiry` 识别方式;不删除通知中心临期项,不改变其他未读通知的顺序和展示。`popup-candidates` 的风险换货/运营链路不受影响。 列表、未读数保持原入参,不增加文档未定义的 identifier/asset_id;后端基于当前认证资产过滤临期通知,并非所有通知都只属于当前资产。切换资产后刷新列表和计数,不将 A 的未读结果拼接到 B。 全部已读按用户最新确认使用 PUT,保留通知中心主动触发;失败不提前清除未读状态。成功后以刷新结果对齐本地列表/未读数,不能把其他资产的临期通知置已读。单条已读继续 PUT。风险换货、运营及临期弹窗均不得自动调用全部已读。 通知卡片保留常规边框,移除左侧未读强调边框和小圆点。标题右侧显示绿色“未读”或灰色“已读”小按钮;点击整张卡片或状态按钮调用现有单条 PUT,成功后显示“已读”并刷新计数,失败保留“未读”。按钮阻止事件冒泡,避免一次点击触发两次标记。 ## Risks / Trade-offs - 一次性提醒在任何 asset/info 读取时就可能被后端消费:集中接收并回归首页并发、绑定预填、其他页面进入,不能只给首页 getInfo 的响应加判断。 - 客户号码与资产号码不同:服务端布尔事实和客户号分离,历史本地缓存不再覆盖服务端。 - 没有最新 YAML:按已确认的新文档实施;如实际契约缺字段或与四场景不符,提供响应结构差异(不含真实号码/令牌)并先询问用户。 - 尚未归档的旧提案仍含相反规则:本提案显式声明覆盖范围,不顺带改写或归档历史提案。 ## Migration Plan 1. 提案批准后,先完成状态拆分及旧缓存失效,再调整绑定/换号行为。 2. 加入临期提醒共享接收和居中弹窗及排队显示,排除首页旧临期通知弹窗来源,调整通知阅读状态按钮。 3. 保持全部已读为 PUT,回归当前资产的列表与未读数。 4. 完成任务中的场景验证及 H5 构建,记录实际结果;不以静态构建代替真实接口联调。 5. 需要回退时只回退本次前端改动;服务端新契约不变,不能恢复“客户换号即资产迁移”的旧假设作为兼容策略。 ## Open Questions 提案范围和优先契约已由用户确认,无需再次决定代理注册或 read-all 方法。实施前仍应检查接口是否实际提供所需字段及一次性提醒跨页面传递条件;只在发现具体矛盾时报告并询问,不擅自扩展接口。