# 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/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 保护）
│   ├── tag-groups/                # 标签分组 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`、`tagFontColor`、`tagGroupId`、`sortOrder`、`timing` |
| `TagGroup` | 标签分组（含 `groupName`、`groupColor`、`groupIcon`、`sortOrder`），删除分组时组内 tag 的 `tagGroupId` 通过 `onDelete: SetNull` 自动置空 |
| `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/tags` `GET` | 公开标签列表（带 `group` 字段，按 group 排序） | 公开 |
| `/public/tag-groups` `GET` | 公开标签分组列表 | 公开 |
| `/public/goods` `GET` | 分页商品（支持 `countryId/categoryId/tagIds(逗号分隔)/keyword/page/pageSize`，`tagIds` 为 AND 关系） | 公开 |
| `/public/goods/:id` `GET` | 商品详情 | 公开 |
| `/categories` `/tags` `/tag-groups` `/countries` `/positions` | 后台 CRUD | JWT |
| `/tags/sort` `PATCH` | 批量更新 tag 排序和分组归属 | JWT |
| `/tag-groups/sort` `PATCH` | 批量更新分组排序 | 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 | 公开 |

## 子项目 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        # 原始商品快照
│   │   └── 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`）。

## 子项目 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`。

## 数据库

- 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` |
