feat(product-center): implement full product center with NestJS backend, Vue3 admin, and Nuxt4 website page

- NestJS backend: Prisma + PostgreSQL, JWT auth, CRUD for goods/categories/countries/tags/positions, SDS sync with cron, public API, Swagger docs
- Vue3 Admin: Element Plus, goods/categories/countries/tags/positions/sync management pages, batch operations, login with JWT
- Nuxt4 Website: product-center page with sidebar navigation, country filter, search, product grid, pagination, skeleton loading
- Nitro proxy routes for backend API
- 54 backend tests passing
- Updated docs and README
This commit is contained in:
yeuimu
2026-06-15 10:09:42 +08:00
parent 72bc89ab03
commit c3472a449b
125 changed files with 22206 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
# InkReach Product Center — Monorepo Structure
> 全局根目录结构说明。三个子项目独立维护各自 `package.json` 与技术栈,通过约定的端口协同工作。
## 根目录
```
inkreach-official/
├── .agents/ # AI Agent 共享技能(brainstorming / TDD / git-spec / ...
├── docs/ # 跨子项目文档
│ ├── dev/ # 设计文档(数据库表设计、PRD、原型图)
│ │ ├── database-table-design.md
│ │ ├── product-center-prd.md
│ │ └── product-center-{1,2,3}.png
│ ├── references/
│ │ └── structs.md # 本文件
│ └── superpowers/specs/ # superpowers 规格说明
├── plans/ # 跨子项目计划
│ └── feature/
├── AGENTS.md # 根级 AI Agent 开发规范
├── skills-lock.json # 技能版本锁定
├── .env # 根级环境变量(DATABASE_URL 等)
├── .git/ # 单一 Git 仓库,trunk-based
├── inkreach-official-nestjs/ # 子项目 1NestJS 后端
├── inkreach-official-admin/ # 子项目 2Vue 3 后台
└── inkreach-official-website/ # 子项目 3Nuxt 4 官网
```
## 端口与子项目
| 子项目 | 端口 | 启动命令 | 说明 |
|--------|------|----------|------|
| inkreach-official-nestjs | 3001 | `npm run start:dev` | NestJS + Prisma + PostgreSQL 后端 APISwagger 文档 `/api/docs` |
| inkreach-official-admin | 5173 | `npm run dev` | Vue 3 + Element Plus 后台管理;通过 Vite proxy `/api → :3001` |
| inkreach-official-website | 3000 | `npm run dev` | Nuxt 4 官网;通过 Nitro `server/api/backend/*` 代理 `:3001` |
启动顺序:先启动 `inkreach-official-nestjs`,再启动另两个。
## 子项目 1inkreach-official-nestjs(后端)
```
inkreach-official-nestjs/
├── prisma/
│ ├── schema.prisma # 数据模型(OriginGood/Country/Category/Tag/Position/Good/User/SyncLog
│ └── migrations/ # Prisma migrate 历史
├── 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 保护)
│ ├── positions/ # 坑位 CRUD(受 JWT 保护)
│ ├── origin-goods/ # SDS 原始商品快照(只读分页)
│ ├── 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`(唯一) |
| `Country` | 国家,关联 goods / positions |
| `Category` | 自引用树形品类,可选 `sds_category_id` |
| `Tag` | 标签,含 `tagColor``timing` |
| `Position` | 坑位:`(country, category)` 维度,关联多个 goods |
| `Good` | 商品:`originGood × country × category × tag? × position?`,含 `goodPriority` |
| `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 /sync/*` 路由受 `JwtAuthGuard` 保护;`/public/*``/auth/*` 公开。
### API 路由
| 前缀 | 说明 | 鉴权 |
|------|------|------|
| `/auth/register` `POST` | 注册后台用户 | 公开 |
| `/auth/login` `POST` | 登录获取 JWT | 公开 |
| `/public/categories` `GET` | 公开品类树(仅含已挂商品的品类) | 公开 |
| `/public/countries` `GET` | 公开国家列表(仅含已挂商品的国家) | 公开 |
| `/public/goods` `GET` | 分页商品(支持 `countryId/categoryId/tagId/keyword/page/pageSize` | 公开 |
| `/public/goods/:id` `GET` | 商品详情 | 公开 |
| `/categories` `/tags` `/countries` `/positions` | 后台 CRUD | JWT |
| `/origin-goods` `GET` | SDS 原始商品快照分页 | 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 | 公开 |
## 子项目 2inkreach-official-admin(后台)
```
inkreach-official-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
│ │ ├── positions.ts # 坑位 CRUD
│ │ ├── origin-goods.ts # 原始商品快照
│ │ └── 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
│ │ ├── 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`)。
## 子项目 3inkreach-official-website(官网)
```
inkreach-official-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
│ │ ├── 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、代理路由、组件响应式断点等详细信息见 `inkreach-official-website/docs/references/structs.md`。
## 数据库
- PostgreSQL 14+Prisma 5.x
- 连接配置在根 `.env``DATABASE_URL`
- 所有 `TIMESTAMPTZ` 列:`@db.Timestamptz(6)`
- 所有主键:`BigInt @default(autoincrement())`
- 表名与列名通过 `@map` / `@@map` 映射为 `snake_case`
- 迁移位于 `inkreach-official-nestjs/prisma/migrations/`
## 环境变量总览
| 变量 | 位置 | 用途 | 默认 |
|------|------|------|------|
| `DATABASE_URL` | 根 `.env` | PostgreSQL 连接串 | — |
| `JWT_SECRET` | inkreach-official-nestjs | JWT 签名密钥 | — |
| `PORT` | inkreach-official-nestjs | 后端端口 | `3001` |
| `SDS_API_*` | inkreach-official-nestjs | 同步上游 SDS 接口凭据 | — |
| `VITE_API_BASE` | inkreach-official-admin | axios baseURL | `/api` |
| `NUXT_PUBLIC_BACKEND_URL` | inkreach-official-website | NestJS 后端地址 | `http://localhost:3001` |