Files
inkreach-official-website/docs/references/structs.md
T
yeuimu 9eafac7204 fix(goods): 商品展示名统一「品名 SKU」——剥 ASCII 型号限定词、纯款号补描述
左栏商品名显示成光款号(如 PLTK016)的根因是双层解析不一致:api 写路径按
首括号对解析产出「童装…(DTG180) PLTK016」,admin 展示解析按末括号取品名
误显示成 PLTK016。

- admin parseLinkName 对齐 api 首括号对解析 + 无 SKU 段中文品名守卫,
  cleanLinkName 剥离 ASCII 型号限定词(中文括号内容保留)
- api 新增 cleanGoodDisplayName/describePureSkuGoodName,接入 create/update/
  batchCreate(batchCreate 补上缺失的 normalize);纯款号按 SDS 分类名补描述
- 新增幂等存量修复脚本 fix:good-names(默认 dry-run,--apply 写库)
- 已执行:V2 栈 34 条限定词名、V1 栈 213 条原始链接名规范化,复跑幂等 0

Tests: api 22 suites/199 passed, admin 27 passed, tsc clean
2026-09-03 01:27:26 +08:00

317 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# InkReach Product Center — Monorepo Structure
> pnpm workspaces + Turborepo 统一管理三个子项目与共享包。所有子项目位于 `apps/`,共享代码位于 `packages/`。
## 根目录
```
inkreach-official/
├── apps/
│ ├── api/ # NestJS 后端 (package: @inkreach/api)
│ ├── admin/ # Vue 3 后台 (package: @inkreach/admin)
│ └── website/ # Nuxt 4 官网 (package: @inkreach/website)
├── packages/
│ ├── tsconfig/ # 共享 TypeScript 配置 (base / api / vue)
│ └── shared-types/ # 共享类型定义(PaginatedResult, BackendEnvelope, 实体接口)
├── .agents/ # AI Agent 共享技能
├── docs/ # 跨子项目文档
│ ├── dev/ # 设计文档(数据库表设计、PRD、原型图)
│ ├── references/
│ │ └── structs.md # 本文件
│ └── superpowers/specs/ # superpowers 规格说明
├── plans/ # 跨子项目计划
├── package.json # 根 workspace 配置(scripts + devDependencies
├── pnpm-workspace.yaml # pnpm workspace 定义(apps/*, packages/*
├── turbo.json # Turborepo 任务流水线
├── .prettierrc # 统一代码格式化
├── .gitignore # 全局忽略规则
├── .env # 根级环境变量(DATABASE_URL 等)
├── AGENTS.md # 根级 AI Agent 开发规范
└── README.md
```
## 端口与子项目
| 子项目 | 包名 | 端口 | 启动命令 | 说明 |
|--------|------|------|----------|------|
| `apps/api` | `@inkreach/api` | 3001 | `pnpm --filter @inkreach/api start:dev` | NestJS + Prisma + PostgreSQL 后端 APISwagger 文档 `/api/docs` |
| `apps/admin` | `@inkreach/admin` | 5173 | `pnpm --filter @inkreach/admin dev` | Vue 3 + Element Plus 后台管理;通过 Vite proxy `/api → :3001` |
| `apps/website` | `@inkreach/website` | 3000 | `pnpm --filter @inkreach/website dev` | Nuxt 4 官网;通过 Nitro `server/api/backend/*` 代理 `:3001` |
启动顺序:先启动 `apps/api`,再启动另两个。
## 子项目 1apps/api(后端,@inkreach/api
```
apps/api/
├── prisma/
│ ├── schema.prisma # 数据模型(OriginGood/ProductFamily/FamilyPriceOverride/Country/Category/Tag/Position/Good/User/SyncLog
│ ├── migrations/ # Prisma migrate 历史
│ ├── backfill-product-families.ts # 产品族回填脚本(解析列→自动建族→全量重算,幂等)
│ └── fix-pure-sku-good-names.ts # 商品名存量修复脚本(剥型号限定词/纯款号补描述,幂等,dry-run 默认)
├── src/
│ ├── main.ts # 入口:CORS、ValidationPipe、Swagger、BigInt JSON 序列化
│ ├── app.module.ts # 根模块,聚合所有业务模块
│ ├── prisma/ # PrismaService 封装
│ ├── auth/ # JWT 认证:register / login / JwtStrategy / JwtAuthGuard
│ ├── countries/ # 国家 CRUD(受 JWT 保护)
│ ├── categories/ # 品类 CRUD(受 JWT 保护,自引用树)
│ ├── tags/ # 标签 CRUD(受 JWT 保护)
│ ├── tag-groups/ # 标签分组 CRUD(受 JWT 保护,含批量排序)
│ ├── positions/ # 坑位 CRUD(受 JWT 保护)
│ ├── origin-goods/ # SDS 原始商品快照(只读分页 + 配置状态树 + 链接级标签人工接管)
<<<<<<< HEAD
│ ├── product-families/ # 产品族(SPU 层):CRUD / auto-group / 成员管理 / 自定义成员 / 价格覆盖 / 重算 / 按链接名称派生标签(auto-tag-rules
│ ├── goods/ # 商品 CRUD + 批量优先级 + 批量创建
=======
│ ├── product-families/ # 产品族(SPU 层):CRUD / auto-group(并入已有族优先,familyNameKey 族语义键)/ attachToMatchingFamily(单链接自动归族)/ consolidateFragments(碎片族合并)/ 成员管理 / 自定义成员 / 价格覆盖 / 重算 / 按链接名称派生标签(auto-tag-rules,含 热转印→烫画 别名)
│ ├── goods/ # 商品 CRUD + 批量优先级 + 批量创建 + 展示名规范化(品名 SKU,剥 ASCII 型号限定词,纯款号按 SDS 分类名补描述)
>>>>>>> c3be8ab (fix(goods): 商品展示名统一「品名 SKU」——剥 ASCII 型号限定词、纯款号补描述)
│ ├── sync/ # SDS 同步:分类 / 商品 / 同步日志
│ ├── public/ # 公开 API:分类树 / 国家 / 商品分页 / 商品详情
│ └── common/ # 全局装饰器 / 过滤器 / 拦截器
│ ├── decorators/current-user.decorator.ts
│ ├── filters/http-exception.filter.ts
│ └── interceptors/transform.interceptor.ts
├── test/ # e2e 测试
├── dist/ # 构建产物
├── nest-cli.json
├── tsconfig.json / tsconfig.build.json
├── jest.config.js
└── package.json
```
### 数据模型(Prisma
| 模型 | 说明 |
|------|------|
| `OriginGood` | SDS 原始商品缓存,关联 `sds_good_id`(唯一);含四个链接名解析列(`skuCode/logisticsLabel/craftLabel/warehouseLabel`,格式 `国家(物流)品名-SKU-工艺[-仓库]`)与 `familyId` 族归属 |
| `OriginGoodDetail` | SDS `/products/{id}` 详情缓存(文本字段 + `sizeChart/packageSpecs/options/media` JSON |
| `OriginGoodVariant` | SDS 子 SKU 缓存(尺码 × 颜色 × 价格),价格矩阵推导的数据源 |
| `ProductFamily` | 产品族(SPU 层):同 SDS 分类(产品模型)的多条链接合并为一族;物化并集尺码表/包装规则与五维价格矩阵(`priceMatrix` JSON);`autoManaged=false` 为人工锁定(重算只置 `stale`);`primaryOriginGoodId` 主链接(无 FK,服务层维护) |
| `FamilyPriceOverride` | 人工按格改价:`(familyId, sizeId, colorId, craft, logistics)` 唯一 → `price`;独立于推导矩阵,重算永不覆盖 |
| `Country` | 国家,关联 goods / positions |
| `Category` | 自引用树形品类,可选 `sds_category_id` |
| `Tag` | 标签,含 `tagColor``tagFontColor``tagGroupId``sortOrder``timing` |
| `TagGroup` | 标签分组(含 `groupName``groupColor``groupIcon``sortOrder`),删除分组时组内 tag 的 `tagGroupId` 通过 `onDelete: SetNull` 自动置空 |
| `Position` | 坑位:`(country, category)` 维度,关联多个 goods |
| `Good` | 商品:`originGood × country × category × tag? × position?`,含 `goodPriority``familyId` 为派生数据(主链接所属族,创建时自动填充,族成员变更时联动) |
| `GoodOriginGood` | 副源关联中间表(多对一):一个 Good 可关联多个副源 OriginGood;主源走 `goods.origin_good_id` 不入表。用于把名称相同但工厂/仓库不同的多个原产品合并为一个商品展示 |
| `User` | 后台用户(bcrypt 哈希) |
| `SyncLog` | 同步任务日志,含 `SyncType`CATEGORIES / PRODUCTS)和 `SyncStatus` |
### 关键设计
- **所有主键为 `BigInt`**,路由解析后 `BigInt(id)` 处理,序列化时通过 `BigInt.prototype.toJSON` 转为字符串。
- **全局 `TransformInterceptor`**:把响应包装为 `{ data: T, success: true }`,前端读取 `response.data`
- **全局 `ValidationPipe`**`whitelist + transform + forbidNonWhitelisted`
- **全局 `HttpExceptionFilter`**:统一错误响应形态。
- **CORS 白名单**`http://localhost:5173`admin)和 `http://localhost:3000`website)。
- **JWT**:所有 `/goods /categories /countries /tags /positions /origin-goods /product-families /sync/*` 路由受 `JwtAuthGuard` 保护;`/public/*``/auth/*` 公开。
### API 路由
| 前缀 | 说明 | 鉴权 |
|------|------|------|
| `/auth/register` `POST` | 注册后台用户 | 公开 |
| `/auth/login` `POST` | 登录获取 JWT | 公开 |
| `/public/categories` `GET` | 公开品类树(仅含已挂商品的品类) | 公开 |
| `/public/countries` `GET` | 公开国家列表(仅含已挂商品的国家) | 公开 |
| `/public/tags` `GET` | 公开标签列表(带 `group` 字段,按 group 排序) | 公开 |
| `/public/tag-groups` `GET` | 公开标签分组列表 | 公开 |
| `/public/goods` `GET` | 分页商品(**族化契约:一族一条**,`goodId`=族ID`price`=族起价;无族商品不返回;支持 `countryId/categoryId/tags(JSON)/keyword/minPrice/maxPrice/sort/page/pageSize`)。**默认排序 = 款序树**:国家(`countries.sort_order`) → 款所属二级(`categories.sort_order`) → 款(`categories.sort_order`) → `good_priority`;商品经 `origin_goods.sds_category_id` 定位到款(新树三级节点=合并后的款,顺序值由 `排序表.md` 经回填脚本写入),同一款下多条 Good 聚在一起、款内按优先级分先后;`sort=PRICE_ASC/PRICE_DESC/NEWEST` 不受影响 | 公开 |
| `/public/goods/:id` `GET` | 款级详情,`:id` = **族 ID**(唯一公开键,SDS 链接 ID 404);公共字段取代表 Good,`variants` = 全体族成员 旧副源(去重),`sizeChart/packageSpecs` = 族并集;默认输出 `family` 块(并集尺码表/包装 + 严格五维价格矩阵 尺码×颜色×印花数量×工艺×物流 + 族起价);`PUBLIC_DETAIL_FROM_FAMILY=false` 应急回退 | 公开 |
| `/categories` `/tags` `/tag-groups` `/countries` `/positions` | 后台 CRUD | JWT |
| `/countries/sort` `PATCH` | 批量保存国家拖拽排序(`items=[{id,sortOrder}]` 全量提交;公开/后台国家列表均按 sortOrder 排序) | JWT |
| `/tags/sort` `PATCH` | 批量更新 tag 排序和分组归属 | JWT |
| `/tag-groups/sort` `PATCH` | 批量更新分组排序 | JWT |
| `/origin-goods` `GET` | SDS 原始商品快照分页 | JWT |
| `/origin-goods/tree` `GET` | 配置状态树(叶子含 `familyId/familyName/familyCode/familyStale` | JWT |
| `/origin-goods/:id/tags` `GET/PUT/DELETE` | 链接级标签:查(含 manual 标记)/ 人工接管全量替换 / 恢复按名称自动派生;写入后镜像到名下商品 | JWT |
| `/product-families` `GET/POST` | 产品族分页列表(`keyword` 匹配名称/编码)/ 建族(可直挂成员) | JWT |
| `/product-families/organize` `POST` | 整理原产品库(显式人工动作):回填解析列 → 派生标签(人工接管不动)→ 自动建族 → 全量重算;CLI 等价 `pnpm --filter @inkreach/api organize` | JWT |
| `/product-families/auto-group` `POST` | 自动成族:按 SDS 分类(产品模型)聚合无族链接;`{apply:false}` 仅预览,`{apply:true}` 落库并逐族重算(幂等) | JWT |
| `/product-families/:id` `GET/PATCH` | 族详情(成员+覆盖)/ 编辑 canonical 字段、`autoManaged`、主链接 | JWT |
| `/product-families/:id/recompute` `POST` | 手动重算并集与价格矩阵 | JWT |
| `/product-families/:id/members` `POST` | 成员增删 `{addOriginGoodIds, removeOriginGoodIds}`;移除主链接后 primary 落到剩余成员 | JWT |
| `/product-families/:id/members/custom` `POST` | 族内创建自定义成员(人工商品:物流/工艺归因必填 + 变体价格 + 可选尺码表/包装) | JWT |
| `/product-families/:id/price-overrides` `GET/PUT/DELETE` | 人工改价:查(含推导价对照与差额)/ 批量 upsert / 按格删除恢复推导价;格子五键 `sizeId+colorId+printCount+craft+logistics` 必须存在于族矩阵选项 | JWT |
| `/goods` | 后台商品 CRUD + `POST /goods/batch` + `PATCH /goods/batch-priority` | JWT |
| `/sync/categories` `POST` | 手动触发分类同步 | JWT |
| `/sync/products` `POST` | 手动触发商品同步 | JWT |
| `/sync/status` `GET` | 最近同步日志(`?limit=20` | JWT |
| `/api/docs` | Swagger UI | 公开 |
## 子项目 2apps/admin(后台,@inkreach/admin
```
apps/admin/
├── public/ # 静态资源
├── src/
│ ├── main.ts # 入口:Pinia + Vue Router + ElementPlus
│ ├── App.vue
│ ├── style.css # 全局样式(含品牌色变量)
│ ├── api/ # 按业务模块拆分的 API 客户端
│ │ ├── request.ts # axios 实例 + JWT 拦截 + 全局错误处理
│ │ ├── auth.ts # /auth/login, /auth/me, /auth/logout
│ │ ├── goods.ts # 商品 CRUD + 批量
│ │ ├── categories.ts # 品类 CRUD
│ │ ├── countries.ts # 国家 CRUD
│ │ ├── tags.ts # 标签 CRUD(含批量排序)
│ │ ├── tag-groups.ts # 标签分组 CRUD
│ │ ├── positions.ts # 坑位 CRUD
│ │ ├── origin-goods.ts # 原始商品快照
│ │ ├── product-families.ts # 产品族 CRUD / auto-group / 成员 / 覆盖 / 重算
│ │ └── sync.ts # 同步触发 + 日志
│ ├── layouts/DefaultLayout.vue # 侧边栏 + 顶部条 + 用户菜单
│ ├── router/index.ts # 路由 + 登录守卫
│ ├── stores/
│ │ ├── auth.ts # 登录态 + token + user(持久化到 localStorage
│ │ └── app.ts # 侧边栏折叠
│ ├── types/index.ts # 共享类型
│ ├── views/
│ │ ├── login/LoginView.vue # 登录
│ │ ├── goods/GoodsView.vue # 商品配置主页面(左右树 + 筛选 + 全局列表)
│ │ ├── goods/components/ # 弹窗/弹层组件:GoodsEditDialog(编辑+族成员标签/改价)、
│ │ │ # GoodsConfigDialog(配置合并)、CustomGoodDialog、TagFilterPopover
│ │ ├── categories/CategoriesView.vue
│ │ ├── countries/CountriesView.vue
│ │ ├── tags/TagsView.vue
│ │ ├── positions/PositionsView.vue
│ │ └── sync/SyncView.vue
│ ├── auto-imports.d.ts # 自动生成的自动导入类型
│ └── components.d.ts # 自动生成的组件类型
├── index.html
├── vite.config.ts # Vite + ElementPlus 自动导入 + /api → :3001 代理
├── tsconfig.json / tsconfig.app.json / tsconfig.node.json
└── package.json
```
### 路由
| 路径 | 视图 | 鉴权 |
|------|------|------|
| `/login` | LoginView | 公开 |
| `/goods` | GoodsView(默认页) | JWT |
| `/categories` | CategoriesView | JWT |
| `/countries` | CountriesView | JWT |
| `/tags` | TagsView | JWT |
| `/positions` | PositionsView | JWT |
| `/sync` | SyncView | JWT |
| `/:pathMatch(.*)*` | 重定向到 `/goods` | — |
### 关键设计
- **Vite 代理**`/api/*` 代理到 `http://localhost:3001`rewrite 去掉 `/api` 前缀。
- **axios 拦截器**:请求注入 `Authorization: Bearer <token>`;响应直接返回 `response.data`401 自动登出跳转。
- **ElementPlus 自动导入**:通过 `unplugin-auto-import` + `unplugin-vue-components` + `ElementPlusResolver`
- **路由守卫**:未登录访问受保护路由跳 `/login`;已登录访问 `/login``/`
- **Pinia 持久化**`auth` store 主动读写 `localStorage``token` + `user`)。
## 子项目 3apps/website(官网,@inkreach/website
```
apps/website/
├── app/
│ ├── app.vue # 根容器:<NuxtPage />
│ ├── pages/
│ │ ├── index.vue # 首页
│ │ └── product-center.vue # 产品中心(侧边栏 + 国家/筛选 + 网格 + 分页)
│ ├── assets/css/tailwind.css # Tailwind v4 主题(@theme 定义颜色与动画)
│ ├── composables/
│ │ ├── useNavData.ts # 导航数据(选品推荐/解决方案)
│ │ ├── usePodProducts.ts # 首页 POD 产品(SDS
│ │ └── useProductCenter.ts # 产品中心数据(调用 /api/backend/*
│ └── components/
│ ├── AppHeader.vue / AppFooter.vue
│ ├── nav/ # 导航下拉面板
│ │ ├── NavMegaMenu.vue
│ │ └── NavColumnMenu.vue
│ ├── product/ # 产品中心专用组件
│ │ ├── ProductSidebar.vue # 树形品类侧边栏
│ │ ├── ProductCountryFilter.vue # 国家 pill 筛选
│ │ ├── ProductFilterBar.vue # 搜索输入 + 按钮
│ │ ├── ProductCard.vue # 商品卡片
│ │ ├── ProductCardSkeleton.vue # 骨架占位
│ │ ├── ProductGrid.vue # 网格容器
│ │ └── ProductPagination.vue # 分页器
│ ├── HeroBanner.vue
│ ├── TrustSection.vue
│ ├── StepProcess.vue
│ ├── PodProducts.vue
│ ├── FeatureCards.vue
│ ├── WhyInkReach.vue
│ ├── CompanyProfile.vue
│ ├── CustomerCases.vue
│ └── CtaBanner.vue
├── server/ # Nitro 后端
│ ├── api/
│ │ ├── pod/ # 原有 SDS POD API 代理
│ │ └── backend/ # NestJS 后端代理(同源 + Nitro 缓存 60s
│ │ ├── categories.get.ts # → GET :3001/public/categories
│ │ ├── countries.get.ts # → GET :3001/public/countries
│ │ ├── tags.get.ts # → GET :3001/public/tags
│ │ ├── tag-groups.get.ts # → GET :3001/public/tag-groups
│ │ ├── goods.get.ts # → GET :3001/public/goods
│ │ └── goods/[id].get.ts # → GET :3001/public/goods/:id
│ └── utils/pod-api.ts
├── public/ # 静态资源
├── plans/feature/ # 历史功能计划
├── docs/
│ ├── references/structs.md # 子项目级结构文档
│ └── superpowers/specs/
├── nuxt.config.ts # runtimeConfig.public.backendUrl
├── .env # NUXT_PUBLIC_BACKEND_URL=http://localhost:3001
├── AGENTS.md # 子项目 AI Agent 规范
├── README.md
└── package.json
```
> 子项目内部的页面、组件、composable、代理路由、组件响应式断点等详细信息见 `apps/website/docs/references/structs.md`。
## 数据库
- `apps/api/public/product-center/`:产品中心 Figma 国家旗帜与一级品类图标,由 `/assets/product-center/*` 对外提供。
- `apps/api/prisma/configure-product-center-icons.ts`:按名称幂等写入 `countryIcon/categoryIcon`,不创建业务记录。
## 产品中心设计还原(2026-07-16
- `apps/website/app/components/product/ProductCenterHeader.vue`Figma 产品中心专用 80px 导航栏。
- `apps/website/app/components/product/ProductSidebar.vue`:240px 分类树及移动端抽屉内容。
- `apps/website/app/components/product/ProductCountryFilter.vue`:国家胶囊筛选。
- `apps/website/app/components/product/ProductTagFilter.vue`:物流与工艺分组筛选。
- `apps/website/app/components/product/ProductCard.vue`270 x 382 桌面商品卡片。
- `apps/website/app/components/product/ProductPagination.vue`:总数、页码、每页数量和跳转。
- `apps/website/test/useProductCenter.test.ts`:默认筛选名称映射测试。
- `docs/references/product-center.md`:产品中心使用与验证说明。
- `skills/inkreach-official-website/SKILL.md`Agent 使用说明。
## 官网首页设计还原(2026-07-16
- `apps/website/app/pages/index.vue`:官网首页组合入口。
- `apps/website/app/components/HeroBanner.vue``AppFooter.vue`:首页分区组件。
- `apps/website/public/case-*.png`Figma 用户案例商品图。
- `docs/references/homepage.md`:首页结构、尺寸基准和验证说明。
- `apps/website/test/HomepageNavigationCarousel.test.ts`:首页当前页 CTA、自动轮播、主视觉图片和临时导航隐藏的行为测试。
## 数据库
- `apps/api/src/public/public.service.ts`:官网公共商品序列化边界;响应中的商品 `id` 使用 `originGood.sdsGoodId`,不暴露本地 `goods.good_id`
- PostgreSQL 14+Prisma 5.x
- 连接配置在根 `.env``DATABASE_URL`
- 所有 `TIMESTAMPTZ` 列:`@db.Timestamptz(6)`
- 所有主键:`BigInt @default(autoincrement())`
- 表名与列名通过 `@map` / `@@map` 映射为 `snake_case`
- 迁移位于 `apps/api/prisma/migrations/`
## 环境变量总览
| 变量 | 位置 | 用途 | 默认 |
|------|------|------|------|
| `DATABASE_URL` | 根 `.env` | PostgreSQL 连接串 | — |
| `JWT_SECRET` | `apps/api` | JWT 签名密钥 | — |
| `PORT` | `apps/api` | 后端端口 | `3001` |
| `SDS_API_*` | `apps/api` | 同步上游 SDS 接口凭据 | — |
| `VITE_API_BASE` | `apps/admin` | axios baseURL | `/api` |
| `NUXT_PUBLIC_BACKEND_URL` | `apps/website` | NestJS 后端地址 | `http://localhost:3001` |