feat: persist customer product and shop favorites through the live API

Replace mall fixture favorites with customer-scoped endpoints, and send signed-out shoppers back to the page they left after sign-in.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Chengdong Zhang
2026-09-21 18:51:45 +08:00
co-authored by Cursor
parent 94a64ec712
commit 6c1357ec4d
34 changed files with 1783 additions and 119 deletions
+33 -41
View File
@@ -1,10 +1,9 @@
# TBD — marketing capabilities with no backend (mall mock holdouts)
The mock→live migration finished at `replace-mock-api-wave-7` (address book, 2026-09-18).
Every domain that has an API is live; what follows is what still renders from
`apps/mall/mock/data.ts` because **no backend capability exists for it**. Each entry is a
new capability (schema + routes + contract + pages), not a domain flip — pick one up by
opening an OpenSpec change, the same way waves 17 did.
The mock→live migration and the first marketing implementation wave are complete. Every domain
with an API is live; only the unchecked items below still render from `apps/mall/mock/data.ts`
because no backend capability exists for them. Each remaining entry is a new capability, not a
domain flip, and requires its own OpenSpec change.
**How to use:** check a box only once the behaviour is implemented *and* verified against
the live backend (`cargo run -p vmall-api`, `node scripts/seed-demo.mjs`), then remove the
@@ -15,9 +14,9 @@ mock data and the page's `~/mock/data` import in the same change.
---
## Planning report — 2026-09-18
## Implementation report — 2026-09-21
Five independent, implementation-ready OpenSpec changes were created and validated strictly:
Five independent OpenSpec changes were implemented, verified, archived, and synced to main specs:
| Change | Business boundary | Prerequisite |
| --- | --- | --- |
@@ -27,10 +26,10 @@ Five independent, implementation-ready OpenSpec changes were created and validat
| `add-flash-sales` | Timed sessions, SKU activity inventory, limits, and checkout pricing | None |
| `add-group-buying` | Activities, group lifecycle, paid membership, and payment-time capacity | None |
Each change has `proposal.md`, `design.md`, `specs/`, and `tasks.md` in
`openspec/changes/<change>/`; all five passed `openspec change validate <change> --strict`.
Their planning documents use generic B2B2C storefront terminology rather than a source-project
name.
Each change is archived under `openspec/changes/archive/2026-09-18-<change>/`; its requirements
are present in the main OpenSpec specs. Backend migrations `0010` through `0014`, API modules,
shared contracts, and live frontend domain picks are present. Source-project names are absent from
the planning artifacts.
**Captured decisions:** coupons are template plus customer-owned snapshot and one per shop order;
activity-priced shop orders (flash or group) reject coupons; a SKU cannot be in overlapping flash
@@ -40,38 +39,32 @@ flash sales use activity-reserved stock and server-calculated prices; group seat
payment rather than checkout; an unpaid opener cancel closes an empty group; catalog product-detail
does not show flash or group prices in these changes.
**Recommended implementation order:** start `add-customer-accounts` and/or
`add-shop-coupons`; archive accounts; then `add-points-mall`; implement `add-flash-sales` after
the coupon checkout shape is settled; implement `add-group-buying` last because it expands the
payment state machine. **Favorites** can be a separate OpenSpec change at any time (no order
coupling). The deferred designs below intentionally have no change yet.
**Implemented order:** `add-customer-accounts`, `add-shop-coupons`, `add-points-mall`,
`add-flash-sales`, then `add-group-buying`. Favorites is implemented as `add-favorites`. The
deferred designs below intentionally have no change yet.
## Mock holdouts with a mall UI today
- [ ] **Coupons**covered by in-progress `add-shop-coupons` (not yet applied). Mall still
reads `MOCK_COUPONS` until that change is implemented.
- [ ] **Favorites**`user/favorites.vue` lists `MOCK_FAVORITES` (products tab +
stores tab); `user/index.vue` derives counts from it; `goods/[id].vue` heart is local
`ref(false)`. Ready for its own OpenSpec change whenever convenient: no checkout
coupling. Schema: `favorites(user_id, product_id | shop_id)` with a partial unique
index per target kind (see deferred design). APIs: `GET/POST/DELETE /api/favorites`.
- [ ] **Account stats** — covered by in-progress `add-customer-accounts` (not yet applied).
Mall still reads `USER_STATS` until that change lands. The change already includes
append-only ledgers and guarded mutation; public entry listing stays out of scope.
Frozen balance is a reserved kind and stays zero until a freeze flow exists.
- [x] **Coupons**implemented and archived as `2026-09-18-add-shop-coupons`; mall product,
customer coupon, and checkout surfaces use the live coupon API.
- [x] **Favorites**implemented as `add-favorites`; customer product/shop favorites persist
through the live API. Mall product/store detail, buyer-center list, and dashboard preview
use the selected adapter instead of `MOCK_FAVORITES` or local heart state.
- [x] **Account stats** — implemented and archived as `2026-09-18-add-customer-accounts`.
The mall reads the live summary; append-only ledgers and guarded mutation back later flows.
Frozen balance remains zero until a freeze flow exists.
## Marketing pages that are display-only mock
## Implemented marketing pages
These exist as full pages (`seckill.vue`, `collective.vue`, `integral.vue`) linked from
the home navigation; all three read fixtures directly.
The former display-only pages now use live backend domains; their fixtures remain only inside the
fixed-data adapter as the required rollback implementation.
- [ ] **Seckill (秒杀)**covered by in-progress `add-flash-sales`. Page still reads
`SECKILL_SESSIONS`. Catalog product-detail does not show flash prices in that change.
- [ ] **Collective / 拼团**covered by in-progress `add-group-buying`. Page still reads
fixture counts. Paid seats at payment; unpaid opener cancel closes an empty group.
- [ ] **Integral mall / 积分商城**covered by in-progress `add-points-mall` after
`add-customer-accounts` archives. Demo spendable points are seed-credited through the
ledger; earning campaigns stay out of that change.
- [x] **Seckill (秒杀)**implemented and archived as `2026-09-18-add-flash-sales`.
Catalog product detail intentionally does not show activity prices.
- [x] **Collective / 拼团**implemented and archived as `2026-09-18-add-group-buying`.
Paid seats are claimed at payment; unpaid opener cancellation closes an empty group.
- [x] **Integral mall / 积分商城**implemented and archived as
`2026-09-18-add-points-mall`; demo points are ledger credits and earning campaigns remain out.
## Domains typical B2B2C storefronts have and this MVP does not (no UI here)
@@ -102,10 +95,9 @@ transaction rules before implementation.
threshold and reduction in one currency, active window, and an explicit best-eligible rule.
Coupon vs flash/group is already exclusive (reject coupon on activity-priced shop orders).
Keep 满减 independent from coupons until a stacking policy with those shop coupons is specified.
- [ ] **Favorites**do not use a polymorphic `target_id`/type pair. Keep the existing
product-or-shop design with separate nullable foreign keys, a target-kind check, and one
partial unique index per target type so duplicate claims are impossible. This can be a
dedicated OpenSpec change; it is the smallest remaining mall mock with a UI.
- [x] **Favorites**implemented as `add-favorites` with explicit nullable product/shop
foreign keys, an exactly-one-target check, and one partial unique index per kind.
Do not revisit a polymorphic `target_id`/type pair.
- [ ] **Reviews / 评价** — a future review belongs to a fulfilled order item, not just a
product. Preserve an immutable rating/content snapshot, allow one shop reply, and make
public visibility and platform moderation explicit lifecycle states.