The merge creates the store-directory capability (two requirements, six scenarios) and updates the mall's store-and-marketing requirement so the store directory and store home read live shops and order surfaces name their shop. openspec validate --all --strict stays green at 13 specs.
4.3 KiB
Design
Context
See proposal.md — Why. Today the store surfaces read MOCK_STORES (pages/stores/index.vue, pages/stores/[id].vue, and the store card in pages/goods/[id].vue), whose shape is { id, slug, name, company, logo, banner, region, address, distanceKm, rate{score,agree,service,speed}, notice, afterSale }.
The live shops table holds only (id, name, slug, status, created_at) (apps/api/migrations/*), and Shop is the shared model returned by create_shop and set_shop_status to both the admin console and shop-admin. Products already carry shop_id, and listProducts({ shop_id }) already filters by it.
After wave 3 the cart line carries shop_name, but pages/checkout/pay.vue and pages/user/orders/index.vue still resolve a name through storeById and fall back to a generic "Shop" label, because an order carries only shop_id.
Goals / Non-Goals
Goals:
- The store directory, store home and product-page store card render real shops with real products.
- Order surfaces show real shop names.
- No field is invented to fill a UI slot.
Non-Goals:
- No reviews model. The four scores are stored profile values set by the platform, not derived ratings.
- No geo data: a shop has no coordinates, so distance cannot be computed.
- No merchant self-service profile editing, and no work on the seckill, collective or integral pages.
Decisions
1. A shop_profiles table rather than new columns on shops.
Keeps identity (name, slug, status) separate from marketing content, and leaves the Shop model — and therefore the admin and shop-admin contracts — untouched. The public read composes the two with one join.
Alternative: widening shops would ripple through both consoles' payloads for data neither of them uses.
2. The four scores are stored profile values. The store pages show a score row and the product page shows a store rating. Nothing derives them: there is no review model, and inventing a formula over order counts would dress fabricated numbers as computed ones. Storing them as platform-set profile fields is the honest option, and it is named as such in the non-goals so nobody mistakes them for review aggregates.
3. Distance is dropped, not faked.
distanceKm sorts the directory and prints "x.x km away". There is no user location and no shop coordinates, so any number is fiction. The distance column and its sort option go, and the tracker records it.
Alternative: store a distance per shop — meaningless without a reference point.
4. seed-demo.mjs fills the profiles, not a migration.
Profiles hang off shops that the seed script creates, so a migration cannot reference them. Schema stays in the migration; demo profile content goes where the shops are made.
Consequence to accept: the mall must render a shop with no profile row rather than assume one exists, which the spec makes explicit.
5. Order surfaces resolve names from one cached public read.
pages/checkout/pay.vue and pages/user/orders/index.vue share a useAsyncData key over GET /api/shops and map shop_id to a name, so no order or shipment payload grows a denormalised shop name.
Alternative: add shop_name to every order view — more backend surface, and it would duplicate data the shop read already owns.
Risks / Trade-offs
- [The directory now lists the four real demo shops, not the mock's fictional ones] → expected: this is real data. The layout is unchanged; the names and logos differ from the mock.
- [Removing the distance sort removes a UI feature] → deliberate and recorded; it can return with a geo model.
- [Stored scores could be read as real ratings] → named as profile values in the capability spec and the non-goals.
- [A shop without a profile must not render blank boxes] → the spec requires the fields to be absent rather than invented, and the pages guard each block.
- [One extra request on cart, payment and order pages] → cached under a single key, and it replaces lookups that currently miss and print a placeholder.
Migration Plan
- Ship the migration (schema only) and the two endpoints; nothing reads them yet.
- Teach
seed-demo.mjsto set each shop's profile, then re-run it. - Flip the store pages, the store card and the order surfaces in one commit.
- Rollback: revert those pages.
shop_profilesis additive and harmless if left in place.