- 生产数据操作(未改源代码):11 条光板/不打印链接 family_id 置空(族 1/2/63/135/148), 名下 3 商品脱族,族 148 主链接 964→965、族 135 主链接置空 - 容器内直调已部署 FamilyRecomputeService 重算 5 族:全库矩阵不再含「不打印」, 族 148 = 烫画×单面印花×不包邮(min 19),public 端口验证通过 - 回滚存档:deploy/backups/20260903-noprint-removal/(全库 dump + 受影响行 CSV + RESTORE.md) - 文档:product-center/structs 记录数据约定与回流风险(organize/autoGroup/人工改标签会回挂散链接); AGENTS.md 沉淀 占位借词表值/物化JSON需显式重算/数据修复三件套 三条经验
23 KiB
23 KiB
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 后端 API;Swagger 文档 /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,再启动另两个。
生产部署(单栈,2026-09-03 收敛后)
- 唯一部署源:本仓库 develop(
/opt/inkreach),deploy/docker-compose.yml单 compose 编排 4 容器:admin(边缘 nginx,80/443 + 证书 + 全站路由)、v2-api、v2-admin、v2-postgres(数据卷deploy-v2_pgdataexternal 引用,v2 数据未迁移)。 - 域名路由:
/v2/admin/(SPA)、/v2-api/(小程序/后台 API,去前缀转发)、/public/(H5 同源 API)、/uploads/、/assets/均由 v2-api 服务;/302 到/v2/admin/。 - v1(旧 api+postgres,曾支撑
/public/与旧后台)已退役:切换时容器停用未删, 库终档与全部配置快照及回滚手册见deploy/backups/consolidation-*/RESTORE.md。 - 原 v2 独立栈(
docker-compose.v2.yml、/opt/inkreach-v2检出)已并入单栈并删除, 其血统(refactor/v2)已完全合入 develop;后续所有开发只走 develop。
子项目 1:apps/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 默认)
│ └── fix-category-parens.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 原始商品快照(只读分页 + 配置状态树 + 链接级标签人工接管)
│ ├── product-families/ # 产品族(SPU 层):CRUD / auto-group(并入已有族优先,familyNameKey 族语义键)/ attachToMatchingFamily(单链接自动归族)/ consolidateFragments(碎片族合并)/ 成员管理 / 自定义成员 / 价格覆盖 / 重算 / 按链接名称派生标签(auto-tag-rules,含 热转印→烫画 别名)。数据约定(2026-09-03 起):光板/不打印链接不入族、矩阵不计算不打印工艺;auto-group/organize/人工改标签会把散链接回挂,运行前须排查(备份见 deploy/backups/20260903-noprint-removal/)
│ ├── goods/ # 商品 CRUD + 批量优先级 + 批量创建 + 展示名规范化(品名 SKU,剥 ASCII 型号限定词,纯款号按 SDS 分类名补描述)
│ ├── 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 |
整理原产品库(显式人工动作):回填解析列 → 派生标签(人工接管不动)→ 自动建族(并入已有同款族优先)→ 碎片族合并 → 全量重算;返回 familiesMerged/fragmentsConsolidated/fragmentsManualReview;CLI 等价 pnpm --filter @inkreach/api organize |
JWT |
/product-families/auto-group POST |
自动成族:按 SDS 分类(产品模型)聚合无族链接,无分类回退族语义名称键(familyNameKey,物流/工艺不分族);已有同款 autoManaged 族则并入(不建 -2 碎片族),同款仅人工锁定族则跳过;{apply:false} 预览含 action: create/merge/skip,{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 | 公开 |
子项目 2:apps/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 持久化:
authstore 主动读写localStorage(token+user)。
子项目 3:apps/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 |