- Country 加 sort_order(默认 0 保持 id 序);PATCH /countries/sort 批量保存 顺序(对齐 /tags/sort 模式);findAll 与公开 /public/countries 均按 sortOrder 排序 - CountriesView 表格改为可拖拽行列表:拖动松开即全量保存新顺序,失败回滚 - 商品编辑弹窗成员展开面板:同步详情按钮常驻(已同步显示 重新同步详情), 不再只在未同步态出现 - goods.service.spec 的 FamilyRecomputeService mock 补齐 syncFamilyTags 等 方法(全量并行时其他套件的扫名归族会把本套件夹具收进族,create/update 会调用到,mock 缺方法导致偶发 TypeError) - api 164/164、admin typecheck+22/22+构建全绿
20 KiB
20 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,再启动另两个。
子项目 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 # 产品族回填脚本(解析列→自动建族→全量重算,幂等)
├── 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 / 成员管理 / 自定义成员 / 价格覆盖 / 重算 / 按链接名称派生标签(auto-tag-rules)
│ ├── goods/ # 商品 CRUD + 批量优先级 + 批量创建
│ ├── 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) |
公开 |
/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/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 | 公开 |
子项目 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 |