diff --git a/.specs/plans/frontend-modal-identity-badges.md b/.specs/plans/frontend-modal-identity-badges.md new file mode 100644 index 00000000..bb503a93 --- /dev/null +++ b/.specs/plans/frontend-modal-identity-badges.md @@ -0,0 +1,80 @@ +# Plan: Frontend Modal Identity and Badges + +## Architectural Decisions + +- **Dependency**: This plan starts only after `frontend-modal-runtime` Phase 2 supplies `AksrtModalRuntime`, loader/cache/state contracts, telemetry marks, and the rollout reader. +- **Authentication snapshot**: `AuthApply` and `ApplicationRepository` expose one current-user snapshot containing verification state, latest application, form payload, allowed next action, and poll metadata. The REST handler and owner permission callback reuse that loaded application instead of querying by UUID twice. +- **External provider boundary**: Aliyun/Tencent validation and refresh stay behind `ApplicationService`. Provider HTTP work starts after the shell is visible, is cancellable/visibility-aware where possible, and cannot prevent the modal from opening. +- **Badge canonical path**: The JSON REST path is the only new badge read/write path. User-center and public author entry points share the same privacy-aware DTO; old HTML AJAX modal registrations and post-wear remote reopening are removed after cutover. +- **Privacy**: Cache keys include user/object/context and public-display mode. Authentication evidence remains owner/capability protected and no-store; public badge data follows `list_for_user()` privacy rules. No private DTO is persisted in browser storage. +- **Domain cache**: Badge owned/wear maps and authentication snapshots use short in-memory/request-context memoization. Any submit, review, wear, or status transition invalidates the affected tag before rendering the next view. +- **Admin boundary**: Existing native authentication review and badge management remain the operational surface. This plan does not move them into the Admin SPA. + +--- + +## Phase 1 — Authentication status appears quickly and refreshes safely + +**User stories**: US8, US9, US13. + +### What to build + +Replace the three independent SSR facade reads in `inc/helpers/user-center-renderers/profile-auth.php` with one typed snapshot from `src/User/AuthApply.php`/`src/User/ApplicationRepository.php`. Shape the current identity REST response around the same snapshot and reuse the application loaded by `owns_application()` in `src/Identity/RestController.php` inside get/refresh/manual handlers. Update `assets/js/routes/user-center-auth.js` to open the unified shell immediately, render snapshot state, defer provider refresh until the modal is open or the user explicitly refreshes, pause while hidden, apply backoff, and cancel stale refreshes. Preserve the existing create, manual evidence, provider callback, nonce, owner, capability, and sensitive no-store boundaries. + +### Done when + +- [ ] `profile-auth.php`, `AuthApply`, and `ApplicationRepository` produce one active/latest/verified snapshot per request; browser harness scenario `auth-snapshot-query-count` with `SAVEQUERIES` shows no repeated identical application SELECT for the active panel. +- [ ] `RestController` owner permission and get/refresh/manual handlers reuse one loaded application per request; `php -l src/User/AuthApply.php src/User/ApplicationRepository.php src/Identity/RestController.php src/Identity/ApplicationService.php` passes. +- [ ] Browser scenario `auth-modal-deferred-refresh` verifies shell-first rendering, no polling while the modal is closed, pause on hidden visibility, minimum interval/backoff, cancellation on close, and a useful error when the provider exceeds its timeout. +- [ ] Browser/API scenarios cover create, provider-pending, processing, verified, rejected/manual retry, evidence upload, duplicate submit, unauthorized owner, and sensitive evidence isolation; existing native review changes are reflected on the next snapshot. + +--- + +## Phase 2 — Badge modal uses one cached JSON path + +**User stories**: US10, US11, US12, US13. + +### What to build + +Refactor `src/User/Badges.php` so `list_for_user()`, `get_owned_map()`, and `get_wear()` share one context and do not rebuild owned/wear dictionaries repeatedly. Remove duplicate `Badges::init()` calls from `functions.php`. Update `assets/js/user-badges.js` to use the runtime loader, short cache, skeleton/error/retry states, and tag invalidation after wear. Replace the user-center and public author HTML/AJAX modal entry points in `inc/helpers/user-center-renderers/profile-header.php`, `templates/parts/user/author-hero.php`, and `assets/js/core/remote-modal.js` with the canonical privacy-aware JSON path. The old `wp_ajax(_nopriv)_aksrt_*badge*modal` handlers and post-wear remote reopen must disappear after this cutover. + +### Done when + +- [ ] `SAVEQUERIES` scenario `badge-read-query-count` shows one shared owned/wear/dictionary read context per badge response, and a repeated open for the same user/context uses the runtime cache without cross-user reuse. +- [ ] `assets/js/user-badges.js` displays skeleton/empty/error/retry states, re-renders the current list after wear, and no longer calls `AksrtRemoteModal.openRemote` after a successful wear; `node --check assets/js/user-badges.js assets/js/core/remote-modal.js` passes. +- [ ] `git grep -n "aksrt_user_badge_info_modal\|aksrt_single_badge_info_modal\|get_info_link"` shows no active target entry after the canonical path is enabled, and `functions.php` contains one badge singleton initialization. +- [ ] Browser/API scenarios cover no badges, earned/unearned/in-progress/detail, wear success/failure, public author privacy, self/manager visibility, unauthenticated access, REST nonce, and no leakage through cache keys. + +--- + +## Phase 3 — Authentication and badges are independently releasable + +**User stories**: US1, US2, US3, US11, US12, US13, US14. + +### What to build + +Connect identity and badge loaders to the per-domain rollout values from `frontend-modal-runtime`. Make successful authentication transitions, manual review results, badge wear, and badge management invalidate the appropriate runtime tags and update header/profile signals without a full reload. Exercise the two domains as separate gray releases, then remove the old domain-specific modal branches once each switch is permanently enabled. + +### Done when + +- [ ] The authentication and badge rollout values independently select the new loader path; toggling one does not change the other and does not create a second data store or dual-read branch. +- [ ] `php tools/audit-admin-contracts.php` exits 0 and `node --check assets/js/routes/user-center-auth.js assets/js/user-badges.js` passes after the final route/schema changes. +- [ ] Desktop/mobile scenarios verify shell/ready/error budgets, state refresh behind the modal, focus/keyboard behavior, no horizontal overflow, and no stale private data after logout or account switch. +- [ ] The gray-release checklist records shell/ready/error/status-mismatch metrics and demonstrates pausing the next rollout stage when identity or badge errors exceed the agreed threshold. + +--- + +## Out of Scope + +- 修改会员价格、会员权益、支付网关规则、订单账务、退款核算、认证资格/审核政策或徽章授予条件;这些规则只允许被新的流程正确调用和展示。 +- 新增支付渠道、替换支付供应商、在浏览器保存支付凭证,或改变支付回调/异步结算机制。 +- 新增公开认证资料库、向其他用户暴露认证文档、跨用户查看订单或徽章数据。 +- 把订单、售后、认证审核或徽章运营工作台迁移进配置型 SPA;日常运营仍属于原生 WordPress 后台。 +- 全站页面导航、用户中心信息架构、品牌设计系统或所有非弹窗页面的全面重做;本期只统一弹窗运行时和四类流程所需的轻量体验。 +- 强制页面加载时预取所有弹窗数据、长期持久化敏感弹窗数据、建立订单/认证/徽章的第二套事实存储或长期保留旧新双路实现。 +- 以新增兼容别名、旧 option 双读、静默降级或数据迁移层解决旧实现问题;流程切换后以新的规范化链路为唯一实现。 +- 建立独立的数据仓库、复杂转化漏斗或第三方用户行为追踪系统;本期观测只服务于弹窗性能、错误和业务状态闭环。 + +## Open Questions + +- 认证云供应商是否支持在请求取消/页面隐藏时安全停止远端刷新;若不支持,客户端只能停止轮询并让服务端回调/下次打开完成状态确认。 +- 徽章公开作者入口最终是否必须支持未登录访问;默认保留现有公开可见 DTO,但统一路径仍必须执行业务层 privacy 过滤。 diff --git a/.specs/plans/frontend-modal-loading-upgrade.md b/.specs/plans/frontend-modal-loading-upgrade.md new file mode 100644 index 00000000..4438355c --- /dev/null +++ b/.specs/plans/frontend-modal-loading-upgrade.md @@ -0,0 +1,84 @@ +# Plan: Frontend Modal Loading Upgrade + +This is the coordination plan for the three executable plans below. Implementation work should run in dependency order; the detailed phase checklists live in the linked plan files. + +## Architectural Decisions + +- **Execution split**: The shared modal runtime is a prerequisite. Identity/badges and orders/VIP can then proceed in parallel, each with its own domain API, UI, query optimization, and gray-release evidence. +- **One canonical runtime**: `AksrtModalRuntime` owns shell, skeleton, lifecycle, cancellation, cache, state, focus, and telemetry. Domain scripts only provide loaders and view models. +- **Canonical data boundaries**: Keep `aksrt/v1`, existing domain services, native operational admin, and `AKSRT_Settings` as the boundaries. No duplicate facts, compatibility reads, or long-lived old/new modal implementations. +- **No core table replacement**: Use existing order, identity, user-meta, payment, and badge data. The orders plan may add the required composite index through the existing DB migrator and schema-version process. +- **Staged release**: Roll out identity/badges first, then orders, then VIP payment. Each domain has an independent temporary switch and must meet the performance/error budget before the next switch is enabled. + +--- + +## Phase 1 — Shared modal runtime is ready for domain work + +**Detailed plan**: `.specs/plans/frontend-modal-runtime.md` + +**User stories**: US1–3, US12–14. + +### What to build + +Complete both runtime phases: baseline/rollout observability and the cancellable shell/loader/cache/state contract. This phase is a foundation exception because all four existing flows currently own incompatible modal shells and request lifecycles; the runtime is demoable through the existing remote-modal entry path before domain JSON migrations begin. + +### Done when + +- [ ] `.specs/plans/frontend-modal-runtime.md` Phase 1 and Phase 2 checklists are checked, `php tools/audit-admin-contracts.php` exits 0, and `node --check` passes for all runtime JS. +- [ ] Browser scenarios `modal-baseline`, `modal-shell-lifecycle`, and `modal-cache-dedupe` show shell-first behavior, cancellation, deduplication, invalidation, focus restoration, and cold/warm metrics. +- [ ] `git grep` inventory identifies every old remote-modal caller and labels the remaining domain migration owner before either business plan starts. + +--- + +## Phase 2 — Identity/badges and orders/VIP have vertical slices + +**Detailed plans**: `.specs/plans/frontend-modal-identity-badges.md` and `.specs/plans/frontend-modal-orders-vip.md` + +**User stories**: US4–11, US13. + +### What to build + +Run the identity/badge plan and orders/VIP plan in parallel after Phase 1. Each plan must finish its read path, write path, query/API/UI integration, permission handling, and browser regression scenarios before its gray-release phase. + +### Done when + +- [ ] `.specs/plans/frontend-modal-identity-badges.md` Phases 1–2 and `.specs/plans/frontend-modal-orders-vip.md` Phases 1–2 are checked with their named `SAVEQUERIES`, PHP, JS, API, and browser scenarios passing. +- [ ] Identity/badge and order/VIP loaders use independent cache/invalidation tags and cannot reuse another user or domain's payload. +- [ ] The native order/after-sale and identity/badge operational surfaces remain usable; no operational workbench is added to the Admin SPA. + +--- + +## Phase 3 — All domains are gray-released and old paths are removed + +**Detailed plans**: the Phase 3 sections of all three detailed plans. + +**User stories**: US1–14. + +### What to build + +Release identity/badges, then orders, then VIP payment using independent switches. Compare shell/ready/error/status-mismatch metrics against the PRD budgets, pause the next domain on regression, update user-center summaries after successful writes, and remove old HTML/AJAX/domain-specific modal calls after each domain is permanently enabled. + +### Done when + +- [ ] The Phase 3 checklist in each detailed plan is checked, with `php tools/audit-admin-contracts.php`, changed-file `php -l`, and changed-file `node --check` all passing. +- [ ] Desktop and mobile evidence shows approximately 100ms shell visibility, 200ms cache-hit content, p75 ≤800ms cold content, and p95 ≤1.5s initial budget, with errors/status mismatches recorded per domain. +- [ ] `git grep` confirms no active target old HTML badge modal, duplicate VIP purchase/poll coordinator, hard-coded order REST root, or bypassing domain loader remains after cutover. +- [ ] The final release record identifies the removal version for temporary rollout settings and confirms no compatibility alias, old option double-read, second fact store, or long-lived dual implementation remains. + +--- + +## Out of Scope + +- 修改会员价格、会员权益、支付网关规则、订单账务、退款核算、认证资格/审核政策或徽章授予条件;这些规则只允许被新的流程正确调用和展示。 +- 新增支付渠道、替换支付供应商、在浏览器保存支付凭证,或改变支付回调/异步结算机制。 +- 新增公开认证资料库、向其他用户暴露认证文档、跨用户查看订单或徽章数据。 +- 把订单、售后、认证审核或徽章运营工作台迁移进配置型 SPA;日常运营仍属于原生 WordPress 后台。 +- 全站页面导航、用户中心信息架构、品牌设计系统或所有非弹窗页面的全面重做;本期只统一弹窗运行时和四类流程所需的轻量体验。 +- 强制页面加载时预取所有弹窗数据、长期持久化敏感弹窗数据、建立订单/认证/徽章的第二套事实存储或长期保留旧新双路实现。 +- 以新增兼容别名、旧 option 双读、静默降级或数据迁移层解决旧实现问题;流程切换后以新的规范化链路为唯一实现。 +- 建立独立的数据仓库、复杂转化漏斗或第三方用户行为追踪系统;本期观测只服务于弹窗性能、错误和业务状态闭环。 + +## Open Questions + +- 部署是否已有统一的浏览器 RUM/Server-Timing 收集端;默认先提供 marks、响应头和现有部署观测,不新增主题指标表。 +- 复合订单索引与临时 rollout 设置的删除版本需要在最终发布记录中确认。 diff --git a/.specs/plans/frontend-modal-orders-vip.md b/.specs/plans/frontend-modal-orders-vip.md new file mode 100644 index 00000000..ce086a07 --- /dev/null +++ b/.specs/plans/frontend-modal-orders-vip.md @@ -0,0 +1,83 @@ +# Plan: Frontend Modal Orders and VIP + +## Architectural Decisions + +- **Dependency**: This plan starts only after `frontend-modal-runtime` Phase 2 supplies the canonical shell, loader, cache/state, cancellation, and telemetry contracts. +- **Order view model**: `UserOrders` and `OrderCenterRest` return a minimal list/detail view model with server-derived `allowed_actions`, status version, updated time, and invalidation tags. The front end never derives payment/cancel/after-sale eligibility from local status. +- **Order query shape**: Combine list/stat work where possible, batch resource decoration, and add the missing `user_id/status/created_at` index through `AKSRT_DB_Migrator` with a schema version bump. No new order table or duplicate fact store. +- **Reconciliation boundary**: `Gateway::reconcile_order()` remains the authority for pending external orders but runs after the detail shell is visible and only when needed. A normal completed/non-external detail must not wait for remote provider HTTP. +- **VIP JSON path**: Replace server-generated full HTML from `VipServiceModal` with a minimal JSON view model. `pay-vip.js` and the VIP shop in `credits.js` share one purchase coordinator; catalog, member state, order object, and payment attempt are reused within a request. +- **Payment safety**: Payment gateways and `PaymentOrchestrator` stay server-side. Client polling is bounded, backoff-aware, visibility-aware, cancellable, and terminated by a terminal order state. Core price, eligibility, ledger, callback, and VIP fulfillment rules are unchanged. +- **API configuration**: All order/VIP requests use the globally injected API root and REST nonce. Remove `order-center.js` hard-coded REST construction and do not add compatibility route aliases. +- **Release**: Order and VIP rollout switches are independent. Old paths may remain inactive while a domain is not released, but each domain has one canonical implementation after its switch is permanently enabled. + +--- + +## Phase 1 — Order details appear before remote state reconciliation + +**User stories**: US6, US7, US11, US13. + +### What to build + +Optimize `src/Payment/UserOrders.php` and `src/Payment/OrderRepositoryCore.php` so the order list does not perform a fixed list plus four COUNT queries and resource rows do not cause post/meta N+1 work. Add the composite order index in `src/Core/DBMigratorPart2.php` and bump the schema version through the existing migrator. Shape `src/Payment/OrderCenterRest.php` responses around minimal list/detail view models and preserve owner checks. Update `src/Payment/Gateway.php` so pending external reconciliation is deferred or represented as a refresh state after the shell is visible. Replace `order-center.js` hard-coded `siteUrl/wp-json` construction with injected `apiUrl/restUrl`, then move detail, pay, cancel, refresh, and invalidation through `AksrtModalRuntime`. + +### Done when + +- [ ] `SAVEQUERIES` scenario `order-list-query-count` verifies that the normal first page no longer issues list + four independent COUNT queries and that resource-order decoration has no per-row post/meta N+1; empty/overflow pages remain correct. +- [ ] `DBMigratorPart2` defines the `user_id/status/created_at` query index and the active schema version is bumped; after migration, `SHOW INDEX` or the WordPress DB harness verifies the index. +- [ ] Browser scenario `order-detail-shell` verifies summary skeleton first, ordinary detail does not wait for provider HTTP, pending reconciliation reports refresh state, and detail/status/list update after pay/cancel/after-sale responses. +- [ ] `order-center.js` reads the injected API root, and `php -l src/Payment/UserOrders.php src/Payment/OrderRepositoryCore.php src/Payment/OrderCenterRest.php src/Payment/Gateway.php src/Core/DBMigratorPart2.php` plus `node --check assets/js/order-center.js` pass. +- [ ] API/browser cases cover another-tab status changes, unauthorized order numbers, duplicate action clicks, timeout/retry, pagination, and no cross-user cache reuse. + +--- + +## Phase 2 — VIP purchase reaches payment result through one coordinator + +**User stories**: US4, US5, US11, US13. + +### What to build + +Change `src/Content/VipServiceRest.php` and `src/Content/VipServiceModal.php` from full HTML modal responses to a minimal JSON view model that contains the server-calculated catalog, member state, payment methods, allowed actions, and version metadata. Refactor `src/Content/VipServicePurchase.php` and `src/Payment/PaymentOrchestrator.php` to reuse the catalog/member/order context instead of reading the same data after create and again during pay. Consolidate the remote modal path in `assets/js/pay-vip.js` and the embedded shop path in `assets/js/credits.js` into one purchase/poll coordinator; keep the existing `credits-vip.php` and `vip-tab.php` entry points but route their actions through it. Replace the fixed 40 × 3-second loop with bounded backoff, visibility handling, abort, and terminal-state invalidation. + +### Done when + +- [ ] `SAVEQUERIES` scenario `vip-purchase-query-count` shows no duplicate member-level/catalog/order reads for modal load and purchase create/pay; `VipServiceRest` reuses the catalog user level instead of recomputing it. +- [ ] `pay-vip.js` and `credits.js` share one VIP purchase/poll coordinator; `git grep -n "setInterval\|pay/content/order\|pay-vip/modal" assets/js/pay-vip.js assets/js/credits.js` shows no second fixed 40 × 3-second loop or old full-HTML injection path. +- [ ] Browser scenario `vip-purchase-flow` covers not-enrolled, active, expired, renewal/upgrade, package/price, points exchange, gateway redirect, cancel, failure, async success, duplicate click, and return to order detail. +- [ ] Payment response inspection confirms no gateway secret reaches the browser; existing gateway, order callback, VIP fulfillment, core price, and entitlement rules pass regression scenarios. +- [ ] `php -l src/Content/VipServiceRest.php src/Content/VipServiceModal.php src/Content/VipServicePurchase.php src/Payment/PaymentOrchestrator.php` and `node --check assets/js/pay-vip.js assets/js/credits.js` pass. + +--- + +## Phase 3 — Orders and VIP are independently gray-releasable + +**User stories**: US1–US7, US11–US14. + +### What to build + +Connect order and VIP coordinators to the independent rollout switches, invalidate user-center order/VIP summaries after successful writes, and verify native order/after-sale operations remain the operational backend. Release orders first, measure shell/ready/error/status mismatch, then release VIP payment only after the order slice is stable. Remove each domain's old modal calls once its switch is permanently enabled. + +### Done when + +- [ ] Order and VIP rollout values independently select the new loaders; toggling one does not change the other or create a second order/member fact store. +- [ ] `php tools/audit-admin-contracts.php` exits 0, all changed PHP files pass `php -l`, and `node --check assets/js/order-center.js assets/js/pay-vip.js assets/js/credits.js` passes. +- [ ] Desktop/mobile scenarios verify shell/ready budgets, cache-hit and cold-load p75/p95, payment/order state synchronization, permission errors, retry behavior, focus, and no horizontal overflow. +- [ ] Gray-release evidence shows orders reaching the agreed 100ms shell, 200ms cache-hit, p75 ≤800ms, and p95 ≤1.5s initial budgets before VIP is enabled; rollback is demonstrated by disabling only the affected domain switch. + +--- + +## Out of Scope + +- 修改会员价格、会员权益、支付网关规则、订单账务、退款核算、认证资格/审核政策或徽章授予条件;这些规则只允许被新的流程正确调用和展示。 +- 新增支付渠道、替换支付供应商、在浏览器保存支付凭证,或改变支付回调/异步结算机制。 +- 新增公开认证资料库、向其他用户暴露认证文档、跨用户查看订单或徽章数据。 +- 把订单、售后、认证审核或徽章运营工作台迁移进配置型 SPA;日常运营仍属于原生 WordPress 后台。 +- 全站页面导航、用户中心信息架构、品牌设计系统或所有非弹窗页面的全面重做;本期只统一弹窗运行时和四类流程所需的轻量体验。 +- 强制页面加载时预取所有弹窗数据、长期持久化敏感弹窗数据、建立订单/认证/徽章的第二套事实存储或长期保留旧新双路实现。 +- 以新增兼容别名、旧 option 双读、静默降级或数据迁移层解决旧实现问题;流程切换后以新的规范化链路为唯一实现。 +- 建立独立的数据仓库、复杂转化漏斗或第三方用户行为追踪系统;本期观测只服务于弹窗性能、错误和业务状态闭环。 + +## Open Questions + +- 订单/支付环境是否提供现有 Server-Timing 或网关状态监控收集端;默认使用响应头和现有部署观测,不新增主题内指标表。 +- 复合订单索引在生产大表上的部署窗口需要由发布流程确认;迁移必须遵循现有 `AKSRT_DB_Migrator` 原子升级约束。 diff --git a/.specs/plans/frontend-modal-runtime.md b/.specs/plans/frontend-modal-runtime.md new file mode 100644 index 00000000..5fc34207 --- /dev/null +++ b/.specs/plans/frontend-modal-runtime.md @@ -0,0 +1,72 @@ +# Plan: Frontend Modal Runtime + +## Architectural Decisions + +- **Canonical runtime**: Refactor `assets/js/core/remote-modal.js` into the single `AksrtModalRuntime` seam for shell creation, lifecycle, focus, loading, error, cancellation, and refresh. Domain scripts provide loaders and view-model renderers; they do not create competing modal shells. +- **Loader contract**: A loader receives a domain context and abort signal and returns a minimal view model plus render/update callbacks. It may read only the active modal's data. +- **Cache contract**: Use in-memory, user-scoped cache entries with short TTLs, in-flight request deduplication, explicit invalidation tags, and complete clearing on logout/account switch. Do not persist private modal payloads in localStorage. +- **State contract**: All domains use the same `idle`, `loading`, `ready`, `empty`, `submitting`, `success`, `error`, `unauthorized`, and `stale` states. Skeletons match the final layout and never represent business truth. +- **API boundary**: Existing `aksrt/v1` domain routes remain the service boundary; HTML modal responses are replaced domain by domain with minimal JSON view models. Every changed REST route keeps a `permission_callback`; AJAX is not a new data path. +- **Request adapter**: Add one thin `AksrtRequest` adapter for the injected REST root, nonce headers, JSON/error envelopes, 401 handling, and abort signals. Domain files stop maintaining their own REST-root and response-parsing variants. +- **Rehydrate and feedback**: Remote HTML that remains during migration must call `AksrtFrontend.rehydrate(body, 'modal')`; shared feedback must call the existing callable `AksrtToast` API (with `aksrtNotify`/alert fallback), not assume an incompatible `.show` method. Runtime binding must be idempotent so `pay-vip.js` does not register both registry and immediate fallback handlers. +- **Rollout**: Add a temporary per-domain rollout section under the existing Tools configuration surface, read through `AKSRT_Settings`. One canonical new implementation is enabled per domain; the switch is for staged release, not a compatibility data layer. +- **Telemetry**: Use browser `performance.mark/measure` and server `Server-Timing`/existing deployment observability by default. Do not add a private payload or payment/identity-content event store in this foundation plan. +- **Build boundary**: Frontend assets are plain theme JavaScript, not the Admin SPA bundle. Run `node --check` for changed JS and run the admin contract audit only when the rollout schema/controller changes. + +--- + +## Phase 1 — Modal latency is measurable and rollout is controllable + +**User stories**: US1, US2, US3, US14. + +### What to build + +Create the typed rollout reader and configuration contract, then add a small telemetry module that marks the existing VIP, order, identity, and badge open flows without changing their business behavior. Establish cold-cache and warm-cache baselines for click-to-shell, click-to-first-content, click-to-ready, request count, SQL count, response size, and error outcome. The rollout reader must be consumed through `AKSRT_Settings` and the existing Tools schema/controller path, not a raw option read. + +Primary integration points: + +- `AKSRT_Settings` and the Tools schema/controller for `frontend_modal_rollout`. +- `assets/js/core/modal-telemetry.js` (new) and the existing feature enqueue/config injection in `src/Core/ScriptsPart1c.php` and the surface script enqueue modules. +- Existing flows in `assets/js/pay-vip.js`, `assets/js/order-center.js`, `assets/js/routes/user-center-auth.js`, and `assets/js/user-badges.js` for baseline marks only. + +### Done when + +- [ ] The Tools schema, REST response, and `AKSRT_Settings` reader expose independent VIP, order, identity, and badge rollout values; `php tools/audit-admin-contracts.php` exits 0. +- [ ] `assets/js/core/modal-telemetry.js` emits click, shell, first-content, ready, action-start, action-end, and error marks without including payment credentials, order contents, or identity evidence. +- [ ] Browser scenario `modal-baseline` records cold and warm measurements for all four current entry points, and the baseline artifact names the request/SQL counts used for later comparison. +- [ ] `node --check assets/js/core/modal-telemetry.js assets/js/pay-vip.js assets/js/order-center.js assets/js/routes/user-center-auth.js assets/js/user-badges.js` passes. + +--- + +## Phase 2 — Any modal opens with a cancellable shell and reusable cache + +**User stories**: US1, US2, US3, US12, US13. + +### What to build + +Refactor `AksrtRemoteModal` in `assets/js/core/remote-modal.js` into the canonical runtime while preserving its existing visual shell as the first implementation. Add focused runtime modules for loader registration, cache/state, telemetry integration, and the `AksrtRequest` adapter. The runtime must support a skeleton before data, JSON or HTML during the migration boundary, abort signals on close/replacement, request deduplication, explicit invalidation, focus return, keyboard/overlay close, mobile bottom-sheet behavior, and retryable errors. When HTML is still returned, invoke `AksrtFrontend.rehydrate` for modal-scoped handlers and route notifications through the callable Toast adapter. Wire the existing generic remote-modal trigger set through this runtime, make feature registration idempotent, remove the pay-vip registry/immediate-bind double path, and inventory every remaining caller before domain migrations begin. + +### Done when + +- [ ] `assets/js/core/remote-modal.js` exposes one canonical `AksrtModalRuntime.open/close/refresh` lifecycle, `AksrtRequest` owns REST root/nonce/envelope handling, and no domain script creates a second global modal host. +- [ ] `node --check assets/js/core/remote-modal.js assets/js/core/request.js assets/js/core/modal-runtime.js assets/js/core/modal-cache.js assets/js/core/modal-state.js assets/js/pay-vip.js` passes, using the actual new filenames if the implementation keeps modules consolidated. +- [ ] Browser scenario `modal-shell-lifecycle` verifies shell visibility within the 100ms initial budget, skeleton/error/retry states, ESC and overlay close, focus return, request cancellation, mobile bottom-sheet behavior, callable Toast fallback, and `AksrtFrontend.rehydrate(body, 'modal')` after remote HTML. +- [ ] Browser scenario `modal-cache-dedupe` verifies one read for concurrent/repeated opens, invalidation after a successful write, no reuse after logout/account switch, and one pay-vip binding; `git grep -n "AksrtRemoteModal\|data-toggle=\"AksrtRemoteModal\"\|data-aksrt-remote-modal"` produces an explicit migration inventory. + +--- + +## Out of Scope + +- 修改会员价格、会员权益、支付网关规则、订单账务、退款核算、认证资格/审核政策或徽章授予条件;这些规则只允许被新的流程正确调用和展示。 +- 新增支付渠道、替换支付供应商、在浏览器保存支付凭证,或改变支付回调/异步结算机制。 +- 新增公开认证资料库、向其他用户暴露认证文档、跨用户查看订单或徽章数据。 +- 把订单、售后、认证审核或徽章运营工作台迁移进配置型 SPA;日常运营仍属于原生 WordPress 后台。 +- 全站页面导航、用户中心信息架构、品牌设计系统或所有非弹窗页面的全面重做;本期只统一弹窗运行时和四类流程所需的轻量体验。 +- 强制页面加载时预取所有弹窗数据、长期持久化敏感弹窗数据、建立订单/认证/徽章的第二套事实存储或长期保留旧新双路实现。 +- 以新增兼容别名、旧 option 双读、静默降级或数据迁移层解决旧实现问题;流程切换后以新的规范化链路为唯一实现。 +- 建立独立的数据仓库、复杂转化漏斗或第三方用户行为追踪系统;本期观测只服务于弹窗性能、错误和业务状态闭环。 + +## Open Questions + +- 部署环境是否已有 RUM/Server-Timing 收集端?默认先交付浏览器 marks 与响应头,不在主题内新增持久化指标表。 +- `frontend_modal_rollout` 的临时开关在所有领域切换完成后的删除版本,需要在发布记录中确定。 diff --git a/.specs/prds/frontend-modal-loading-upgrade.md b/.specs/prds/frontend-modal-loading-upgrade.md new file mode 100644 index 00000000..6c8cbb78 --- /dev/null +++ b/.specs/prds/frontend-modal-loading-upgrade.md @@ -0,0 +1,196 @@ +# 前台弹窗统一加载与业务流程升级 + +## Problem Statement + +前台多个用户操作弹窗在点击后需要等待较长时间才出现可用内容。受影响的场景包括会员中心的续费/购买、订单中心的订单详情、官方认证以及我的徽章;同类问题还可能存在于其他前台弹窗。 + +当前体验的问题不是单一的视觉延迟,而是弹窗入口、数据请求、业务操作和结果刷新之间缺少统一的快速反馈与状态协议: + +- 点击后可能出现空白等待,用户无法判断弹窗是否已经打开、请求是否正在进行或操作是否失败。 +- 同一用户短时间内重复打开弹窗时,可能重复请求相同数据,且关闭弹窗后仍可能留下未完成的请求或重复初始化。 +- 会员购买、订单操作、认证提交和徽章查看各自拥有不同的加载、成功、失败和刷新反馈,用户难以形成稳定预期。 +- 业务状态变更后,弹窗和背后的用户中心/订单列表可能继续显示旧数据。 +- 移动端弹窗的可用空间、按钮触达和错误反馈需要与桌面端保持一致。 +- 站点已有支付、商城订单、认证、徽章和原生后台操作能力,但缺少围绕弹窗体验的统一性能预算、缓存失效和前后台状态闭环。 + +本需求将前台弹窗性能优化升级为一套统一的弹窗运行时和四类业务流程改造:会员购买链路、订单处理链路、官方认证链路、徽章获取链路。流程可以重排、合并和改善状态反馈,但会员价格/权益、订单账务、认证资格与审核规则、徽章授予条件等核心业务规则继续由现有配置和领域服务决定。 + +## Current State + +- 前台资源采用模块化 JavaScript 与 CSS;用户中心、商城、支付和其他前台功能存在多个独立交互入口。具体资源拆分、重复初始化和浏览器主线程耗时仍需在实现前用基线测量确认。 +- 用户中心只对当前活动面板做服务端渲染,但用户中心外壳、顶部信息和仪表盘仍会计算统计、用户元数据和公开身份信号;打开一个弹窗前可能已经承担与当前操作无关的页面级计算。 +- 会员弹窗当前由服务端生成完整 HTML;前端每次打开都会重新请求该 modal,服务端渲染目录后又重复读取会员等级。目录和弹窗没有持久缓存,设置只有请求内记忆。无 VIP 元数据时,会员等级还会回退查询有效会员订单;外部支付完成后前端每 3 秒轮询一次,最多轮询 40 次。 +- 订单中心列表目前会执行列表查询、总数查询和多项统计查询,首屏通常包含多次 COUNT;订单行装饰可能按行读取标题、缩略图和链接,资源订单存在 N+1 风险。订单表现有索引没有覆盖用户、状态和创建时间的复合查询。待处理的外部订单详情会触发网关远程对账,虽然有短暂节流,但没有详情视图缓存。 +- 订单前端有硬编码 REST 基础地址而没有统一使用已注入的地址回退;这会增加环境配置和部署差异带来的失败面。 +- 官方认证用户中心面板会服务端读取验证状态、最新申请和申请数据;这些 facade 调用会重复查询申请记录。认证 REST 的所有者权限检查与处理器也会重复按 UUID 查询。待审核页面初始化即轮询,页面重新可见时立即刷新;刷新可能调用外部云服务,HTTP 超时可达 20 秒,当前没有轮询或对象缓存。 +- 徽章入口每次打开都会请求用户徽章列表,没有客户端缓存;佩戴成功后又通过旧 AJAX 打开单个徽章 modal,形成 REST 与 AJAX 混流。旧 AJAX 仍包含公开入口和 HTML modal 路径,部分路径没有 nonce;徽章服务的初始化也存在重复调用。 +- 商城、订单、支付、认证和徽章已有领域服务与原生后台操作能力;本需求优先复用这些能力,而不是在弹窗中复制业务规则。商城、订单/售后和认证审核等运营工作继续属于原生 WordPress 后台,配置内容继续属于管理端 SPA。 +- 当前仓库没有统一的前台弹窗性能预算或通用的“开壳 → 数据加载 → 内容就绪 → 业务操作 → 刷新”协议;四类功能除请求内设置 memo 和支付对账的短暂节流外,没有统一的持久/对象/视图缓存。 +- WordPress REST 端点必须位于现有 `aksrt/v1` 命名空间并具备权限回调;AJAX 操作必须继续使用 nonce 校验。仓库没有已提交的 PHPUnit、JavaScript 单元测试或 phpcs 测试套件;验证依赖静态契约审计、构建检查、WordPress 运行环境和浏览器场景验收。 + +## Solution + +### 统一弹窗体验 + +所有前台弹窗统一采用以下交互时序: + +1. 用户点击入口后立即创建弹窗壳,先显示标题、关闭控件和与最终布局一致的骨架屏;不得在数据返回前显示长时间空白。 +2. 只加载当前打开弹窗所需的数据,不因打开一个弹窗而请求其他弹窗的数据。弹窗壳和最小运行时可在页面初始化阶段准备,但不强制预取全部业务数据。 +3. 数据返回后以稳定布局替换骨架,减少内容跳动;数据为空、无权限、请求失败和超时都使用明确的状态面板,不使用误导性的空白。 +4. 关闭弹窗时取消仍未需要的请求和监听器;再次打开时复用短时间内成功取得的数据,并使用单请求去重避免并发重复请求。 +5. 业务操作显示处理中状态,防止重复点击;操作成功后只刷新受影响的领域数据和背后的页面摘要,不进行整页刷新。 +6. 操作失败时保留用户已填写或已选择的内容,在不泄露敏感数据的前提下提供可重试反馈;需要重新认证、无权限或状态过期时给出明确下一步。 +7. 统一桌面端和移动端的标题栏、关闭方式、焦点管理、键盘操作、触摸区域、按钮反馈、骨架和错误状态;保留每类业务自身的信息层级,不进行无必要的完整视觉重做。 + +### 四类业务流程 + +#### 会员购买与续费 + +- 在一个连续流程中展示可购买/续费/升级的套餐、既有会员状态、权益和价格信息。 +- 提交前明确订单金额、优惠/权益适用状态和支付承接方式;不在前端重新计算或修改服务端价格。 +- 支付创建、跳转、取消、失败和异步完成均有明确状态;成功后刷新会员状态、订单摘要和相关入口,失败后允许安全重试。 +- 复用现有支付网关和订单原语,不在弹窗中保存或暴露支付密钥,不改变支付回调和账务规则。 +- 用最小化 JSON 视图模型替代每次打开都生成完整 HTML;目录、会员状态和支付结果按阶段加载,支付轮询改为有上限、有退避、可见性敏感且可被最终状态终止的同步机制。 + +#### 订单详情与处理 + +- 订单详情先显示订单状态和摘要骨架,再加载商品/服务明细、金额、时间线和允许的操作。 +- 允许的取消、支付、退款/售后等操作由服务端返回,前端不得根据本地状态臆造操作权限。 +- 操作提交后立即进入处理中状态;服务端状态变化后刷新订单详情和订单列表摘要,避免继续显示旧状态。 +- 将列表统计和订单行装饰改为可批量取得或按需取得,消除首屏无关 COUNT 与资源订单 N+1;详情对账只在必要时触发,避免打开普通详情就同步等待外部网关。 +- 支持支付异步完成、请求超时、订单已被其他页面处理以及重复点击等边界状态。 + +#### 官方认证 + +- 弹窗根据当前用户返回未申请、待审核、已通过、已拒绝或需要补充资料等状态,并只显示当前状态允许的操作。 +- 认证资料展示、提交、补交和结果反馈使用统一校验与错误提示;提交期间防止重复提交,失败时保留可安全恢复的输入。 +- 后台审核人员可以查看必要资料、更新审核结果并填写可对用户展示的原因;用户端在审核状态变化后可主动刷新或在下次打开时得到新状态。 +- 将状态、最新申请和申请表单数据合并为一次最小读取;所有者检查与处理器复用同一份已加载申请,避免重复 UUID 查询。待审核刷新采用节流、退避和页面可见性控制,外部云服务调用不得阻塞弹窗壳。 +- 认证资料属于敏感用户数据,响应只返回当前用户可见字段;后台能力继续执行现有角色和权限校验。 + +#### 我的徽章 + +- 以分组或分类方式展示已获得、未获得和进行中的徽章状态;详情中说明现有授予条件或进度。 +- 徽章列表和低频变化的说明数据可使用短缓存;领取、达成或后台调整后立即失效并刷新相关列表。 +- 统一 REST 与旧 AJAX 的读取/详情路径,最终以一个带权限和 nonce 边界的规范化数据链路为唯一实现;移除旧 HTML modal 混流,避免佩戴后再次重复请求。 +- 空列表、未满足条件、数据加载失败和徽章详情不存在均有明确状态,不把“未获得”误显示为请求失败。 +- 复用现有徽章定义和授予规则,不在前端复制条件判断或直接授予徽章。 + +### 前后台闭环与分阶段灰度 + +- 前台弹窗、REST/AJAX 数据源、领域服务和原生后台操作共同组成一条状态闭环。 +- 首先交付公共弹窗运行时和徽章/认证流程,其次交付订单流程,最后切换会员支付流程;每阶段以性能、错误率和业务回归指标达标后再放量。 +- 灰度使用按领域启用的临时发布控制,不创建两套长期业务数据或双读兼容层。每个领域切换到新流程后,以新流程作为唯一前台实现。 +- 运营工作仍在原生 WordPress 后台处理;SPA 只承载配置类内容。若认证审核、订单售后或徽章管理缺少前台所需的后台承接,应补齐对应原生操作能力,而不是把操作台塞入配置 SPA。 + +## User Stories + +1. 作为已登录用户,我点击任一前台弹窗入口后,可以在约定的时间预算内先看到弹窗标题、关闭控件和骨架,而不是面对空白页面。 +2. 作为用户,我打开同一弹窗时只触发当前业务所需的数据请求;短时间内再次打开可以复用最近成功数据,不必重复等待。 +3. 作为用户,我关闭弹窗或快速切换弹窗时,旧请求不会覆盖新弹窗内容,也不会造成重复监听、重复请求或页面卡顿。 +4. 作为会员用户,我可以在一个连续流程中完成续费或升级选择、价格确认、支付承接和结果确认;取消、失败和异步支付完成都有可理解的反馈。 +5. 作为普通用户,我可以查看可购买的会员套餐和权益,但价格与购买资格以服务端返回为准,前端不会显示可执行但实际无权完成的操作。 +6. 作为用户,我打开订单详情时可以先看到订单状态摘要,再查看明细和时间线;订单状态变化后详情与订单列表会同步更新。 +7. 作为用户,我可以在订单允许时执行支付、取消或售后操作;重复点击、状态已改变、请求超时和服务端拒绝均不会产生重复订单操作。 +8. 作为用户,我可以在认证弹窗看到当前状态、可执行的下一步和审核反馈;提交或补交资料时不会因重复点击创建重复申请。 +9. 作为认证审核人员,我可以在原生后台查看必要资料、处理认证结果并填写用户可见原因;前台重新打开或刷新后能看到最新状态。 +10. 作为用户,我可以快速查看已获得、未获得和进行中的徽章,并理解现有条件或进度;徽章数据为空与请求失败不会混淆。 +11. 作为用户,我完成购买、订单操作、认证提交或徽章状态变化后,相关弹窗和背后的用户中心/订单列表不会继续展示明显过期的数据。 +12. 作为移动端用户,我可以在窄屏中打开、关闭、滚动和提交弹窗,主要按钮有足够触摸区域,加载、错误和成功反馈不被遮挡。 +13. 作为未登录或无权限用户,我不会通过弹窗获得私有订单、认证资料、会员状态或其他用户徽章数据;系统会给出登录或权限提示。 +14. 作为站点管理员,我可以按阶段查看弹窗性能、失败率和业务操作错误,并在某个领域异常时暂停该阶段放量,而不影响其他领域。 + +## Implementation Decisions + +### New Modules + +- **统一弹窗运行时**:提供 `open(kind, context)`、`close(handle)`、`refresh(handle)` 三个主要入口;负责弹窗壳、骨架、焦点、生命周期、移动端行为和请求取消,不承载具体业务规则。 +- **弹窗注册与加载器**:提供 `register(kind, loader)` 和 `load(kind, context, signal)`;按弹窗类型懒加载对应模块和最小数据,拒绝页面级的全量预取。 +- **短缓存与请求去重层**:提供 `read(key)`、`write(key, value, ttl)`、`invalidate(tags)`;按用户和业务上下文隔离缓存,合并相同的进行中请求,登出/切换用户时清空私有缓存。 +- **统一状态模型**:定义 `idle`、`loading`、`ready`、`empty`、`submitting`、`success`、`error`、`unauthorized` 和 `stale` 等可观察状态,供所有弹窗复用骨架、反馈和重试行为。 +- **会员流程协调器**:提供套餐/状态加载、购买或续费提交、支付结果同步和领域失效;只调用现有会员、订单和支付服务。 +- **订单流程协调器**:提供订单详情加载、允许操作读取、支付/取消/售后提交和状态刷新;以服务端返回的允许操作为唯一依据。 +- **认证流程协调器**:提供认证状态加载、资料校验/提交、补交和刷新,以及后台审核结果同步;负责敏感字段的最小化传输。 +- **徽章流程协调器**:提供徽章列表/详情加载、分类与进度视图模型和状态失效;不复制徽章授予条件。 +- **后台流程承接模块**:在原生 WordPress 后台提供订单处理、认证审核和徽章管理所需的操作界面或补齐现有操作能力;配置型开关仍遵守 SPA 配置边界。 +- **弹窗性能观测器**:提供 `mark(event, context)` 和 `measure(openId)`;记录点击、弹窗壳可见、请求开始、首个内容可见、内容就绪、操作开始、操作完成和错误,不记录支付凭证或认证敏感资料。 + +### Architectural Decisions + +- **单一弹窗协议**:所有前台弹窗通过统一运行时打开,业务模块只提供视图模型和操作结果;不得继续为每个弹窗复制一套壳、加载动画、错误处理和缓存逻辑。 +- **主动打开优先,按需加载数据**:点击时先显示壳和骨架,再加载当前弹窗数据;不以页面加载时全量请求或强制预取换取表面速度。经过基线验证后,低成本的浏览器空闲预取可以作为后续优化,但不能阻塞首屏。 +- **缓存只解决重复等待,不覆盖业务事实**:动态数据使用短 TTL 的内存缓存,订单、支付、认证和会员状态在任何成功写操作后立即按标签失效并重新读取;徽章说明等低频数据可以采用较长但仍有限的 TTL。 +- **响应以最小视图模型为边界**:每个弹窗只接收首屏需要的字段和服务端计算出的允许操作;明细、历史或低优先级内容分段加载,避免一个大 payload 拖慢首屏。 +- **服务端状态为唯一事实源**:价格、会员资格、订单状态、退款/售后权限、认证状态和徽章授予结果均由服务端领域服务决定;前端只负责展示和发起操作。 +- **操作必须可重入且可去重**:支付创建、订单操作、认证提交和任何领取动作应沿用现有 nonce/权限边界,并使用请求锁或幂等标识避免重复提交;异步支付结果继续由现有回调/状态机制确认。 +- **服务端查询按首屏需要收敛**:会员目录/会员状态、认证状态/最新申请/表单、订单统计与行装饰、徽章拥有关系与佩戴状态应在同一领域读取上下文中复用,优先批量查询,避免重复 COUNT、N+1 和同请求重复按 UUID 查找。 +- **外部服务不阻塞开壳**:支付对账、认证云服务刷新等远端调用必须在弹窗壳之后执行,并有超时、退避、可见性控制和最终状态终止条件;普通订单详情不应无条件同步等待远程对账。 +- **服务端地址与数据链路统一**:前端请求统一使用主题注入的 API 地址与 nonce 配置,不在功能脚本中硬编码 REST 根地址;同一业务不得继续混用 REST 和无 nonce 的旧 HTML AJAX modal。 +- **用户数据隔离**:缓存键至少区分当前用户、业务对象、站点上下文和必要的版本信息;登出、切换账户和权限变化时不能复用前一个用户的数据。 +- **前后台职责不变**:前台负责用户流程,原生后台负责运营和审核,SPA 负责配置;新增或调整 REST 控制器必须有 `permission_callback`,所有输出遵守现有清理和转义规则。 +- **渐进发布但不保留兼容双路**:灰度按领域启用同一套新运行时和流程;领域完成切换后移除旧的弹窗调用路径,不添加旧 option 双读、旧 API 别名、双模板或数据迁移兼容层。 +- **性能以真实测量为准**:记录基线、缓存命中与冷加载的 p75/p95,不以“感觉更快”替代测量;后端同时观察请求耗时、SQL 数量、响应体大小和错误率。 + +### Schema Changes + +- 不新增会员、订单、认证或徽章的核心数据表;继续使用现有领域数据、配置和状态机。 +- 为支持流程状态同步,可以在现有响应模型中补充最小化的 `allowed_actions`、`updated_at`、状态版本或失效标签等元数据,但不得复制领域状态或在前端保存业务事实。 +- 如灰度控制尚无合适的现有发布配置,可新增按领域的临时发布开关,并明确移除时点;该开关只控制新流程放量,不承担旧数据兼容或业务规则切换。 +- 性能观测默认不新增认证资料、支付凭证或订单内容的持久化表;只记录脱敏的计时、结果类别和错误类型,具体存储方式在实现阶段依据现有观测能力确定。 + +### API Contracts + +- **弹窗读取合约**:各领域提供按当前用户和上下文读取首屏视图模型的 GET 能力;响应包含数据、状态版本、更新时间、允许操作和可选的失效标签。不得返回当前用户无权查看的订单、认证资料或其他用户徽章信息。 +- **会员合约**:读取会员状态、可用套餐和服务端计算的购买/续费信息;写操作返回订单/支付承接结果和可追踪的状态标识,不返回支付密钥。支付异步完成后通过现有状态查询或回调链路确认。 +- **订单合约**:读取订单摘要、明细、时间线和服务端允许的操作;支付、取消、退款/售后等操作返回最新状态或明确的处理中状态,并支持重复请求安全失败或幂等返回。列表统计和资源商品展示必须避免把无关的多次 COUNT 或逐行查询带入弹窗首屏。 +- **认证合约**:读取当前用户认证状态和最小必要资料;提交/补交接口返回审核状态和用户可见反馈,后台审核接口只对具备相应能力的人员开放,并区分内部备注与用户可见原因。待审核刷新不能因外部云服务暂时不可用而使弹窗壳无法打开。 +- **徽章合约**:读取分类、获得状态、进度和详情;任何领取或状态变化操作返回最新徽章状态并触发相关缓存失效。授予条件由服务端执行;旧无 nonce 的 HTML modal 路径在切换后不再作为公共入口。 +- **公共约束**:新旧调整后的 REST 路由统一使用 `aksrt/v1` 命名空间、明确的 `permission_callback`、参数验证、输出转义和结构化错误;AJAX 路径继续验证 nonce。接口不得为兼容旧弹窗而长期保留两套响应形状。 +- **并发与失效合约**:读取请求可以携带客户端上下文和取消信号;写操作成功后响应必须足以让前端失效相关缓存并重新读取,避免只能整页刷新才能得到最新状态。 + +### Navigation + +- 前台保留现有用户中心、会员中心、订单中心、认证入口和徽章入口;入口的深链和返回行为继续可用,但打开方式统一进入新的弹窗运行时。 +- 会员入口进入会员购买/续费流程;订单入口进入订单详情并根据服务端状态展示可用操作;认证入口进入当前认证状态对应的下一步;徽章入口进入徽章列表或详情。 +- 弹窗关闭后返回原页面滚动位置和上下文;成功操作后更新背后页面的摘要、余额、会员状态或订单列表,不强制整页跳转。 +- 原生后台继续承载订单/售后、认证审核、徽章运营等日常操作;管理端 SPA 只承载对应的配置内容和必要的发布开关。 +- 灰度顺序为:公共弹窗运行时与徽章/认证 → 订单 → 会员支付。每一阶段独立观察并确认后再启用下一阶段。 + +## Testing Decisions + +采用“静态契约审计 + 后端性能基线 + 浏览器交互与网络条件验收 + 分阶段灰度观测”的组合,不依赖仓库中不存在的单元测试工具。 + +### 性能与加载 + +- 在桌面和移动视口分别记录点击到弹窗壳可见、点击到首个内容可见、点击到内容就绪、写操作完成的时间。 +- 先记录旧实现基线,再以初始预算验收:弹窗壳 p75 约 100ms 内可见;缓存命中内容 p75 约 200ms 内可见;冷加载内容 p75 ≤ 800ms、p95 ≤ 1.5s。预算需在真实 WordPress 环境和网络条件下校准,不以本地开发机单点结果代替。 +- 检查冷缓存、热缓存、慢网络、请求超时、接口 5xx、浏览器返回页面、快速重复点击和关闭后重新打开;确认没有重复请求、过大的首屏响应或旧请求覆盖新内容。 +- 对每个阶段观察首屏可见时间、内容就绪时间、请求/SQL 数量、响应体大小、失败率、重复提交率和业务状态不一致率。 +- 重点回归已确认的瓶颈:会员完整 HTML 和重复会员等级读取、支付高频轮询;订单多次 COUNT、资源订单 N+1、详情远程对账;认证重复申请查询和外部刷新超时;徽章 REST/AJAX 混流、重复读取和旧 modal 请求。 + +### 业务流程 + +- 会员:未开通、有效会员、过期会员、续费、升级、支付取消、支付失败、异步支付成功、重复点击和回到订单详情。 +- 订单:待支付、处理中、已完成、已取消、退款/售后中、操作权限变化、订单已被其他页面更新、重复提交和超时重试。 +- 认证:未申请、待审核、通过、拒绝、补交资料、字段校验失败、重复提交、无权限、后台审核后前台刷新和敏感字段隔离。 +- 徽章:有/无徽章、已获得、未获得、进行中、详情缺失、后台规则变化后的刷新、领取或状态变化(若现有能力支持)以及请求失败。 +- 验证所有成功写操作都会刷新相关弹窗和背后页面,且核心价格、账务、权限、审核资格和授予规则仍由服务端决定。 + +### 安全、兼容与可访问性 + +- 验证未登录用户、普通用户、目标对象所有者、无权用户和后台不同角色的读取与写入边界;确认 REST 权限回调、AJAX nonce、参数清理和输出转义完整。徽章旧公开入口的权限和 nonce 边界必须在统一链路切换时重新验收。 +- 验证订单、认证资料、支付状态和徽章数据不会因缓存键、并发请求或浏览器后退而跨用户泄露。 +- 验证弹窗键盘可达、焦点不会丢失、关闭方式明确、错误不只依赖颜色、骨架不会造成明显布局跳动;移动端无横向溢出。 +- 如变更后台 SPA schema、路由或 REST 合约,运行管理员契约审计并完成一次生产构建;按仓库要求以完整构建产物原子部署。 +- 每个灰度阶段完成 WordPress 运行环境人工验收和性能/错误指标检查后,才进入下一阶段;异常时暂停后续放量,不通过兼容分支掩盖错误。 + +## Out of Scope + +- 修改会员价格、会员权益、支付网关规则、订单账务、退款核算、认证资格/审核政策或徽章授予条件;这些规则只允许被新的流程正确调用和展示。 +- 新增支付渠道、替换支付供应商、在浏览器保存支付凭证,或改变支付回调/异步结算机制。 +- 新增公开认证资料库、向其他用户暴露认证文档、跨用户查看订单或徽章数据。 +- 把订单、售后、认证审核或徽章运营工作台迁移进配置型 SPA;日常运营仍属于原生 WordPress 后台。 +- 全站页面导航、用户中心信息架构、品牌设计系统或所有非弹窗页面的全面重做;本期只统一弹窗运行时和四类流程所需的轻量体验。 +- 强制页面加载时预取所有弹窗数据、长期持久化敏感弹窗数据、建立订单/认证/徽章的第二套事实存储或长期保留旧新双路实现。 +- 以新增兼容别名、旧 option 双读、静默降级或数据迁移层解决旧实现问题;流程切换后以新的规范化链路为唯一实现。 +- 建立独立的数据仓库、复杂转化漏斗或第三方用户行为追踪系统;本期观测只服务于弹窗性能、错误和业务状态闭环。 diff --git a/assets/admin/src/constants/sections/tools.js b/assets/admin/src/constants/sections/tools.js index 253abd94..02272dcf 100644 --- a/assets/admin/src/constants/sections/tools.js +++ b/assets/admin/src/constants/sections/tools.js @@ -25,6 +25,16 @@ export const toolsSection = { canonicalDomain: 'tools', section: 'backup', }), + schemaPage({ + key: 'tools-frontend-modal-rollout', + name: 'ToolsFrontendModalRollout', + label: '前台弹窗灰度', + route: '/tools/frontend-modal-rollout', + title: '前台弹窗灰度', + desc: '按领域控制前台弹窗新流程的分阶段发布。', + canonicalDomain: 'tools', + section: 'frontend-modal-rollout', + }), customPage({ key: 'tools-health', name: 'ToolsHealth', diff --git a/assets/admin/src/schema/sections/tools/frontend-modal-rollout.js b/assets/admin/src/schema/sections/tools/frontend-modal-rollout.js new file mode 100644 index 00000000..a3f9ca43 --- /dev/null +++ b/assets/admin/src/schema/sections/tools/frontend-modal-rollout.js @@ -0,0 +1,45 @@ +export const frontendModalRolloutSchema = { + key: 'tools.frontend-modal-rollout', + title: '前台弹窗灰度', + icon: 'fas fa-bolt', + description: '按领域控制前台弹窗新流程的分阶段发布。', + defaults: { + frontend_modal_rollout_identity: false, + frontend_modal_rollout_badge: false, + frontend_modal_rollout_order: false, + frontend_modal_rollout_vip: false, + }, + groups: [ + { + key: 'rollout', + title: '弹窗新流程开关', + icon: 'fas fa-toggle-on', + fields: [ + { + key: 'frontend_modal_rollout_identity', + label: '官方认证新流程', + type: 'switch', + help: '启用统一弹窗运行时与认证快照流程。', + }, + { + key: 'frontend_modal_rollout_badge', + label: '徽章新流程', + type: 'switch', + help: '启用统一 JSON 徽章流程和短缓存。', + }, + { + key: 'frontend_modal_rollout_order', + label: '订单新流程', + type: 'switch', + help: '启用订单详情快速开壳与状态同步流程。', + }, + { + key: 'frontend_modal_rollout_vip', + label: '会员支付新流程', + type: 'switch', + help: '启用统一会员购买、支付和结果同步流程。', + }, + ], + }, + ], +} diff --git a/assets/admin/src/schema/sections/tools/index.js b/assets/admin/src/schema/sections/tools/index.js index 603b9d52..047e4c75 100644 --- a/assets/admin/src/schema/sections/tools/index.js +++ b/assets/admin/src/schema/sections/tools/index.js @@ -7,10 +7,12 @@ import { importExportSchema } from './import-export.js' import { healthSchema } from './health.js' import { emailRollbackSchema } from './email-rollback.js' import { backupSchema } from './backup.js' +import { frontendModalRolloutSchema } from './frontend-modal-rollout.js' export const toolsSchemas = { 'import-export': importExportSchema, health: healthSchema, 'email-rollback': emailRollbackSchema, backup: backupSchema, + 'frontend-modal-rollout': frontendModalRolloutSchema, } diff --git a/assets/js/core/modal-telemetry.js b/assets/js/core/modal-telemetry.js new file mode 100644 index 00000000..43a713a9 --- /dev/null +++ b/assets/js/core/modal-telemetry.js @@ -0,0 +1,112 @@ +/** + * Frontend modal performance marks. + * + * Records only bounded domain/event names and timing data. Never include + * payment credentials, order contents, identity evidence, or user payloads. + * + * @package AKSRT + */ +(function (window) { + 'use strict'; + + var allowedKinds = { + identity: true, + badges: true, + orders: true, + vip: true, + }; + var allowedEvents = { + click: true, + shell: true, + 'first-content': true, + ready: true, + 'action-start': true, + 'action-end': true, + unauthorized: true, + error: true, + }; + var sessions = Object.create(null); + var sequence = 0; + var maxSessions = 100; + + function normalize(value, fallback) { + var normalized = String(value || fallback || '').toLowerCase().replace(/[^a-z0-9-]/g, ''); + return normalized || fallback; + } + + function now() { + return window.performance && typeof window.performance.now === 'function' + ? window.performance.now() + : 0; + } + + function markName(session, event) { + return 'aksrt-modal-' + session.kind + '-' + session.id + '-' + event; + } + + function emit(session, event) { + if (!window.performance || typeof window.performance.mark !== 'function') return; + + var name = markName(session, event); + window.performance.mark(name); + session.marks[event] = name; + + if (typeof window.performance.measure === 'function' && session.marks.click && event !== 'click') { + try { + window.performance.measure( + 'aksrt-modal-' + session.kind + '-' + session.id + '-click-to-' + event, + session.marks.click, + name + ); + } catch (error) { + // A missing mark must never affect the modal interaction. + } + } + } + + function start(kind) { + var safeKind = normalize(kind, 'unknown'); + if (!allowedKinds[safeKind]) safeKind = 'unknown'; + + sequence += 1; + var id = String(sequence); + var session = { + id: id, + kind: safeKind, + startedAt: now(), + marks: Object.create(null), + }; + + sessions[id] = session; + emit(session, 'click'); + + var ids = Object.keys(sessions); + while (ids.length > maxSessions) { + delete sessions[ids.shift()]; + } + + return id; + } + + function mark(id, event) { + var session = sessions[String(id || '')]; + var safeEvent = normalize(event, 'error'); + if (!session || !allowedEvents[safeEvent]) return; + emit(session, safeEvent); + } + + function finish(id) { + mark(id, 'ready'); + } + + function fail(id) { + mark(id, 'error'); + } + + window.AksrtModalTelemetry = { + start: start, + mark: mark, + finish: finish, + error: fail, + }; +})(window); diff --git a/assets/js/order-center.js b/assets/js/order-center.js index 0cede03f..2549413f 100644 --- a/assets/js/order-center.js +++ b/assets/js/order-center.js @@ -98,18 +98,28 @@ function openDetail(orderNumber) { if (!detailModal || !detailBody) return; detailOrderNumber = orderNumber; + var telemetry = window.AksrtModalTelemetry; + var telemetryId = telemetry && typeof telemetry.start === "function" ? telemetry.start("orders") : ""; + function telemetryMark(event) { + if (telemetry && telemetryId && typeof telemetry.mark === "function") telemetry.mark(telemetryId, event); + } detailBody.innerHTML = '
加载中…
'; detailFooter.innerHTML = ""; detailModal.hidden = false; document.documentElement.classList.add("aksrt-modal-open"); + telemetryMark("shell"); request(orderPath(orderNumber), undefined, "GET").then(function (result) { if (orderNumber !== detailOrderNumber) return; if (!result.success || !result.data) { + telemetryMark("error"); detailBody.innerHTML = '' + escapeHtml(result.message || "订单不存在") + "
"; return; } + telemetryMark("first-content"); renderDetail(result.data); + telemetryMark("ready"); }).catch(function () { + telemetryMark("error"); detailBody.innerHTML = '网络错误,请稍后重试
'; }); } diff --git a/assets/js/pay-vip.js b/assets/js/pay-vip.js index 78f64be3..9a32b42c 100644 --- a/assets/js/pay-vip.js +++ b/assets/js/pay-vip.js @@ -252,21 +252,29 @@ } if (loading) return; loading = true; + var telemetry = window.AksrtModalTelemetry; + var telemetryId = telemetry && typeof telemetry.start === "function" ? telemetry.start("vip") : ""; + function telemetryMark(event) { + if (telemetry && telemetryId && typeof telemetry.mark === "function") telemetry.mark(telemetryId, event); + } var shell = ensureModalShell(); var body = shell.querySelector("[data-aksrt-vip-modal-body]"); if (body) body.innerHTML = '