# 账户/设置/联系 信息架构重构 — Flutter 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 把已评审的 IA(`docs/account-ia-reorg-design.html`)落成 Flutter:账户/设置/联系去重归位,Account 提为桌面一级项,顶栏加通知铃铛,账户页加邀请入口,并为 Spec ②推广激励 / ③系统通知 留占位屏与接口。 **Architecture:** 复用现有 Riverpod + shell(desktop/mobile/tablet) + `open(NavView, mobilePage)` 下钻模式。多数是改造现有页面(account_page/settings_page/account_screens)+ 导航(navigation_provider/desktop_shell);新建仅 2 个占位屏(InviteScreen/NotificationsScreen)+ 铃铛 widget。**唯一后端改动 = 订单过期态(Task 0,display-only 零迁移)**——付款/订单核心后端已完成并部署生产。邀请奖励引擎、通知下发机制**不在本计划**(各自 Spec ②③,前后端一起)。 **Tech Stack:** Flutter / Dart · flutter_riverpod 2.x · 现有 `PangolinIcons`(生成,勿手改) · `AppText` 抽象基类 + 6 语言 strings。 ## Global Constraints - **l10n 完整性闸**:`app_text.dart` 每加一个抽象 getter,必须在 6 份 `strings_{zh,en,ja,ko,ru,es}.dart` 全部实现,否则 `node tools/check-l1-sync.mjs` red。默认语言英文。 - **图标单源**:`client/lib/widgets/pangolin_icons.dart` 是生成文件**勿手改**;真源 `design/prototype/icons.js`。本计划所需图标(bell/gift/crown/ticket/monitorSmartphone/users/settings/messageCircle)均已在。 - **颜色单源**:禁裸 `Color(0x)`/`Colors.x`,走 `context.pangolin` 语义色;例外加 `// ds-ignore: 理由`。 - **验收**:每任务后 `cd client && flutter analyze`(0 error) + 相关 `flutter test` 绿;改到有 golden 的屏随功能 `--update-goldens` 重录。 - **占位屏边界**:InviteScreen/NotificationsScreen 本计划只做**静态占位 UI + 导航接线**;数据/奖励/下发逻辑属 Spec ②③,用 `// TODO(spec-2)` / `// TODO(spec-3)` 标注接口点。 --- ### Task 0: 后端 — 订单过期态(display-only,零迁移) **Files:** - Modify: `server/internal/pay/handler.go`(`orderViewFromRow` + `orderPendingTTL` 常量) - Test: `server/internal/pay/handler_test.go` **Interfaces:** - Produces: 订单视图 `status` 新增可能值 `"expired"`(供前端映射)。判定:本地台账 `status=="created"` 且 `created_at + orderPendingTTL < now` → 视图显 `expired`(DB 不改,与金额兜底同为展示层)。 - [ ] **Step 1: 写过期判定测试(失败)** 在 `handler_test.go` 加: ```go // 老 created 单超 TTL → 列表视图显 expired;近期 created → 仍 created。 func TestList_ExpiredWhenStale(t *testing.T) { router, st := newHandlerRig(t, nil) ctx := context.Background() // 直接构造两条 created 单,改 created_at 到很久以前 / 刚刚 if err := st.Insert(ctx, 1, "uuid-1", "pro_month", "old", "alipay", 2999, "CNY"); err != nil { t.Fatal(err) } if err := st.Insert(ctx, 1, "uuid-1", "pro_month", "fresh", "alipay", 2999, "CNY"); err != nil { t.Fatal(err) } // 把 old 的 created_at 回拨到 TTL 之前 if _, err := st.db.ExecContext(ctx, `UPDATE pay_purchases SET created_at = ? WHERE out_trade_no = 'old'`, time.Now().Add(-2*orderPendingTTL).UTC()); err != nil { t.Fatal(err) } w := httptest.NewRecorder() router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodGet, "/v1/pay/orders", nil), 1)) var resp struct{ Orders []orderView `json:"orders"` } _ = json.Unmarshal(w.Body.Bytes(), &resp) got := map[string]string{} for _, o := range resp.Orders { got[o.OrderNo] = o.Status } if got["old"] != "expired" { t.Fatalf("old 应过期, got %q", got["old"]) } if got["fresh"] != "created" { t.Fatalf("fresh 应 created, got %q", got["fresh"]) } } ``` > 注:`st.db` 是 `Store` 的私有字段,测试同包(`package pay`)可访问。 - [ ] **Step 2: 跑测试确认失败** Run: `cd server && go test ./internal/pay/ -run TestList_ExpiredWhenStale` Expected: FAIL(old 仍为 created)。 - [ ] **Step 3: 实现过期判定** `handler.go` 加常量 + 在 `orderViewFromRow` 里判定: ```go // 未付订单展示层过期窗口(近似 pay 会话时效;仅影响列表/详情显示,不改台账)。 const orderPendingTTL = 30 * time.Minute ``` 在 `orderViewFromRow` 组装 `v` 后、`return v` 前加: ```go if row.Status == "created" && row.CreatedAt.Add(orderPendingTTL).Before(time.Now()) { v.Status = "expired" // 展示层:台账仍 created,可继续支付/取消 } ``` - [ ] **Step 4: 跑测试确认通过** Run: `cd server && go test ./internal/pay/` Expected: ok(含新测试 + 原有全绿)。 - [ ] **Step 5: Commit** ```bash git add server/internal/pay/handler.go server/internal/pay/handler_test.go git commit -m "feat(server/pay): 订单展示层过期态(created 超 TTL 显 expired,零迁移)" ``` > **部署**:本任务改 server,需重编 linux 二进制 + 部署 pangolin1(Task 8 统一部署,或单独发)。 --- ### Task 1: 导航枚举加 invite / notifications **Files:** - Modify: `client/lib/state/navigation_provider.dart:7,10` **Interfaces:** - Produces: `NavView.invite`、`NavView.notifications` 枚举值;`kAccountSubViews` 含 `invite`。 - [ ] **Step 1: 加枚举值 + 子页登记** `navigation_provider.dart` 第 7 行 `enum NavView` 末尾加 `, invite, notifications`: ```dart enum NavView { connect, servers, stats, account, contact, settings, plans, redeem, devices, purchase, payment, orders, invite, notifications } ``` 第 10 行 `kAccountSubViews` 加 `invite`(notifications 走全屏,不入子页返回集): ```dart const Set kAccountSubViews = {NavView.plans, NavView.redeem, NavView.devices, NavView.purchase, NavView.payment, NavView.orders, NavView.invite}; ``` - [ ] **Step 2: 编译校验** Run: `cd client && flutter analyze lib/state/navigation_provider.dart` Expected: 出现「missing case NavView.invite/notifications」类 error(desktop_shell 的穷举 switch 未覆盖)——**预期**,Task 5 修复。仅确认本文件无语法 error。 - [ ] **Step 3: Commit** ```bash git add client/lib/state/navigation_provider.dart git commit -m "feat(nav): NavView 加 invite/notifications + kAccountSubViews 收录 invite" ``` --- ### Task 2: l10n — 邀请 + 通知 + IA 归组文案 **Files:** - Modify: `client/lib/l10n/app_text.dart`(抽象 getter) - Modify: `client/lib/l10n/strings_{zh,en,ja,ko,ru,es}.dart`(6 份实现) **Interfaces:** - Produces getter(供后续任务用):`inviteEntry, inviteEntrySub, inviteTitle, inviteCodeLabel, inviteLinkLabel, inviteCopyCode, inviteCopyLink, inviteRewardsTitle, inviteRewardRegister, inviteRewardFirstBuy, inviteRewardJoinTg, inviteProgressTitle`;`notifTitle, notifEmpty, notifTypeNews, notifTypeFeature, notifTypeImportant`;`settingsGroupConn, settingsGroupAppearance, settingsGroupAbout`。 - [ ] **Step 1: app_text.dart 加抽象 getter** 在 `app_text.dart` 的「购买/支付」段后加一段(照现有注释风格): ```dart // ── 邀请好友(Spec ②占位) ── String get inviteEntry; // 邀请好友得会员 / Invite friends, earn membership String get inviteEntrySub; // 好友注册 · 双方都得会员天数 / Friend signs up · both earn days String get inviteTitle; // 邀请好友 / Invite friends String get inviteCodeLabel; // 邀请码 / Invite code String get inviteLinkLabel; // 邀请链接 / Invite link String get inviteCopyCode; // 复制邀请码 / Copy code String get inviteCopyLink; // 复制链接 / Copy link String get inviteRewardsTitle; // 奖励规则 / Rewards String get inviteRewardRegister; // 好友注册 · 双方各得 3 天 / Sign up · +3 days each String get inviteRewardFirstBuy; // 好友首购 · 双方各得 15 天 / First purchase · +15 days each String get inviteRewardJoinTg; // 加入 TG 频道 · 得 3 天 / Join TG · +3 days String get inviteProgressTitle; // 我的奖励 / My rewards // ── 通知(Spec ③占位) ── String get notifTitle; // 通知 / Notifications String get notifEmpty; // 暂无通知 / No notifications String get notifTypeNews; // 新闻 / News String get notifTypeFeature; // 功能 / Feature String get notifTypeImportant; // 重要 / Important // ── 设置分组 ── String get settingsGroupConn; // 连接 / Connection String get settingsGroupAppearance;// 外观 / Appearance String get settingsGroupAbout; // 关于 / About // ── 订单过期态(Task 0 后端产出) ── String get orderStatusExpired; // 已过期 / Expired ``` - [ ] **Step 2: 6 份 strings 各实现(带 @override)** 每份 `strings_.dart` 在 `qrNotSupported` 之后加对应 20 个 `@override String get X => '<译文>';`。译文(zh 见注释;en 见注释;ja/ko/ru/es 按语义翻译,与已有条目风格一致)。 - [ ] **Step 3: 跑 l10n 闸** Run: `cd /Users/wangjia/code/pangolin/.claude/worktrees/pay-v2-integration && node tools/check-l1-sync.mjs` Expected: `✓ l10n 完整 OK —— NNN 个 getter × 6 语言全实现`(getter 数 +20)。 - [ ] **Step 4: analyze** Run: `cd client && flutter analyze lib/l10n/` Expected: No issues found(仅 annotate_overrides 若漏 @override 会 info,补齐)。 - [ ] **Step 5: Commit** ```bash git add client/lib/l10n/ git commit -m "feat(l10n): 邀请/通知/设置分组文案 ×6 语言(Spec ②③占位)" ``` --- ### Task 3: 联系渠道单源 + Redeem 删「去哪买」 **Files:** - Create: `client/lib/services/contact_channels.dart` - Modify: `client/lib/screens/contact_page.dart:22-26` - Modify: `client/lib/widgets/account_screens.dart:377-381,420-425,461-465` **Interfaces:** - Produces: `const List kContactChannels`(single source);`class ContactChannel { IconData icon; String name; String sub; bool accent; }`。 - [ ] **Step 1: 建单源常量** `contact_channels.dart`: ```dart import 'package:flutter/widgets.dart'; import '../widgets/pangolin_icons.dart'; class ContactChannel { const ContactChannel({required this.icon, required this.name, required this.sub, this.accent = false}); final IconData icon; final String name; // 展示名(Telegram 用字面量;邮箱/官网用 l10n,在页面侧传入) final String sub; // handle:@x / a@b / domain(launchChannel 据此推导 URL) final bool accent; } // name 用 key 占位,页面按 t 映射;Telegram 固定字面量 const List<({IconData icon, String nameKey, String sub, bool accent})> kContactChannels = [ (icon: PangolinIcons.send, nameKey: 'telegram', sub: '@pangolin_app', accent: true), (icon: PangolinIcons.mail, nameKey: 'email', sub: 'support@yanmeiai.com', accent: false), (icon: PangolinIcons.globe, nameKey: 'website', sub: 'pangolin.yanmeiai.com', accent: false), ]; ``` > 说明:`nameKey` 在页面侧映射(`telegram`→字面量 'Telegram',`email`→`t.contactEmail`,`website`→`t.contactWebsite`),避免把 AppText 塞进常量。 - [ ] **Step 2: contact_page.dart 用单源** `contact_page.dart:22-26` 的三元素内联 list 换成 `kContactChannels.map(...)`,name 按 nameKey 映射。 - [ ] **Step 3: ContactScreen 用单源** `account_screens.dart:461-465`(ContactScreen 的 channels)同样换成 `kContactChannels`。 - [ ] **Step 4: RedeemScreen 删「去哪买」** `account_screens.dart`:删 `channels`(377-381)、删 build 里「去哪买」引流卡(`buyTitle`/`buySub` + channel 列表,420-425)、删 `_channel` 方法(430+)。RedeemScreen 只保留兑换码输入(386-419)。 - [ ] **Step 5: analyze + 手测** Run: `cd client && flutter analyze lib/screens/contact_page.dart lib/widgets/account_screens.dart lib/services/contact_channels.dart` Expected: No issues found。 - [ ] **Step 6: Commit** ```bash git add client/lib/services/contact_channels.dart client/lib/screens/contact_page.dart client/lib/widgets/account_screens.dart git commit -m "refactor(contact): 渠道抽单源常量 + RedeemScreen 删去哪买引流卡(仅留兑换码)" ``` --- ### Task 4: 账户页重排 + 邀请入口 + 删偏好卡 + formFactor 门控 **Files:** - Modify: `client/lib/screens/account_page.dart:52-141,381,423` - Create: `client/lib/screens/invite_page.dart` - Test: `client/test/widget/account_ia_test.dart` **Interfaces:** - Consumes: `NavView.invite`(Task 1)、`t.inviteEntry/inviteEntrySub`(Task 2)、`open(NavView, Widget)`(现有)。 - Produces: `class InviteScreen extends ConsumerWidget { InviteScreen({required AppText t, VoidCallback? onBack, bool embedded}); }`(占位)。 - [ ] **Step 1: 建 InviteScreen 占位屏** `invite_page.dart`:`_SubScaffold`(复用 account_screens 的骨架不便跨文件,简单自建 Scaffold/embedded)标题 `t.inviteTitle`,内容 = 邀请码卡(可复制,值写死 `PANGOLIN-DEMO`)+ 链接卡 + 奖励规则列表(register/firstBuy/joinTg 三条,用 `t.inviteReward*`)+ 「我的奖励」占位卡。全走 `context.pangolin`/`PangolinIcons.gift`。顶部 `// TODO(spec-2): 邀请码/链接由后端签发,奖励进度来自 /v1/invite`。 - [ ] **Step 2: 写账户页 IA 结构测试(失败)** `account_ia_test.dart`:desktop 态(`debugDefaultTargetPlatformOverride=windows` + 宽 surface + provider override,参考 `test/widget/account_desktop_nav_test.dart`)pump `AccountPage(isWide:true)`,断言: ```dart // 邀请入口存在 expect(find.text(t.inviteEntry), findsOneWidget); // 语言/主题不在账户页(移到 Settings) expect(find.text(t.language), findsNothing); // 兑换在账户页(购买&订单&兑换块) expect(find.text(t.redeemEntry), findsOneWidget); ``` - [ ] **Step 3: 跑测试确认失败** Run: `cd client && flutter test test/widget/account_ia_test.dart` Expected: FAIL(邀请入口不存在 / 语言仍在)。 - [ ] **Step 4: 改 account_page.dart** - 购买&订单&兑换块(现 80-94):把兑换码行(现在设备卡里的 `t.redeemEntry`)并入,成为 购买套餐 / 我的订单 / 兑换码 三行。 - 其后加**邀请卡**:`_NavRow(icon: PangolinIcons.gift, title: t.inviteEntry, sub: t.inviteEntrySub, onTap: () => open(NavView.invite, InviteScreen(t: t)))`。 - 设备/设置/兑换/联系卡(现 97-105):兑换移走(见上);设备保持下钻行;设置/联系两行包 `if (context.formFactor == FormFactor.mobile)`(桌面侧栏已有,不重复)。 - **删语言/主题/协议卡**(108-130)+ `_LangSwitch`(381)+ `_PangolinSwitch`(423)。 - import `invite_page.dart` + `core/responsive/form_factor.dart`。 - [ ] **Step 5: 跑测试确认通过** Run: `cd client && flutter test test/widget/account_ia_test.dart` Expected: PASS。 - [ ] **Step 6: analyze + golden 重录(账户页有 golden 则录)** Run: `cd client && flutter analyze lib/screens/account_page.dart lib/screens/invite_page.dart && flutter test --update-goldens 2>&1 | tail -3` - [ ] **Step 7: Commit** ```bash git add client/lib/screens/account_page.dart client/lib/screens/invite_page.dart client/test/widget/account_ia_test.dart client/test/golden/ git commit -m "feat(account): IA 重排+邀请入口+删偏好卡+设置/联系 formFactor 门控" ``` --- ### Task 5: 桌面侧栏加 Account + 顶栏铃铛 + invite 视图接线 **Files:** - Modify: `client/lib/shell/desktop_shell.dart:34-40,42-55,59-88,92-96,104` - Modify: `client/lib/shell/tablet_shell.dart:39-46` - Create: `client/lib/widgets/notification_bell.dart` - Modify: `client/lib/widgets/content_top_bar.dart:44-66` **Interfaces:** - Consumes: `NavView.invite/notifications`(Task 1)、`InviteScreen`(Task 4)、`NotificationsScreen`(Task 6,先前向引用——本任务铃铛 onTap 先 `go(NavView.notifications)`,Task 6 补屏)。 - Produces: `class NotificationBell extends ConsumerWidget { NotificationBell({required VoidCallback onTap, bool hasUnread}); }`。 - [ ] **Step 1: 建 NotificationBell widget** `notification_bell.dart`:`IconButton`(`PangolinIcons.bell`) + 右上未读红点(`Positioned` 小圆 `context.pangolin.danger`,`hasUnread` 才显)。`hasUnread` 本任务先写死 `true`(演示),`// TODO(spec-3): 未读态来自 notices provider`。 - [ ] **Step 2: 侧栏加 Account 一级项** `desktop_shell.dart:34-40` `items` 在 stats 后 settings 前插: ```dart (icon: PangolinIcons.user, label: t.tabMe, view: NavView.account), ``` - [ ] **Step 3: 顶栏挂铃铛** `content_top_bar.dart` 的 trailing 区(主题切换旁)加 `NotificationBell(hasUnread: true, onTap: onBell)`;`ContentTopBar` 加 `VoidCallback? onBell` 入参。`desktop_shell.dart:104` 传 `onBell: () => go(NavView.notifications)`。 - [ ] **Step 4: content()/titles 补 invite + notifications** `desktop_shell.dart` `titles`(42-55)加 `NavView.invite: t.inviteTitle, NavView.notifications: t.notifTitle`;`content()`(59-88)加 `case NavView.invite: return InviteScreen(t: t, embedded: true);` 和 `case NavView.notifications: return NotificationsScreen(t: t, embedded: true);`(NotificationsScreen 由 Task 6 建,本步先 import 占位——若 Task 6 未完成,临时 `return const SizedBox()` 并 `// TODO`)。`backTarget()`(92-96)靠 `kAccountSubViews` 已含 invite → 自动回 account;notifications 不在集内,返回走顶栏无返回(全屏态)——给 notifications 也返回 account:在 `backTarget` 加 `if (view == NavView.notifications) return () => go(NavView.account);`。 - [ ] **Step 5: tablet_shell titles 补一条** `tablet_shell.dart:39-46` `titles` 加 `NavView.invite: t.inviteTitle, NavView.notifications: t.notifTitle`(内容走 default→AccountPage/push)。 - [ ] **Step 6: analyze(全绿——穷举 switch 现已覆盖)** Run: `cd client && flutter analyze lib/shell/ lib/widgets/notification_bell.dart lib/widgets/content_top_bar.dart` Expected: No issues found。 - [ ] **Step 7: Commit** ```bash git add client/lib/shell/desktop_shell.dart client/lib/shell/tablet_shell.dart client/lib/widgets/notification_bell.dart client/lib/widgets/content_top_bar.dart git commit -m "feat(shell): 桌面侧栏加 Account 一级项 + 顶栏通知铃铛 + invite/notifications 视图接线" ``` --- ### Task 6: 通知占位屏 + 移动顶栏铃铛 **Files:** - Create: `client/lib/screens/notifications_page.dart` - Modify: `client/lib/widgets/app_top_bar.dart`(trailing 铃铛) - Modify: `client/lib/screens/account_page.dart:146`(AppTopBar 传 trailing) - Modify: `client/lib/shell/desktop_shell.dart`(把 Task 5 的占位 import 换成真 NotificationsScreen) **Interfaces:** - Consumes: `t.notifTitle/notifEmpty/notifType*`(Task 2)、`NotificationBell`(Task 5)、`open(NavView.notifications, ...)`。 - Produces: `class NotificationsScreen extends ConsumerWidget { NotificationsScreen({required AppText t, VoidCallback? onBack, bool embedded}); }`。 - [ ] **Step 1: 建 NotificationsScreen 占位** `notifications_page.dart`:标题 `t.notifTitle`,内容 = 3 条示例通知(类型 pill 用 `t.notifType*`,未读点)+ 空态 `t.notifEmpty`(数据为空时)。`// TODO(spec-3): 列表来自 GET /v1/notices,已读态本地持久化`。 - [ ] **Step 2: 桌面 shell 换真屏** `desktop_shell.dart` `content()` 的 notifications case 从占位 `SizedBox` 换成 `NotificationsScreen(t: t, embedded: true)` + import。 - [ ] **Step 3: 移动顶栏挂铃铛** `app_top_bar.dart` 加 `Widget? trailing` 入参并渲染在右侧;`account_page.dart:146` `AppTopBar(brand: t.brand, trailing: NotificationBell(hasUnread: true, onTap: () => open(NavView.notifications, NotificationsScreen(t: t))))`。 - [ ] **Step 4: analyze** Run: `cd client && flutter analyze lib/screens/notifications_page.dart lib/widgets/app_top_bar.dart lib/screens/account_page.dart lib/shell/desktop_shell.dart` Expected: No issues found。 - [ ] **Step 5: Commit** ```bash git add client/lib/screens/notifications_page.dart client/lib/widgets/app_top_bar.dart client/lib/screens/account_page.dart client/lib/shell/desktop_shell.dart git commit -m "feat(notif): 通知占位屏 + 移动顶栏铃铛(Spec ③接口占位)" ``` --- ### Task 7: 设置页归三组 + Web 链接下沉 **Files:** - Modify: `client/lib/screens/settings_page.dart:20-22,54-112` - Test: `client/test/widget/settings_ia_test.dart` **Interfaces:** - Consumes: `t.settingsGroupConn/Appearance/About`(Task 2)。 - [ ] **Step 1: 写设置分组测试(失败)** `settings_ia_test.dart`:pump `SettingsPage`(provider override),断言: ```dart expect(find.text(t.settingsGroupConn), findsOneWidget); expect(find.text(t.settingsGroupAppearance), findsOneWidget); expect(find.text(t.settingsGroupAbout), findsOneWidget); // 购买入口不在设置页(归 Account) expect(find.text(t.purchaseTitle), findsNothing); ``` - [ ] **Step 2: 跑测试确认失败** Run: `cd client && flutter test test/widget/settings_ia_test.dart` Expected: FAIL。 - [ ] **Step 3: 改 settings_page.dart** - 删第一张「购买套餐 + 我的订单」卡(75-89)+ 顶部 `orders_page/payment_page/purchase_page` import(20-22)+ `open()` helper(61-67)。 - 三张卡分组:① `_SectionLabel(t.settingsGroupConn)` + 开关组(autostart/autoConnect/smartRoute/killSwitch);② `_SectionLabel(t.settingsGroupAppearance)` + 语言 + 深色;③ `_SectionLabel(t.settingsGroupAbout)` + 协议 + 检查更新 + Version + **底部** webUserCenter 行(降到最后)。 - [ ] **Step 4: 跑测试确认通过** Run: `cd client && flutter test test/widget/settings_ia_test.dart` Expected: PASS。 - [ ] **Step 5: analyze + golden** Run: `cd client && flutter analyze lib/screens/settings_page.dart && flutter test --update-goldens 2>&1 | tail -3` - [ ] **Step 6: Commit** ```bash git add client/lib/screens/settings_page.dart client/test/widget/settings_ia_test.dart client/test/golden/ git commit -m "feat(settings): 删购买/订单卡 + 归连接/外观/关于三组 + Web 链接下沉底部" ``` --- ### Task 7.5: 订单页处理 expired 态 **Files:** - Modify: `client/lib/screens/orders_page.dart`(状态→pill 映射) **Interfaces:** - Consumes: 后端 `status=="expired"`(Task 0)、`t.orderStatusExpired`(Task 2)。 - [ ] **Step 1: orders_page 状态映射加 expired** `orders_page.dart` 的 `_statusPill`(created→connecting / paid→connected / canceled→neutral)加一支: ```dart 'expired' => (status: PangolinStatus.neutral, label: t.orderStatusExpired), ``` 过期单动作沿用 created 的「继续支付」(台账仍可 retry;retry 若遇 pay 会话过期会 409→客户端已有重新下单兜底)。 - [ ] **Step 2: analyze** Run: `cd client && flutter analyze lib/screens/orders_page.dart` Expected: No issues found。 - [ ] **Step 3: Commit** ```bash git add client/lib/screens/orders_page.dart git commit -m "feat(client/orders): 订单列表/详情显示 expired 过期态" ``` --- ### Task 8: 全量验收 + 收尾 **Files:** 无(验收) - [ ] **Step 1: 全量 analyze** Run: `cd client && flutter analyze` Expected: 0 error(仅 pre-existing 3 条 info)。 - [ ] **Step 2: 全量测试** Run: `cd client && flutter test 2>&1 | tail -3` Expected: All tests passed。 - [ ] **Step 3: DS 双闸** Run: `cd /Users/wangjia/code/pangolin/.claude/worktrees/pay-v2-integration && node tools/check-l1-sync.mjs && node design/prototype/tools/check-ds.mjs` Expected: 图标同集 OK + l10n 完整 OK + check-ds 5 项过。 - [ ] **Step 4: 服务端重编 + 部署(Task 0 改了 server)** ```bash cd server && go test ./... && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -o /tmp/pgl-new ./cmd/server scp /tmp/pgl-new pangolin1:/tmp/pgl-new ssh pangolin1 'cp -a /usr/local/bin/pangolin-server /usr/local/bin/pangolin-server.bak-ia && mv /tmp/pgl-new /usr/local/bin/pangolin-server && chmod +x /usr/local/bin/pangolin-server && systemctl restart pangolin-server && sleep 2 && systemctl is-active pangolin-server' ``` Expected: `active`。验证 `curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/v1/pay/orders` → 401(路由在)。 - [ ] **Step 5: 更新决策日志** `docs/pay-v2-dev-log.md` 追加「IA 重构落地 + 订单过期态」step。 - [ ] **Step 6: Commit** ```bash git add docs/pay-v2-dev-log.md git commit -m "docs(pay-v2): IA 重构 + 订单过期态 落地验收记录" ``` --- ## Self-Review **Spec coverage**(对照 `account-ia-reorg-design.html` §5 落地改动清单): - navigation_provider 加 invite ✅ Task 1 - desktop_shell 侧栏 Account + 顶栏铃铛 + content 分支 ✅ Task 5 - account_page 重排/邀请/删偏好卡/formFactor 门控 ✅ Task 4 - settings_page 归组 + Web 下沉 ✅ Task 7 - account_screens RedeemScreen 删去哪买 ✅ Task 3 - 顶栏铃铛槽(content_top_bar + app_top_bar)✅ Task 5/6 - 邀请/通知占位屏 ✅ Task 4/6 - l10n ✅ Task 2 **已就绪不做**(盘点确认):设备⋮菜单、登录/注册、SegSwitch、图标、支付/订单 l10n。 **留给后续 spec**:邀请奖励引擎/归因/防刷(Spec ②)、通知 model/provider/api/下发/已读持久化(Spec ③)——本计划仅占位屏 + `// TODO(spec-N)` 接口标注。 **依赖顺序**:1→2→(3,4 可并)→5→6→7→8。Task 5 前向引用 NotificationsScreen(Task 6),已注明临时占位处理。