feat(deploy): production deployment setup and fixes

- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
yeuimu
2026-08-26 14:23:09 +08:00
parent be0b90e68f
commit 6c61a4e871
982 changed files with 74156 additions and 179393 deletions
+38 -38
View File
@@ -1,38 +1,38 @@
# Product External ID Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the public goods API return the SDS/InkPOD product ID as `id`, so product-card links target the correct InkPOD detail page.
**Architecture:** Keep the local `goods.good_id` as the database identity used by admin and relations. At the public API serialization boundary, map `originGood.sdsGoodId` to the existing response `id` field; the website can continue consuming `product.id` without a second identifier.
**Tech Stack:** NestJS, Prisma, Jest, Nuxt/Vue
---
### Task 1: Lock the public API ID contract
**Files:**
- Modify: `apps/api/src/public/public.service.spec.ts`
- [x] Add an assertion that a listed public product has `id` equal to the seeded `OriginGood.sdsGoodId`, not the local `Good.id`.
- [x] Run the focused API test and confirm it fails because the service currently returns `good.id`.
### Task 2: Return the SDS product ID
**Files:**
- Modify: `apps/api/src/public/public.service.ts`
- [x] Include `sdsGoodId` in the serializer's `originGood` type.
- [x] Return `good.originGood.sdsGoodId` as `PublicGoodDto.id`.
- [x] Run the focused API test and confirm it passes.
### Task 3: Verify consumers and document the contract
**Files:**
- Modify: `docs/references/product-center.md`
- Modify: `docs/references/structs.md`
- [x] Document that public product `id` is the SDS/InkPOD external product ID.
- [x] Run API public-service tests, website tests, and the website build.
- [ ] Verify the running public endpoint returns `126673` for `200g纯棉T恤KRTM001` when local services are available. (The API was not listening on port 3001 during verification.)
# Product External ID Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the public goods API return the SDS/InkPOD product ID as `id`, so product-card links target the correct InkPOD detail page.
**Architecture:** Keep the local `goods.good_id` as the database identity used by admin and relations. At the public API serialization boundary, map `originGood.sdsGoodId` to the existing response `id` field; the website can continue consuming `product.id` without a second identifier.
**Tech Stack:** NestJS, Prisma, Jest, Nuxt/Vue
---
### Task 1: Lock the public API ID contract
**Files:**
- Modify: `apps/api/src/public/public.service.spec.ts`
- [x] Add an assertion that a listed public product has `id` equal to the seeded `OriginGood.sdsGoodId`, not the local `Good.id`.
- [x] Run the focused API test and confirm it fails because the service currently returns `good.id`.
### Task 2: Return the SDS product ID
**Files:**
- Modify: `apps/api/src/public/public.service.ts`
- [x] Include `sdsGoodId` in the serializer's `originGood` type.
- [x] Return `good.originGood.sdsGoodId` as `PublicGoodDto.id`.
- [x] Run the focused API test and confirm it passes.
### Task 3: Verify consumers and document the contract
**Files:**
- Modify: `docs/references/product-center.md`
- Modify: `docs/references/structs.md`
- [x] Document that public product `id` is the SDS/InkPOD external product ID.
- [x] Run API public-service tests, website tests, and the website build.
- [ ] Verify the running public endpoint returns `126673` for `200g纯棉T恤KRTM001` when local services are available. (The API was not listening on port 3001 during verification.)
+33 -33
View File
@@ -1,33 +1,33 @@
# Product Filter Selected Tags Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Show selected logistics and process filters as removable gray tags matching Figma.
**Architecture:** Keep `selectedTagIds` as the only source of truth. `ProductTagFilter` derives the selected `Tag` objects from its props and emits the existing `toggle-tag` event when a selected tag is removed.
**Tech Stack:** Nuxt 4, Vue 3 Composition API, TypeScript, scoped CSS, Vitest, Vue Test Utils.
---
### Task 1: Selected Filter Tags
**Files:**
- Modify: `apps/website/app/components/product/ProductTagFilter.vue`
- Create: `apps/website/test/ProductTagFilter.test.ts`
- [x] **Step 1: Write the failing component test**
Mount the component with selected logistics and process IDs. Assert that `包邮` and `烫画` appear in `.selected-tag`, and clicking the first `.selected-tag-remove` emits `toggle-tag` with the selected ID.
- [x] **Step 2: Run the focused test and verify RED**
Run `pnpm --filter @inkreach/website test -- --run test/ProductTagFilter.test.ts` and expect failure because `.selected-tag` does not exist.
- [x] **Step 3: Implement the selected tag row**
Add a pure `computed` mapping from `selectedTagIds` to tags. Render each selected tag below the dropdown row with `#f2f2f2` background, 30px height, 16px horizontal padding, 6px gap, 60px radius, 16px black text, and a 20px remove icon button.
- [x] **Step 4: Verify GREEN and regressions**
Run the focused test, the complete website test suite, and the website production build. Browser-test selection and removal at `http://localhost:3000/product-center`.
# Product Filter Selected Tags Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Show selected logistics and process filters as removable gray tags matching Figma.
**Architecture:** Keep `selectedTagIds` as the only source of truth. `ProductTagFilter` derives the selected `Tag` objects from its props and emits the existing `toggle-tag` event when a selected tag is removed.
**Tech Stack:** Nuxt 4, Vue 3 Composition API, TypeScript, scoped CSS, Vitest, Vue Test Utils.
---
### Task 1: Selected Filter Tags
**Files:**
- Modify: `apps/website/app/components/product/ProductTagFilter.vue`
- Create: `apps/website/test/ProductTagFilter.test.ts`
- [x] **Step 1: Write the failing component test**
Mount the component with selected logistics and process IDs. Assert that `包邮` and `烫画` appear in `.selected-tag`, and clicking the first `.selected-tag-remove` emits `toggle-tag` with the selected ID.
- [x] **Step 2: Run the focused test and verify RED**
Run `pnpm --filter @inkreach/website test -- --run test/ProductTagFilter.test.ts` and expect failure because `.selected-tag` does not exist.
- [x] **Step 3: Implement the selected tag row**
Add a pure `computed` mapping from `selectedTagIds` to tags. Render each selected tag below the dropdown row with `#f2f2f2` background, 30px height, 16px horizontal padding, 6px gap, 60px radius, 16px black text, and a 20px remove icon button.
- [x] **Step 4: Verify GREEN and regressions**
Run the focused test, the complete website test suite, and the website production build. Browser-test selection and removal at `http://localhost:3000/product-center`.
+86 -86
View File
@@ -1,86 +1,86 @@
# SDS Sync Guard Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Prevent the SDS (mapi.sdspod.com) sync from deleting the category/product library when the upstream API returns a degenerate/partial response, and recover the data lost in the 2026-08-20 incident.
**Background / Root cause:**
- On 2026-08-20 06:31 a manual sync ran while `category/tree/3` returned only 2 nodes (normal: 226).
- `syncCategories` treats SDS as the single source of truth: `staleDeleted=225` deleted all previously synced SDS categories.
- With SDS categories gone, `syncProducts` had `leafCategories=0/2``delisted=523` marked 523/536 origin goods as delisted.
- Result: all 536 origin goods `delisted=true`; all 240 website `Good` rows reference delisted origin goods → `/public/goods` returns `total: 0` → product-center page appears empty.
**Architecture:** Add safety guards to `SyncService` and `SdsClientService` so a partial/failed upstream response never triggers destructive operations (`staleDeleted` / delist marking). Recovery is a data fix (re-enable delisted origin goods) followed by normal re-sync.
**Tech Stack:** NestJS, TypeScript, Prisma, Jest.
---
### Task 1: Data recovery — re-enable all origin goods
**Files:**
- Run ad-hoc prisma script (read+write to DB via apps/api @prisma/client)
- [ ] **Step 1: Reset `delisted=false` for all `originGood` rows**
All 536 rows were mis-flagged by the 08-20 partial sync. Run
`prisma.originGood.updateMany({ where: {}, data: { delisted: false } })`.
- [ ] **Step 2: Trigger category sync then product sync**
Verify `/sync/status` logs show SUCCESS with healthy numbers
(categories ≈ 225+, products ≈ 519+).
- [ ] **Step 3: Verify `/public/goods` non-empty and product-center page renders products.**
### Task 2: Hardening — category sync stale-deletion guard
**Files:**
- Modify: `apps/api/src/sync/sync.service.ts`
- Modify: `apps/api/src/sync/sync.service.spec.ts`
- [ ] **Step 1: Write failing test**
Given existing SDS-linked categories in DB, when `fetchCategoryTree` returns a
degenerate small tree (< MIN_CATEGORY_COUNT), `syncCategories` must NOT delete
existing categories (no stale deletion) and must log a warning.
- [ ] **Step 2: Run test, verify RED.**
- [ ] **Step 3: Implement guard**
Before the stale-deletion block, compute `existingSdsCount = categories with sdsCategoryId`.
Skip stale deletion when `flat.length < MIN_CATEGORY_COUNT (10)` OR
`flat.length < MIN_CATEGORY_RATIO (0.5) * existingSdsCount`. Log warning with counts.
- [ ] **Step 4: Verify GREEN + no regressions.**
### Task 3: Hardening — product sync delist guard
**Files:**
- Modify: `apps/api/src/sync/sync.service.ts`
- Modify: `apps/api/src/sync/sync.service.spec.ts`
- [ ] **Step 1: Write failing test**
When there are very few leaf categories (< MIN_LEAF_CATEGORIES) or very few seen
products (< MIN_SEEN_GOODS), `syncProducts` must skip the delist/reactivate marking
and log a warning instead of mass-delisting.
- [ ] **Step 2: Run test, verify RED.**
- [ ] **Step 3: Implement guard**
Only run delist detection when `leafRows.length >= MIN_LEAF_CATEGORIES (10)` AND
`seenSdsGoodIds.size >= MIN_SEEN_GOODS (50)`. Otherwise skip and log warning.
- [ ] **Step 4: Verify GREEN + no regressions.**
### Task 4: Hardening — SDS client response sanity validation
**Files:**
- Modify: `apps/api/src/sync/sds-client.service.ts`
- Modify: `apps/api/src/sync/sds-client.service.spec.ts` (create if absent)
- [ ] **Step 1: Write failing test**
`fetchCategoryTree` throws when the returned array is degenerate (e.g. fewer than
`MIN_SDS_CATEGORY_NODES`), so callers never operate on a bad tree.
- [ ] **Step 2: Run test, verify RED.**
- [ ] **Step 3: Implement**
If the fetched category array length < MIN_SDS_CATEGORY_NODES (10), throw an Error
with the received count so sync logs FAILED (non-destructive) instead of deleting data.
- [ ] **Step 4: Verify GREEN + no regressions.**
### Task 5: Full verification
- [ ] **Step 1:** Run `apps/api` full jest suite — all green.
- [ ] **Step 2:** Typecheck `apps/api` (`tsc -p tsconfig.json --noEmit` or nest build).
- [ ] **Step 3:** Trigger a real sync via the admin UI / HTTP; confirm sync logs show
healthy category/product counts and `/public/goods` returns data.
- [ ] **Step 4:** Confirm product-center page at `http://192.168.124.137:3000/product-center` renders products.
# SDS Sync Guard Fix Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Prevent the SDS (mapi.sdspod.com) sync from deleting the category/product library when the upstream API returns a degenerate/partial response, and recover the data lost in the 2026-08-20 incident.
**Background / Root cause:**
- On 2026-08-20 06:31 a manual sync ran while `category/tree/3` returned only 2 nodes (normal: 226).
- `syncCategories` treats SDS as the single source of truth: `staleDeleted=225` deleted all previously synced SDS categories.
- With SDS categories gone, `syncProducts` had `leafCategories=0/2``delisted=523` marked 523/536 origin goods as delisted.
- Result: all 536 origin goods `delisted=true`; all 240 website `Good` rows reference delisted origin goods → `/public/goods` returns `total: 0` → product-center page appears empty.
**Architecture:** Add safety guards to `SyncService` and `SdsClientService` so a partial/failed upstream response never triggers destructive operations (`staleDeleted` / delist marking). Recovery is a data fix (re-enable delisted origin goods) followed by normal re-sync.
**Tech Stack:** NestJS, TypeScript, Prisma, Jest.
---
### Task 1: Data recovery — re-enable all origin goods
**Files:**
- Run ad-hoc prisma script (read+write to DB via apps/api @prisma/client)
- [ ] **Step 1: Reset `delisted=false` for all `originGood` rows**
All 536 rows were mis-flagged by the 08-20 partial sync. Run
`prisma.originGood.updateMany({ where: {}, data: { delisted: false } })`.
- [ ] **Step 2: Trigger category sync then product sync**
Verify `/sync/status` logs show SUCCESS with healthy numbers
(categories ≈ 225+, products ≈ 519+).
- [ ] **Step 3: Verify `/public/goods` non-empty and product-center page renders products.**
### Task 2: Hardening — category sync stale-deletion guard
**Files:**
- Modify: `apps/api/src/sync/sync.service.ts`
- Modify: `apps/api/src/sync/sync.service.spec.ts`
- [ ] **Step 1: Write failing test**
Given existing SDS-linked categories in DB, when `fetchCategoryTree` returns a
degenerate small tree (< MIN_CATEGORY_COUNT), `syncCategories` must NOT delete
existing categories (no stale deletion) and must log a warning.
- [ ] **Step 2: Run test, verify RED.**
- [ ] **Step 3: Implement guard**
Before the stale-deletion block, compute `existingSdsCount = categories with sdsCategoryId`.
Skip stale deletion when `flat.length < MIN_CATEGORY_COUNT (10)` OR
`flat.length < MIN_CATEGORY_RATIO (0.5) * existingSdsCount`. Log warning with counts.
- [ ] **Step 4: Verify GREEN + no regressions.**
### Task 3: Hardening — product sync delist guard
**Files:**
- Modify: `apps/api/src/sync/sync.service.ts`
- Modify: `apps/api/src/sync/sync.service.spec.ts`
- [ ] **Step 1: Write failing test**
When there are very few leaf categories (< MIN_LEAF_CATEGORIES) or very few seen
products (< MIN_SEEN_GOODS), `syncProducts` must skip the delist/reactivate marking
and log a warning instead of mass-delisting.
- [ ] **Step 2: Run test, verify RED.**
- [ ] **Step 3: Implement guard**
Only run delist detection when `leafRows.length >= MIN_LEAF_CATEGORIES (10)` AND
`seenSdsGoodIds.size >= MIN_SEEN_GOODS (50)`. Otherwise skip and log warning.
- [ ] **Step 4: Verify GREEN + no regressions.**
### Task 4: Hardening — SDS client response sanity validation
**Files:**
- Modify: `apps/api/src/sync/sds-client.service.ts`
- Modify: `apps/api/src/sync/sds-client.service.spec.ts` (create if absent)
- [ ] **Step 1: Write failing test**
`fetchCategoryTree` throws when the returned array is degenerate (e.g. fewer than
`MIN_SDS_CATEGORY_NODES`), so callers never operate on a bad tree.
- [ ] **Step 2: Run test, verify RED.**
- [ ] **Step 3: Implement**
If the fetched category array length < MIN_SDS_CATEGORY_NODES (10), throw an Error
with the received count so sync logs FAILED (non-destructive) instead of deleting data.
- [ ] **Step 4: Verify GREEN + no regressions.**
### Task 5: Full verification
- [ ] **Step 1:** Run `apps/api` full jest suite — all green.
- [ ] **Step 2:** Typecheck `apps/api` (`tsc -p tsconfig.json --noEmit` or nest build).
- [ ] **Step 3:** Trigger a real sync via the admin UI / HTTP; confirm sync logs show
healthy category/product counts and `/public/goods` returns data.
- [ ] **Step 4:** Confirm product-center page at `http://192.168.124.137:3000/product-center` renders products.