# 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_pgdata` external 引用,v2 数据未迁移)。 - 域名路由:`/v2/admin/`(SPA)、`/v2-api/`(小程序/后台 API,去前缀转发)、`/public/` (H5 同源 API)、`/uploads/`、`/assets/` 均由 v2-api 服务;`/` 302 到 `/v2/admin/`。 - 性能整改 P0(2026-09-03,plans/refactor/public-capacity-10k-refactor.md):边缘 nginx 主配置改为仓库文件 `deploy/nginx/nginx.conf`(worker_connections 16384 + gzip + 访问日志, 经 admin.Dockerfile COPY,改后须重建镜像);admin.conf 含 upstream keepalive/限流(429)/ 代理超时/静态缓存头;v2-postgres 显式参数(statement_timeout=10s、max_connections=200、 shared_buffers=512MB);v2-api 的 DATABASE_URL 带 `connection_limit=50&pool_timeout=3` (pool_timeout 单位为秒);admin 服务 ulimits nofile 65536。改 PG 参数/重启前按全局约束 先 pg_dump + 配置快照落 `deploy/backups/<时间戳>/`。 - 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:分类树 / 国家 / 商品分页 / 商品详情;public-cache.service.ts 为进程内分域缓存(meta/goods/matrix 版本域 + TTL 兜底 + in-flight 合并,P0-1 性能整改),全部写路径(admin CRUD/sync/族重算/整理)事务提交后 bump 对应域即时失效;env 开关 PUBLIC_CACHE_DISABLED / PUBLIC_CACHE_TTL_MS / PUBLIC_CACHE_MAX_ENTRIES │ └── 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 `;响应直接返回 `response.data`;401 自动登出跳转。 - **ElementPlus 自动导入**:通过 `unplugin-auto-import` + `unplugin-vue-components` + `ElementPlusResolver`。 - **路由守卫**:未登录访问受保护路由跳 `/login`;已登录访问 `/login` 跳 `/`。 - **Pinia 持久化**:`auth` store 主动读写 `localStorage`(`token` + `user`)。 ## 子项目 3:apps/website(官网,@inkreach/website) ``` apps/website/ ├── app/ │ ├── app.vue # 根容器: │ ├── 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` |