chore: init monorepo with existing website and plans

This commit is contained in:
yeuimu
2026-06-15 02:31:15 +08:00
commit 72bc89ab03
488 changed files with 115304 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
-- 1. 原商品表(外部来源商品)
CREATE TABLE origin_goods (
origin_good_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sds_good_id TEXT NOT NULL, -- SDS商品ID,假设为外部系统标识
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE origin_goods IS '原商品表,记录外部来源商品';
COMMENT ON COLUMN origin_goods.origin_good_id IS '原商品ID(主键)';
COMMENT ON COLUMN origin_goods.sds_good_id IS 'SDS商品ID';
-- 2. 国家表
CREATE TABLE countries (
country_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
country_name TEXT NOT NULL UNIQUE, -- 国家名称唯一
country_icon TEXT, -- 图标URL或路径
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE countries IS '国家表';
COMMENT ON COLUMN countries.country_id IS '国家ID(主键)';
COMMENT ON COLUMN countries.country_name IS '国家名称';
COMMENT ON COLUMN countries.country_icon IS '国家图标';
-- 3. 品类表(支持层级)
CREATE TABLE categories (
category_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
parent_category_id BIGINT REFERENCES categories(category_id) ON DELETE RESTRICT,
category_name TEXT NOT NULL,
category_icon TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE categories IS '品类表,支持自关联层级';
COMMENT ON COLUMN categories.category_id IS '品类ID(主键)';
COMMENT ON COLUMN categories.parent_category_id IS '父品类IDNULL表示顶级品类';
COMMENT ON COLUMN categories.category_name IS '品类名称';
COMMENT ON COLUMN categories.category_icon IS '品类图标';
-- 索引:外键列
CREATE INDEX idx_categories_parent_id ON categories(parent_category_id);
-- 4. 标签表
CREATE TABLE tags (
tag_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tag_name TEXT NOT NULL UNIQUE,
tag_color TEXT, -- 十六进制颜色码,如 '#FF0000'
timing TEXT, -- 时机(如促销时段)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE tags IS '标签表';
COMMENT ON COLUMN tags.tag_id IS '标签ID(主键)';
COMMENT ON COLUMN tags.tag_name IS '标签名';
COMMENT ON COLUMN tags.tag_color IS '标签颜色(前端展示用)';
COMMENT ON COLUMN tags.timing IS '时机(例如节日促销、季节限定)';
-- 5. 位置表(展示位置定义)
-- 原设计中的 Indexes 表,为避免与数据库索引关键字混淆,重命名为 positions
CREATE TABLE positions (
position_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
index_val INTEGER NOT NULL, -- 位置排序值或权重
country_id BIGINT REFERENCES countries(country_id) ON DELETE CASCADE,
category_id BIGINT REFERENCES categories(category_id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE positions IS '展示位置表,定义商品在特定国家/品类下的展示排序';
COMMENT ON COLUMN positions.position_id IS '位置ID(主键)';
COMMENT ON COLUMN positions.index_val IS '位置值(如排序序号、优先级权重)';
COMMENT ON COLUMN positions.country_id IS '关联的国家IDNULL表示不限国家';
COMMENT ON COLUMN positions.category_id IS '关联的品类IDNULL表示不限品类';
-- 索引:外键列
CREATE INDEX idx_positions_country_id ON positions(country_id);
CREATE INDEX idx_positions_category_id ON positions(category_id);
-- 复合索引用于快速查询某个国家+品类下的位置列表
CREATE INDEX idx_positions_country_category ON positions(country_id, category_id);
-- 6. 商品表(核心表)
CREATE TABLE goods (
good_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
origin_good_id BIGINT REFERENCES origin_goods(origin_good_id) ON DELETE RESTRICT,
country_id BIGINT NOT NULL REFERENCES countries(country_id) ON DELETE RESTRICT,
category_id BIGINT NOT NULL REFERENCES categories(category_id) ON DELETE RESTRICT,
tag_id BIGINT REFERENCES tags(tag_id) ON DELETE SET NULL,
position_id BIGINT REFERENCES positions(position_id) ON DELETE SET NULL,
good_name TEXT NOT NULL,
good_priority INTEGER NOT NULL DEFAULT 0 CHECK (good_priority >= 0), -- 优先级,数值越高越靠前
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE goods IS '商品主表';
COMMENT ON COLUMN goods.good_id IS '商品ID(主键)';
COMMENT ON COLUMN goods.origin_good_id IS '原商品ID(关联外部来源)';
COMMENT ON COLUMN goods.country_id IS '国家ID(必填)';
COMMENT ON COLUMN goods.category_id IS '品类ID(必填)';
COMMENT ON COLUMN goods.tag_id IS '标签ID(可为空)';
COMMENT ON COLUMN goods.position_id IS '展示位置ID(可为空)';
COMMENT ON COLUMN goods.good_name IS '商品名称';
COMMENT ON COLUMN goods.good_priority IS '商品优先级(全局排序,越大越靠前)';
-- 外键索引(必须手动创建)
CREATE INDEX idx_goods_origin_good_id ON goods(origin_good_id);
CREATE INDEX idx_goods_country_id ON goods(country_id);
CREATE INDEX idx_goods_category_id ON goods(category_id);
CREATE INDEX idx_goods_tag_id ON goods(tag_id);
CREATE INDEX idx_goods_position_id ON goods(position_id);
-- 常用查询索引
CREATE INDEX idx_goods_priority ON goods(good_priority DESC); -- 按优先级排序
CREATE INDEX idx_goods_country_priority ON goods(country_id, good_priority DESC); -- 按国家+优先级
CREATE INDEX idx_goods_category_country ON goods(category_id, country_id); -- 品类+国家过滤
-- 可选:部分索引,仅索引活跃商品(如果有软删除或状态字段)
-- 当前设计无状态字段,暂不添加
-- 7. (可选)商品与标签的多对多关联表
-- 原设计中 goods 表仅有单一 tag_id,限制了多标签场景。
-- 若业务需要多标签,建议增加关联表,并移除 goods.tag_id 字段。
-- 以下为扩展设计(可根据实际需求决定是否启用):
/*
CREATE TABLE goods_tags (
goods_id BIGINT NOT NULL REFERENCES goods(good_id) ON DELETE CASCADE,
tag_id BIGINT NOT NULL REFERENCES tags(tag_id) ON DELETE CASCADE,
PRIMARY KEY (goods_id, tag_id)
);
CREATE INDEX idx_goods_tags_tag_id ON goods_tags(tag_id);
*/
-- 注意:若使用 goods_tags 关联表,则应删除 goods 表中的 tag_id 列。
-- 保留原单标签设计还是采用多标签,请根据业务需求选择。
-- 8. 自动更新 updated_at 的触发器函数(可选)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 为每个表应用自动更新 updated_at 的触发器
CREATE TRIGGER trigger_origin_goods_updated_at
BEFORE UPDATE ON origin_goods
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_countries_updated_at
BEFORE UPDATE ON countries
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_categories_updated_at
BEFORE UPDATE ON categories
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_tags_updated_at
BEFORE UPDATE ON tags
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_positions_updated_at
BEFORE UPDATE ON positions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trigger_goods_updated_at
BEFORE UPDATE ON goods
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

+12
View File
@@ -0,0 +1,12 @@
官网的首页我已经做好了, 位于 inkreach-official-website, 仅仅做了首页, 现在我需要做"产品中心"这个二级页面
具体的后台数据设计我已放在 docs/dev/database-table-design.md, 请参考
具体的数据库账号密码放在 .env, 请参考
我最终想要的效果是:
1. 一个前端二级页: 前端的"产品中心"实现成: docs/dev/product-center-1.png \ docs/dev/product-center-2.png \ docs/dev/product-center-3.png, 这几张UI设计稿
2. 一个前端后台: 通过设置产品的各种配置调控官网产品中心页面的展示效果
3. 一个后端服务: 为前两者服务
SDS 获取的产品列表(pod-api.ts), 其原始数据是一个树形结构, 通过后台二次配置, 再展现到前端首页
@@ -0,0 +1,361 @@
# 产品中心 (Product Center) 设计规格
## 概述
为 InkReach 官网新增"产品中心"二级页面,包含三个子系统:
1. **NestJS 后端服务** — Prisma + PostgreSQLRESTful APISDS 定时同步
2. **Vue 3 Admin 后台** — Element Plus,商品/品类/国家/标签/位置管理
3. **Nuxt 4 官网页面** — 产品中心二级页,带侧边栏分类、国家筛选、搜索、分页
## 架构
- NestJS 监听 `:3001`,提供 RESTful API
- Admin 直连 NestJS API(开发环境 Vite proxy 解决跨域)
- Website 通过 Nitro server route 代理 NestJS API
## 实施顺序
后端服务 → Admin 后台 → 官网产品中心页
---
## 1. NestJS 后端服务
### 技术栈
| 技术 | 版本 | 用途 |
|------|------|------|
| NestJS | 10+ | 后端框架 |
| Prisma | 6+ | ORM |
| PostgreSQL | 16+ | 数据库 |
| @nestjs/schedule | - | 定时同步 SDS |
| passport + jwt | - | JWT 认证 |
| class-validator | - | DTO 验证 |
### 项目结构
```
inkreach-official-nestjs/
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── auth/
│ │ ├── auth.module.ts
│ │ ├── auth.controller.ts
│ │ ├── auth.service.ts
│ │ ├── jwt.strategy.ts
│ │ └── dto/
│ ├── goods/
│ │ ├── goods.module.ts
│ │ ├── goods.controller.ts
│ │ ├── goods.service.ts
│ │ └── dto/
│ ├── categories/
│ ├── countries/
│ ├── tags/
│ ├── positions/
│ ├── origin-goods/
│ └── sync/
│ ├── sync.module.ts
│ ├── sync.service.ts
│ └── sync.controller.ts
├── test/
├── .env
├── nest-cli.json
├── tsconfig.json
└── package.json
```
### Prisma Schema
`docs/dev/database-table-design.md` 的 DDL 转换为 Prisma schema6 个核心模型:`OriginGoods`, `Country`, `Category`, `Tag`, `Position`, `Good`
关系映射:
- `Good``OriginGoods` (多对一, RESTRICT)
- `Good``Country` (多对一, RESTRICT)
- `Good``Category` (多对一, RESTRICT)
- `Good``Tag` (多对一, SET NULL)
- `Good``Position` (多对一, SET NULL)
- `Category` 自关联 (parent_category_id)
- `Position``Country` (多对一, CASCADE)
- `Position``Category` (多对一, CASCADE)
### API 设计
#### 认证
| 方法 | 路径 | 描述 |
|------|------|------|
| POST | `/auth/login` | 管理员登录,返回 JWT |
| POST | `/auth/register` | 注册(仅初始化用) |
#### 商品管理 (Admin, 需认证)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/goods` | 分页查询商品(支持 country/category/tag/position/keyword 筛选) |
| GET | `/goods/:id` | 商品详情 |
| POST | `/goods` | 创建商品 |
| PATCH | `/goods/:id` | 更新商品 |
| DELETE | `/goods/:id` | 删除商品 |
| PATCH | `/goods/batch-priority` | 批量更新优先级 |
| POST | `/goods/batch` | 批量创建商品(从 origin_goods 选取) |
#### 品类管理 (Admin)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/categories` | 获取品类树 |
| GET | `/categories/:id` | 品类详情 |
| POST | `/categories` | 创建品类 |
| PATCH | `/categories/:id` | 更新品类 |
| DELETE | `/categories/:id` | 删除品类 |
#### 国家管理 (Admin)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/countries` | 国家列表 |
| POST | `/countries` | 创建国家 |
| PATCH | `/countries/:id` | 更新国家 |
| DELETE | `/countries/:id` | 删除国家 |
#### 标签管理 (Admin)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/tags` | 标签列表 |
| POST | `/tags` | 创建标签 |
| PATCH | `/tags/:id` | 更新标签 |
| DELETE | `/tags/:id` | 删除标签 |
#### 位置管理 (Admin)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/positions` | 位置列表 |
| POST | `/positions` | 创建位置 |
| PATCH | `/positions/:id` | 更新位置 |
| DELETE | `/positions/:id` | 删除位置 |
#### 原商品管理 (Admin)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/origin-goods` | 原商品列表(来自 SDS 同步) |
#### 同步 (Admin)
| 方法 | 路径 | 描述 |
|------|------|------|
| POST | `/sync/categories` | 手动触发品类同步 |
| POST | `/sync/products` | 手动触发产品同步 |
| GET | `/sync/status` | 获取最近同步状态 |
#### 公开 API (Website, 无需认证)
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/public/categories` | 品类树(仅含已配置商品的品类) |
| GET | `/public/countries` | 国家列表(仅含已配置商品的国家) |
| GET | `/public/goods` | 分页查询商品(支持 country/category/tag/keyword 筛选) |
| GET | `/public/goods/:id` | 商品详情 |
### SDS 同步逻辑
1. **品类同步**: 调用 `https://mapi.sdspod.com/category/tree/3`,将返回的树形结构扁平化写入 `categories`
2. **产品同步**: 按 SDS 分类遍历产品,写入 `origin_goods`
3. **定时策略**: 每小时自动同步一次 (`@Cron('0 * * * *')`)
4. **增量策略**: 对比现有记录,存在则跳过,不存在则插入
### 商品查询逻辑 (公开 API)
排序: `good_priority DESC → position.index_val ASC → created_at DESC`
筛选参数:
- `countryId` — 按国家筛选
- `categoryId` — 按品类筛选(含子品类)
- `tagId` — 按标签筛选
- `keyword` — 按商品名模糊搜索
- `page` / `pageSize` — 分页
---
## 2. Admin 后台
### 技术栈
| 技术 | 版本 | 用途 |
|------|------|------|
| Vue | 3.5+ | UI 框架 |
| Vite | 6+ | 构建工具 |
| Element Plus | 2.9+ | UI 组件库 |
| Vue Router | 4+ | 路由 |
| Pinia | 3+ | 状态管理 |
| Axios | 1.7+ | HTTP 客户端 |
| TypeScript | 5+ | 类型安全 |
### 项目结构
```
inkreach-official-admin/
├── src/
│ ├── main.ts
│ ├── App.vue
│ ├── router/index.ts
│ ├── stores/
│ │ ├── auth.ts
│ │ └── app.ts
│ ├── api/
│ │ ├── request.ts
│ │ ├── auth.ts
│ │ ├── goods.ts
│ │ ├── categories.ts
│ │ ├── countries.ts
│ │ ├── tags.ts
│ │ ├── positions.ts
│ │ ├── origin-goods.ts
│ │ └── sync.ts
│ ├── layouts/
│ │ └── DefaultLayout.vue
│ ├── views/
│ │ ├── login/LoginView.vue
│ │ ├── goods/GoodsView.vue
│ │ ├── categories/CategoriesView.vue
│ │ ├── countries/CountriesView.vue
│ │ ├── tags/TagsView.vue
│ │ ├── positions/PositionsView.vue
│ │ └── sync/SyncView.vue
│ ├── components/
│ └── types/index.ts
├── .env
├── vite.config.ts
├── tsconfig.json
└── package.json
```
### 核心页面功能
**商品管理 (GoodsView)**:
- 表格展示:商品名、国家、品类、标签、位置、优先级
- 筛选:按国家/品类/标签筛选
- 操作:新增、编辑、删除、调整优先级
- 批量添加:从 origin_goods 选择商品,批量配置国家/品类/标签/优先级
**品类管理 (CategoriesView)**:
- 树形表格展示层级关系
- 新增/编辑/删除品类
**同步管理 (SyncView)**:
- 显示最近同步时间、同步状态
- 手动触发品类同步/产品同步按钮
---
## 3. 官网产品中心页
### 页面结构
左右两栏布局:左侧固定宽度侧边栏(品类导航),右侧主内容区(国家筛选 + 筛选栏 + 商品网格 + 分页)。
### 组件拆分
```
app/pages/product-center.vue # 页面入口
app/components/product/
├── ProductSidebar.vue # 侧边栏品类导航
├── ProductCountryFilter.vue # 国家标签栏
├── ProductFilterBar.vue # 筛选下拉 + 已选标签 + 搜索
├── ProductGrid.vue # 商品卡片网格
├── ProductCard.vue # 单个商品卡片
├── ProductCardSkeleton.vue # 骨架屏卡片
└── ProductPagination.vue # 分页控件
app/composables/
└── useProductCenter.ts # 产品中心数据逻辑
```
### 商品卡片设计
- 商品图片 (1:1 方形)
- 商品名 (单行截断)
- 标签行 (pill 样式,颜色体系:绿色=包邮,橙色=工艺,灰色=其他)
- 价格 (粗体,"¥XX.XX起")
### 侧边栏品类导航
- 顶级品类带图标 + 折叠箭头
- 展开子品类列表,左侧橙色竖条标识当前选中
- 选中子品类背景 `bg-inkreach-homepage-3` + 橙色文字
### 国家标签栏
- Pill 形状按钮,每个带国旗图标
- "全部" 为默认选中,橙色边框 + 橙色文字
- 未选中为灰色边框
### 搜索
- 分体式搜索框:左侧输入框 + 右侧橙色"搜索"按钮
- 支持按商品名搜索
### 分页
- 左侧:"总计 N 个产品"
- 中间:页码按钮(方形)
- 右侧:每页条数下拉 + 跳转输入框
### 骨架屏
数据加载时显示骨架屏:
- 卡片区域显示 ProductCardSkeleton (4x3 网格)
- 分页隐藏
- 侧边栏和筛选区正常显示
### Nitro API 代理
```
server/api/backend/categories.get.ts -> GET :3001/public/categories
server/api/backend/countries.get.ts -> GET :3001/public/countries
server/api/backend/goods.get.ts -> GET :3001/public/goods
server/api/backend/goods/[id].get.ts -> GET :3001/public/goods/:id
```
### 数据流
1. 页面加载 → `/api/backend/categories` → 渲染侧边栏
2. 页面加载 → `/api/backend/countries` → 渲染国家标签栏
3. 筛选/分页变化 → `/api/backend/goods?countryId=&categoryId=&keyword=&page=&pageSize=` → 渲染商品网格
### 关联改动
- 首页 `PodProducts.vue` 的"更多产品"按钮改为 `<NuxtLink to="/product-center">`
- `AppHeader.vue` 的"产品中心"链接改为 `<NuxtLink to="/product-center">`
---
## 跨项目配置
### 环境变量
**inkreach-official-nestjs/.env**:
- DATABASE_URL=postgresql://postgres:yoyoki219765.@localhost:5432/inkreach-official
- JWT_SECRET=(generate-secure-secret)
- SDS_API_BASE=https://mapi.sdspod.com
- PORT=3001
**inkreach-official-admin/.env**:
- VITE_API_BASE=http://localhost:3001
**inkreach-official-website**:
- NUXT_PUBLIC_BACKEND_URL=http://localhost:3001
### 端口分配
| 项目 | 端口 |
|------|------|
| NestJS Backend | 3001 |
| Admin (Vite dev) | 5173 |
| Website (Nuxt dev) | 3000 |