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

25 KiB
Raw Blame History

账户/设置/联系 信息架构重构 — 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 加:

// 老 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.dbStore 的私有字段,测试同包(package pay)可访问。

  • Step 2: 跑测试确认失败

Run: cd server && go test ./internal/pay/ -run TestList_ExpiredWhenStale Expected: FAIL(old 仍为 created)。

  • Step 3: 实现过期判定

handler.go 加常量 + 在 orderViewFromRow 里判定:

// 未付订单展示层过期窗口(近似 pay 会话时效;仅影响列表/详情显示,不改台账)。
const orderPendingTTL = 30 * time.Minute

orderViewFromRow 组装 v 后、return v 前加:

	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
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.inviteNavView.notifications 枚举值;kAccountSubViewsinvite

  • Step 1: 加枚举值 + 子页登记

navigation_provider.dart 第 7 行 enum NavView 末尾加 , invite, notifications

enum NavView { connect, servers, stats, account, contact, settings, plans, redeem, devices, purchase, payment, orders, invite, notifications }

第 10 行 kAccountSubViewsinvite(notifications 走全屏,不入子页返回集):

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
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, inviteProgressTitlenotifTitle, notifEmpty, notifTypeNews, notifTypeFeature, notifTypeImportantsettingsGroupConn, settingsGroupAppearance, settingsGroupAbout

  • Step 1: app_text.dart 加抽象 getter

app_text.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>.dartqrNotSupported 之后加对应 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
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

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'emailt.contactEmailwebsitet.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
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.dartdesktop 态(debugDefaultTargetPlatformOverride=windows + 宽 surface + provider override,参考 test/widget/account_desktop_nav_test.dart)pump AccountPage(isWide:true),断言:

// 邀请入口存在
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
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.dartIconButton(PangolinIcons.bell) + 右上未读红点(Positioned 小圆 context.pangolin.dangerhasUnread 才显)。hasUnread 本任务先写死 true(演示)// TODO(spec-3): 未读态来自 notices provider

  • Step 2: 侧栏加 Account 一级项

desktop_shell.dart:34-40 items 在 stats 后 settings 前插:

(icon: PangolinIcons.user, label: t.tabMe, view: NavView.account),
  • Step 3: 顶栏挂铃铛

content_top_bar.dart 的 trailing 区(主题切换旁)加 NotificationBell(hasUnread: true, onTap: onBell)ContentTopBarVoidCallback? onBell 入参。desktop_shell.dart:104onBell: () => go(NavView.notifications)

  • Step 4: content()/titles 补 invite + notifications

desktop_shell.dart titles(42-55)加 NavView.invite: t.inviteTitle, NavView.notifications: t.notifTitlecontent()(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:在 backTargetif (view == NavView.notifications) return () => go(NavView.account);

  • Step 5: tablet_shell titles 补一条

tablet_shell.dart:39-46 titlesNavView.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
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.dartWidget? 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
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.dartpump SettingsPage(provider override),断言:

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
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)加一支:

'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
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)
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
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),已注明临时占位处理。