feat(mall): serve home marketing content from the API
Wave 4 of replacing the fixed-data mock adapter, and the first capability the
mall never had a backend for: the home page's banners, promo tiles, quick links
and floor advert art move out of local arrays.
- four explicit tables (`banners`, `promos`, `quick_links`, `floor_adverts`)
rather than one JSONB payload table, so Postgres enforces each shape
- a migration seeds them from the assets the page already rendered, so the flip
is visually a no-op. Destinations are real routes now: the mock's promo links
pointed at dangling `?category=c1` ids and its first banner used `sort=sales`,
which the catalog API rejects
- `GET /api/content/home` is public and returns the four active, ordered lists,
always including a key so a page can render a missing block
- `GET /api/admin/content` and `PUT /api/admin/content/{kind}` let a platform
admin read everything and replace one kind transactionally, with positions
reindexed from the submitted order and a rejected list changing nothing
- the mall's fixed-data adapter learns `getHomeContent`, and a `content` domain
joins the per-domain switch so the rollback path still renders the page
Verified: 23 backend tests green including six new content tests; all three
frontends build; the home page renders the same four blocks as before, an admin
reorder and deactivation change the rendered carousel, and the fixed-data
rollback renders every block with the backend stopped.
OpenSpec change: openspec/changes/replace-mock-api-wave-4
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
-- Storefront marketing content for the home page.
|
||||
--
|
||||
-- Four kinds live in explicit tables rather than one JSONB payload table, so
|
||||
-- Postgres enforces each shape instead of the API hand-validating opaque blobs.
|
||||
-- Seeded from the assets the page already rendered, so moving this content into
|
||||
-- the database is visually a no-op.
|
||||
|
||||
CREATE TABLE banners (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE promos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE quick_links (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
label JSONB NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
-- inline SVG path data, 24x24 viewBox
|
||||
glyph TEXT NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- An ordered pool: the home page assigns these to floors in order and wraps,
|
||||
-- so the destination stays the floor's own category.
|
||||
CREATE TABLE floor_adverts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image TEXT NOT NULL,
|
||||
position INT NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO banners (image, url, position)
|
||||
VALUES
|
||||
('/mock/banner-1.svg', '/search?sort=price&order=desc', 0),
|
||||
('/mock/banner-2.svg', '/seckill', 1),
|
||||
('/mock/banner-3.svg', '/collective', 2);
|
||||
|
||||
INSERT INTO promos (image, url, position)
|
||||
VALUES
|
||||
('/mock/promo-1.svg', '/seckill', 0),
|
||||
('/mock/promo-2.svg', '/collective', 1),
|
||||
('/mock/promo-3.svg', '/integral', 2);
|
||||
|
||||
INSERT INTO quick_links (label, url, glyph, position)
|
||||
VALUES
|
||||
('{"en": "Verification", "zh": "实名认证"}', '/user',
|
||||
'M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6l8-4z', 0),
|
||||
('{"en": "Points Mall", "zh": "积分商城"}', '/integral',
|
||||
'M12 2a10 10 0 100 20 10 10 0 000-20zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z', 1),
|
||||
('{"en": "Group Buy", "zh": "优惠团购"}', '/collective',
|
||||
'M20 7h-3.2A3 3 0 0012 3.5 3 3 0 007.2 7H4v4h7V9h2v2h7V7zM4 13v8h7v-8H4zm9 8h7v-8h-7v8z', 2),
|
||||
('{"en": "Flash Sale", "zh": "秒杀活动"}', '/seckill',
|
||||
'M13 2L4 14h6l-1 8 9-12h-6l1-8z', 3),
|
||||
('{"en": "Notice", "zh": "商城公告"}', '/user',
|
||||
'M12 22a2.5 2.5 0 002.5-2.5h-5A2.5 2.5 0 0012 22zm7-6v-5a7 7 0 00-5-6.7V4a2 2 0 00-4 0v.3A7 7 0 005 11v5l-2 2v1h18v-1l-2-2z', 4),
|
||||
('{"en": "Become a Seller", "zh": "入驻商家"}', '/stores',
|
||||
'M4 4h16l2 5v2a3 3 0 01-3 3 3 3 0 01-3-3 3 3 0 01-3 3 3 3 0 01-3-3 3 3 0 01-3 3 3 3 0 01-3-3V9l2-5zm0 10.7V20h7v-5h2v5h7v-5.3a4.98 4.98 0 01-2 .4V20H6v-5.7a4.98 4.98 0 01-2 .4z', 5);
|
||||
|
||||
INSERT INTO floor_adverts (image, position)
|
||||
VALUES
|
||||
('/mock/floor-adv-1.svg', 0),
|
||||
('/mock/floor-adv-2.svg', 1),
|
||||
('/mock/floor-adv-3.svg', 2),
|
||||
('/mock/floor-adv-4.svg', 3),
|
||||
('/mock/floor-adv-5.svg', 4),
|
||||
('/mock/floor-adv-6.svg', 5);
|
||||
Reference in New Issue
Block a user