Files
pangolin/docs/superpowers/plans/2026-07-12-account-ia-reorg.md
T

529 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 账户/设置/联系 信息架构重构 — 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 0display-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<NavView> 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_<lang>.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<ContactChannel> 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 / domainlaunchChannel 据此推导 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 → 自动回 accountnotifications 不在集内,返回走顶栏无返回(全屏态)——给 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),已注明临时占位处理。