docs(openspec): plan shop coupons, accounts, points, flash sales, and group buying

Capture exclusive promotion composition, empty-group cancel, and mall surface scope so the five marketing changes can be implemented without stacking or lifecycle ambiguity.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Chengdong Zhang
2026-09-18 17:32:08 +08:00
co-authored by Cursor
parent e10cae5789
commit 53548196f8
38 changed files with 894 additions and 31 deletions
+81 -31
View File
@@ -15,41 +15,65 @@ mock data and the page's `~/mock/data` import in the same change.
---
## Planning report — 2026-09-18
Five independent, implementation-ready OpenSpec changes were created and validated strictly:
| Change | Business boundary | Prerequisite |
| --- | --- | --- |
| `add-shop-coupons` | Shop templates, customer claims, per-shop checkout redemption, and cancellation restoration | None |
| `add-customer-accounts` | Monetary/points accounts, append-only ledger, and live account stats | None |
| `add-points-mall` | Platform points catalog, atomic redemption, customer history, and admin fulfillment | `add-customer-accounts` must archive first |
| `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.
**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
and group windows; account debits use guarded atomic updates with immutable entries; points
redemption uses a separate order lifecycle and demo points are seed-credited through the ledger;
flash sales use activity-reserved stock and server-calculated prices; group seats are claimed at
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.
## Mock holdouts with a mall UI today
- [ ] **Coupons**`user/coupons.vue` lists `MOCK_COUPONS`; `goods/[id].vue` shows a
claim strip off the same fixture.
Missing: `coupon_templates` (shop-issued: amount/threshold/window/stock), `coupons`
(user-held, order-bound status). APIs: `GET /api/coupons` (mine),
`POST /api/coupons/claim` (from a template), shop-admin template CRUD, and checkout
application (select → discount minor → bind to order). Money math stays minor units.
- [ ] **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.
Missing: `favorites(user_id, product_id | shop_id)` with a partial unique index per
target kind. APIs: `GET/POST/DELETE /api/favorites` (product & shop variants).
- [ ] **Account stats**`user/index.vue` shows `USER_STATS` (balance 128.00, points
2680, frozen 0) and `integral.vue` reuses the points figure.
Missing: balance/points accounts and ledgers (`money_logs` in the reference).
MVP shape: `GET /api/me/stats` returning `{ balance_minor, points, frozen_minor }`;
real ledgers only when a flow (recharge, refund-to-balance, points earn/spend) needs them.
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.
## Marketing pages that are display-only mock
These exist as full pages (`seckill.vue`, `collective.vue`, `integral.vue`) linked from
the home navigation; all three read fixtures directly.
- [ ] **Seckill (秒杀)**`SECKILL_SESSIONS` + `seckillProducts()` (price override,
sold %).
Missing: `seckill_sessions`, `seckill_products` (activity price, isolated stock),
`GET /api/seckill/sessions`, and checkout price resolution honouring the active session.
- [ ] **Collective / 拼团**`collectiveProducts()` (need/joined counts).
Missing: `collective_activities`, `collective_groups` (open/join/expire, success on
fill); orders bind to a group; refund/rollback policy on expiry.
- [ ] **Integral mall / 积分商城**`INTEGRAL_PRODUCTS` + points from `USER_STATS`.
Missing: points ledger (earn/spend), `integral_products`, points-denominated checkout
(`integral/orders` in the reference).
- [ ] **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.
## Domains the reference has and this MVP does not (no UI here)
## Domains typical B2B2C storefronts have and this MVP does not (no UI here)
Recorded so the gap is explicit, not because all of them belong in scope:
@@ -58,15 +82,41 @@ Recorded so the gap is explicit, not because all of them belong in scope:
Missing: `order_comments` (order-item bound, rated, replyable), public read on product
pages, shop reply, admin moderation. Writing/moderating/displaying reviews is a feature
with its own lifecycle.
- [ ] **Distribution / 分销**, **cashes / 提现**, **money logs** — qwshop user-center
modules; no mock, no UI, no model here.
- [ ] **Help center / articles** — nav links exist in the footer (`帮助中心`); no article
model. Cheap version: static content pages; full version: admin-managed articles.
- [ ] **OAuth login, SMS/captcha** — reference `users/oauth` + captcha plugin; here auth
is email+password only.
- [ ] **Distribution / 分销**, **cashes / 提现** — common B2B2C account modules; no mock,
no UI, no model here. Customer-account freeze/credit is the intended foundation.
Public money-log listing is not in `add-customer-accounts`. Deferred design notes below.
- [ ] **Help center / articles** — header nav `shell.nav.help` currently links to `/user`;
the footer has no help links. No article model. Cheap version: static locale pages;
full version: admin-managed articles.
- [ ] **OAuth login, SMS/captcha** — common storefront plugins; here auth is email+password only.
- [ ] **Freight templates / 运费模板** — shop-side shipping-fee rules; checkout currently
charges no shipping at all.
## Deferred designs from general B2B2C storefronts
These patterns are recorded for later product discovery only. They are not approved scope,
have no OpenSpec change, and must be redesigned under VMall's minor-unit, JSONB, RBAC, and
transaction rules before implementation.
- [ ] **Threshold reductions / 满减** — merchant rules need a shop scope, localized name,
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.
- [ ] **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.
- [ ] **Distribution / 分销** — a future merchant activity may configure per-product levels,
but commissions must be created as order-item ledger entries and become payable only after
the order reaches its chosen settlement condition. Do not use floating commission rates or
mutate a balance without a ledger entry.
- [ ] **Withdrawals / 提现** — model a payout request with account, amount minor, fee minor,
destination snapshot, and `requested → approved | rejected | paid` transitions. Funds must
be frozen and released or debited atomically with the transition.
## Deliberately out of scope — does not block deleting this file
- **The fixed-data adapter itself** (`apps/mall/mock/api.ts`, `~/mock/data`): the