docs(plan): keep legacy tree as nav, treat sds leaves as style-order metadata, drop tree-switch tasks
This commit is contained in:
@@ -2,23 +2,22 @@
|
|||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
**Goal:** `GET /public/goods` 默认排序变为「国家 → 二级分类 → 三级分类(款)→ good_priority」,`GET /public/categories` 内容源切换为新树(国家根→二级→三级款),支持 1/2/3 级任意筛选组合,接口输入输出 DTO 结构完全不变。
|
**Goal:** `GET /public/goods` 默认排序变为「国家 → 分类(款所属二级)→ 款 → good_priority」,把排序表中每个"款"下的 1~N 个已配置商品排到该款的位置上。**分类树保持老树完全不动**,接口输入输出 DTO 结构完全不变。
|
||||||
|
|
||||||
**Architecture:** `categories` 新增 `sort_order`(二/三级顺序)与 `country_id`(树根关联国家,一级顺序复用 `countries.sort_order`)。`getGoods` 内存中按四层键排序(量级 295,分组/分页本就内存执行);`categoryId` 仍是单个 id(DTO 不动)+ 双通道过滤(新树走 `origin_goods.sds_category_id`,老树回退 `goods.category_id`)。数据一次性回填脚本解析 `排序表.md`。
|
**Architecture:** 领域模型:老树(男士服装→T恤)是正式分类体系,小程序导航与 `categoryId` 过滤全部照旧;新树(国家根→男士T恤→DG001…)的二级/三级节点**只作为"款排序元数据层"**——"款"是合并后的具体衣服(一个款经 family 合并映射 1~N 个源商品/已配置商品)。`categories` 新增 `sort_order` 承载款顺序;`getGoods` 内存中按四层键排序(量级 295,分组/分页本就内存执行),商品经 `origin_goods.sds_category_id` 找到所属款。数据一次性回填脚本解析 `排序表.md`。
|
||||||
|
|
||||||
**Tech Stack:** NestJS + Prisma + PostgreSQL + Jest(真实库集成测试)
|
**Tech Stack:** NestJS + Prisma + PostgreSQL + Jest(真实库集成测试)
|
||||||
|
|
||||||
**已确认的决策:**
|
**已确认的决策:**
|
||||||
1. **前端调用形态(真实 URL 已确认)**:无论选中哪一层,`categoryId` 永远只传**一个**节点 id(DTO 完全不动,无逗号多 id),可选伴随 `countryId`。goods 全挂老树,老树节点天然跨国家:
|
1. **分类树不动**:`/public/categories` 继续返回老树,老树分类是对的
|
||||||
- 只选老树节点:`?categoryId=659`(女士服装,跨国家)→ 按 ①国家→②新树二级→③新树款→④priority 排("美国的女士T恤全排完到下个国家")
|
2. **款 = 排序单元**:排序表的三级(DG001…)是款,不是分类;一个款在"已配置"里有 1~多个商品(同款不同工艺/物流拆分的,经 family 合并),它们排在一起、占该款的位置(如 DG004 的商品整体排在 DG001 后面)
|
||||||
- 选到老树叶子:`?categoryId=666`(女士/裤装,跨国家)→ 同上
|
3. **前端调用形态(真实 URL 已确认)**:`categoryId` 永远只传一个老树节点 id,可选伴随 `countryId`,DTO 不动:
|
||||||
- 国家 + 分类:`?countryId=49&categoryId=658` → 该国内按 ②③④
|
- 只选分类:`?categoryId=659`(女士服装,跨国家)→ ①国家→②款顺序→③priority("美国的女士T恤各款排完到下个国家")
|
||||||
- 只选国家:`?countryId=49` → 同上
|
- 选到叶子:`?categoryId=666` → 同上
|
||||||
- 什么都不选 → 全局 ①②③④
|
- 国家 + 分类:`?countryId=49&categoryId=658` → 该国内按 ②③
|
||||||
(以上全部由"绝对排序键 + 子集过滤"自然满足,无特殊分支逻辑)
|
- 什么都不选 → 全局 ①②③
|
||||||
2. 款内顺序复用现有 `good_priority`(只需保证三级款之间顺序正确)
|
(全部由"绝对排序键 + 子集过滤"自然满足,无特殊分支)
|
||||||
3. 「中东」改为「沙特」写入 countries;防晒衣(good_id=137) 不处理(排序沉底)
|
4. 「中东」改为「沙特」写入 countries(需求方口径);新树根改名「沙特本地工厂直发」;防晒衣(good_id=137) 的款不在树中(sds_category_id=2393 无对应节点),排序在日本组内沉底,不修复
|
||||||
4. `/public/categories` 切换为新树内容源(结构不变);空国家/空二级(productCount=0)不返回,与旧行为、`getCountries` 口径一致。切换后前端将传新树 id(某国的二级/款),`categoryId` 过滤走双通道兼容两种 id
|
|
||||||
|
|
||||||
**硬性约束:**
|
**硬性约束:**
|
||||||
- 所有 public 接口入参、出参 DTO 字段**一个都不能变**,只改内部逻辑
|
- 所有 public 接口入参、出参 DTO 字段**一个都不能变**,只改内部逻辑
|
||||||
@@ -38,24 +37,25 @@ docker run --rm --network deploy-v2_default \
|
|||||||
-v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine npx <命令>
|
-v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine npx <命令>
|
||||||
```
|
```
|
||||||
|
|
||||||
**排序键定义(getGoods DEFAULT):**
|
**排序键定义(getGoods DEFAULT,商品经 origin_goods.sds_category_id 定位到款):**
|
||||||
|
|
||||||
```
|
```
|
||||||
① countries.sort_order(经 goods.country_id) 缺失 → MAX_SAFE_INTEGER
|
① countries.sort_order(经 goods.country_id) 缺失 → MAX_SAFE_INTEGER
|
||||||
② 叶子.parent_category_id 的 sort_order(二级) 缺失 → MAX_SAFE_INTEGER
|
② 款所属二级(如"男士T恤")的 sort_order 缺失 → MAX_SAFE_INTEGER
|
||||||
③ 叶子自身 sort_order(三级,经 origin_goods.sds_category_id)缺失 → MAX_SAFE_INTEGER
|
③ 款自身的 sort_order 缺失 → MAX_SAFE_INTEGER
|
||||||
(缺失沉底发生在「所属国家分组内」:如防晒衣(137) 的 sds_category_id=2393 不在新树,
|
|
||||||
它会排到日本组末尾,不会破坏国家间顺序,可照常售卖/搜索)
|
|
||||||
④ good_priority desc, id asc(现有字段,不变)
|
④ good_priority desc, id asc(现有字段,不变)
|
||||||
```
|
```
|
||||||
|
(缺失沉底发生在「所属国家分组内」:如防晒衣(137) 的 sds_category_id=2393 不在新树,
|
||||||
|
它会排到日本组末尾,不会破坏国家间顺序,可照常售卖/搜索。
|
||||||
|
同一款下 1~N 个商品(单面/双面印花等)共享 ②③,仅靠 ④ 分先后,自然聚在一起。)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Task 1: Schema 迁移 — categories 加 sort_order + country_id
|
### Task 1: Schema 迁移 — categories 加 sort_order
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `apps/api/prisma/schema.prisma`(Category model 约 L157-174、Country model 约 L141-154)
|
- Modify: `apps/api/prisma/schema.prisma`(Category model 约 L157-174)
|
||||||
- Create: `apps/api/prisma/migrations/20260901000000_add_category_sort_fields/migration.sql`
|
- Create: `apps/api/prisma/migrations/20260901000000_add_category_sort_order/migration.sql`
|
||||||
|
|
||||||
- [ ] **Step 1.1: 先检查迁移状态(共享库防漂移)**
|
- [ ] **Step 1.1: 先检查迁移状态(共享库防漂移)**
|
||||||
|
|
||||||
@@ -68,44 +68,23 @@ Expected: `Database schema is up to date!`。若报 drift/已存在类错误,
|
|||||||
|
|
||||||
- [ ] **Step 1.2: 修改 schema.prisma**
|
- [ ] **Step 1.2: 修改 schema.prisma**
|
||||||
|
|
||||||
Category model:`sdsCategoryId` 行后新增两行,并新增 relation:
|
Category model 的 `sdsCategoryId` 行后新增:
|
||||||
|
|
||||||
```prisma
|
```prisma
|
||||||
sortOrder Int @default(0) @map("sort_order")
|
sortOrder Int @default(0) @map("sort_order")
|
||||||
countryId BigInt? @map("country_id")
|
|
||||||
country Country? @relation(fields: [countryId], references: [id], onDelete: SetNull, onUpdate: NoAction)
|
|
||||||
```
|
|
||||||
|
|
||||||
Country model:`families ProductFamily[]` 后新增:
|
|
||||||
|
|
||||||
```prisma
|
|
||||||
categories Category[]
|
|
||||||
```
|
|
||||||
|
|
||||||
Category model 底部 `@@index([parentCategoryId])` 后新增:
|
|
||||||
|
|
||||||
```prisma
|
|
||||||
@@index([countryId])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 1.3: 手写迁移 SQL(应用与登记分离,避免 shadow DB)**
|
- [ ] **Step 1.3: 手写迁移 SQL(应用与登记分离,避免 shadow DB)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p apps/api/prisma/migrations/20260901000000_add_category_sort_fields
|
mkdir -p apps/api/prisma/migrations/20260901000000_add_category_sort_order
|
||||||
```
|
```
|
||||||
|
|
||||||
创建 `apps/api/prisma/migrations/20260901000000_add_category_sort_fields/migration.sql`:
|
创建 `apps/api/prisma/migrations/20260901000000_add_category_sort_order/migration.sql`:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "categories" ADD COLUMN "sort_order" INTEGER NOT NULL DEFAULT 0;
|
ALTER TABLE "categories" ADD COLUMN "sort_order" INTEGER NOT NULL DEFAULT 0;
|
||||||
ALTER TABLE "categories" ADD COLUMN "country_id" BIGINT;
|
|
||||||
|
|
||||||
-- ForeignKey
|
|
||||||
ALTER TABLE "categories" ADD CONSTRAINT "categories_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("country_id") ON DELETE SET NULL ON UPDATE NO ACTION;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "categories_country_id_idx" ON "categories"("country_id");
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 1.4: 应用 SQL、登记迁移、重新生成 client**
|
- [ ] **Step 1.4: 应用 SQL、登记迁移、重新生成 client**
|
||||||
@@ -114,12 +93,12 @@ CREATE INDEX "categories_country_id_idx" ON "categories"("country_id");
|
|||||||
docker run --rm --network deploy-v2_default \
|
docker run --rm --network deploy-v2_default \
|
||||||
-e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' \
|
-e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' \
|
||||||
-v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine \
|
-v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine \
|
||||||
npx prisma db execute --file prisma/migrations/20260901000000_add_category_sort_fields/migration.sql
|
npx prisma db execute --file prisma/migrations/20260901000000_add_category_sort_order/migration.sql
|
||||||
|
|
||||||
docker run --rm --network deploy-v2_default \
|
docker run --rm --network deploy-v2_default \
|
||||||
-e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' \
|
-e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' \
|
||||||
-v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine \
|
-v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine \
|
||||||
npx prisma migrate resolve --applied 20260901000000_add_category_sort_fields
|
npx prisma migrate resolve --applied 20260901000000_add_category_sort_order
|
||||||
|
|
||||||
docker run --rm --network deploy-v2_default \
|
docker run --rm --network deploy-v2_default \
|
||||||
-e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' \
|
-e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' \
|
||||||
@@ -130,39 +109,38 @@ Expected: 三条命令均成功。
|
|||||||
- [ ] **Step 1.5: 验证列存在**
|
- [ ] **Step 1.5: 验证列存在**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec deploy-v2-postgres-1 psql -U inkreach -d inkreach -c "\d categories" | grep -E 'sort_order|country_id'
|
docker exec deploy-v2-postgres-1 psql -U inkreach -d inkreach -c "\d categories" | grep sort_order
|
||||||
```
|
```
|
||||||
Expected: 两列均存在(`sort_order integer not null default 0`、`country_id bigint`)。
|
Expected: `sort_order | integer | | not null | 0`
|
||||||
|
|
||||||
- [ ] **Step 1.6: Commit**
|
- [ ] **Step 1.6: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add apps/api/prisma/schema.prisma apps/api/prisma/migrations/20260901000000_add_category_sort_fields
|
git add apps/api/prisma/schema.prisma apps/api/prisma/migrations/20260901000000_add_category_sort_order
|
||||||
git commit -m "feat(api): add sort_order and country_id to categories"
|
git commit -m "feat(api): add sort_order column to categories"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Task 2: 数据回填脚本 — 排序表 → sort_order + 沙特 + 根关联国家
|
### Task 2: 数据回填脚本 — 排序表 → sort_order + 沙特
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Create: `apps/api/prisma/backfill-category-sort-order.ts`
|
- Create: `apps/api/prisma/backfill-category-sort-order.ts`
|
||||||
|
|
||||||
**行为(幂等,可重复执行):**
|
**行为(幂等,可重复执行):**
|
||||||
1. 解析仓库根 `排序表.md`:`# ` = 一级国家(跳过「全部」「# 工厂直发国家/地区列表」)、`## ` = 二级、`### ` = 三级
|
1. 解析 `排序表.md`:`# ` = 一级国家(跳过「全部」「# 工厂直发国家/地区列表」)、`## ` = 二级(男士T恤等)、`### ` = 款
|
||||||
2. 国家名 → 新树根 category 映射(写死映射表);`中东` 条目:countries upsert「沙特」+ 根重命名 `沙特本地工厂直发`
|
2. 国家名 → 新树根 category 映射(写死映射表);`中东` 条目:countries upsert「沙特」(sort_order=表内顺序 6)+ 根重命名「沙特本地工厂直发」
|
||||||
3. 根节点写入 `country_id`(关联 countries 行;`国内工厂` 无对应国家行 → 保持 null)
|
3. 每个根内:二级按出现顺序写 `sort_order=1..n`;款在所属二级范围内写 `sort_order=1..n`
|
||||||
4. 每个根内:二级按出现顺序写 `sort_order=1..n`;三级在所属二级范围内写 `sort_order=1..n`
|
4. 匹配规则:先按规范化名称(trim+压缩空白)精确匹配;失败再按「首个货号 token」前缀匹配(如 `GBTM011长袖T` 命中 `GBTM011长袖T`);**跨根禁止匹配**
|
||||||
5. 匹配规则:先按规范化名称(trim+压缩空白)精确匹配;失败再按「首个货号 token」前缀匹配(如 `GBTM011长袖T` 命中 `GBTM011长袖T`);**跨根禁止匹配**
|
5. 未匹配条目打印 `UNMATCHED:` 清单并退出码 1(人工对齐排序表/库名后重跑);`中国(国内工厂)` 跳过 countries 部分,sort_order 照常回填
|
||||||
6. 未匹配条目打印 `UNMATCHED:` 清单并退出码 1;`中国(国内工厂)` 跳过 countries 部分,sort_order 照常回填
|
6. countries.sort_order 按表内顺序 1..13 重写(美国 英国 日本 墨西哥 巴西 沙特 波兰 西班牙 德国 意大利 加拿大 澳大利亚 韩国)
|
||||||
7. countries.sort_order 按表内顺序 1..13 重写(美国 英国 日本 墨西哥 巴西 沙特 波兰 西班牙 德国 意大利 加拿大 澳大利亚 韩国)
|
|
||||||
|
|
||||||
- [ ] **Step 2.1: 编写脚本**
|
- [ ] **Step 2.1: 编写脚本**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
/**
|
/**
|
||||||
* 一次性回填:解析 排序表.md → categories.sort_order(二/三级)+ country_id(根)
|
* 一次性回填:解析 排序表.md → categories.sort_order(二级/款)+ countries.sort_order
|
||||||
* + countries.sort_order + 沙特国家行 + 中东根更名
|
* + 沙特国家行 + 中东根更名
|
||||||
* 幂等:可重复执行;SDS 同步若覆盖根名称,重跑本脚本即可恢复
|
* 幂等:可重复执行;SDS 同步若覆盖根名称,重跑本脚本即可恢复
|
||||||
*/
|
*/
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
@@ -219,7 +197,6 @@ async function main() {
|
|||||||
const unmatched: string[] = [];
|
const unmatched: string[] = [];
|
||||||
|
|
||||||
// 1) countries:沙特 upsert + 顺序重写
|
// 1) countries:沙特 upsert + 顺序重写
|
||||||
const countryIdByName = new Map<string, bigint>();
|
|
||||||
for (let i = 0; i < countryOrder.length; i++) {
|
for (let i = 0; i < countryOrder.length; i++) {
|
||||||
const name = countryOrder[i];
|
const name = countryOrder[i];
|
||||||
const dbCountry = name === '中东' ? '沙特' : name;
|
const dbCountry = name === '中东' ? '沙特' : name;
|
||||||
@@ -227,10 +204,8 @@ async function main() {
|
|||||||
const existing = await prisma.country.findUnique({ where: { countryName: dbCountry } });
|
const existing = await prisma.country.findUnique({ where: { countryName: dbCountry } });
|
||||||
if (existing) {
|
if (existing) {
|
||||||
await prisma.country.update({ where: { id: existing.id }, data: { sortOrder } });
|
await prisma.country.update({ where: { id: existing.id }, data: { sortOrder } });
|
||||||
countryIdByName.set(dbCountry, existing.id);
|
|
||||||
} else if (dbCountry === '沙特') {
|
} else if (dbCountry === '沙特') {
|
||||||
const created = await prisma.country.create({ data: { countryName: '沙特', sortOrder } });
|
await prisma.country.create({ data: { countryName: '沙特', sortOrder } });
|
||||||
countryIdByName.set('沙特', created.id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,20 +216,11 @@ async function main() {
|
|||||||
rootByName.set('沙特本地工厂直发', meRoot);
|
rootByName.set('沙特本地工厂直发', meRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) 根节点关联 country_id
|
// 3) 二级/款 sort_order
|
||||||
for (const [tableName, rootName] of Object.entries(ROOT_MAP)) {
|
|
||||||
const root = rootByName.get(tableName === '中东' ? '沙特本地工厂直发' : rootName);
|
|
||||||
if (!root) { unmatched.push(`ROOT MISS: ${tableName}`); continue; }
|
|
||||||
const dbCountry = tableName === '中东' ? '沙特' : tableName === '中国(国内工厂)' ? null : tableName;
|
|
||||||
const cid = dbCountry ? (countryIdByName.get(dbCountry) ?? null) : null;
|
|
||||||
await prisma.category.update({ where: { id: root.id }, data: { countryId: cid } });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) 二/三级 sort_order
|
|
||||||
for (const countryName of countryOrder) {
|
for (const countryName of countryOrder) {
|
||||||
const rootName = ROOT_MAP[countryName];
|
const rootName = ROOT_MAP[countryName];
|
||||||
const root = rootByName.get(countryName === '中东' ? '沙特本地工厂直发' : rootName);
|
const root = rootByName.get(countryName === '中东' ? '沙特本地工厂直发' : rootName);
|
||||||
if (!root) continue; // 已在 ROOT MISS 记录
|
if (!root) { unmatched.push(`ROOT MISS: ${countryName}`); continue; }
|
||||||
const l2s = root.children;
|
const l2s = root.children;
|
||||||
const l2NamesInOrder: string[] = [];
|
const l2NamesInOrder: string[] = [];
|
||||||
for (const row of tree) if (row.country === countryName && !l2NamesInOrder.includes(row.l2)) l2NamesInOrder.push(row.l2);
|
for (const row of tree) if (row.country === countryName && !l2NamesInOrder.includes(row.l2)) l2NamesInOrder.push(row.l2);
|
||||||
@@ -305,16 +271,16 @@ Expected: `backfill done`,退出码 0。已知候选 UNMATCHED:美国/内衣
|
|||||||
```bash
|
```bash
|
||||||
docker exec deploy-v2-postgres-1 psql -U inkreach -d inkreach -c "
|
docker exec deploy-v2-postgres-1 psql -U inkreach -d inkreach -c "
|
||||||
select country_name, sort_order from countries order by sort_order;" -c "
|
select country_name, sort_order from countries order by sort_order;" -c "
|
||||||
select root.category_name, root.country_id, mid.category_name, mid.sort_order, count(leaf.category_id) leaves
|
select root.category_name, mid.category_name, mid.sort_order, count(leaf.category_id) leaves
|
||||||
from categories root
|
from categories root
|
||||||
join categories mid on mid.parent_category_id = root.category_id
|
join categories mid on mid.parent_category_id = root.category_id
|
||||||
left join categories leaf on leaf.parent_category_id = mid.category_id
|
left join categories leaf on leaf.parent_category_id = mid.category_id
|
||||||
where root.parent_category_id is null and root.sds_category_id is not null
|
where root.parent_category_id is null and root.sds_category_id is not null
|
||||||
group by 1,2,3,4 order by 1, mid.sort_order;" | head -70
|
group by 1,2,3 order by 1, mid.sort_order;" | head -70
|
||||||
```
|
```
|
||||||
Expected: countries 13 行(沙特=6);美国根下 男士T恤=1、女士T恤=2…;各二级下三级 sort_order 连续 1..n;每个新树根 country_id 非空(国内工厂除外)。
|
Expected: countries 13 行(沙特=6);美国根下 男士T恤=1、女士T恤=2…;各二级下款 sort_order 连续 1..n。
|
||||||
|
|
||||||
抽查美国/男士T恤三级顺序:
|
抽查美国/男士T恤款顺序:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec deploy-v2-postgres-1 psql -U inkreach -d inkreach -tAc "
|
docker exec deploy-v2-postgres-1 psql -U inkreach -d inkreach -tAc "
|
||||||
@@ -372,7 +338,7 @@ describe('getGoods tree-order sorting', () => {
|
|||||||
orderedGoodIds = [a1High.id, a1Low.id, a2.id, b1.id];
|
orderedGoodIds = [a1High.id, a1Low.id, a2.id, b1.id];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DEFAULT: country > l2 > l3 > priority', async () => {
|
it('DEFAULT: country > mid > leaf > priority', async () => {
|
||||||
const res = await service.getGoods({ page: 1, pageSize: 50 });
|
const res = await service.getGoods({ page: 1, pageSize: 50 });
|
||||||
const idx = res.items.map((i) => BigInt(i.goodId));
|
const idx = res.items.map((i) => BigInt(i.goodId));
|
||||||
const pos = orderedGoodIds.map((id) => idx.indexOf(id));
|
const pos = orderedGoodIds.map((id) => idx.indexOf(id));
|
||||||
@@ -380,7 +346,7 @@ describe('getGoods tree-order sorting', () => {
|
|||||||
expect(pos).toEqual([...pos].sort((a, b) => a - b)); // 相对有序
|
expect(pos).toEqual([...pos].sort((a, b) => a - b)); // 相对有序
|
||||||
expect(idx.indexOf(orderedGoodIds[0])).toBeLessThan(idx.indexOf(orderedGoodIds[1])); // 同款内 priority desc
|
expect(idx.indexOf(orderedGoodIds[0])).toBeLessThan(idx.indexOf(orderedGoodIds[1])); // 同款内 priority desc
|
||||||
expect(idx.indexOf(orderedGoodIds[1])).toBeLessThan(idx.indexOf(orderedGoodIds[2])); // 款顺序
|
expect(idx.indexOf(orderedGoodIds[1])).toBeLessThan(idx.indexOf(orderedGoodIds[2])); // 款顺序
|
||||||
expect(idx.indexOf(orderedGoodIds[2])).toBeLessThan(idx.indexOf(orderedGoodIds[3])); // 国家/分类顺序优先于 priority
|
expect(idx.indexOf(orderedGoodIds[2])).toBeLessThan(idx.indexOf(orderedGoodIds[3])); // 国家/款顺序优先于 priority
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@@ -447,7 +413,7 @@ if (!query.sort || query.sort === 'DEFAULT') {
|
|||||||
rows.sort((a, b) => this.compareByTreeOrder(meta, a, b));
|
rows.sort((a, b) => this.compareByTreeOrder(meta, a, b));
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
(分组代表行 = 树序第一条,与现有「排序最前为代表」契约一致。)
|
(分组代表行 = 树序第一条,与现有「排序最前为代表」契约一致;同一款下 1~N 个商品共享 ②③ 键,仅靠 ④ 分先后。)
|
||||||
|
|
||||||
- [ ] **Step 3.4: 跑测试确认通过(含既有用例)**
|
- [ ] **Step 3.4: 跑测试确认通过(含既有用例)**
|
||||||
|
|
||||||
@@ -460,278 +426,56 @@ Expected: 全部 PASS(现有 getGoods 断言若依赖 DEFAULT 全局顺序按
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add apps/api/src/public/public.service.ts apps/api/src/public/public.service.spec.ts
|
git add apps/api/src/public/public.service.ts apps/api/src/public/public.service.spec.ts
|
||||||
git commit -m "feat(api): order public goods by country > category > leaf > priority"
|
git commit -m "feat(api): order public goods by country > mid-category > style > priority"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Task 4: categoryId 新树双通道过滤(TDD)
|
### Task 4: 全量验证 + 交付
|
||||||
|
|
||||||
**Files:**
|
- [ ] **Step 4.1: api 全量测试**
|
||||||
- Modify: `apps/api/src/public/public.service.ts`(getGoods categoryId 过滤块,约 L194-196)
|
|
||||||
|
|
||||||
**语义(DTO 完全不动,`categoryId` 仍是单个 id):**
|
|
||||||
- 传入 id 属于新树(自身/子孙存在 sds_category_id)→ 收集该节点全部子孙叶子的 `sds_category_id` 集合,过滤改为 `where.originGood.sdsCategoryId in 集合`(选二级 = 该二级下所有款)
|
|
||||||
- 传入 id 属于老树(如 658/659/666)→ 回退现行逻辑 `where.categoryId in collectCategoryDescendants(...)`(老树节点天然跨国家,行为不变)
|
|
||||||
- 新树 originGood 条件与 minPrice/maxPrice 的 originGood 条件合并为同一对象,互不覆盖
|
|
||||||
|
|
||||||
**真实调用形态(验收基准):**
|
|
||||||
- `?categoryId=659`(老树一级,跨国家)→ 全部女士服装商品,按 ①国家→②新树二级→③新树款→④priority
|
|
||||||
- `?categoryId=666`(老树叶子,跨国家)→ 同上
|
|
||||||
- `?countryId=49&categoryId=658` → 英国内按 ②③④
|
|
||||||
- 切新树后前端会传新树 id(如某国"男士T恤"或某款)→ 走 sds 通道
|
|
||||||
|
|
||||||
- [ ] **Step 4.1: 写失败测试**
|
|
||||||
|
|
||||||
```ts
|
|
||||||
describe('getGoods categoryId filter (dual-path)', () => {
|
|
||||||
it('new-tree mid id filters via originGood.sdsCategoryId', async () => {
|
|
||||||
const midA = await prisma.category.findFirst({ where: { categoryName: `MidA ${stamp2}` } });
|
|
||||||
const res = await service.getGoods({ page: 1, pageSize: 50, categoryId: midA!.id.toString() });
|
|
||||||
const names = res.items.map((i) => i.goodName).filter((n) => n.includes(stamp2));
|
|
||||||
expect(names.length).toBe(3); // A1Low/A1High/A2
|
|
||||||
expect(names.some((n) => n.startsWith('A2'))).toBe(true);
|
|
||||||
expect(names.every((n) => !n.startsWith('B1'))).toBe(true); // B1 不属于 MidA
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new-tree leaf id filters to that leaf only', async () => {
|
|
||||||
const leafA2 = await prisma.category.findFirst({ where: { categoryName: `LeafA2 ${stamp2}` } });
|
|
||||||
const res = await service.getGoods({ page: 1, pageSize: 50, categoryId: leafA2!.id.toString() });
|
|
||||||
const names = res.items.map((i) => i.goodName).filter((n) => n.includes(stamp2));
|
|
||||||
expect(names.length).toBe(1);
|
|
||||||
expect(names[0].startsWith('A2')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('categoryId + minPrice merge originGood filters', async () => {
|
|
||||||
const midA = await prisma.category.findFirst({ where: { categoryName: `MidA ${stamp2}` } });
|
|
||||||
const res = await service.getGoods({
|
|
||||||
page: 1, pageSize: 50, categoryId: midA!.id.toString(), minPrice: '999999',
|
|
||||||
});
|
|
||||||
expect(res.items.filter((i) => i.goodName.includes(stamp2)).length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
(老树 id 路径由既有用例覆盖:原始 fixture 的分类无 sds_category_id,走回退分支,行为不变。)
|
|
||||||
|
|
||||||
- [ ] **Step 4.2: 跑测试确认失败**
|
|
||||||
|
|
||||||
同 Task 3 命令,`-t 'categoryId'`。Expected: FAIL(新树 mid/leaf id 走旧路径查空)。
|
|
||||||
|
|
||||||
- [ ] **Step 4.3: 实现**
|
|
||||||
|
|
||||||
`getGoods` 内替换原 categoryId 过滤块:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
if (query.categoryId) {
|
|
||||||
const sds = await this.resolveSdsCategoryFilter(BigInt(query.categoryId));
|
|
||||||
if (sds) {
|
|
||||||
where.originGood = {
|
|
||||||
...(where.originGood as Prisma.OriginGoodWhereInput | undefined),
|
|
||||||
sdsCategoryId: { in: sds },
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
where.categoryId = { in: await this.collectCategoryDescendants(BigInt(query.categoryId)) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
新增方法:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
/**
|
|
||||||
* 新树过滤:节点自身或子孙存在 sds_category_id → 返回全部子孙(含自身)的
|
|
||||||
* sds_category_id 集合(选二级 = 其下所有款);否则返回 null(老树回退)。
|
|
||||||
* 根/二级自身的 sds id 不出现在 origin_goods 上,多收集无害。
|
|
||||||
*/
|
|
||||||
private async resolveSdsCategoryFilter(rootId: bigint): Promise<string[] | null> {
|
|
||||||
const ids: bigint[] = [rootId];
|
|
||||||
let frontier: bigint[] = [rootId];
|
|
||||||
while (frontier.length) {
|
|
||||||
const children = await this.prisma.category.findMany({
|
|
||||||
where: { parentCategoryId: { in: frontier } },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
frontier = children.map((c) => c.id);
|
|
||||||
ids.push(...frontier);
|
|
||||||
}
|
|
||||||
const rows = await this.prisma.category.findMany({
|
|
||||||
where: { id: { in: ids }, sdsCategoryId: { not: null } },
|
|
||||||
select: { sdsCategoryId: true },
|
|
||||||
});
|
|
||||||
const sds = rows.map((r) => r.sdsCategoryId!).filter((s) => s !== '');
|
|
||||||
return sds.length ? sds : null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4.4: 跑全部 public 测试确认通过**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm ... npx jest src/public
|
|
||||||
```
|
|
||||||
Expected: 全部 PASS(老树回退路径保证旧行为不变)。
|
|
||||||
|
|
||||||
- [ ] **Step 4.5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add apps/api/src/public/public.service.ts apps/api/src/public/public.service.spec.ts
|
|
||||||
git commit -m "feat(api): support new-tree categoryId filter via origin sds category"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: getCategoriesTree 切换新树(TDD)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `apps/api/src/public/public.service.ts`(getCategoriesTree 约 L85-127)
|
|
||||||
|
|
||||||
**语义:**
|
|
||||||
- 内容源:新树(根 = `parentCategoryId=null AND sdsCategoryId!=null`),老树不再返回
|
|
||||||
- 排序:根按 `country.sortOrder`(null 最后)→ 根 sortOrder;二级/三级按各自 `sort_order`
|
|
||||||
- `countryId` 过滤:根的 `country_id` 精确匹配(不再按 goods 归属)
|
|
||||||
- productCount:叶子 = 关联 origin_goods.sds_category_id 的在售族化 goods 数(`family_id IS NOT NULL AND delisted=false`);二级/根 = 后代求和(现有 `buildTree` 语义)
|
|
||||||
- productCount=0 的二级/根剪掉(空国家不出现,如沙特在配货前不展示)
|
|
||||||
|
|
||||||
- [ ] **Step 5.1: 写失败测试**
|
|
||||||
|
|
||||||
```ts
|
|
||||||
describe('getCategoriesTree (new tree)', () => {
|
|
||||||
it('returns new-tree roots ordered by country sort_order with counts', async () => {
|
|
||||||
const tree = await service.getCategoriesTree();
|
|
||||||
const names = tree.map((n) => n.categoryName);
|
|
||||||
// 新树根在返回中,老树根不在
|
|
||||||
expect(names).toContain(`RootA ${stamp2}`);
|
|
||||||
expect(names).not.toContain(`Pub Cat ${stamp}`); // 老树 fixture(无 sds_category_id)
|
|
||||||
const rootA = tree.find((n) => n.categoryName === `RootA ${stamp2}`)!;
|
|
||||||
expect(rootA.children.map((c) => c.categoryName)).toEqual([`MidA ${stamp2}`]);
|
|
||||||
expect(rootA.children[0].children.length).toBe(2); // LeafA1/LeafA2
|
|
||||||
expect(rootA.children[0].children[0].productCount).toBe(2); // A1Low/A1High
|
|
||||||
expect(rootA.children[0].children[1].productCount).toBe(1); // A2
|
|
||||||
expect(rootA.productCount).toBe(3);
|
|
||||||
// 国家顺序:RootA(sort=1) 在 RootB(sort=2) 前
|
|
||||||
expect(names.indexOf(`RootA ${stamp2}`)).toBeLessThan(names.indexOf(`RootB ${stamp2}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('countryId filters to that country root', async () => {
|
|
||||||
const cA = await prisma.country.findFirst({ where: { countryName: `TA ${stamp2}` } });
|
|
||||||
// Task 2 回填不会给测试 fixture 关联 country_id,此处手动关联
|
|
||||||
const rootA = await prisma.category.findFirst({ where: { categoryName: `RootA ${stamp2}` } });
|
|
||||||
await prisma.category.update({ where: { id: rootA!.id }, data: { countryId: cA!.id } });
|
|
||||||
const tree = await service.getCategoriesTree(cA!.id.toString());
|
|
||||||
expect(tree.map((n) => n.categoryName)).toEqual([`RootA ${stamp2}`]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:Task 3 fixture 创建根时未设 countryId,本 describe 的 beforeAll 中补齐:RootA→cA、RootB→cB(同上 update 方式),并给 leafA1/leafA2 的 goods 数依赖 Task 3 fixture(A1 两条、A2 一条)。若 describe 执行顺序影响 fixture 创建,将根/二级/叶子与国家的关联移入本 describe 自己的 beforeAll(按 stamp2 查询)。
|
|
||||||
|
|
||||||
- [ ] **Step 5.2: 跑测试确认失败**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm ... npx jest src/public/public.service.spec.ts -t 'new tree'
|
|
||||||
```
|
|
||||||
Expected: FAIL(现实现返回老树,`RootA` 不在结果中)。
|
|
||||||
|
|
||||||
- [ ] **Step 5.3: 实现**
|
|
||||||
|
|
||||||
`getCategoriesTree` 整体替换:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
async getCategoriesTree(countryId?: string): Promise<PublicCategoryNodeDto[]> {
|
|
||||||
const countryFilter = countryId ? { countryId: BigInt(countryId) } : {};
|
|
||||||
const roots = await this.prisma.category.findMany({
|
|
||||||
where: { parentCategoryId: null, sdsCategoryId: { not: null }, ...countryFilter },
|
|
||||||
orderBy: [{ country: { sortOrder: 'asc' } }, { sortOrder: 'asc' }, { id: 'asc' }],
|
|
||||||
include: {
|
|
||||||
children: {
|
|
||||||
orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }],
|
|
||||||
include: { children: { orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const countRows = await this.prisma.$queryRaw<Array<{ sds: string; cnt: bigint | number }>>`
|
|
||||||
SELECT og.sds_category_id AS sds, COUNT(*) AS cnt
|
|
||||||
FROM goods g
|
|
||||||
JOIN origin_goods og ON og.origin_good_id = g.origin_good_id
|
|
||||||
WHERE g.family_id IS NOT NULL
|
|
||||||
AND og.delisted = false
|
|
||||||
AND og.sds_category_id IS NOT NULL
|
|
||||||
${countryId ? Prisma.sql`AND g.country_id = ${BigInt(countryId)}` : Prisma.empty}
|
|
||||||
GROUP BY og.sds_category_id`;
|
|
||||||
const counts = new Map(countRows.map((r) => [r.sds, Number(r.cnt)]));
|
|
||||||
|
|
||||||
const build = (row: (typeof roots)[number]): PublicCategoryNodeDto => {
|
|
||||||
const children = row.children.map(build).filter((c) => c.productCount > 0);
|
|
||||||
const own = row.sdsCategoryId ? (counts.get(row.sdsCategoryId) ?? 0) : 0;
|
|
||||||
const node = PublicCategoryNodeDto.from(row, children);
|
|
||||||
node.productCount = own + children.reduce((sum, c) => sum + c.productCount, 0);
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
return roots.map(build).filter((r) => r.productCount > 0);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
(原 `buildTree` 若无其他调用方则删除;`Prisma.sql`/`Prisma.empty` 需 import `Prisma`,已有。)
|
|
||||||
|
|
||||||
- [ ] **Step 5.4: 跑全部 public 测试确认通过**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm ... npx jest src/public
|
|
||||||
```
|
|
||||||
Expected: 全部 PASS。既有 `getCategoriesTree` 用例(老树断言)改为新树断言或按新语义重写。
|
|
||||||
|
|
||||||
- [ ] **Step 5.5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add apps/api/src/public/public.service.ts apps/api/src/public/public.service.spec.ts
|
|
||||||
git commit -m "feat(api): serve public categories tree from sds country-category tree"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 6: 全量验证 + 交付
|
|
||||||
|
|
||||||
- [ ] **Step 6.1: api 全量测试**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm --network deploy-v2_default -e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' -v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine npx jest
|
docker run --rm --network deploy-v2_default -e DATABASE_URL='postgresql://inkreach:2628adbdf875727ae1b5556b08cb00452bb4e1f80490f6b6@postgres:5432/inkreach' -v /opt/inkreach-v2/apps/api:/app -w /app node:20-alpine npx jest
|
||||||
```
|
```
|
||||||
Expected: 全部 PASS。任何失败必须修复(含其他模块被 schema 变更波及的用例)。
|
Expected: 全部 PASS。任何失败必须修复(含其他模块被 schema 变更波及的用例)。
|
||||||
|
|
||||||
- [ ] **Step 6.2: verification-before-completion 自检清单**
|
- [ ] **Step 4.2: verification-before-completion 自检清单**
|
||||||
|
|
||||||
- [ ] `prisma migrate status` 干净(无未应用/未登记迁移)
|
- [ ] `prisma migrate status` 干净(无未应用/未登记迁移)
|
||||||
- [ ] 回填 UNMATCHED 清单为空(或已逐条人工处理并记录)
|
- [ ] 回填 UNMATCHED 清单为空(或已逐条人工处理并记录)
|
||||||
- [ ] (先完成 Step 6.4 重建容器)冒烟(真实调用形态):`/public/goods?pageSize=10` DEFAULT = 美国 DG001 族 → DG004 族 …;`/public/goods?categoryId=659`(女士服装,跨国)按国家顺序排;`/public/goods?categoryId=666` 同上;`/public/goods?countryId=49&categoryId=658` 只返回英国男装且按 ②③④;`/public/categories` 返回新树且沙特(空)不出现
|
- [ ] 冒烟(真实调用形态,先完成 Step 4.4 重建容器):`/public/goods?pageSize=10` DEFAULT = 美国 DG001 族 → DG004 族 …;`/public/goods?categoryId=659`(女士服装,跨国)按国家顺序排;`/public/goods?categoryId=666` 同上;`/public/goods?countryId=49&categoryId=658` 只返回英国男装且按 ②③④;`/public/categories` 输出与改造前完全一致(老树,结构内容均不变)
|
||||||
- [ ] DTO 字段逐个对比改动前后(PublicGoodDto / PublicCategoryNodeDto / 分页结构)无增删
|
- [ ] DTO 字段逐个对比改动前后(PublicGoodDto / 分页结构)无增删
|
||||||
- [ ] `git status` 干净,全部提交
|
- [ ] `git status` 干净,全部提交
|
||||||
|
|
||||||
- [ ] **Step 6.3: 合并回 refactor/v2**
|
- [ ] **Step 4.3: 合并回 refactor/v2**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout refactor/v2 && git merge --no-ff feature/category-tree-sort -m "merge: feature/category-tree-sort (public goods tree-order + new category tree)"
|
git checkout refactor/v2 && git merge --no-ff feature/category-tree-sort -m "merge: feature/category-tree-sort (public goods style-order sorting)"
|
||||||
git branch -d feature/category-tree-sort
|
git branch -d feature/category-tree-sort
|
||||||
```
|
```
|
||||||
(本仓库 v2 工作线为 `refactor/v2`;不推远端,部署时机由用户确认。)
|
(本仓库 v2 工作线为 `refactor/v2`;不推远端,部署时机由用户确认。)
|
||||||
|
|
||||||
- [ ] **Step 6.4: 重建 v2-api 容器使新逻辑生效(部署步骤,执行前向用户确认)**
|
- [ ] **Step 4.4: 重建 v2-api 容器使新逻辑生效(部署步骤,执行前向用户确认)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/inkreach-v2/deploy && docker compose -f docker-compose.v2.yml up -d --build api
|
cd /opt/inkreach-v2/deploy && docker compose -f docker-compose.v2.yml up -d --build api
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 6.5: 按项目规则沉淀**
|
- [ ] **Step 4.5: 按项目规则沉淀**
|
||||||
|
|
||||||
- 更新 `docs/references/structs.md`(categories 新字段、public 排序语义)
|
- 更新 `docs/references/structs.md`(categories.sort_order 新字段、public 排序语义、款/family 领域说明)
|
||||||
- 更新 `docs/references/` 使用文档与 `README.md`(新排序规则、categoryId 多 id 用法)
|
- 更新 `docs/references/` 使用文档与 `README.md`(新排序规则)
|
||||||
- 通用经验追加到 `AGENTS.md`(如:共享库迁移「手写 SQL + db execute + resolve」通道、树形排序的「绝对排序键 + 子集过滤」模式)
|
- 通用经验追加到 `AGENTS.md`(如:共享库迁移「手写 SQL + db execute + resolve」通道、「绝对排序键 + 子集过滤」模式、排序表驱动回填的幂等脚本设计)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 范围外(明确不做)
|
## 范围外(明确不做)
|
||||||
|
|
||||||
|
- **分类树接口与导航**(老树不动;新树仅作款排序元数据层)
|
||||||
- `getHomeGoods`(首页位次排序维持 position.indexVal 优先,另行需求再调)
|
- `getHomeGoods`(首页位次排序维持 position.indexVal 优先,另行需求再调)
|
||||||
- 防晒衣(good_id=137) 的 sds_category_id=2393 修复(排序中沉底)
|
- 防晒衣(good_id=137) 的 sds_category_id=2393 修复(排序中日本组内沉底)
|
||||||
- admin 后台分类拖拽排序 UI(本期用脚本回填;后续如需可视化调整再立项)
|
- admin 后台款顺序拖拽管理(本期用脚本回填;后续如需可视化调整再立项)
|
||||||
- 中东根更名后 SDS 同步覆盖名称的持久对抗(重跑回填脚本即可恢复)
|
- 中东根更名后 SDS 同步覆盖名称的持久对抗(重跑回填脚本即可恢复)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -742,7 +486,7 @@ cd /opt/inkreach-v2/deploy && docker compose -f docker-compose.v2.yml up -d --bu
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| DB 全库快照 | `backups/inkreach_snapshot_20260901_185233.dump`(pg_dump -Fc,3.4M) | 2026-09-01 |
|
| DB 全库快照 | `backups/inkreach_snapshot_20260901_185233.dump`(pg_dump -Fc,3.4M) | 2026-09-01 |
|
||||||
| 运行容器快照 | docker 镜像 `inkreach-api-snapshot:20260901`(711MB,commit 自 deploy-v2-api-1) | 2026-09-01 |
|
| 运行容器快照 | docker 镜像 `inkreach-api-snapshot:20260901`(711MB,commit 自 deploy-v2-api-1) | 2026-09-01 |
|
||||||
| 代码 | 分支 `feature/category-tree-sort`(develop/refactor-v2 未动) | 实时 |
|
| 代码 | 分支 `feature/category-tree-sort`(refactor/v2 未动) | 实时 |
|
||||||
|
|
||||||
**DB 恢复(覆盖式,先停 api 容器避免写入竞争):**
|
**DB 恢复(覆盖式,先停 api 容器避免写入竞争):**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user