forked from yoone/API
1
0
Fork 0

Compare commits

...

58 Commits

Author SHA1 Message Date
黄珑 402ec4ceec Fix: search by phone in customers 2026-01-07 19:04:38 +08:00
zhuotianyuan 324008472c refactor(entity): 将可选字段明确标记为可选类型 2026-01-06 11:03:40 +00:00
zhuotianyuan 924f9723fb fix(webhook): 更新webhook控制器中的密钥值 2026-01-06 11:03:40 +00:00
zhuotianyuan 19e083dc67 feat(webhook): 添加对shoppy平台webhook的支持
- 在site.entity.ts中添加webhookUrl字段
- 在auth.middleware.ts中添加/shoppy路由到白名单
- 在webhook.controller.ts中实现shoppy平台webhook处理逻辑
2026-01-06 11:03:40 +00:00
tikkhun 817f368522 feat: 添加产品查询过滤条件DTO并优化Swagger配置
添加 ProductWhereFilterDTO 用于产品查询过滤条件
修改 api.dto.ts 中 where 和 orderBy 的类型为 any 以解决 openapit2ts 问题
配置 Swagger 支持嵌套查询参数
移除 product.service.ts 中的多余空行
2026-01-06 18:49:17 +08:00
tikkhun a22e302c4e refactor(order.service): 简化订单ID验证逻辑并优化查询条件
移除冗余的空值检查,使用可选链操作符简化验证
仅在validIds非空时添加查询条件
2026-01-06 10:54:47 +08:00
tikkhun 907228297d feat(订单): 添加订单导出功能并优化数据验证
在订单服务中添加导出功能,支持通过ID列表导出订单数据为CSV格式。同时优化了ID列表的验证逻辑,过滤无效ID并添加空值检查,确保数据安全性和可靠性。
2026-01-06 10:33:55 +08:00
tikkhun 934085fd64 feat(订单): 重构订单履约功能并完善相关实体和服务
refactor(订单服务): 优化订单同步逻辑,增加履约信息处理

feat(实体): 新增订单履约实体并更新关联关系

fix(适配器): 修正Shopyy和WooCommerce履约数据映射

docs(dto): 更新订单和履约相关DTO定义

style: 格式化代码并修正拼写错误
2026-01-05 22:38:48 +08:00
tikkhun 99bd7009cc fix: 将origin_id类型从number改为string并重构客户创建逻辑
修改customer.dto.ts中的origin_id类型为string,以保持数据类型一致性
重构customer.service.ts中的客户数据映射逻辑,移除冗余的origin_id转换
在order.service.ts中使用customerService处理客户创建,替代直接操作model
2026-01-05 16:30:48 +08:00
tikkhun 70948ef977 refactor(adapter): 优化ShopYY订单状态映射和查询参数转换
重构ShopYY适配器中的订单状态映射逻辑,将shopyyOrderAutoNextStatusMap重命名为shopyyOrderStatusMap
新增mapUnifiedOrderQueryToShopyyQuery方法处理查询参数转换
移除site-api.controller中多余的where参数处理
2026-01-05 15:36:24 +08:00
tikkhun 22a950d0a0 feat(woocommerce): 添加订单客户IP地址字段
当客户IP地址不存在时,使用空字符串作为默认值
2026-01-04 21:45:58 +08:00
tikkhun edd91185a1 feat(woocommerce): 添加订单元数据字段
添加 date_paid、utm_source、device_type 和 source_type 字段到订单数据转换逻辑中,以支持订单来源追踪功能
2026-01-04 21:32:47 +08:00
tikkhun 338625c3d2 fix(product): 修复产品创建和更新时的属性校验问题
调整产品DTO中attributes字段的校验规则,使其在type为'single'时必填,为'bundle'时可选
移除不必要的siteSkus处理逻辑,简化产品创建和更新流程
2026-01-04 20:05:37 +08:00
tikkhun 58ae594d5e feat(实体): 在字典项实体中新增描述字段并调整字段顺序
添加 description 字段以支持字典项描述信息
将 shortName 字段调整至与其他字段更合理的顺序
2025-12-31 15:05:50 +08:00
tikkhun 28fb8e4ce6 fix: 将origin_id字段统一转换为字符串类型
修复订单和客户服务中origin_id字段类型不一致的问题,确保所有相关操作中origin_id都作为字符串处理
2025-12-31 14:33:53 +08:00
tikkhun 43e0d8d40d feat: 修复产品与站点同步诸多问题
1. 新增产品与站点同步相关DTO和服务方法
2. 重构产品实体与站点SKU的关联关系
3. 优化分类实体,增加短名字段用于SKU生成
4. 完善API响应DTO的Swagger注解
5. 新增Dockerfile支持容器化部署
6. 重构订单同步接口,返回更详细的同步结果
7. 优化物流服务接口命名,使用fulfillment替代shipment
8. 新增数据库初始化逻辑,自动创建数据库
9. 重构产品控制器,支持批量同步操作
10. 更新模板配置,支持站点SKU前缀
11. 删除废弃的迁移文件和实体
12. 优化产品查询接口,支持更灵活的过滤条件
2025-12-31 11:55:59 +08:00
zhuotianyuan 84beb1a65e feat(order_shipping): 添加订单配送信息实体和相关接口
- 添加 order_shipping.entity.ts 实体类定义
- 更新 shopyy.adapter.ts 支持订单配送数据处理
- 更新 woocommerce.adapter.ts 支持配送信息适配
- 完善 site-adapter.interface.ts 接口定义
- 优化 order.service.ts 配送相关逻辑
- 更新相关 DTO 类以支持配送信息
2025-12-30 11:07:37 +08:00
zhuotianyuan 2f99e27f0f refactor(shopyy): 重构订单DTO和服务逻辑
fix: 修复本地数据库配置端口和密码
feat(statistics): 支持按日/周/月分组统计订单数据
feat(order): 添加订单导出CSV功能
style: 清理无用代码和注释
2025-12-27 16:06:37 +08:00
zhuotianyuan 361b05117c feat(订单): 添加订单配送方式、费用项和优惠券项支持
扩展订单DTO和适配器以支持配送方式、费用项和优惠券项数据
实现Shopyy平台getAllOrders方法并添加分页并发处理
优化订单状态自动更新逻辑,支持Shopyy平台状态映射
2025-12-26 19:56:10 +08:00
黄珑 823967a268 Fix: auto-draft error 2025-12-24 16:51:35 +08:00
tikkhun feeeded13b refactor(entity): 将site实体表名改为site_v2 2025-12-24 16:26:59 +08:00
tikkhun 2df777b73e refactor: 移除Variation实体及相关引用
删除Variation实体及其在服务、DTO和配置中的引用
添加产品表重命名的迁移脚本
2025-12-24 16:26:59 +08:00
tikkhun e1891df4f6 chore: 还原 config.local.ts 2025-12-24 16:26:59 +08:00
tikkhun 185a786b2e refactor: 移除废弃的WordPress产品相关代码
清理不再使用的WordPress产品模块代码,包括实体、DTO、服务和控制器
统一使用新的产品模块接口
2025-12-24 16:26:59 +08:00
tikkhun 4e0101e0f2 refactor(service): 移除查询中未使用的tags字段 2025-12-24 16:26:59 +08:00
tikkhun 8f7f35c538 feat(adapter): 添加getAll方法支持批量获取数据
实现WooCommerceAdapter中的getAllProducts、getAllOrders等方法
添加ISiteAdapter接口中的对应方法定义
更新customer.service使用getAllCustomers方法
2025-12-24 16:26:59 +08:00
tikkhun 8e7ec2372d feat(customer): 实现客户数据同步功能并增强客户管理
重构客户服务层,添加客户数据同步功能
扩展客户实体字段以支持完整客户信息存储
优化客户列表查询性能并添加统计功能
移除废弃的WpSite相关代码和配置
2025-12-24 16:26:59 +08:00
tikkhun a02758a926 refactor(api): 统一查询参数处理逻辑,使用where对象集中管理
将分散的查询参数如status、customer_id、ids等统一迁移到where对象中处理
简化DTO结构,移除冗余字段
适配器层统一从where对象获取查询条件
2025-12-24 16:26:59 +08:00
tikkhun bc1d4de446 feat: 重构产品,新增 shopyy 平台
重构实体字段命名规范,统一使用更简洁的命名方式
新增区域管理、字典管理、模板管理等功能模块
完善数据库迁移和种子数据初始化逻辑
增强站点管理功能,支持区域和仓库点关联
优化服务层逻辑,增加错误处理和日志记录
2025-12-24 16:26:57 +08:00
zhuotianyuan ac4b925aee fix: 修复订单同步时未限制时间范围的问题 2025-12-24 16:25:51 +08:00
zhuotianyuan 72b28e7c2b feat(订单状态): 添加自动草稿状态并更新订单服务逻辑
在订单状态枚举中添加 AUTO_DRAFT 状态
当订单状态为自动草稿时跳过后续更新操作
2025-12-23 19:17:55 +08:00
zhuotianyuan d884369915 fix(order): 修复订单状态更新逻辑
确保当订单已存在时正确更新订单状态,避免状态不一致问题
2025-12-23 18:24:02 +08:00
zhuotianyuan 0057585da3 refactor: 移除未使用的 WpSite 接口导入 2025-12-23 17:50:24 +08:00
tikkhun 5ca3c94afc refactor: 移除未使用的站点同步配置代码
清理不再需要的站点同步功能代码,简化配置和服务逻辑
2025-12-23 11:44:50 +08:00
zhuotianyuan ae34d1fab0 fix: 修复订单同步时未限制时间范围的问题
修改订单同步逻辑,默认只同步最近7天的订单数据
移除站点服务中不必要的字段解构
扩展WP服务getOrders方法支持参数传递
2025-12-19 17:28:51 +08:00
tikkhun 927857a795 refactor: 统一从数据库获取站点信息替代硬编码配置 2025-11-24 10:08:49 +08:00
tikkhun a59d5a7b75 fix: 修复站点相关接口的类型转换和字段处理
修复 order.service 中 email 字段返回空字符串以保持兼容
统一 webhook.controller 和 wp_product.service 中的 site.id 类型为字符串
在 wp.service 中添加接口返回内容类型检查并返回 total 字段
优化 site.service 的代码注释和字段处理逻辑
2025-11-24 09:48:53 +08:00
tikkhun a64e611294 refactor(service): 统一使用site.apiUrl并优化产品同步逻辑
移除对wpApiUrl的兼容处理,统一使用apiUrl
修复产品同步查询的分页问题,使用Infinity获取全部数据
优化代码格式和缩进
2025-11-24 09:24:55 +08:00
tikkhun c7480ccc8a refactor(站点管理): 移除配置中的站点数组,统一通过数据库获取站点信息
重构多个控制器和服务,将硬编码的站点配置替换为通过 SiteService 从数据库获取
使用批量查询优化站点名称映射,避免 N+1 查询问题
兼容新旧站点数据结构,确保平滑过渡
2025-11-22 11:41:49 +08:00
tikkhun 1d62730ca0 feat(db): 添加数据库种子文件 2025-11-22 10:47:10 +08:00
tikkhun c75d620516 feat(站点): 实现站点管理功能
添加站点实体、服务层和控制器,支持站点的CRUD操作
同步配置中的站点信息到数据库
提供站点禁用/启用功能
2025-11-22 10:30:30 +08:00
tikkhun ec6a8c3154 docs(enums): 添加静态验证码的注释说明 2025-11-21 17:36:21 +08:00
tikkhun 1bdae88c11 fix(订单服务): 修复关联订单列表去重逻辑并优化查询结果
确保关联订单列表中的条目不会重复,通过使用Set来跟踪已处理的外部ID
移除不必要的orders数组填充,简化返回数据结构
2025-11-21 11:58:13 +08:00
tikkhun 27935f113d feat(订单服务): 添加订阅相关字段到订单查询
在订单查询结果中添加 isSubscription 标志和 related 订阅信息字段,以便前端展示订单的订阅状态和关联订阅详情
2025-11-21 11:48:13 +08:00
tikkhun 12dc5ac876 feat(订单服务): 添加订单关联数据查询并简化相关订单查询逻辑
在订单详情中增加关联数据查询,包括订阅和相关订单,用于前端展示
简化getRelatedByOrder方法的实现,移除不必要的查询条件
2025-11-21 11:28:42 +08:00
tikkhun 8778b8138d feat(订单): 添加订单商品列表和关联订单查询功能
- 新增getOrderItemList接口用于查询订单商品列表
- 新增getRelatedByOrder接口用于查询关联订单
- 在QueryOrderDTO中添加isSubscriptionOnly字段用于筛选订阅订单
- 优化订单查询SQL,添加订阅订单过滤条件
- 为日期参数添加默认值处理
2025-11-19 15:51:10 +08:00
tikkhun eff9efc2c3 fix: 修正product.entty拼写错误并创建正确的product.entity文件 2025-11-18 17:31:39 +08:00
tikkhun 11814a7c39 feat(订单): 添加获取订单项接口和扩展订单项实体字段
添加获取订单项列表的接口/getOrderItems
扩展OrderItem实体字段以支持更多订单项信息,包括税类、税明细、元数据、全局ID、商品图片等
2025-11-18 17:25:47 +08:00
tikkhun 3b5e3ec906 chore: 但存在 order 的时候不重复保存 2025-11-18 16:47:40 +08:00
tikkhun 79b7e96175 build: 添加 @woocommerce/woocommerce-rest-api 依赖 2025-11-17 16:11:10 +08:00
tikkhun 4ce5cb8bb0 chore: 使用@woocommerce/woocommerce-rest-api获取 2025-11-17 16:10:56 +08:00
tikkhun e94ea5ed58 refactor(service): 统一将wPService重命名为wpService以符合命名规范 2025-11-17 11:03:25 +08:00
tikkhun dc070fadde feat(service): 添加buildURL方法规范URL路径拼接
新增buildURL私有方法用于规范URL各路径段的斜杠,避免因多/或少/导致请求失败
替换原有字符串拼接方式为buildURL调用,提升URL构建的可靠性
2025-11-17 10:42:07 +08:00
tikkhun 2d36370acf docs: 完善订阅相关模块的注释和文档
- 在 DTO、实体和服务中添加详细注释说明字段用途
- 补充订阅同步和列表查询的业务逻辑说明
- 明确分页返回数据的包装结构
2025-11-14 16:26:26 +08:00
tikkhun 533b2cd726 chore: package-lock.json 2025-11-14 10:52:57 +08:00
tikkhun 795b13ce31 feat(订阅): 添加WooCommerce订阅管理功能
实现订阅模块的完整功能,包括:
- 添加订阅状态枚举
- 创建订阅实体和DTO
- 实现订阅同步和查询服务
- 添加订阅控制器提供API接口
- 配置订阅实体到数据库连接
2025-11-13 15:10:20 +08:00
tikkhun b8290d0cda chore: 更新.gitignore文件,添加container目录忽略 2025-11-13 14:35:08 +08:00
zksu d0b3b54ad8 feat:更新 package.json 添加缺失依赖 2025-11-10 08:04:22 +00:00
124 changed files with 29655 additions and 2777 deletions

View File

@ -1,6 +1,6 @@
{ {
"extends": "./node_modules/mwts/", "extends": "./node_modules/mwts/",
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"], "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings", "scripts"],
"env": { "env": {
"jest": true "jest": true
} }

5
.gitignore vendored
View File

@ -14,3 +14,8 @@ run/
yarn.lock yarn.lock
**/config.prod.ts **/config.prod.ts
**/config.local.ts **/config.local.ts
container
scripts
tmp_uploads/
.trae
docs

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# 使用 Node.js 作为基础镜像
FROM node:22-alpine
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm install --production
# 复制源代码
COPY . .
# 构建项目
RUN npm run build
# 暴露端口
EXPOSE 7001
# 启动服务
CMD ["npm", "run", "prod"]

254
area-api-doc.md Normal file
View File

@ -0,0 +1,254 @@
# Area 区域管理 API 文档
## 概述
本文档详细描述了区域管理相关的API接口,包括增删改查操作以及新增的坐标功能.
## API 接口列表
### 1. 创建区域
**请求信息**
- URL: `/api/area`
- Method: `POST`
- Headers: `Authorization: Bearer {token}`
**请求体 (JSON)**
```json
{
"name": "欧洲",
"latitude": 48.8566,
"longitude": 2.3522
}
```
**参数说明**
- `name`: 区域名称 (必填)
- `latitude`: 纬度 (-90 到 90 之间,可选)
- `longitude`: 经度 (-180 到 180 之间,可选)
**响应示例**
```json
{
"code": 0,
"message": "创建成功",
"data": {
"id": 1,
"name": "欧洲",
"latitude": 48.8566,
"longitude": 2.3522,
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
}
}
```
### 2. 更新区域
**请求信息**
- URL: `/api/area/{id}`
- Method: `PUT`
- Headers: `Authorization: Bearer {token}`
**请求体 (JSON)**
```json
{
"name": "欧洲区域",
"latitude": 48.8566,
"longitude": 2.3522
}
```
**参数说明**
- `name`: 区域名称 (可选)
- `latitude`: 纬度 (-90 到 90 之间,可选)
- `longitude`: 经度 (-180 到 180 之间,可选)
**响应示例**
```json
{
"code": 0,
"message": "更新成功",
"data": {
"id": 1,
"name": "欧洲区域",
"latitude": 48.8566,
"longitude": 2.3522,
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:30:00Z"
}
}
```
### 3. 删除区域
**请求信息**
- URL: `/api/area/{id}`
- Method: `DELETE`
- Headers: `Authorization: Bearer {token}`
**响应示例**
```json
{
"code": 0,
"message": "删除成功",
"data": null
}
```
### 4. 获取区域列表(分页)
**请求信息**
- URL: `/api/area`
- Method: `GET`
- Headers: `Authorization: Bearer {token}`
- Query Parameters:
- `currentPage`: 当前页码 (默认 1)
- `pageSize`: 每页数量 (默认 10)
- `name`: 区域名称(可选,用于搜索)
**响应示例**
```json
{
"code": 0,
"message": "查询成功",
"data": {
"list": [
{
"id": 1,
"name": "欧洲",
"latitude": 48.8566,
"longitude": 2.3522,
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
}
],
"total": 1
}
}
```
### 5. 获取所有区域
**请求信息**
- URL: `/api/area/all`
- Method: `GET`
- Headers: `Authorization: Bearer {token}`
**响应示例**
```json
{
"code": 0,
"message": "查询成功",
"data": [
{
"id": 1,
"name": "欧洲",
"latitude": 48.8566,
"longitude": 2.3522,
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
},
{
"id": 2,
"name": "亚洲",
"latitude": 35.6762,
"longitude": 139.6503,
"createdAt": "2024-01-01T12:10:00Z",
"updatedAt": "2024-01-01T12:10:00Z"
}
]
}
```
### 6. 根据ID获取区域详情
**请求信息**
- URL: `/api/area/{id}`
- Method: `GET`
- Headers: `Authorization: Bearer {token}`
**响应示例**
```json
{
"code": 0,
"message": "查询成功",
"data": {
"id": 1,
"name": "欧洲",
"latitude": 48.8566,
"longitude": 2.3522,
"createdAt": "2024-01-01T12:00:00Z",
"updatedAt": "2024-01-01T12:00:00Z"
}
}
```
## 世界地图实现建议
对于前端实现世界地图并显示区域坐标,推荐以下方案:
### 1. 使用开源地图库
- **Leaflet.js**: 轻量级开源地图库,易于集成
- **Mapbox**: 提供丰富的地图样式和交互功能
- **Google Maps API**: 功能强大但需要API密钥
### 2. 实现步骤
1. **获取区域数据**:
使用 `/api/area/all` 接口获取所有包含坐标信息的区域
2. **初始化地图**:
```javascript
// Leaflet示例
const map = L.map('map').setView([0, 0], 2);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
```
3. **添加区域标记**:
```javascript
// 假设areas是从API获取的数据
areas.forEach(area => {
if (area.latitude && area.longitude) {
const marker = L.marker([area.latitude, area.longitude]).addTo(map);
marker.bindPopup(`<b>${area.name}</b>`);
}
});
```
4. **添加交互功能**:
- 点击标记显示区域详情
- 搜索和筛选功能
- 编辑坐标功能(调用更新API)
### 3. 坐标输入建议
在区域管理界面,可以添加以下功能来辅助坐标输入:
1. 提供搜索框,根据地点名称自动获取坐标
2. 集成小型地图,允许用户点击选择位置
3. 添加验证,确保输入的坐标在有效范围内
## 数据模型说明
### Area 实体
| 字段名 | 类型 | 描述 | 是否必填 |
|--------|------|------|----------|
| id | number | 区域ID | 否(自动生成) |
| name | string | 区域名称 | 是 |
| latitude | number | 纬度(范围:-90 到 90) | 否 |
| longitude | number | 经度(范围:-180 到 180) | 否 |
| createdAt | Date | 创建时间 | 否(自动生成) |
| updatedAt | Date | 更新时间 | 否(自动生成) |
## 错误处理
API可能返回的错误信息:
- `区域名称已存在`: 当尝试创建或更新区域名称与现有名称重复时
- `区域不存在`: 当尝试更新或删除不存在的区域时
- `权限错误`: 当请求缺少有效的授权令牌时

54
debug_sync.log Normal file

File diff suppressed because one or more lines are too long

49
migration-guide.md Normal file
View File

@ -0,0 +1,49 @@
# 数据库迁移指南
为了支持区域坐标功能,需要执行数据库迁移操作,将新增的 `latitude``longitude` 字段添加到数据库表中.
## 执行迁移步骤
### 1. 生成迁移文件
运行以下命令生成迁移文件:
```bash
npm run migration:generate -- ./src/db/migrations/AddCoordinatesToArea
```
### 2. 检查迁移文件
生成的迁移文件会自动包含添加 `latitude``longitude` 字段的SQL语句.您可以检查文件内容确保迁移逻辑正确.
### 3. 执行迁移
运行以下命令执行迁移,将更改应用到数据库:
```bash
npm run migration:run
```
## 手动迁移SQL(可选)
如果需要手动执行SQL,可以使用以下语句:
```sql
ALTER TABLE `area`
ADD COLUMN `latitude` DECIMAL(10,6) NULL AFTER `name`,
ADD COLUMN `longitude` DECIMAL(10,6) NULL AFTER `latitude`;
```
## 回滚迁移(如需)
如果遇到问题,可以使用以下命令回滚迁移:
```bash
npm run typeorm -- migration:revert -d src/db/datasource.ts
```
## 注意事项
- 确保在执行迁移前备份数据库
- 迁移不会影响现有数据,新增字段默认为 NULL
- 迁移后,可以通过API开始为区域添加坐标信息

24
output.log Normal file
View File

@ -0,0 +1,24 @@
> my-midway-project@1.0.0 dev
> cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js

[10:37:17 AM] Starting compilation in watch mode...
[10:37:19 AM] Found 0 errors. Watching for file changes.
2025-12-01 10:37:20.106 INFO 58678 [SyncProductJob] start job SyncProductJob
2025-12-01 10:37:20.106 INFO 58678 [SyncShipmentJob] start job SyncShipmentJob
2025-12-01 10:37:20.109 INFO 58678 [SyncProductJob] complete job SyncProductJob
Node.js server started in 732 ms
➜ Local: http://127.0.0.1:7001/
➜ Network: http://192.168.5.100:7001/ 
2025-12-01 10:37:20.110 INFO 58678 [SyncShipmentJob] complete job SyncShipmentJob

5801
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,15 +15,26 @@
"@midwayjs/logger": "^3.1.0", "@midwayjs/logger": "^3.1.0",
"@midwayjs/swagger": "^3.20.2", "@midwayjs/swagger": "^3.20.2",
"@midwayjs/typeorm": "^3.20.0", "@midwayjs/typeorm": "^3.20.0",
"@midwayjs/upload": "^3.20.16",
"@midwayjs/validate": "^3.20.2", "@midwayjs/validate": "^3.20.2",
"axios": "^1.7.9", "@woocommerce/woocommerce-rest-api": "^1.0.2",
"axios": "^1.13.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"csv-parse": "^6.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"mysql2": "^3.11.5", "eta": "^4.4.1",
"i18n-iso-countries": "^7.14.0",
"mysql2": "^3.15.3",
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"npm-check-updates": "^19.1.2",
"qs": "^6.14.0",
"sharp": "^0.33.3",
"swagger-ui-dist": "^5.18.2", "swagger-ui-dist": "^5.18.2",
"typeorm": "^0.3.20", "typeorm": "^0.3.27",
"typeorm-extension": "^3.7.2",
"wpapi": "^1.2.2",
"xlsx": "^0.18.5",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"engines": { "engines": {
@ -35,10 +46,15 @@
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js", "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js",
"test": "cross-env NODE_ENV=unittest jest", "test": "cross-env NODE_ENV=unittest jest",
"cov": "jest --coverage", "cov": "jest --coverage",
"lint": "mwts check", "lint": "mwtsc check",
"lint:fix": "mwts fix", "lint:fix": "mwtsc fix",
"ci": "npm run cov", "ci": "npm run cov",
"build": "mwtsc --cleanOutDir" "build": "mwtsc --cleanOutDir",
"seed": "ts-node src/db/seed/index.ts",
"seed:run": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d src/db/datasource.ts",
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
"migration:generate": "npm run typeorm -- -d src/db/datasource.ts migration:generate",
"migration:run": "npm run typeorm -- migration:run -d src/db/datasource.ts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -47,6 +63,10 @@
"author": "anonymous", "author": "anonymous",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@midwayjs/mock": "^3.20.11" "@midwayjs/mock": "^3.20.11",
"cross-env": "^10.1.0",
"mwtsc": "^1.15.2",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
} }
} }

184
permutation_fix.md Normal file
View File

@ -0,0 +1,184 @@
# Permutation页面列表显示问题分析和修复方案
## 问题分析
经过代码分析,发现了以下几个可能导致列表不显示的问题:
### 1. API路径不匹配
前端代码中引用的API函数名与后端控制器中的路径不一致
- 前端:`productcontrollerGetcategoriesall`、`productcontrollerGetcategoryattributes`、`productcontrollerGetproductlist`
- 后端实际的API路径`/product/categories/all`、`/product/category/:id/attributes`、`/product/list`
### 2. 数据格式问题
- `getCategoryAttributes`返回的数据结构与前端期望的不匹配
- 属性值获取逻辑可能存在问题
### 3. 组合生成逻辑问题
- 在生成排列组合时,数据结构和键值对应可能不正确
## 修复方案
### 后端修复
1. **修改getCategoryAttributes方法** - 在`/Users/zksu/Developer/work/workcode/API/src/service/product.service.ts`中:
```typescript
// 获取分类下的属性配置
async getCategoryAttributes(categoryId: number): Promise<any[]> {
const category = await this.categoryModel.findOne({
where: { id: categoryId },
relations: ['attributes', 'attributes.attributeDict', 'attributes.attributeDict.items'],
});
if (!category) {
return [];
}
// 格式化返回,匹配前端期望的数据结构
return category.attributes.map(attr => ({
id: attr.id,
dictId: attr.attributeDict.id,
name: attr.attributeDict.name, // 用于generateKeyFromPermutation
title: attr.attributeDict.title, // 用于列标题
dict: {
id: attr.attributeDict.id,
name: attr.attributeDict.name,
title: attr.attributeDict.title,
items: attr.attributeDict.items || []
}
}));
}
```
2. **确保dict/items接口可用** - 检查字典项获取接口:
在`/Users/zksu/Developer/work/workcode/API/src/controller/dict.controller.ts`中添加或确认:
```typescript
@Get('/items')
async getDictItems(@Query('dictId') dictId: number) {
try {
const dict = await this.dictModel.findOne({
where: { id: dictId },
relations: ['items']
});
if (!dict) {
return [];
}
return dict.items || [];
} catch (error) {
return errorResponse(error?.message || error);
}
}
```
### 前端修复建议
1. **添加错误处理和调试信息**
```typescript
// 在获取属性值的地方添加错误处理
const fetchData = async () => {
setLoading(true);
try {
// 1. Fetch Attributes
const attrRes = await productcontrollerGetcategoryattributes({
id: categoryId,
});
console.log('Attributes response:', attrRes); // 调试用
const attrs = Array.isArray(attrRes) ? attrRes : attrRes?.data || [];
setAttributes(attrs);
// 2. Fetch Attribute Values (Dict Items)
const valuesMap: Record<string, any[]> = {};
for (const attr of attrs) {
const dictId = attr.dict?.id || attr.dictId;
if (dictId) {
try {
const itemsRes = await request('/dict/items', {
params: { dictId },
});
console.log(`Dict items for ${attr.name}:`, itemsRes); // 调试用
valuesMap[attr.name] = itemsRes || [];
} catch (error) {
console.error(`Failed to fetch items for dict ${dictId}:`, error);
valuesMap[attr.name] = [];
}
}
}
setAttributeValues(valuesMap);
// 3. Fetch Existing Products
await fetchProducts(categoryId);
} catch (error) {
console.error('Error in fetchData:', error);
message.error('获取数据失败');
} finally {
setLoading(false);
}
};
```
2. **修复组合生成逻辑**
```typescript
// 修改generateKeyFromPermutation函数
const generateKeyFromPermutation = (perm: any) => {
const parts = Object.keys(perm).map((attrName) => {
const valItem = perm[attrName];
const val = valItem.name || valItem.value; // 兼容不同的数据格式
return `${attrName}:${val}`;
});
return parts.sort().join('|');
};
// 修改generateAttributeKey函数
const generateAttributeKey = (attrs: any[]) => {
const parts = attrs.map((a) => {
const key = a.dict?.name || a.dictName || a.name;
const val = a.name || a.value;
return `${key}:${val}`;
});
return parts.sort().join('|');
};
```
3. **添加空状态处理**
```typescript
// 在ProTable中添加空状态提示
<ProTable
// ... 其他属性
locale={{
emptyText: permutations.length === 0 && !loading ? '暂无数据,请检查分类属性配置' : '暂无数据'
}}
/>
```
## 调试步骤
1. **检查网络请求**
- 打开浏览器开发者工具
- 检查 `/product/categories/all` 请求是否成功
- 检查 `/product/category/:id/attributes` 请求返回的数据格式
- 检查 `/dict/items?dictId=:id` 请求是否成功
- 检查 `/product/list` 请求是否成功
2. **检查控制台日志**
- 查看属性数据是否正确加载
- 查看属性值是否正确获取
- 查看排列组合是否正确生成
3. **检查数据结构**
- 确认 `attributes` 数组是否正确
- 确认 `attributeValues` 对象是否正确填充
- 确认 `permutations` 数组是否正确生成
## 测试验证
1. 选择一个有属性配置的分类
2. 确认属性有对应的字典项
3. 检查排列组合是否正确显示
4. 验证现有产品匹配是否正确

3052
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,902 @@
import { ISiteAdapter } from '../interface/site-adapter.interface';
import { ShopyyService } from '../service/shopyy.service';
import {
UnifiedCustomerDTO,
UnifiedMediaDTO,
UnifiedOrderDTO,
UnifiedOrderLineItemDTO,
UnifiedProductDTO,
UnifiedProductVariationDTO,
UnifiedSubscriptionDTO,
UnifiedReviewPaginationDTO,
UnifiedReviewDTO,
UnifiedWebhookDTO,
UnifiedWebhookPaginationDTO,
CreateWebhookDTO,
UpdateWebhookDTO,
UnifiedAddressDTO,
UnifiedShippingLineDTO,
OrderFulfillmentStatus,
FulfillmentDTO
} from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import {
ShopyyCustomer,
ShopyyOrder,
ShopyyProduct,
ShopyyVariant,
ShopyyWebhook,
} from '../dto/shopyy.dto';
import {
OrderStatus,
} from '../enums/base.enum';
export class ShopyyAdapter implements ISiteAdapter {
shopyyFinancialStatusMap= {
'200': '待支付',
'210': "支付中",
'220':"部分支付",
'230':"已支付",
'240':"支付失败",
'250':"部分退款",
'260':"已退款",
'290':"已取消",
}
constructor(private site: any, private shopyyService: ShopyyService) {
this.mapCustomer = this.mapCustomer.bind(this);
this.mapProduct = this.mapProduct.bind(this);
this.mapVariation = this.mapVariation.bind(this);
this.mapOrder = this.mapOrder.bind(this);
this.mapMedia = this.mapMedia.bind(this);
// this.mapSubscription = this.mapSubscription.bind(this);
}
private mapMedia(item: any): UnifiedMediaDTO {
// 映射媒体项目
return {
id: item.id,
date_created: item.created_at,
date_modified: item.updated_at,
source_url: item.src,
title: item.alt || '',
media_type: '', // Shopyy API未提供,暂时留空
mime_type: '', // Shopyy API未提供,暂时留空
};
}
private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any {
const { search, page, per_page } = params;
const shopyyParams: any = {
page: page || 1,
limit: per_page || 10,
};
if (search) {
shopyyParams.query = search;
}
return shopyyParams;
}
private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO {
// 映射产品状态
function mapProductStatus(status: number) {
return status === 1 ? 'publish' : 'draft';
}
return {
id: item.id,
name: item.name || item.title,
type: String(item.product_type ?? ''),
status: mapProductStatus(item.status),
sku: item.variant?.sku || '',
regular_price: String(item.variant?.price ?? ''),
sale_price: String(item.special_price ?? ''),
price: String(item.price ?? ''),
stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock',
stock_quantity: item.inventory_quantity,
images: (item.images || []).map((img: any) => ({
id: img.id || 0,
src: img.src,
name: '',
alt: img.alt || '',
// 排序
position: img.position || '',
})),
attributes: (item.options || []).map(option => ({
id: option.id || 0,
name: option.option_name || '',
options: (option.values || []).map(value => value.option_value || ''),
})),
tags: (item.tags || []).map((t: any) => ({
id: t.id || 0,
name: t.name || '',
})),
// shopyy叫做专辑
categories: item.collections.map((c: any) => ({
id: c.id || 0,
name: c.title || '',
})),
variations: item.variants?.map(this.mapVariation.bind(this)) || [],
permalink: item.permalink,
date_created:
typeof item.created_at === 'number'
? new Date(item.created_at * 1000).toISOString()
: String(item.created_at ?? ''),
date_modified:
typeof item.updated_at === 'number'
? new Date(item.updated_at * 1000).toISOString()
: String(item.updated_at ?? ''),
raw: item,
};
}
private mapVariation(variant: ShopyyVariant): UnifiedProductVariationDTO {
// 映射变体
return {
id: variant.id,
name: variant.sku || '',
sku: variant.sku || '',
regular_price: String(variant.price ?? ''),
sale_price: String(variant.special_price ?? ''),
price: String(variant.price ?? ''),
stock_status:
variant.inventory_tracking === 1 ? 'instock' : 'outofstock',
stock_quantity: variant.inventory_quantity,
};
}
shopyyOrderStatusMap = {//订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消;
[100]: OrderStatus.PENDING, // 100 未完成 转为 pending
[110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing
// 已发货
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
}
private mapOrder(item: ShopyyOrder): UnifiedOrderDTO {
// 提取账单和送货地址 如果不存在则为空对象
const billing = (item as any).billing_address || {};
const shipping = (item as any).shipping_address || {};
// 构建账单地址对象
const billingObj: UnifiedAddressDTO = {
first_name: billing.first_name || item.firstname || '',
last_name: billing.last_name || item.lastname || '',
fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(),
company: billing.company || '',
email: item.customer_email || item.email || '',
phone: billing.phone || (item as any).telephone || '',
address_1: billing.address1 || item.payment_address || '',
address_2: billing.address2 || '',
city: billing.city || item.payment_city || '',
state: billing.province || item.payment_zone || '',
postcode: billing.zip || item.payment_postcode || '',
method_title: item.payment_method || '',
country:
billing.country_name ||
billing.country_code ||
item.payment_country ||
'',
};
// 构建送货地址对象
const shippingObj: UnifiedAddressDTO = {
first_name: shipping.first_name || item.firstname || '',
last_name: shipping.last_name || item.lastname || '',
fullname: shipping.name || '',
company: shipping.company || '',
address_1:
shipping.address1 ||
(typeof item.shipping_address === 'string'
? item.shipping_address
: '') ||
'',
address_2: shipping.address2 || '',
city: shipping.city || item.shipping_city || '',
state: shipping.province || item.shipping_zone || '',
postcode: shipping.zip || item.shipping_postcode || '',
method_title: item.payment_method || '',
country:
shipping.country_name ||
shipping.country_code ||
item.shipping_country ||
'',
};
// 构建送货地址对象
const shipping_lines: UnifiedShippingLineDTO[] = [
{
id: item.fulfillments?.[0]?.id || 0,
method_title: item.payment_method || '',
method_id: item.payment_method || '',
total: Number(item.current_shipping_price).toExponential(2) || '0.00',
total_tax: '0.00',
taxes: [],
meta_data: [],
},
];
// 格式化地址为字符串
const formatAddress = (addr: UnifiedAddressDTO) => {
return [
addr.fullname,
addr.company,
addr.address_1,
addr.address_2,
addr.city,
addr.state,
addr.postcode,
addr.country,
addr.phone,
]
.filter(Boolean)
.join(', ');
};
const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
(p: any) => ({
id: p.id,
name: p.product_title || p.name,
product_id: p.product_id,
quantity: p.quantity,
total: String(p.price ?? ''),
sku: p.sku || p.sku_code || '',
price: String(p.price ?? ''),
})
);
// 货币符号
const currencySymbols: Record<string, string> = {
'EUR': '€',
'USD': '$',
'GBP': '£',
'JPY': '¥',
'AUD': 'A$',
'CAD': 'C$',
'CHF': 'CHF',
'CNY': '¥',
'HKD': 'HK$',
'NZD': 'NZ$',
'SGD': 'S$'
// 可以根据需要添加更多货币代码和符号
};
// 映射订单状态,如果不存在则默认 pending
const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING;
const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status]
// 发货状态
const fulfillment_status = this.shopyyFulfillmentStatusMap[item.fulfillment_status];
return {
id: item.id || item.order_id,
number: item.order_number || item.order_sn,
status,
financial_status: finalcial_status,
currency: item.currency_code || item.currency,
total: String(item.total_price ?? item.total_amount ?? ''),
customer_id: item.customer_id || item.user_id,
customer_name:
item.customer_name || `${item.firstname} ${item.lastname}`.trim(),
email: item.customer_email || item.email,
customer_email: item.customer_email || item.email,
line_items: lineItems,
sales: lineItems, // 兼容前端
billing: billingObj,
shipping: shippingObj,
billing_full_address: formatAddress(billingObj),
shipping_full_address: formatAddress(shippingObj),
payment_method: item.payment_method,
shipping_lines: shipping_lines || [],
fee_lines: item.fee_lines || [],
coupon_lines: item.coupon_lines || [],
customer_ip_address: item.ip || '',
device_type: item.source_device || '',
utm_source: item.utm_source || '',
source_type: 'shopyy', // FIXME
date_paid: typeof item.pay_at === 'number'
? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString()
: null,
refunds: [],
currency_symbol: (currencySymbols[item.currency] || '$') || '',
date_created:
typeof item.created_at === 'number'
? new Date(item.created_at * 1000).toISOString()
: item.date_added ||
(typeof item.created_at === 'string' ? item.created_at : ''),
date_modified:
typeof item.updated_at === 'number'
? new Date(item.updated_at * 1000).toISOString()
: item.date_updated ||
item.last_modified ||
(typeof item.updated_at === 'string' ? item.updated_at : ''),
fulfillment_status,
fulfillments: item.fulfillments?.map?.((f) => ({
id: f.id,
tracking_number: f.tracking_number || '',
shipping_provider: f.tracking_company || '',
shipping_method: f.tracking_company || '',
date_created: typeof f.created_at === 'number'
? new Date(f.created_at * 1000).toISOString()
: f.created_at || '',
// status: f.payment_tracking_status
})) || [],
raw: item,
};
}
shopyyFulfillmentStatusMap = {
// 未发货
'300': OrderFulfillmentStatus.PENDING,
// 部分发货
'310': OrderFulfillmentStatus.PARTIALLY_FULFILLED,
// 已发货
'320': OrderFulfillmentStatus.FULFILLED,
// 已取消
'330': OrderFulfillmentStatus.CANCELLED,
// 确认发货
}
private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO {
// 处理多地址结构
const addresses = item.addresses || [];
const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {});
// 尝试从地址列表中获取billing和shipping
// 如果没有明确区分,默认使用默认地址或第一个地址
const billingAddress = defaultAddress;
const shippingAddress = defaultAddress;
const billing = {
first_name: billingAddress.first_name || item.first_name || '',
last_name: billingAddress.last_name || item.last_name || '',
fullname: billingAddress.name || `${billingAddress.first_name || item.first_name || ''} ${billingAddress.last_name || item.last_name || ''}`.trim(),
company: billingAddress.company || '',
email: item.email || '',
phone: billingAddress.phone || item.contact || '',
address_1: billingAddress.address1 || '',
address_2: billingAddress.address2 || '',
city: billingAddress.city || '',
state: billingAddress.province || '',
postcode: billingAddress.zip || '',
country: billingAddress.country_name || billingAddress.country_code || item.country?.country_name || ''
};
const shipping = {
first_name: shippingAddress.first_name || item.first_name || '',
last_name: shippingAddress.last_name || item.last_name || '',
fullname: shippingAddress.name || `${shippingAddress.first_name || item.first_name || ''} ${shippingAddress.last_name || item.last_name || ''}`.trim(),
company: shippingAddress.company || '',
address_1: shippingAddress.address1 || '',
address_2: shippingAddress.address2 || '',
city: shippingAddress.city || '',
state: shippingAddress.province || '',
postcode: shippingAddress.zip || '',
country: shippingAddress.country_name || shippingAddress.country_code || item.country?.country_name || ''
};
return {
id: item.id || item.customer_id,
orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0),
total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0),
first_name: item.first_name || item.firstname || '',
last_name: item.last_name || item.lastname || '',
fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(),
email: item.email || item.customer_email || '',
phone: item.contact || billing.phone || item.phone || '',
billing,
shipping,
date_created:
typeof item.created_at === 'number'
? new Date(item.created_at * 1000).toISOString()
: (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''),
date_modified:
typeof item.updated_at === 'number'
? new Date(item.updated_at * 1000).toISOString()
: (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''),
raw: item,
};
}
async getProducts(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedProductDTO>> {
const response = await this.shopyyService.fetchResourcePaged<ShopyyProduct>(
this.site,
'products/list',
params
);
const { items = [], total, totalPages, page, per_page } = response;
const finalItems = items.map((item) => ({
...item,
permalink: `${this.site.websiteUrl}/products/${item.handle}`,
})).map(this.mapProduct.bind(this))
return {
items: finalItems as UnifiedProductDTO[],
total,
totalPages,
page,
per_page,
};
}
async getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]> {
// Shopyy getAllProducts 暂未实现
throw new Error('Shopyy getAllProducts 暂未实现');
}
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
// 使用ShopyyService获取单个产品
const product = await this.shopyyService.getProduct(this.site, id);
return this.mapProduct(product);
}
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
const res = await this.shopyyService.createProduct(this.site, data);
return this.mapProduct(res);
}
async updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean> {
// Shopyy update returns boolean?
// shopyyService.updateProduct returns boolean.
// So I can't return the updated product.
// I have to fetch it again or return empty/input.
// Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock).
await this.shopyyService.updateProduct(this.site, String(id), data);
return true;
}
async updateVariation(productId: string | number, variationId: string | number, data: any): Promise<any> {
await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data);
return { ...data, id: variationId };
}
async getOrderNotes(orderId: string | number): Promise<any[]> {
return await this.shopyyService.getOrderNotes(this.site, orderId);
}
async createOrderNote(orderId: string | number, data: any): Promise<any> {
return await this.shopyyService.createOrderNote(this.site, orderId, data);
}
async deleteProduct(id: string | number): Promise<boolean> {
// Use batch delete
await this.shopyyService.batchProcessProducts(this.site, { delete: [id] });
return true;
}
async batchProcessProducts(
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
): Promise<any> {
return await this.shopyyService.batchProcessProducts(this.site, data);
}
mapUnifiedOrderQueryToShopyyQuery(params: UnifiedSearchParamsDTO) {
const { where = {} as any, ...restParams } = params || {}
const statusMap = {
'pending': '100', // 100 未完成
'processing': '110', // 110 待处理
'completed': "180", // 180 已完成(确认收货)
'cancelled': '190', // 190 取消
}
const normalizedParams: any = {
...restParams,
}
if (where) {
normalizedParams.where = {
...where,
}
if (where.status) {
normalizedParams.where.status = statusMap[where.status];
}
}
return normalizedParams
}
async getOrders(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
const normalizedParams = this.mapUnifiedOrderQueryToShopyyQuery(params);
const { items, total, totalPages, page, per_page } =
await this.shopyyService.fetchResourcePaged<any>(
this.site,
'orders',
normalizedParams
);
return {
items: items.map(this.mapOrder.bind(this)),
total,
totalPages,
page,
per_page,
};
}
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
const data = await this.shopyyService.getAllOrders(this.site.id, params);
return data.map(this.mapOrder.bind(this));
}
async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
const data = await this.shopyyService.getOrder(this.site.id, String(id));
return this.mapOrder(data);
}
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
const createdOrder = await this.shopyyService.createOrder(this.site, data);
return this.mapOrder(createdOrder);
}
async updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
return await this.shopyyService.updateOrder(this.site, String(id), data);
}
async deleteOrder(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteOrder(this.site, id);
}
async fulfillOrder(orderId: string | number, data: {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any> {
// 订单履行(发货)
try {
// 判断是否为部分发货(包含 items
if (data.items && data.items.length > 0) {
// 部分发货
const partShipData = {
order_number: String(orderId),
note: data.shipping_method || '',
tracking_company: data.shipping_provider || '',
tracking_number: data.tracking_number || '',
courier_code: '1', // 默认快递公司代码
products: data.items.map(item => ({
quantity: item.quantity,
order_product_id: String(item.order_item_id)
}))
};
return await this.shopyyService.partFulfillOrder(this.site, partShipData);
} else {
// 批量发货(完整发货)
const batchShipData = {
order_number: String(orderId),
tracking_company: data.shipping_provider || '',
tracking_number: data.tracking_number || '',
courier_code: 1, // 默认快递公司代码
note: data.shipping_method || '',
mode: null // 新增模式
};
return await this.shopyyService.batchFulfillOrders(this.site, batchShipData);
}
} catch (error) {
throw new Error(`履行失败: ${error.message}`);
}
}
async cancelFulfillment(orderId: string | number, data: {
reason?: string;
shipment_id?: string;
}): Promise<any> {
// 取消订单履行
try {
// 调用 ShopyyService 的取消履行方法
const cancelShipData = {
order_id: String(orderId),
fulfillment_id: data.shipment_id || ''
};
const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData);
return {
success: result,
order_id: orderId,
shipment_id: data.shipment_id,
reason: data.reason,
cancelled_at: new Date().toISOString()
};
} catch (error) {
throw new Error(`取消履行失败: ${error.message}`);
}
}
/**
*
* @param orderId ID
* @returns
*/
async getOrderFulfillments(orderId: string | number): Promise<any[]> {
return await this.shopyyService.getFulfillments(this.site, String(orderId));
}
/**
*
* @param orderId ID
* @param data
* @returns
*/
async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise<any> {
// 调用 Shopyy Service 的 createFulfillment 方法
const fulfillmentData = {
tracking_number: data.tracking_number,
carrier_code: data.shipping_provider,
carrier_name: data.shipping_provider,
shipping_method: data.shipping_method || 'standard'
};
return await this.shopyyService.createFulfillment(this.site, String(orderId), fulfillmentData);
}
/**
*
* @param orderId ID
* @param fulfillmentId ID
* @param data
* @returns
*/
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
tracking_number?: string;
tracking_provider?: string;
date_shipped?: string;
status_shipped?: string;
}): Promise<any> {
return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
}
/**
*
* @param orderId ID
* @param fulfillmentId ID
* @returns
*/
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId);
}
async getSubscriptions(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
throw new Error('Shopyy does not support subscriptions.');
}
async getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]> {
// Shopyy getAllSubscriptions 暂未实现
throw new Error('Shopyy getAllSubscriptions 暂未实现');
}
async getMedia(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
const requestParams = this.mapMediaSearchParams(params);
const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged<any>(
this.site,
'media', // Shopyy的媒体API端点可能需要调整
requestParams
);
return {
items: items.map(this.mapMedia.bind(this)),
total,
totalPages,
page,
per_page,
};
}
async getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]> {
// Shopyy getAllMedia 暂未实现
throw new Error('Shopyy getAllMedia 暂未实现');
}
async createMedia(file: any): Promise<UnifiedMediaDTO> {
const createdMedia = await this.shopyyService.createMedia(this.site, file);
return this.mapMedia(createdMedia);
}
async updateMedia(id: string | number, data: any): Promise<UnifiedMediaDTO> {
const updatedMedia = await this.shopyyService.updateMedia(this.site, id, data);
return this.mapMedia(updatedMedia);
}
async deleteMedia(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteMedia(this.site, id);
}
async getReviews(
params: UnifiedSearchParamsDTO
): Promise<UnifiedReviewPaginationDTO> {
const requestParams = this.mapReviewSearchParams(params);
const { items, total, totalPages, page, per_page } = await this.shopyyService.getReviews(
this.site,
requestParams
);
return {
items: items.map(this.mapReview),
total,
totalPages,
page,
per_page,
};
}
async getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]> {
// Shopyy getAllReviews 暂未实现
throw new Error('Shopyy getAllReviews 暂未实现');
}
async getReview(id: string | number): Promise<UnifiedReviewDTO> {
const review = await this.shopyyService.getReview(this.site, id);
return this.mapReview(review);
}
private mapReview(review: any): UnifiedReviewDTO {
// 将ShopYY评论数据映射到统一评论DTO格式
return {
id: review.id || review.review_id,
product_id: review.product_id || review.goods_id,
author: review.author_name || review.username || '',
email: review.author_email || review.user_email || '',
content: review.comment || review.content || '',
rating: Number(review.score || review.rating || 0),
status: String(review.status || 'approved'),
date_created:
typeof review.created_at === 'number'
? new Date(review.created_at * 1000).toISOString()
: String(review.created_at || review.date_added || '')
};
}
private mapReviewSearchParams(params: UnifiedSearchParamsDTO): any {
const { search, page, per_page, where } = params;
const shopyyParams: any = {
page: page || 1,
limit: per_page || 10,
};
if (search) {
shopyyParams.search = search;
}
if (where.status) {
shopyyParams.status = where.status;
}
// if (product_id) {
// shopyyParams.product_id = product_id;
// }
return shopyyParams;
}
async createReview(data: any): Promise<UnifiedReviewDTO> {
const createdReview = await this.shopyyService.createReview(this.site, data);
return this.mapReview(createdReview);
}
async updateReview(id: string | number, data: any): Promise<UnifiedReviewDTO> {
const updatedReview = await this.shopyyService.updateReview(this.site, id, data);
return this.mapReview(updatedReview);
}
async deleteReview(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteReview(this.site, id);
}
// Webhook相关方法
private mapWebhook(item: ShopyyWebhook): UnifiedWebhookDTO {
return {
id: item.id,
name: item.webhook_name || `Webhook-${item.id}`,
topic: item.event_code || '',
delivery_url: item.url || '',
status: 'active',
};
}
async getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO> {
const { items, total, totalPages, page, per_page } = await this.shopyyService.getWebhooks(this.site, params);
return {
items: items.map(this.mapWebhook),
total,
totalPages,
page,
per_page,
};
}
async getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]> {
// Shopyy getAllWebhooks 暂未实现
throw new Error('Shopyy getAllWebhooks 暂未实现');
}
async getWebhook(id: string | number): Promise<UnifiedWebhookDTO> {
const webhook = await this.shopyyService.getWebhook(this.site, id);
return this.mapWebhook(webhook);
}
async createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO> {
const createdWebhook = await this.shopyyService.createWebhook(this.site, data);
return this.mapWebhook(createdWebhook);
}
async updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO> {
const updatedWebhook = await this.shopyyService.updateWebhook(this.site, id, data);
return this.mapWebhook(updatedWebhook);
}
async deleteWebhook(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteWebhook(this.site, id);
}
async getLinks(): Promise<Array<{ title: string, url: string }>> {
// ShopYY站点的管理后台链接通常基于apiUrl构建
const url = this.site.websiteUrl
// 提取基础域名,去掉可能的路径部分
const baseUrl = url.replace(/\/api\/.*$/i, '');
const links = [
{ title: '访问网站', url: baseUrl },
{ title: '管理后台', url: `${baseUrl}/admin/` },
{ title: '订单管理', url: `${baseUrl}/admin/orders.htm` },
{ title: '产品管理', url: `${baseUrl}/admin/products.htm` },
{ title: '客户管理', url: `${baseUrl}/admin/customers.htm` },
{ title: '插件管理', url: `${baseUrl}/admin/apps.htm` },
{ title: '店铺设置', url: `${baseUrl}/admin/settings.htm` },
{ title: '营销中心', url: `${baseUrl}/admin/marketing.htm` },
];
return links;
}
async getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>> {
const { items, total, totalPages, page, per_page } =
await this.shopyyService.fetchCustomersPaged(this.site, params);
return {
items: items.map(this.mapCustomer.bind(this)),
total,
totalPages,
page,
per_page
};
}
async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]> {
// Shopyy getAllCustomers 暂未实现
throw new Error('Shopyy getAllCustomers 暂未实现');
}
async getCustomer(id: string | number): Promise<UnifiedCustomerDTO> {
const customer = await this.shopyyService.getCustomer(this.site, id);
return this.mapCustomer(customer);
}
async createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const createdCustomer = await this.shopyyService.createCustomer(this.site, data);
return this.mapCustomer(createdCustomer);
}
async updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data);
return this.mapCustomer(updatedCustomer);
}
async deleteCustomer(id: string | number): Promise<boolean> {
return await this.shopyyService.deleteCustomer(this.site, id);
}
async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<any> {
throw new Error('Shopyy getVariations 暂未实现');
}
async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]> {
throw new Error('Shopyy getAllVariations 暂未实现');
}
async getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO> {
throw new Error('Shopyy getVariation 暂未实现');
}
async createVariation(productId: string | number, data: any): Promise<UnifiedProductVariationDTO> {
throw new Error('Shopyy createVariation 暂未实现');
}
async deleteVariation(productId: string | number, variationId: string | number): Promise<boolean> {
throw new Error('Shopyy deleteVariation 暂未实现');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
import { MidwayConfig } from '@midwayjs/core'; import { MidwayConfig } from '@midwayjs/core';
import { Product } from '../entity/product.entty'; import { join } from 'path';
import { Category } from '../entity/category.entity'; import { Product } from '../entity/product.entity';
import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity';
import { User } from '../entity/user.entity'; import { User } from '../entity/user.entity';
import { PurchaseOrder } from '../entity/purchase_order.entity'; import { PurchaseOrder } from '../entity/purchase_order.entity';
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity'; import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
@ -11,13 +9,14 @@ import { StockPoint } from '../entity/stock_point.entity';
import { StockRecord } from '../entity/stock_record.entity'; import { StockRecord } from '../entity/stock_record.entity';
import { Order } from '../entity/order.entity'; import { Order } from '../entity/order.entity';
import { OrderItem } from '../entity/order_item.entity'; import { OrderItem } from '../entity/order_item.entity';
import { OrderCoupon } from '../entity/order_copon.entity'; import { OrderCoupon } from '../entity/order_coupon.entity';
import { OrderFee } from '../entity/order_fee.entity'; import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity'; import { OrderRefund } from '../entity/order_refund.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity'; import { OrderRefundItem } from '../entity/order_refund_item.entity';
import { OrderSale } from '../entity/order_sale.entity'; import { OrderSale } from '../entity/order_sale.entity';
import { OrderSaleOriginal } from '../entity/order_item_original.entity'; import { OrderItemOriginal } from '../entity/order_item_original.entity';
import { OrderShipping } from '../entity/order_shipping.entity'; import { OrderShipping } from '../entity/order_shipping.entity';
import { OrderFulfillment } from '../entity/order_fulfillment.entity';
import { Service } from '../entity/service.entity'; import { Service } from '../entity/service.entity';
import { ShippingAddress } from '../entity/shipping_address.entity'; import { ShippingAddress } from '../entity/shipping_address.entity';
import { OrderNote } from '../entity/order_note.entity'; import { OrderNote } from '../entity/order_note.entity';
@ -26,12 +25,22 @@ import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity'; import { ShipmentItem } from '../entity/shipment_item.entity';
import { Transfer } from '../entity/transfer.entity'; import { Transfer } from '../entity/transfer.entity';
import { TransferItem } from '../entity/transfer_item.entity'; import { TransferItem } from '../entity/transfer_item.entity';
import { Strength } from '../entity/strength.entity';
import { Flavors } from '../entity/flavors.entity';
import { CustomerTag } from '../entity/customer_tag.entity'; import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity'; import { Customer } from '../entity/customer.entity';
import { DeviceWhitelist } from '../entity/device_whitelist'; import { DeviceWhitelist } from '../entity/device_whitelist';
import { AuthCode } from '../entity/auth_code'; import { AuthCode } from '../entity/auth_code';
import { Subscription } from '../entity/subscription.entity';
import { Site } from '../entity/site.entity';
import { Dict } from '../entity/dict.entity';
import { DictItem } from '../entity/dict_item.entity';
import { Template } from '../entity/template.entity';
import { Area } from '../entity/area.entity';
import { ProductStockComponent } from '../entity/product_stock_component.entity';
import { CategoryAttribute } from '../entity/category_attribute.entity';
import { Category } from '../entity/category.entity';
import DictSeeder from '../db/seeds/dict.seeder';
import CategorySeeder from '../db/seeds/category.seeder';
import CategoryAttributeSeeder from '../db/seeds/category_attribute.seeder';
export default { export default {
// use for cookie sign key, should change to your own and keep security // use for cookie sign key, should change to your own and keep security
@ -40,11 +49,7 @@ export default {
default: { default: {
entities: [ entities: [
Product, Product,
Category, ProductStockComponent,
Strength,
Flavors,
WpProduct,
Variation,
User, User,
PurchaseOrder, PurchaseOrder,
PurchaseOrderItem, PurchaseOrderItem,
@ -58,11 +63,12 @@ export default {
OrderRefund, OrderRefund,
OrderRefundItem, OrderRefundItem,
OrderSale, OrderSale,
OrderSaleOriginal, OrderItemOriginal,
OrderShipment, OrderShipment,
ShipmentItem, ShipmentItem,
Shipment, Shipment,
OrderShipping, OrderShipping,
OrderFulfillment,
Service, Service,
ShippingAddress, ShippingAddress,
OrderNote, OrderNote,
@ -72,15 +78,24 @@ export default {
Customer, Customer,
DeviceWhitelist, DeviceWhitelist,
AuthCode, AuthCode,
Subscription,
Site,
Dict,
DictItem,
Template,
Area,
CategoryAttribute,
Category,
], ],
synchronize: true, synchronize: true,
logging: false, logging: false,
seeders: [DictSeeder, CategorySeeder, CategoryAttributeSeeder],
}, },
dataSource: { dataSource: {
default: { default: {
type: 'mysql', type: 'mysql',
host: 'localhost', host: 'localhost',
port: 3306, port: 10014,
username: 'root', username: 'root',
password: 'root', password: 'root',
database: 'inventory', database: 'inventory',
@ -91,23 +106,12 @@ export default {
// origin: '*', // 允许所有来源跨域请求 // origin: '*', // 允许所有来源跨域请求
// allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 // allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
// allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头 // allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
// credentials: true, // 允许携带凭据cookies等 // credentials: true, // 允许携带凭据(cookies等)
// }, // },
// jwt: { // jwt: {
// secret: 'YOONE2024!@abc', // secret: 'YOONE2024!@abc',
// expiresIn: '7d', // expiresIn: '7d',
// }, // },
// wpSite: [
// {
// id: '2',
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
// siteName: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: '',
// },
// ],
swagger: { swagger: {
auth: { auth: {
name: 'authorization', name: 'authorization',
@ -115,6 +119,15 @@ export default {
description: 'Bearer Auth', description: 'Bearer Auth',
addSecurityRequirements: true, addSecurityRequirements: true,
}, },
// 配置 Swagger 支持嵌套查询参数
options: {
// 设置查询参数风格为 deepObject
// 这会告诉 Swagger 使用 JSON 格式来序列化嵌套的查询参数
query: {
style: 'deepObject',
explode: false
}
}
}, },
mailer: { mailer: {
host: 'smtphz.qiye.163.com', host: 'smtphz.qiye.163.com',
@ -124,5 +137,16 @@ export default {
user: 'info@canpouches.com', user: 'info@canpouches.com',
pass: 'WWqQ4aZq4Jrm9uwz', pass: 'WWqQ4aZq4Jrm9uwz',
}, },
} },
upload: {
// mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream
mode: 'file',
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
whitelist: ['.csv', '.xlsx'], // 支持的文件后缀
tmpdir: join(__dirname, '../../tmp_uploads'),
cleanTimeout: 5 * 60 * 1000,
},
koa: {
queryParseMode: 'extended',
},
} as MidwayConfig; } as MidwayConfig;

View File

@ -16,8 +16,10 @@ export default {
dataSource: { dataSource: {
default: { default: {
host: 'localhost', host: 'localhost',
port: "3306",
username: 'root', username: 'root',
password: '12345678', password: 'root',
database: 'inventory',
}, },
}, },
}, },
@ -25,46 +27,12 @@ export default {
origin: '*', // 允许所有来源跨域请求 origin: '*', // 允许所有来源跨域请求
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头 allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
credentials: true, // 允许携带凭据cookies等 credentials: true, // 允许携带凭据(cookies等)
}, },
jwt: { jwt: {
secret: 'YOONE2024!@abc', secret: 'YOONE2024!@abc',
expiresIn: '7d', expiresIn: '7d',
}, },
wpSite: [
{
id: '-1',
siteName: 'Admin',
email: '2469687281@qq.com',
},
{
id: '2',
wpApiUrl: 'http://t2-shop.local/',
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
siteName: 'Local',
email: '2469687281@qq.com',
emailPswd: 'lulin91.',
},
{
id: '3',
wpApiUrl: 'http://t1-shop.local/',
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
siteName: 'Local-test-2',
email: '2469687281@qq.com',
emailPswd: 'lulin91.',
},
// {
// id: '2',
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
// siteName: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: 'lulin91.',
// },
],
freightcom: { freightcom: {
url: 'https://customer-external-api.ssd-test.freightcom.com', url: 'https://customer-external-api.ssd-test.freightcom.com',
token: '6zGj1qPTL1jIkbLmgaiYc6SwHUIXJ2t25htUF8uuFYiCg8ILCY6xnBEbvrX1p79L', token: '6zGj1qPTL1jIkbLmgaiYc6SwHUIXJ2t25htUF8uuFYiCg8ILCY6xnBEbvrX1p79L',

View File

@ -3,20 +3,26 @@ import {
App, App,
Inject, Inject,
MidwayDecoratorService, MidwayDecoratorService,
Logger,
Config,
} from '@midwayjs/core'; } from '@midwayjs/core';
import * as koa from '@midwayjs/koa'; import * as koa from '@midwayjs/koa';
import * as validate from '@midwayjs/validate'; import * as validate from '@midwayjs/validate';
import * as info from '@midwayjs/info'; import * as info from '@midwayjs/info';
import * as orm from '@midwayjs/typeorm'; import * as orm from '@midwayjs/typeorm';
import { DataSource } from 'typeorm';
import { join } from 'path'; import { join } from 'path';
// import { DefaultErrorFilter } from './filter/default.filter'; import { DefaultErrorFilter } from './filter/default.filter';
// import { NotFoundFilter } from './filter/notfound.filter'; import { NotFoundFilter } from './filter/notfound.filter';
import { ReportMiddleware } from './middleware/report.middleware'; import { ReportMiddleware } from './middleware/report.middleware';
import { QueryNormalizeMiddleware } from './middleware/query-normalize.middleware';
import * as swagger from '@midwayjs/swagger'; import * as swagger from '@midwayjs/swagger';
import * as crossDomain from '@midwayjs/cross-domain'; import * as crossDomain from '@midwayjs/cross-domain';
import * as cron from '@midwayjs/cron'; import * as cron from '@midwayjs/cron';
import * as jwt from '@midwayjs/jwt'; import * as jwt from '@midwayjs/jwt';
import * as upload from '@midwayjs/upload';
import { USER_KEY } from './decorator/user.decorator'; import { USER_KEY } from './decorator/user.decorator';
import { SiteService } from './service/site.service';
import { AuthMiddleware } from './middleware/auth.middleware'; import { AuthMiddleware } from './middleware/auth.middleware';
@Configuration({ @Configuration({
@ -32,6 +38,7 @@ import { AuthMiddleware } from './middleware/auth.middleware';
crossDomain, crossDomain,
cron, cron,
jwt, jwt,
upload,
], ],
importConfigs: [join(__dirname, './config')], importConfigs: [join(__dirname, './config')],
}) })
@ -45,11 +52,26 @@ export class MainConfiguration {
@Inject() @Inject()
jwtService: jwt.JwtService; // 注入 JwtService 实例 jwtService: jwt.JwtService; // 注入 JwtService 实例
@Inject()
siteService: SiteService;
@Logger()
logger; // 注入 Logger 实例
@Config('typeorm.dataSource.default')
typeormDataSourceConfig; // 注入 TypeORM 数据源配置
async onConfigLoad() {
// 在组件初始化之前,先检查并创建数据库
await this.initializeDatabase();
}
async onReady() { async onReady() {
// add middleware // add middleware
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]); this.app.useMiddleware([QueryNormalizeMiddleware, ReportMiddleware, AuthMiddleware]);
// add filter // add filter
// this.app.useFilter([NotFoundFilter, DefaultErrorFilter]); this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
this.decoratorService.registerParameterHandler( this.decoratorService.registerParameterHandler(
USER_KEY, USER_KEY,
@ -75,4 +97,70 @@ export class MainConfiguration {
} }
); );
} }
/**
*
*/
private async initializeDatabase(): Promise<void> {
// 使用注入的数据库配置
const typeormConfig = this.typeormDataSourceConfig;
// 创建一个临时的 DataSource不指定数据库用于创建数据库
const tempDataSource = new DataSource({
type: 'mysql',
host: typeormConfig.host,
port: typeormConfig.port,
username: typeormConfig.username,
password: typeormConfig.password,
database: undefined, // 不指定数据库
synchronize: true,
logging: false,
});
try {
this.logger.info('正在检查数据库是否存在...');
// 初始化临时数据源
await tempDataSource.initialize();
// 创建查询运行器
const queryRunner = tempDataSource.createQueryRunner();
try {
// 检查数据库是否存在
const databases = await queryRunner.query(
`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`,
[typeormConfig.database]
);
const databaseExists = Array.isArray(databases) && databases.length > 0;
if (!databaseExists) {
this.logger.info(`数据库 ${typeormConfig.database} 不存在,正在创建...`);
// 创建数据库
await queryRunner.query(
`CREATE DATABASE \`${typeormConfig.database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`
);
this.logger.info(`数据库 ${typeormConfig.database} 创建成功`);
} else {
this.logger.info(`数据库 ${typeormConfig.database} 已存在`);
}
} finally {
// 释放查询运行器
await queryRunner.release();
}
} catch (error) {
this.logger.error('数据库初始化失败:', error);
throw error;
} finally {
// 关闭临时数据源
if (tempDataSource.isInitialized) {
await tempDataSource.destroy();
}
}
}
} }

View File

@ -0,0 +1,118 @@
import { Body, Context, Controller, Del, Get, Inject, Param, Post, Put, Query } from '@midwayjs/core';
import {
ApiBearerAuth,
ApiBody,
ApiExtension,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@midwayjs/swagger';
import { AreaService } from '../service/area.service';
import { CreateAreaDTO, QueryAreaDTO, UpdateAreaDTO } from '../dto/area.dto';
import { errorResponse, successResponse } from '../utils/response.util';
import { Area } from '../entity/area.entity';
import * as countries from 'i18n-iso-countries';
@ApiBearerAuth()
@ApiTags('Area')
@Controller('/area')
export class AreaController {
@Inject()
ctx: Context;
@Inject()
areaService: AreaService;
@ApiOperation({ summary: '获取国家列表' })
@ApiOkResponse({ description: '国家列表' })
@Get('/countries')
async getCountries() {
try {
// 注册中文语言包
countries.registerLocale(require('i18n-iso-countries/langs/zh.json'));
// 获取所有国家的中文名称
const countryNames = countries.getNames('zh', { select: 'official' });
// 格式化为 { code, name } 的数组
const countryList = Object.keys(countryNames).map(code => ({
code,
name: countryNames[code],
}));
return successResponse(countryList, '查询成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ApiOperation({ summary: '创建区域' })
@ApiBody({ type: CreateAreaDTO })
@ApiOkResponse({ type: Area, description: '成功创建的区域' })
@Post('/')
async createArea(@Body() area: CreateAreaDTO) {
try {
const newArea = await this.areaService.createArea(area);
return successResponse(newArea, '创建成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ApiOperation({ summary: '更新区域' })
@ApiBody({ type: UpdateAreaDTO })
@ApiOkResponse({ type: Area, description: '成功更新的区域' })
@Put('/:id')
async updateArea(@Param('id') id: number, @Body() area: UpdateAreaDTO) {
try {
const updatedArea = await this.areaService.updateArea(id, area);
return successResponse(updatedArea, '更新成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ApiOperation({ summary: '删除区域' })
@ApiOkResponse({ description: '删除成功' })
@Del('/:id')
async deleteArea(@Param('id') id: number) {
try {
await this.areaService.deleteArea(id);
return successResponse(null, '删除成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ApiOperation({ summary: '获取区域列表(分页)' })
@ApiOkResponse({ type: [Area], description: '区域列表' })
@ApiExtension('x-pagination', { currentPage: 1, pageSize: 10, total: 100 })
@Get('/')
async getAreaList(@Query() query: QueryAreaDTO) {
try {
const { list, total } = await this.areaService.getAreaList(query);
return successResponse({ list, total }, '查询成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
@ApiOperation({ summary: '根据ID获取区域详情' })
@ApiOkResponse({ type: Area, description: '区域详情' })
@Get('/:id')
async getAreaById(@Param('id') id: number) {
try {
const area = await this.areaService.getAreaById(id);
if (!area) {
return errorResponse('区域不存在');
}
return successResponse(area, '查询成功');
} catch (error) {
console.log(error);
return errorResponse(error?.message || error);
}
}
}

View File

@ -0,0 +1,99 @@
import { Controller, Get, Post, Put, Del, Body, Query, Inject, Param } from '@midwayjs/core';
import { CategoryService } from '../service/category.service';
import { successResponse, errorResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger';
import { CreateCategoryDTO, UpdateCategoryDTO } from '../dto/product.dto';
@Controller('/category')
export class CategoryController {
@Inject()
categoryService: CategoryService;
@ApiOkResponse()
@Get('/all')
async getAll() {
try {
const data = await this.categoryService.getAll();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/')
async getList(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
const data = await this.categoryService.getList({ current, pageSize }, name);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/')
async create(@Body() body: CreateCategoryDTO) {
try {
const data = await this.categoryService.create(body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Put('/:id')
async update(@Param('id') id: number, @Body() body: UpdateCategoryDTO) {
try {
const data = await this.categoryService.update(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Del('/:id')
async delete(@Param('id') id: number) {
try {
await this.categoryService.delete(id);
return successResponse(null);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Get('/attribute/:categoryId')
async getCategoryAttributes(@Param('categoryId') categoryId: number) {
try {
const data = await this.categoryService.getCategoryAttributes(categoryId);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/attribute')
async createCategoryAttribute(@Body() body: { categoryId: number, attributeDictIds: number[] }) {
try {
const data = await this.categoryService.createCategoryAttribute(body.categoryId, body.attributeDictIds);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Del('/attribute/:id')
async deleteCategoryAttribute(@Param('id') id: number) {
try {
await this.categoryService.deleteCategoryAttribute(id);
return successResponse(null);
} catch (error) {
return errorResponse(error?.message || error);
}
}
}

View File

@ -1,82 +1,250 @@
import { import { Controller, Get, Post, Inject, Query, Body, Put, Del, Param } from '@midwayjs/core';
Body, import { successResponse, errorResponse, ApiResponse } from '../utils/response.util';
Context,
Controller,
Del,
Get,
Inject,
Post,
Put,
Query,
} from '@midwayjs/core';
import { CustomerService } from '../service/customer.service'; import { CustomerService } from '../service/customer.service';
import { errorResponse, successResponse } from '../utils/response.util'; import { CustomerQueryParamsDTO, CreateCustomerDTO, UpdateCustomerDTO, GetCustomerDTO, BatchCreateCustomerDTO, BatchUpdateCustomerDTO, BatchDeleteCustomerDTO, SyncCustomersDTO } from '../dto/customer.dto';
import { ApiProperty } from '@midwayjs/swagger';
import { ApiOkResponse } from '@midwayjs/swagger'; import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes } from '../dto/reponse.dto'; import { UnifiedPaginationDTO } from '../dto/api.dto';
import { CustomerTagDTO, QueryCustomerListDTO } from '../dto/customer.dto';
export class CustomerTagDTO {
@ApiProperty({ description: '客户邮箱' })
email: string;
@ApiProperty({ description: '标签名称' })
tag: string;
}
@Controller('/customer') @Controller('/customer')
export class CustomerController { export class CustomerController {
@Inject()
ctx: Context;
@Inject() @Inject()
customerService: CustomerService; customerService: CustomerService;
@ApiOkResponse() @ApiOkResponse({ type: ApiResponse<UnifiedPaginationDTO<GetCustomerDTO>> })
@Get('/list') @Get('/list')
async getCustomerList(@Query() param: QueryCustomerListDTO) { async getCustomerList(@Query() query: CustomerQueryParamsDTO) {
try { try {
const data = await this.customerService.getCustomerList(param); const result = await this.customerService.getCustomerList(query)
return successResponse(data); return successResponse(result);
} catch (error) { } catch (error) {
console.log(error) return errorResponse(error.message);
return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ type: BooleanRes }) @ApiOkResponse({ type: Object })
@Post('/tag/add') @Get('/statistic/list')
async addTag(@Body() dto: CustomerTagDTO) { async getCustomerStatisticList(@Query() query: CustomerQueryParamsDTO) {
try { try {
await this.customerService.addTag(dto.email, dto.tag); const result = await this.customerService.getCustomerStatisticList(query as any);
return successResponse(true); return successResponse(result);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error.message);
} }
} }
@ApiOkResponse({ type: BooleanRes }) @ApiOkResponse({ type: Object })
@Del('/tag/del') @Post('/addtag')
async delTag(@Body() dto: CustomerTagDTO) { async addTag(@Body() body: CustomerTagDTO) {
try { try {
await this.customerService.delTag(dto.email, dto.tag); const result = await this.customerService.addTag(body.email, body.tag);
return successResponse(true); return successResponse(result);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error.message);
} }
} }
@ApiOkResponse() @ApiOkResponse({ type: Object })
@Get('/tags') @Post('/deltag')
async delTag(@Body() body: CustomerTagDTO) {
try {
const result = await this.customerService.delTag(body.email, body.tag);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOkResponse({ type: Object })
@Get('/gettags')
async getTags() { async getTags() {
try { try {
const data = await this.customerService.getTags(); const result = await this.customerService.getTags();
return successResponse(data); return successResponse(result);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error.message);
} }
} }
@ApiOkResponse({ type: Object })
@ApiOkResponse({ type: BooleanRes }) @Post('/setrate')
@Put('/rate') async setRate(@Body() body: { id: number; rate: number }) {
async setRate(@Body() params: { id: number; rate: number }) {
try { try {
await this.customerService.setRate(params); const result = await this.customerService.setRate({ id: body.id, rate: body.rate });
return successResponse(true); return successResponse(result);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error.message);
}
}
/**
*
*
* where和orderBy参数筛选和排序要同步的客户数据
*/
@ApiOkResponse({ type: Object })
@Post('/sync')
async syncCustomers(@Body() body: SyncCustomersDTO) {
try {
const { siteId, params = {} } = body;
// 调用service层的同步方法所有业务逻辑都在service中处理
const syncResult = await this.customerService.syncCustomersFromSite(siteId, params);
return successResponse(syncResult);
} catch (error) {
return errorResponse(error.message);
}
}
// ====================== 单个客户CRUD操作 ======================
/**
*
* 使CreateCustomerDTO进行数据验证
*/
@ApiOkResponse({ type: GetCustomerDTO })
@Post('/')
async createCustomer(@Body() body: CreateCustomerDTO) {
try {
// 调用service层的upsertCustomer方法
const result = await this.customerService.upsertCustomer({...body, origin_id: String(body.origin_id)});
return successResponse(result.customer);
} catch (error) {
return errorResponse(error.message);
}
}
/**
* ID获取单个客户
* GetCustomerDTO格式的客户信息
*/
@ApiOkResponse({ type: GetCustomerDTO })
@Get('/:id')
async getCustomerById(@Param('id') id: number) {
try {
const customer = await this.customerService.customerModel.findOne({ where: { id } });
if (!customer) {
return errorResponse('客户不存在');
}
return successResponse(customer);
} catch (error) {
return errorResponse(error.message);
}
}
/**
*
* 使UpdateCustomerDTO进行数据验证
*/
@ApiOkResponse({ type: GetCustomerDTO })
@Put('/:id')
async updateCustomer(@Param('id') id: number, @Body() body: UpdateCustomerDTO) {
try {
const customer = await this.customerService.updateCustomer(id, body);
if (!customer) {
return errorResponse('客户不存在');
}
return successResponse(customer);
} catch (error) {
return errorResponse(error.message);
}
}
/**
*
* ID删除客户记录
*/
@ApiOkResponse({ type: Object })
@Del('/:id')
async deleteCustomer(@Param('id') id: number) {
try {
// 先检查客户是否存在
const customer = await this.customerService.customerModel.findOne({ where: { id } });
if (!customer) {
return errorResponse('客户不存在');
}
// 删除客户
await this.customerService.customerModel.delete(id);
return successResponse({ message: '删除成功' });
} catch (error) {
return errorResponse(error.message);
}
}
// ====================== 批量客户操作 ======================
/**
*
* 使BatchCreateCustomerDTO进行数据验证
*/
@ApiOkResponse({ type: Object })
@Post('/batch')
async batchCreateCustomers(@Body() body: BatchCreateCustomerDTO) {
try {
const result = await this.customerService.upsertManyCustomers(body.customers.map(c => ({ ...c, origin_id: String(c.origin_id) })));
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
/**
*
* 使BatchUpdateCustomerDTO进行数据验证
*
*/
@ApiOkResponse({ type: Object })
@Put('/batch')
async batchUpdateCustomers(@Body() body: BatchUpdateCustomerDTO) {
try {
const { customers } = body;
// 调用service层的批量更新方法
const result = await this.customerService.batchUpdateCustomers(customers);
return successResponse({
total: result.total,
updated: result.updated,
processed: result.processed,
errors: result.errors,
message: `成功更新${result.updated}个客户,共处理${result.processed}`
});
} catch (error) {
return errorResponse(error.message);
}
}
/**
*
* 使BatchDeleteCustomerDTO进行数据验证
*/
@ApiOkResponse({ type: Object })
@Del('/batch')
async batchDeleteCustomers(@Body() body: BatchDeleteCustomerDTO) {
try {
const { ids } = body;
// 调用service层的批量删除方法
const result = await this.customerService.batchDeleteCustomers(ids);
return successResponse({
total: result.total,
updated: result.updated,
processed: result.processed,
errors: result.errors,
message: `成功删除${result.updated}个客户,共处理${result.processed}`
});
} catch (error) {
return errorResponse(error.message);
} }
} }
} }

View File

@ -0,0 +1,239 @@
import { Inject, Controller, Get, Post, Put, Del, Query, Body, Param, Files, Fields, ContentType } from '@midwayjs/core';
import { DictService } from '../service/dict.service';
import { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
import { Validate } from '@midwayjs/validate';
import { Context } from '@midwayjs/koa';
import { successResponse, errorResponse, ApiResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger';
import { BatchOperationResult } from '../dto/api.dto';
/**
*
* @decorator Controller
*/
@Controller('/dict')
export class DictController {
@Inject()
dictService: DictService;
@Inject()
ctx: Context;
/**
*
* @param files
*/
@Post('/import')
@Validate()
async importDicts(@Files() files: any) {
// 从上传的文件列表中获取第一个文件
const file = files[0];
// 调用服务层方法处理XLSX文件
const result = await this.dictService.importDictsFromXLSX(file.data);
// 返回导入结果
return result;
}
/**
* XLSX模板
*/
@Get('/template')
@ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
async downloadDictTemplate() {
// 设置下载文件的名称
this.ctx.set('Content-Disposition', 'attachment; filename=dict-template.xlsx');
// 返回XLSX模板内容
return this.dictService.getDictXLSXTemplate();
}
/**
*
* @param id ID
*/
@Get('/:id')
async getDict(@Param('id') id: number) {
// 调用服务层方法,并关联查询字典项
return this.dictService.getDict({ id }, ['items']);
}
/**
*
* @param title ()
* @param name ()
*/
@Get('/list')
async getDicts(@Query('title') title?: string, @Query('name') name?: string) {
// 调用服务层方法
return this.dictService.getDicts({ title, name });
}
/**
*
* @param createDictDTO
*/
@Post('/')
@Validate()
async createDict(@Body() createDictDTO: CreateDictDTO) {
try {
// 调用服务层方法
const result = await this.dictService.createDict(createDictDTO);
return successResponse(result, '字典创建成功');
} catch (error) {
return errorResponse(error?.message || '字典创建失败', error?.code || 500);
}
}
/**
*
* @param id ID
* @param updateDictDTO
*/
@Put('/:id')
@Validate()
async updateDict(@Param('id') id: number, @Body() updateDictDTO: UpdateDictDTO) {
try {
// 调用服务层方法
const result = await this.dictService.updateDict(id, updateDictDTO);
return successResponse(result, '字典更新成功');
} catch (error) {
return errorResponse(error?.message || '字典更新失败', error?.code || 500);
}
}
/**
*
* @param id ID
*/
@Del('/:id')
async deleteDict(@Param('id') id: number) {
try {
// 调用服务层方法
const result = await this.dictService.deleteDict(id);
return successResponse(result, '字典删除成功');
} catch (error) {
return errorResponse(error?.message || '字典删除失败', error?.code || 500);
}
}
/**
*
* @param files
* @param fields FormData中的字段
*/
@ApiOkResponse({type:ApiResponse<BatchOperationResult>})
@Post('/item/import')
@Validate()
async importDictItems(@Files() files: any, @Fields() fields: { dictId: number }) {
// 获取第一个文件
const file = files[0];
// 从fields中获取字典ID
const dictId = fields.dictId;
// 调用服务层方法
const result = await this.dictService.importDictItemsFromXLSX(file.data, dictId);
// 返回结果
return successResponse(result)
}
/**
* XLSX模板
*/
@Get('/item/template')
@ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
async downloadDictItemTemplate() {
// 设置下载文件名
this.ctx.set('Content-Disposition', 'attachment; filename=dict-item-template.xlsx');
// 返回模板内容
return this.dictService.getDictItemXLSXTemplate();
}
/**
* XLSX
* @param dictId ID
*/
@Get('/item/export')
@ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
async exportDictItems(@Query('dictId') dictId: number) {
// 设置下载文件名
this.ctx.set('Content-Disposition', 'attachment; filename=dict-items.xlsx');
// 返回导出的 XLSX 文件内容
return this.dictService.exportDictItemsToXLSX(dictId);
}
/**
*
* @param dictId ID ()
*/
@Get('/items')
async getDictItems(
@Query('dictId') dictId?: number,
@Query('name') name?: string,
@Query('title') title?: string,
) {
try {
// 调用服务层方法
const result = await this.dictService.getDictItems({ dictId, name, title });
return successResponse(result, '获取字典项列表成功');
} catch (error) {
return errorResponse(error?.message || '获取字典项列表失败', error?.code || 500);
}
}
/**
*
* @param createDictItemDTO
*/
@Post('/item')
@Validate()
async createDictItem(@Body() createDictItemDTO: CreateDictItemDTO) {
try {
// 调用服务层方法
const result = await this.dictService.createDictItem(createDictItemDTO);
return successResponse(result, '字典项创建成功');
} catch (error) {
return errorResponse(error?.message || '字典项创建失败', error?.code || 500);
}
}
/**
*
* @param id ID
* @param updateDictItemDTO
*/
@Put('/item/:id')
@Validate()
async updateDictItem(@Param('id') id: number, @Body() updateDictItemDTO: UpdateDictItemDTO) {
try {
// 调用服务层方法
const result = await this.dictService.updateDictItem(id, updateDictItemDTO);
return successResponse(result, '字典项更新成功');
} catch (error) {
return errorResponse(error?.message || '字典项更新失败', error?.code || 500);
}
}
/**
*
* @param id ID
*/
@Del('/item/:id')
async deleteDictItem(@Param('id') id: number) {
try {
// 调用服务层方法
const result = await this.dictService.deleteDictItem(id);
return successResponse(result, '字典项删除成功');
} catch (error) {
return errorResponse(error?.message || '字典项删除失败', error?.code || 500);
}
}
/**
*
* @param name
*/
@Get('/items-by-name')
async getDictItemsByDictName(@Query('name') name: string) {
// 调用服务层方法
return this.dictService.getDictItemsByDictName(name);
}
}

View File

@ -0,0 +1,36 @@
import { Controller, Get, Inject, Param } from '@midwayjs/core';
import { DictService } from '../service/dict.service';
/**
*
*/
@Controller('/locales')
export class LocaleController {
@Inject()
dictService: DictService;
/**
*
* @param lang , zh-CN, en-US
* @returns JSON
*/
@Get('/:lang')
async getLocale(@Param('lang') lang: string) {
// 根据语言代码查找对应的字典
const dict = await this.dictService.getDict({ name: lang }, ['items']);
// 如果字典不存在,则返回空对象
if (!dict) {
return {};
}
// 将字典项转换为 key-value 对象
const locale = dict.items.reduce((acc, item) => {
acc[item.name] = item.title;
return acc;
}, {});
return locale;
}
}

View File

@ -0,0 +1,79 @@
import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core';
import { WPService } from '../service/wp.service';
import { successResponse, errorResponse } from '../utils/response.util';
@Controller('/media')
export class MediaController {
@Inject()
wpService: WPService;
@Get('/list')
async list(
@Query('siteId') siteId: number,
@Query('page') page: number = 1,
@Query('pageSize') pageSize: number = 20
) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.getMedia(siteId, page, pageSize);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/upload')
async upload(@Fields() fields, @Files() files) {
try {
const siteId = fields.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
if (!files || files.length === 0) {
return errorResponse('file is required');
}
const file = files[0];
const result = await this.wpService.createMedia(siteId, file);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/update/:id')
async update(@Param('id') id: number, @Body() body) {
try {
const siteId = body.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
// 过滤出需要更新的字段
const { title, caption, description, alt_text } = body;
const data: any = {};
if (title !== undefined) data.title = title;
if (caption !== undefined) data.caption = caption;
if (description !== undefined) data.description = description;
if (alt_text !== undefined) data.alt_text = alt_text;
const result = await this.wpService.updateMedia(siteId, id, data);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Del('/:id')
async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.deleteMedia(siteId, id, force);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
}

View File

@ -9,6 +9,7 @@ import {
Put, Put,
Query, Query,
} from '@midwayjs/core'; } from '@midwayjs/core';
import { SyncOperationResult } from '../dto/api.dto';
import { ApiOkResponse } from '@midwayjs/swagger'; import { ApiOkResponse } from '@midwayjs/swagger';
import { import {
BooleanRes, BooleanRes,
@ -22,6 +23,7 @@ import {
CreateOrderNoteDTO, CreateOrderNoteDTO,
QueryOrderDTO, QueryOrderDTO,
QueryOrderSalesDTO, QueryOrderSalesDTO,
QueryOrderItemDTO,
} from '../dto/order.dto'; } from '../dto/order.dto';
import { User } from '../decorator/user.decorator'; import { User } from '../decorator/user.decorator';
import { ErpOrderStatus } from '../enums/base.enum'; import { ErpOrderStatus } from '../enums/base.enum';
@ -34,11 +36,11 @@ export class OrderController {
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })
@Post('/syncOrder/:siteId') @Post('/sync/:siteId')
async syncOrder(@Param('siteId') siteId: string) { async syncOrders(@Param('siteId') siteId: number, @Body() params: Record<string, any>) {
try { try {
await this.orderService.syncOrders(siteId); const result = await this.orderService.syncOrders(siteId, params);
return successResponse(true); return successResponse(result);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return errorResponse('同步失败'); return errorResponse('同步失败');
@ -46,16 +48,16 @@ export class OrderController {
} }
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: SyncOperationResult,
}) })
@Post('/syncOrder/:siteId/order/:orderId') @Post('/syncOrder/:siteId/order/:orderId')
async syncOrderById( async syncOrderById(
@Param('siteId') siteId: string, @Param('siteId') siteId: number,
@Param('orderId') orderId: string @Param('orderId') orderId: string
) { ) {
try { try {
await this.orderService.syncOrderById(siteId, orderId); const result = await this.orderService.syncOrderById(siteId, orderId);
return successResponse(true); return successResponse(result);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return errorResponse('同步失败'); return errorResponse('同步失败');
@ -97,6 +99,26 @@ export class OrderController {
} }
} }
@ApiOkResponse()
@Get('/getOrderItems')
async getOrderItems(@Query() param: QueryOrderSalesDTO) {
try {
return successResponse(await this.orderService.getOrderItems(param));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse()
@Get('/getOrderItemList')
async getOrderItemList(@Query() param: QueryOrderItemDTO) {
try {
return successResponse(await this.orderService.getOrderItemList(param));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({ @ApiOkResponse({
type: OrderDetailRes, type: OrderDetailRes,
}) })
@ -109,6 +131,16 @@ export class OrderController {
} }
} }
@ApiOkResponse()
@Get('/:orderId/related')
async getRelatedByOrder(@Param('orderId') orderId: number) {
try {
return successResponse(await this.orderService.getRelatedByOrder(orderId));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })
@ -221,4 +253,15 @@ export class OrderController {
return errorResponse(error?.message || '获取失败'); return errorResponse(error?.message || '获取失败');
} }
} }
@ApiOkResponse()
@Post('/export')
async exportOrder(@Body('ids') ids: number[]) {
try {
const csvContent = await this.orderService.exportOrder(ids);
return successResponse(csvContent);
} catch (error) {
return errorResponse(error?.message || '导出失败');
}
}
} }

View File

@ -1,46 +1,37 @@
import { import {
Body,
ContentType,
Controller,
Del,
Files,
Get,
Inject, Inject,
Param,
Post, Post,
Put, Put,
Get,
Body,
Param,
Del,
Query, Query,
Controller,
} from '@midwayjs/core'; } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { ILogger } from '@midwayjs/logger';
import { ApiOkResponse } from '@midwayjs/swagger';
import { UnifiedSearchParamsDTO } from '../dto/api.dto';
import { SyncOperationResultDTO } from '../dto/batch.dto';
import { BatchDeleteProductDTO, BatchUpdateProductDTO, CreateCategoryDTO, CreateProductDTO, ProductWhereFilter, UpdateCategoryDTO, UpdateProductDTO } from '../dto/product.dto';
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
import { BatchSyncProductToSiteDTO, SyncProductToSiteDTO, SyncProductToSiteResultDTO } from '../dto/site-sync.dto';
import { ProductService } from '../service/product.service'; import { ProductService } from '../service/product.service';
import { errorResponse, successResponse } from '../utils/response.util'; import { errorResponse, successResponse } from '../utils/response.util';
import {
BatchSetSkuDTO,
CreateCategoryDTO,
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
QueryCategoryDTO,
QueryFlavorsDTO,
QueryProductDTO,
QueryStrengthDTO,
UpdateCategoryDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
} from '../dto/product.dto';
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
ProductCatListRes,
ProductCatRes,
ProductListRes,
ProductRes,
ProductsRes,
} from '../dto/reponse.dto';
@Controller('/product') @Controller('/product')
export class ProductController { export class ProductController {
@Inject() @Inject()
productService: ProductService; productService: ProductService;
ProductRes;
@Inject()
ctx: Context;
@Inject()
logger: ILogger;
@ApiOkResponse({ @ApiOkResponse({
description: '通过name搜索产品', description: '通过name搜索产品',
@ -77,72 +68,157 @@ export class ProductController {
}) })
@Get('/list') @Get('/list')
async getProductList( async getProductList(
@Query() query: QueryProductDTO @Query() query: UnifiedSearchParamsDTO<ProductWhereFilter>
): Promise<ProductListRes> { ): Promise<ProductListRes> {
const { current = 1, pageSize = 10, name, categoryId } = query;
try { try {
const data = await this.productService.getProductList( const data = await this.productService.getProductList(query);
{ current, pageSize },
name,
categoryId
);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
console.log(error); this.logger.error('获取产品列表失败', error);
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ @ApiOkResponse({ type: ProductRes })
type: ProductRes,
})
@Post('/') @Post('/')
async createProduct(@Body() productData: CreateProductDTO) { async createProduct(@Body() productData: CreateProductDTO) {
try { try {
const data = this.productService.createProduct(productData); const data = await this.productService.createProduct(productData);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ // 导出所有产品 CSV
type: ProductRes, @ApiOkResponse()
}) @Get('/export')
@ContentType('text/csv')
async exportProductsCSV() {
try {
const csv = await this.productService.exportProductsCSV();
// 设置下载文件名(附件形式)
const date = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
const name = `products-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}.csv`;
this.ctx.set('Content-Disposition', `attachment; filename=${name}`);
return csv;
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 导入产品(CSV 文件)
@ApiOkResponse()
@Post('/import')
async importProductsCSV(@Files() files: any) {
try {
// 条件判断:确保存在文件
const file = files?.[0];
if (!file) return errorResponse('未接收到上传文件');
const result = await this.productService.importProductsCSV(file);
return successResponse(result);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: ProductRes })
@Put('/:id') @Put('/:id')
async updateProduct( async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
@Param('id') id: number,
@Body() productData: UpdateProductDTO
) {
try { try {
const data = this.productService.updateProduct(id, productData); const data = await this.productService.updateProduct(id, productData);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ @ApiOkResponse({ type: BooleanRes })
type: ProductRes, @Put('/batch-update')
}) async batchUpdateProduct(@Body() batchUpdateProductDTO: BatchUpdateProductDTO) {
try {
await this.productService.batchUpdateProduct(batchUpdateProductDTO);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Post('/batch-delete')
async batchDeleteProduct(@Body() body: BatchDeleteProductDTO) {
try {
const result = await this.productService.batchDeleteProduct(body.ids);
if (result.failed > 0) {
return errorResponse(`成功删除 ${result.success} 个,失败 ${result.failed} 个。首个错误: ${result.errors[0]}`);
}
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: ProductRes })
@Put('updateNameCn/:id/:nameCn') @Put('updateNameCn/:id/:nameCn')
async updateProductNameCn( async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
@Param('id') id: number,
@Param('nameCn') nameCn: string
) {
try { try {
const data = this.productService.updateProductNameCn(id, nameCn); const data = await this.productService.updatenameCn(id, nameCn);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
// 根据站点SKU查询产品
@ApiOkResponse({ type: ProductRes })
@Get('/site-sku/:siteSku')
async getProductBySiteSku(@Param('siteSku') siteSku: string) {
try {
const product = await this.productService.findProductBySiteSku(siteSku);
return successResponse(product);
} catch (error) {
return errorResponse(error.message || '获取数据失败');
}
}
// 获取产品的站点SKU绑定
@ApiOkResponse()
@Get('/:id/site-skus')
async getProductSiteSkus(@Param('id') id: number) {
try {
const data = await this.productService.getProductSiteSkus(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ // 覆盖式绑定产品的站点SKU列表
type: BooleanRes, @ApiOkResponse()
}) @Post('/:id/site-skus')
async bindProductSiteSkus(@Param('id') id: number, @Body() body: { codes: string[] }) {
try {
const data = await this.productService.bindSiteSkus(id, body?.codes || []);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: ProductRes })
@Get('/:id')
async getProductById(@Param('id') id: number) {
try {
const product = await this.productService.getProductById(id);
return successResponse(product);
} catch (error) {
return errorResponse(error.message || '获取数据失败');
}
}
@ApiOkResponse({ type: BooleanRes })
@Del('/:id') @Del('/:id')
async deleteProduct(@Param('id') id: number) { async deleteProduct(@Param('id') id: number) {
try { try {
@ -153,14 +229,43 @@ export class ProductController {
} }
} }
@ApiOkResponse({ // 获取产品的库存组成
type: ProductCatListRes, @ApiOkResponse()
}) @Get('/:id/components')
@Get('/categories') async getProductComponents(@Param('id') id: number) {
async getCategories(@Query() query: QueryCategoryDTO) {
const { current = 1, pageSize = 10, name } = query;
try { try {
let data = await this.productService.getCategoryList( const data = await this.productService.getProductComponents(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
@ApiOkResponse()
@Post('/:id/components/auto')
async autoBindComponents(@Param('id') id: number) {
try {
const data = await this.productService.autoBindComponentsBySku(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 通用属性接口:分页列表
@ApiOkResponse()
@Get('/attribute')
async getAttributeList(
@Query('dictName') dictName: string,
@Query('current') current = 1,
@Query('pageSize') pageSize = 10,
@Query('name') name?: string
) {
try {
const data = await this.productService.getAttributeList(
dictName,
{ current, pageSize }, { current, pageSize },
name name
); );
@ -170,92 +275,138 @@ export class ProductController {
} }
} }
// 通用属性接口:全部列表
@ApiOkResponse() @ApiOkResponse()
@Get('/categorieAll') @Get('/attributeAll')
async getCategorieAll() { async getAttributeAll(@Query('dictName') dictName: string) {
try { try {
let data = await this.productService.getCategoryAll(); const data = await this.productService.getAttributeAll(dictName);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ // 通用属性接口:创建
type: ProductCatRes, @ApiOkResponse()
}) @Post('/attribute')
@Post('/category') async createAttribute(
async createCategory(@Body() categoryData: CreateCategoryDTO) { @Query('dictName') dictName: string,
try { @Body() body: { title: string; name: string }
const hasCategory = await this.productService.hasCategory(
categoryData.name
);
if (hasCategory) {
return errorResponse('分类已存在');
}
let data = await this.productService.createCategory(categoryData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({
type: ProductCatRes,
})
@Put('/category/:id')
async updateCategory(
@Param('id') id: number,
@Body() categoryData: UpdateCategoryDTO
) { ) {
try { try {
const hasCategory = await this.productService.hasCategory( // 调用 getOrCreateAttribute 方法,如果不存在则创建,如果存在则返回
categoryData.name const data = await this.productService.getOrCreateAttribute(dictName, body.title, body.name);
); return successResponse(data);
if (hasCategory) { } catch (error) {
return errorResponse('分类已存在'); return errorResponse(error?.message || error);
}
}
// 通用属性接口:更新
@ApiOkResponse()
@Put('/attribute/:id')
async updateAttribute(
@Param('id') id: number,
@Query('dictName') dictName: string,
@Body() body: { title?: string; name?: string }
) {
try {
if (body?.name) {
const hasItem = await this.productService.hasAttribute(
dictName,
body.name,
id
);
if (hasItem) return errorResponse('字典项已存在');
} }
const data = this.productService.updateCategory(id, categoryData); const data = await this.productService.updateAttribute(id, body);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ // 通用属性接口:删除
type: BooleanRes, @ApiOkResponse({ type: BooleanRes })
}) @Del('/attribute/:id')
@Del('/category/:id') async deleteAttribute(@Param('id') id: number) {
async deleteCategory(@Param('id') id: number) {
try { try {
const hasProducts = await this.productService.hasProductsInCategory(id); await this.productService.deleteAttribute(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除'); return successResponse(true);
const data = await this.productService.deleteCategory(id);
return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@Post('/batchSetSku') // 兼容旧接口:品牌
@ApiOkResponse({ @ApiOkResponse()
description: '批量设置 sku 的响应结果', @Get('/brandAll')
type: BooleanRes, async compatBrandAll() {
})
async batchSetSku(@Body() body: BatchSetSkuDTO) {
try { try {
const result = await this.productService.batchSetSku(body.skus); const data = await this.productService.getAttributeAll('brand'); // 返回所有品牌字典项
return successResponse(result, '批量设置 sku 成功'); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error.message, 400); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse() @ApiOkResponse()
@Get('/flavorsAll') @Get('/brands')
async getFlavorsAll() { async compatBrands(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try { try {
let data = await this.productService.getFlavorsAll(); const data = await this.productService.getAttributeList('brand', { current, pageSize }, name); // 分页品牌列表
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/brand')
async compatCreateBrand(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
try {
const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
if (has) return errorResponse('品牌已存在');
const data = await this.productService.createAttribute('brand', body); // 创建品牌字典项
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Put('/brand/:id')
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
try {
if (body?.name) {
const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身)
if (has) return errorResponse('品牌已存在');
}
const data = await this.productService.updateAttribute(id, body); // 更新品牌字典项
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Del('/brand/:id')
async compatDeleteBrand(@Param('id') id: number) {
try {
await this.productService.deleteAttribute(id); // 删除品牌字典项
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 兼容旧接口:口味
@ApiOkResponse()
@Get('/flavorsAll')
async compatFlavorsAll() {
try {
const data = await this.productService.getAttributeAll('flavor');
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -264,13 +415,9 @@ export class ProductController {
@ApiOkResponse() @ApiOkResponse()
@Get('/flavors') @Get('/flavors')
async getFlavors(@Query() query: QueryFlavorsDTO) { async compatFlavors(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
const { current = 1, pageSize = 10, name } = query;
try { try {
let data = await this.productService.getFlavorsList( const data = await this.productService.getAttributeList('flavor', { current, pageSize }, name);
{ current, pageSize },
name
);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -279,13 +426,11 @@ export class ProductController {
@ApiOkResponse() @ApiOkResponse()
@Post('/flavors') @Post('/flavors')
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) { async compatCreateFlavors(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
try { try {
const hasFlavors = await this.productService.hasFlavors(flavorsData.name); const has = await this.productService.hasAttribute('flavor', body.name);
if (hasFlavors) { if (has) return errorResponse('口味已存在');
return errorResponse('分类已存在'); const data = await this.productService.createAttribute('flavor', body);
}
let data = await this.productService.createFlavors(flavorsData);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -294,42 +439,36 @@ export class ProductController {
@ApiOkResponse() @ApiOkResponse()
@Put('/flavors/:id') @Put('/flavors/:id')
async updateFlavors( async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
@Param('id') id: number,
@Body() flavorsData: UpdateFlavorsDTO
) {
try { try {
const hasFlavors = await this.productService.hasFlavors(flavorsData.name); if (body?.name) {
if (hasFlavors) { const has = await this.productService.hasAttribute('flavor', body.name, id);
return errorResponse('分类已存在'); if (has) return errorResponse('口味已存在');
} }
const data = this.productService.updateFlavors(id, flavorsData); const data = await this.productService.updateAttribute(id, body);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ @ApiOkResponse({ type: BooleanRes })
type: BooleanRes,
})
@Del('/flavors/:id') @Del('/flavors/:id')
async deleteFlavors(@Param('id') id: number) { async compatDeleteFlavors(@Param('id') id: number) {
try { try {
const hasProducts = await this.productService.hasProductsInFlavors(id); await this.productService.deleteAttribute(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除'); return successResponse(true);
const data = await this.productService.deleteFlavors(id);
return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
// 兼容旧接口:规格
@ApiOkResponse() @ApiOkResponse()
@Get('/strengthAll') @Get('/strengthAll')
async getStrengthAll() { async compatStrengthAll() {
try { try {
let data = await this.productService.getStrengthAll(); const data = await this.productService.getAttributeAll('strength');
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -338,13 +477,9 @@ export class ProductController {
@ApiOkResponse() @ApiOkResponse()
@Get('/strength') @Get('/strength')
async getStrength(@Query() query: QueryStrengthDTO) { async compatStrength(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
const { current = 1, pageSize = 10, name } = query;
try { try {
let data = await this.productService.getStrengthList( const data = await this.productService.getAttributeList('strength', { current, pageSize }, name);
{ current, pageSize },
name
);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -353,15 +488,11 @@ export class ProductController {
@ApiOkResponse() @ApiOkResponse()
@Post('/strength') @Post('/strength')
async createStrength(@Body() strengthData: CreateStrengthDTO) { async compatCreateStrength(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
try { try {
const hasStrength = await this.productService.hasStrength( const has = await this.productService.hasAttribute('strength', body.name);
strengthData.name if (has) return errorResponse('规格已存在');
); const data = await this.productService.createAttribute('strength', body);
if (hasStrength) {
return errorResponse('分类已存在');
}
let data = await this.productService.createStrength(strengthData);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
@ -370,36 +501,252 @@ export class ProductController {
@ApiOkResponse() @ApiOkResponse()
@Put('/strength/:id') @Put('/strength/:id')
async updateStrength( async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
@Param('id') id: number,
@Body() strengthData: UpdateStrengthDTO
) {
try { try {
const hasStrength = await this.productService.hasStrength( if (body?.name) {
strengthData.name const has = await this.productService.hasAttribute('strength', body.name, id);
); if (has) return errorResponse('规格已存在');
if (hasStrength) {
return errorResponse('分类已存在');
} }
const data = this.productService.updateStrength(id, strengthData); const data = await this.productService.updateAttribute(id, body);
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse({ @ApiOkResponse({ type: BooleanRes })
type: BooleanRes,
})
@Del('/strength/:id') @Del('/strength/:id')
async deleteStrength(@Param('id') id: number) { async compatDeleteStrength(@Param('id') id: number) {
try { try {
const hasProducts = await this.productService.hasProductsInStrength(id); await this.productService.deleteAttribute(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除'); return successResponse(true);
const data = await this.productService.deleteStrength(id); } catch (error) {
return errorResponse(error?.message || error);
}
}
// 兼容旧接口:尺寸
@ApiOkResponse()
@Get('/sizeAll')
async compatSizeAll() {
try {
const data = await this.productService.getAttributeAll('size');
return successResponse(data); return successResponse(data);
} catch (error) { } catch (error) {
return errorResponse(error?.message || error); return errorResponse(error?.message || error);
} }
} }
@ApiOkResponse()
@Get('/size')
async compatSize(@Query('current') current = 1, @Query('pageSize') pageSize = 10, @Query('name') name?: string) {
try {
const data = await this.productService.getAttributeList('size', { current, pageSize }, name);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Post('/size')
async compatCreateSize(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
try {
const has = await this.productService.hasAttribute('size', body.name);
if (has) return errorResponse('尺寸已存在');
const data = await this.productService.createAttribute('size', body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse()
@Put('/size/:id')
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
try {
if (body?.name) {
const has = await this.productService.hasAttribute('size', body.name, id);
if (has) return errorResponse('尺寸已存在');
}
const data = await this.productService.updateAttribute(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
@ApiOkResponse({ type: BooleanRes })
@Del('/size/:id')
async compatDeleteSize(@Param('id') id: number) {
try {
await this.productService.deleteAttribute(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 获取所有分类
@ApiOkResponse({ description: '获取所有分类' })
@Get('/categories/all')
async getCategoriesAll() {
try {
const data = await this.productService.getCategoriesAll();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 获取分类下的属性配置
@ApiOkResponse({ description: '获取分类下的属性配置' })
@Get('/category/:id/attributes')
async getCategoryAttributes(@Param('id') id: number) {
try {
const data = await this.productService.getCategoryAttributes(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 创建分类
@ApiOkResponse({ description: '创建分类' })
@Post('/category')
async createCategory(@Body() body: CreateCategoryDTO) {
try {
const data = await this.productService.createCategory(body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 更新分类
@ApiOkResponse({ description: '更新分类' })
@Put('/category/:id')
async updateCategory(@Param('id') id: number, @Body() body: UpdateCategoryDTO) {
try {
const data = await this.productService.updateCategory(id, body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 删除分类
@ApiOkResponse({ description: '删除分类' })
@Del('/category/:id')
async deleteCategory(@Param('id') id: number) {
try {
await this.productService.deleteCategory(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 创建分类属性
@ApiOkResponse({ description: '创建分类属性' })
@Post('/category/attribute')
async createCategoryAttribute(@Body() body: { categoryId: number; dictId: number }) {
try {
const data = await this.productService.createCategoryAttribute(body);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 删除分类属性
@ApiOkResponse({ description: '删除分类属性' })
@Del('/category/attribute/:id')
async deleteCategoryAttribute(@Param('id') id: number) {
try {
await this.productService.deleteCategoryAttribute(id);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 同步库存 SKU 到产品单品
@ApiOkResponse({ description: '同步库存 SKU 到产品单品' })
@Post('/sync-stock')
async syncStockToProduct() {
try {
const data = await this.productService.syncStockToProduct();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 同步单个产品到站点
@ApiOkResponse({ description: '同步单个产品到站点', type: SyncProductToSiteResultDTO })
@Post('/sync-to-site')
async syncToSite(@Body() body: SyncProductToSiteDTO) {
try {
const result = await this.productService.syncToSite(body);
return successResponse(result);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 从站点同步产品到本地
@ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes })
@Post('/sync-from-site')
async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number }) {
try {
const { siteId, siteProductId } = body;
const product = await this.productService.syncProductFromSite(siteId, siteProductId);
return successResponse(product);
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 批量从站点同步产品到本地
@ApiOkResponse({ description: '批量从站点同步产品到本地', type: SyncOperationResultDTO })
@Post('/batch-sync-from-site')
async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) {
try {
const { siteId, siteProductIds } = body;
const result = await this.productService.batchSyncFromSite(siteId, siteProductIds);
// 将服务层返回的结果转换为统一格式
const errors = result.errors.map((error: string) => {
// 提取产品ID部分作为标识符
const match = error.match(/站点产品ID (\d+) /);
const identifier = match ? match[1] : 'unknown';
return {
identifier: identifier,
error: error
};
});
return successResponse({
total: siteProductIds.length,
processed: result.synced + errors.length,
synced: result.synced,
errors: errors
});
} catch (error) {
return errorResponse(error?.message || error);
}
}
// 批量同步产品到站点
@ApiOkResponse({ description: '批量同步产品到站点', type: SyncOperationResultDTO })
@Post('/batch-sync-to-site')
async batchSyncToSite(@Body() body: BatchSyncProductToSiteDTO) {
try {
const { siteId, data } = body;
const result = await this.productService.batchSyncToSite(siteId, data);
return successResponse(result);
} catch (error) {
return errorResponse(error?.message || error);
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,76 @@
import { Config, Controller, Get } from '@midwayjs/core'; import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core';
import { ApiOkResponse } from '@midwayjs/swagger'; import { ApiOkResponse } from '@midwayjs/swagger';
import { WpSitesResponse } from '../dto/reponse.dto'; import { SitesResponse } from '../dto/reponse.dto';
import { successResponse } from '../utils/response.util'; import { errorResponse, successResponse } from '../utils/response.util';
import { WpSite } from '../interface'; import { SiteService } from '../service/site.service';
import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto';
@Controller('/site') @Controller('/site')
export class SiteController { export class SiteController {
@Config('wpSite') @Inject()
sites: WpSite[]; siteService: SiteService;
@ApiOkResponse({ @ApiOkResponse({ description: '关联网站', type: SitesResponse })
description: '关联网站',
type: WpSitesResponse,
})
@Get('/all') @Get('/all')
async all() { async all() {
return successResponse( try {
this.sites.map(v => ({ const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
id: v.id, // 返回完整的站点对象,包括 skuPrefix 等字段
siteName: v.siteName, return successResponse(items);
})) } catch (error) {
); return errorResponse(error?.message || '获取失败');
}
}
@Post('/create')
async create(@Body() body: CreateSiteDTO) {
try {
await this.siteService.create(body);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@Put('/update/:id')
async update(@Param('id') id: string, @Body() body: UpdateSiteDTO) {
try {
await this.siteService.update(Number(id), body);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@Get('/get/:id')
async get(@Param('id') id: string) {
try {
const data = await this.siteService.get(Number(id), false);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@Get('/list')
async list(@Query() query: QuerySiteDTO) {
try {
const data = await this.siteService.list(query, false);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
// 批量查询改为使用 /site/list?ids=1,2,3
@Put('/disable/:id')
async disable(@Param('id') id: string, @Body() body: DisableSiteDTO) {
try {
await this.siteService.disable(Number(id), body.disabled);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
} }
} }

View File

@ -176,9 +176,21 @@ export class StockController {
} }
} }
// 检查某个 SKU 是否有库存(任一仓库数量大于 0)
@ApiOkResponse({ type: BooleanRes })
@Get('/has/:sku')
async hasStock(@Param('sku') sku: string) {
try {
const data = await this.stockService.hasStockBySku(sku);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '查询失败');
}
}
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
description: '更新库存(入库、出库、调整)', description: '更新库存(入库,出库,调整)',
}) })
@Post('/update') @Post('/update')
async updateStock(@Body() body: UpdateStockDTO) { async updateStock(@Body() body: UpdateStockDTO) {

View File

@ -0,0 +1,36 @@
import { Controller, Inject, Param, Post, Get, Query } from '@midwayjs/core';
import { ApiOkResponse } from '@midwayjs/swagger';
import { SubscriptionService } from '../service/subscription.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { BooleanRes, SubscriptionListRes } from '../dto/reponse.dto';
import { QuerySubscriptionDTO } from '../dto/subscription.dto';
@Controller('/subscription')
export class SubscriptionController {
@Inject()
subscriptionService: SubscriptionService;
// 同步订阅:根据站点 ID 拉取并更新本地订阅数据
@ApiOkResponse({ type: BooleanRes })
@Post('/sync/:siteId')
async sync(@Param('siteId') siteId: number) {
try {
const result = await this.subscriptionService.syncSubscriptions(siteId);
return successResponse(result);
} catch (error) {
return errorResponse(error?.message || '同步失败');
}
}
// 订阅列表:分页 + 筛选
@ApiOkResponse({ type: SubscriptionListRes })
@Get('/list')
async list(@Query() query: QuerySubscriptionDTO) {
try {
const data = await this.subscriptionService.getSubscriptionList(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
}

View File

@ -0,0 +1,168 @@
import { Inject, Controller, Get, Post, Put, Del, Body, Param, Query } from '@midwayjs/core';
import { TemplateService } from '../service/template.service';
import { successResponse, errorResponse } from '../utils/response.util';
import { CreateTemplateDTO, UpdateTemplateDTO, RenderTemplateDTO } from '../dto/template.dto';
import { ApiOkResponse, ApiTags } from '@midwayjs/swagger';
import { Template } from '../entity/template.entity';
import { BooleanRes } from '../dto/reponse.dto';
/**
* @controller TemplateController
*/
@ApiTags('Template')
@Controller('/template')
export class TemplateController {
@Inject()
templateService: TemplateService;
/**
* @summary
* @description
*/
@ApiOkResponse({ type: [Template], description: '成功获取模板列表' })
@Get('/list')
async getTemplateList(@Query() params: any) {
// 调用服务层获取列表
return this.templateService.getTemplateList(params);
}
/**
* @summary
* @description
* @param name
*/
@ApiOkResponse({ type: Template, description: '成功获取模板' })
@Get('/:name')
async getTemplateByName(@Param('name') name: string) {
try {
// 调用服务层获取单个模板
const data = await this.templateService.getTemplateByName(name);
// 返回成功响应
return successResponse(data);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
/**
* @summary
* @description ,
* @param templateData
*/
@ApiOkResponse({ type: Template, description: '成功创建模板' })
@Post('/')
async createTemplate(@Body() templateData: CreateTemplateDTO) {
try {
// 调用服务层创建模板
const data = await this.templateService.createTemplate(templateData);
// 返回成功响应
return successResponse(data);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
/**
* @summary
* @description ID
* @param id ID
* @param templateData
*/
@ApiOkResponse({ type: Template, description: '成功更新模板' })
@Put('/:id')
async updateTemplate(
@Param('id') id: number,
@Body() templateData: UpdateTemplateDTO
) {
try {
// 调用服务层更新模板
const data = await this.templateService.updateTemplate(id, templateData);
// 返回成功响应
return successResponse(data);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
/**
* @summary
* @description ID
* @param id ID
*/
@ApiOkResponse({ type: BooleanRes, description: '成功删除模板' })
@Del('/:id')
async deleteTemplate(@Param('id') id: number) {
try {
// 调用服务层删除模板
const data = await this.templateService.deleteTemplate(id);
// 返回成功响应
return successResponse(data);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
/**
* @summary
* @description
* @param name
* @param data
*/
@ApiOkResponse({ type: String, description: '成功渲染模板' })
@Post('/render/:name')
async renderTemplate(
@Param('name') name: string,
@Body() data: Record<string, any>
) {
try {
// 调用服务层渲染模板
const renderedString = await this.templateService.render(name, data);
// 返回成功响应
return successResponse(renderedString);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
/**
* @summary
* @description , testData
*/
@ApiOkResponse({ type: Number, description: '成功回填的数量' })
@Post('/backfill-testdata')
async backfillTestData() {
try {
const count = await this.templateService.backfillMissingTestData();
return successResponse({ updated: count });
} catch (error) {
return errorResponse(error.message);
}
}
/**
* @summary
* @description ,
* @param renderData
*/
@ApiOkResponse({ type: String, description: '成功渲染模板' })
@Post('/render-direct')
async renderTemplateDirect(@Body() renderData: RenderTemplateDTO) {
try {
// 调用服务层渲染模板内容
const renderedString = await this.templateService.renderWithTemplate(
renderData.template,
renderData.data
);
// 返回成功响应
return successResponse(renderedString);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
}

View File

@ -34,16 +34,17 @@ export class UserController {
}) })
@Post('/logout') @Post('/logout')
async logout() { async logout() {
// 可选在这里处理服务端缓存的 token 或 session // 可选:在这里处理服务端缓存的 token 或 session
return successResponse(true); return successResponse(true);
} }
@Post('/add') @Post('/add')
async addUser(@Body() body: { username: string; password: string }) { async addUser(@Body() body: { username: string; password: string; email?: string; remark?: string }) {
const { username, password } = body; const { username, password, email, remark } = body;
try { try {
await this.userService.addUser(username, password); // 新增用户 支持邮箱与备注
await this.userService.addUser(username, password, remark, email);
return successResponse(true); return successResponse(true);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -52,21 +53,91 @@ export class UserController {
} }
@Get('/list') @Get('/list')
async listUsers(@Query() query: { current: number; pageSize: number }) { async listUsers(
const { current = 1, pageSize = 10 } = query; @Query()
return successResponse(await this.userService.listUsers(current, pageSize)); query: {
current: number;
pageSize: number;
remark?: string;
username?: string;
email?: string;
isActive?: string;
isSuper?: string;
isAdmin?: string;
sortField?: string;
sortOrder?: string;
}
) {
const { current = 1, pageSize = 10, remark, username, email, isActive, isSuper, isAdmin, sortField, sortOrder } = query;
// 将字符串布尔转换为真实布尔
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
// 处理排序方向
const order = (sortOrder === 'ascend' || sortOrder === 'ASC') ? 'ASC' : 'DESC';
// 列表移除密码字段
const { items, total } = await this.userService.listUsers(
current,
pageSize,
{
remark,
username,
email,
isActive: toBool(isActive),
isSuper: toBool(isSuper),
isAdmin: toBool(isAdmin),
},
{
field: sortField,
order,
}
);
const safeItems = (items || []).map((it: any) => {
const { password, ...rest } = it || {};
return rest;
});
return successResponse({ items: safeItems, total, current, pageSize });
} }
@Post('/toggleActive') @Post('/toggleActive')
async toggleActive(@Body() body: { userId: number; isActive: boolean }) { async toggleActive(@Body() body: { userId: number; isActive: boolean }) {
return this.userService.toggleUserActive(body.userId, body.isActive); try {
// 调用服务层更新启用状态
const data = await this.userService.toggleUserActive(body.userId, body.isActive);
// 移除密码字段,保证安全
const { password, ...safe } = data as any;
return successResponse(safe);
} catch (error) {
return errorResponse(error?.message || '操作失败');
}
}
// 更新用户(支持用户名/密码/权限/角色更新)
@Post('/update/:id')
async updateUser(
@Body() body: { username?: string; password?: string; email?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
@Query('id') id?: number
) {
try {
// 条件判断:优先从路径参数获取 ID(兼容生成的 API 文件为 POST /user/update/:id)
const userId = Number((this.ctx?.params?.id ?? id));
if (!userId) throw new Error('缺少用户ID');
const data = await this.userService.updateUser(userId, body);
// 移除密码字段,保证安全
const { password, ...safe } = data as any;
return successResponse(safe);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
} }
@ApiOkResponse() @ApiOkResponse()
@Get() @Get()
async getUser(@User() user) { async getUser(@User() user) {
try { try {
return successResponse(await this.userService.getUser(user.id)); // 详情移除密码字段
const data = await this.userService.getUser(user.id);
const { password, ...safe } = (data as any) || {};
return successResponse(safe);
} catch (error) { } catch (error) {
return errorResponse('获取失败'); return errorResponse('获取失败');
} }

View File

@ -1,4 +1,4 @@
import { Config, HttpStatus, Inject } from '@midwayjs/core'; import { HttpStatus, ILogger, Inject, Logger } from '@midwayjs/core';
import { import {
Controller, Controller,
Post, Post,
@ -9,20 +9,19 @@ import {
} from '@midwayjs/decorator'; } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa'; import { Context } from '@midwayjs/koa';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { WpProductService } from '../service/wp_product.service';
import { WPService } from '../service/wp.service'; import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service'; import { OrderService } from '../service/order.service';
import { WpSite } from '../interface';
import {
UnifiedOrderDTO,
} from '../dto/site-api.dto';
@Controller('/webhook') @Controller('/webhook')
export class WebhookController { export class WebhookController {
private secret = 'YOONE24kd$kjcdjflddd'; private secret = 'YOONE24kd$kjcdjflddd';
@Inject() // 平台服务保留按需注入
private readonly wpProductService: WpProductService;
@Inject()
private readonly wpApiService: WPService;
@Inject() @Inject()
private readonly orderService: OrderService; private readonly orderService: OrderService;
@ -30,26 +29,35 @@ export class WebhookController {
@Inject() @Inject()
ctx: Context; ctx: Context;
@Config('wpSite') @Logger()
sites: WpSite[]; logger: ILogger;
@Inject()
private readonly siteService: SiteService;
// 移除配置中的站点数组,来源统一改为数据库
@Get('/') @Get('/')
async test() { async test() {
return 'webhook'; return 'webhook';
} }
// TODO 这里得检查一下是否对 SHOPYY有效,否则得另外书写
@Post('/woocommerce') @Post('/woocommerce')
async handleWooWebhook( async handleWooWebhook(
@Body() body: any, @Body() body: any,
@Query('siteId') siteId: string, @Query('siteId') siteIdStr: string,
@Headers() header: any @Headers() header: any
) { ) {
console.log(`webhook woocommerce`, siteIdStr, body,header)
const signature = header['x-wc-webhook-signature']; const signature = header['x-wc-webhook-signature'];
const topic = header['x-wc-webhook-topic']; const topic = header['x-wc-webhook-topic'];
const source = header['x-wc-webhook-source']; const source = header['x-wc-webhook-source'];
let site = this.sites.find(item => item.id === siteId); const siteId = Number(siteIdStr);
// 从数据库获取站点配置
const site = await this.siteService.get(siteId, true);
if (!site || !source.includes(site.wpApiUrl)) { if (!site || !source?.includes(site.apiUrl)) {
console.log('domain not match'); console.log('domain not match');
return { return {
code: HttpStatus.BAD_REQUEST, code: HttpStatus.BAD_REQUEST,
@ -75,32 +83,10 @@ export class WebhookController {
switch (topic) { switch (topic) {
case 'product.created': case 'product.created':
case 'product.updated': case 'product.updated':
// 变体更新 // 不再写入本地,平台事件仅确认接收
if (body.type === 'variation') {
const variation = await this.wpApiService.getVariation(
site,
body.parent_id,
body.id
);
this.wpProductService.syncVariation(
siteId,
body.parent_id,
variation
);
break;
}
const variations =
body.type === 'variable'
? await this.wpApiService.getVariations(site, body.id)
: [];
await this.wpProductService.syncProductAndVariations(
site.id,
body,
variations
);
break; break;
case 'product.deleted': case 'product.deleted':
await this.wpProductService.delWpProduct(site.id, body.id); // 不再写入本地,平台事件仅确认接收
break; break;
case 'order.created': case 'order.created':
case 'order.updated': case 'order.updated':
@ -134,4 +120,88 @@ export class WebhookController {
console.log(error); console.log(error);
} }
} }
@Post('/shoppy')
async handleShoppyWebhook(
@Body() body: any,
@Query('siteId') siteIdStr: string,
@Query('signature') signature: string,
@Headers() header: any
) {
const topic = header['x-oemsaas-event-type'];
// const source = header['x-oemsaas-shop-domain'];
const siteId = Number(siteIdStr);
const bodys = new UnifiedOrderDTO();
Object.assign(bodys, body);
// 从数据库获取站点配置
const site = await this.siteService.get(siteId, true);
// if (!site || !source?.includes(site.websiteUrl)) {
if (!site) {
console.log('domain not match');
return {
code: HttpStatus.BAD_REQUEST,
success: false,
message: 'domain not match',
};
}
if (!signature) {
return {
code: HttpStatus.BAD_REQUEST,
success: false,
message: 'Signature missing',
};
}
//shopyy 无法提供加密字段校验,注释校验逻辑
// const rawBody = this.ctx.request.rawBody;
// const hash = crypto
// .createHmac('sha256', this.secret)
// .update(rawBody)
// .digest('base64');
try {
if (this.secret === signature) {
switch (topic) {
case 'product.created':
case 'product.updated':
// 不再写入本地,平台事件仅确认接收
break;
case 'product.deleted':
// 不再写入本地,平台事件仅确认接收
break;
case 'orders/create':
case 'orders/update':
await this.orderService.syncSingleOrder(siteId, bodys);
break;
case 'orders/delete':
break;
case 'customer.created':
break;
case 'customer.updated':
break;
case 'customer.deleted':
break;
default:
console.log('Unhandled event:', topic);
}
return {
code: 200,
success: true,
message: 'Webhook processed successfully',
};
} else {
return {
code: 403,
success: false,
message: 'Webhook verification failed',
};
}
} catch (error) {
console.log(error);
}
}
} }

View File

@ -1,207 +0,0 @@
import {
Controller,
Param,
Post,
Inject,
Get,
Query,
Put,
Body,
Config,
} from '@midwayjs/core';
import { WpProductService } from '../service/wp_product.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes, WpProductListRes } from '../dto/reponse.dto';
import {
QueryWpProductDTO,
SetConstitutionDTO,
UpdateVariationDTO,
UpdateWpProductDTO,
} from '../dto/wp_product.dto';
import { WPService } from '../service/wp.service';
import { WpSite } from '../interface';
import {
ProductsRes,
} from '../dto/reponse.dto';
@Controller('/wp_product')
export class WpProductController {
@Inject()
wpService: WPService;
@Config('wpSite')
sites: WpSite[];
getSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
@Inject()
private readonly wpProductService: WpProductService;
@Inject()
private readonly wpApiService: WPService;
@ApiOkResponse({
type: BooleanRes,
})
@Post('/sync/:siteId')
async syncProducts(@Param('siteId') siteId: string) {
try {
await this.wpProductService.syncSite(siteId);
return successResponse(true);
} catch (error) {
console.log(error);
return errorResponse('同步失败');
}
}
@ApiOkResponse({
type: WpProductListRes,
})
@Get('/list')
async getWpProducts(@Query() query: QueryWpProductDTO) {
try {
const data = await this.wpProductService.getProductList(query);
return successResponse(data);
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOkResponse({
type: BooleanRes,
})
@Put('/:id/constitution')
async setConstitution(
@Param('id') id: number,
@Body()
body: SetConstitutionDTO
) {
const { isProduct, constitution } = body;
try {
await this.wpProductService.setConstitution(id, isProduct, constitution);
return successResponse(true);
} catch (error) {
return errorResponse(error.message);
}
}
@ApiOkResponse({
type: BooleanRes
})
@Post('/updateState/:id')
async updateWPProductState(
@Param('id') id: number,
@Body() body: any, // todo
) {
try {
const res = await this.wpProductService.updateProductStatus(id, body?.status, body?.stock_status);
return successResponse(res);
} catch (error) {
return errorResponse(error.message);
}
}
/**
*
* @param productId ID
* @param body
*/
@ApiOkResponse({
type: BooleanRes,
})
@Put('/siteId/:siteId/products/:productId')
async updateProduct(
@Param('siteId') siteId: string,
@Param('productId') productId: string,
@Body() body: UpdateWpProductDTO
) {
try {
const isDuplicate = await this.wpProductService.isSkuDuplicate(
body.sku,
siteId,
productId
);
if (isDuplicate) {
return errorResponse('SKU已存在');
}
const site = await this.wpProductService.getSite(siteId);
const result = await this.wpApiService.updateProduct(
site,
productId,
body
);
if (result) {
this.wpProductService.updateWpProduct(siteId, productId, body);
return successResponse(result, '产品更新成功');
}
return errorResponse('产品更新失败');
} catch (error) {
console.error('更新产品失败:', error);
return errorResponse(error.message || '产品更新失败');
}
}
/**
*
* @param productId ID
* @param variationId ID
* @param body
*/
@Put('/siteId/:siteId/products/:productId/variations/:variationId')
async updateVariation(
@Param('siteId') siteId: string,
@Param('productId') productId: string,
@Param('variationId') variationId: string,
@Body() body: UpdateVariationDTO
) {
try {
const isDuplicate = await this.wpProductService.isSkuDuplicate(
body.sku,
siteId,
productId,
variationId
);
if (isDuplicate) {
return errorResponse('SKU已存在');
}
const site = await this.wpProductService.getSite(siteId);
const result = await this.wpApiService.updateVariation(
site,
productId,
variationId,
body
);
if (result) {
this.wpProductService.updateWpProductVaritation(
siteId,
productId,
variationId,
body
);
return successResponse(result, '产品变体更新成功');
}
return errorResponse('变体更新失败');
} catch (error) {
console.error('更新变体失败:', error);
return errorResponse(error.message || '产品变体更新失败');
}
}
@ApiOkResponse({
description: '通过name搜索产品/订单',
type: ProductsRes,
})
@Get('/search')
async searchProducts(@Query('name') name: string) {
try {
// 调用服务获取产品数据
const products = await this.wpProductService.findProductsByName(name);
return successResponse(products);
} catch (error) {
return errorResponse(error.message || '获取数据失败');
}
}
}

20
src/db/datasource.ts Normal file
View File

@ -0,0 +1,20 @@
import { DataSource, DataSourceOptions } from 'typeorm';
import { SeederOptions } from 'typeorm-extension';
const options: DataSourceOptions & SeederOptions = {
type: 'mysql',
host: '127.0.0.1',
port: 23306,
username: 'root',
password: '12345678',
database: 'inventory',
synchronize: true,
logging: true,
entities: [__dirname + '/../entity/*.ts'],
migrations: ['src/db/migrations/**/*.ts'],
seeds: ['src/db/seeds/**/*.ts'],
};
export const AppDataSource = new DataSource(options);

View File

@ -0,0 +1,34 @@
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { Area } from '../../entity/area.entity';
export default class AreaSeeder implements Seeder {
public async run(
dataSource: DataSource,
factoryManager: SeederFactoryManager
): Promise<any> {
const areaRepository = dataSource.getRepository(Area);
const areas = [
{ name: 'Australia', code: 'AU' },
{ name: 'Canada', code: 'CA' },
{ name: 'United States', code: 'US' },
{ name: 'Germany', code: 'DE' },
{ name: 'Poland', code: 'PL' },
];
for (const areaData of areas) {
const existingArea = await areaRepository.findOne({
where: [
{ name: areaData.name },
{ code: areaData.code }
]
});
if (!existingArea) {
const newArea = areaRepository.create(areaData);
await areaRepository.save(newArea);
}
}
}
}

View File

@ -0,0 +1,39 @@
import { Seeder } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { Category } from '../../entity/category.entity';
export default class CategorySeeder implements Seeder {
public async run(
dataSource: DataSource,
): Promise<any> {
const repository = dataSource.getRepository(Category);
const categories = [
{
name: 'nicotine-pouches',
title: 'Nicotine Pouches',
titleCN: '尼古丁袋',
sort: 1
},
{
name: 'vape',
title: 'vape',
titleCN: '电子烟',
sort: 2
},
{
name: 'pouches-can',
title: 'Pouches Can',
titleCN: '口含烟盒',
sort: 3
},
];
for (const cat of categories) {
const existing = await repository.findOne({ where: { name: cat.name } });
if (!existing) {
await repository.save(cat);
}
}
}
}

View File

@ -0,0 +1,62 @@
import { Seeder } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { Dict } from '../../entity/dict.entity';
import { Category } from '../../entity/category.entity';
import { CategoryAttribute } from '../../entity/category_attribute.entity';
export default class CategoryAttributeSeeder implements Seeder {
public async run(
dataSource: DataSource,
): Promise<any> {
const dictRepository = dataSource.getRepository(Dict);
const categoryRepository = dataSource.getRepository(Category);
const categoryAttributeRepository = dataSource.getRepository(CategoryAttribute);
// 1. 确保属性字典存在
const attributeNames = ['brand', 'strength', 'flavor', 'size', 'humidity'];
const attributeDicts: Dict[] = [];
for (const name of attributeNames) {
let dict = await dictRepository.findOne({ where: { name } });
if (!dict) {
dict = new Dict();
dict.name = name;
dict.title = name.charAt(0).toUpperCase() + name.slice(1);
dict.deletable = false;
dict = await dictRepository.save(dict);
console.log(`Created Dict: ${name}`);
}
attributeDicts.push(dict);
}
// 2. 获取 'nicotine-pouches' 分类 (由 CategorySeeder 创建)
const nicotinePouchesCategory = await categoryRepository.findOne({
where: {
name: 'nicotine-pouches'
}
});
if (!nicotinePouchesCategory) {
console.warn('Category "nicotine-pouches" not found. Skipping attribute linking. Please ensure CategorySeeder runs first.');
return;
}
// 3. 绑定属性到 'nicotine-pouches' 分类
for (const attrDict of attributeDicts) {
const existing = await categoryAttributeRepository.findOne({
where: {
category: { id: nicotinePouchesCategory.id },
attributeDict: { id: attrDict.id }
}
});
if (!existing) {
const link = new CategoryAttribute();
link.category = nicotinePouchesCategory;
link.attributeDict = attrDict;
await categoryAttributeRepository.save(link);
console.log(`Linked ${attrDict.name} to ${nicotinePouchesCategory.name}`);
}
}
}
}

878
src/db/seeds/dict.seeder.ts Normal file
View File

@ -0,0 +1,878 @@
import { Seeder } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { Dict } from '../../entity/dict.entity';
import { DictItem } from '../../entity/dict_item.entity';
export default class DictSeeder implements Seeder {
/**
* kebab-case
* @param name
* @returns
*/
private formatName(name: string): string {
// 只替换空格和下划线
return String(name).replace(/[\_\s]+/g, '-').toLowerCase();
}
public async run(
dataSource: DataSource,
): Promise<any> {
const dictRepository = dataSource.getRepository(Dict);
const dictItemRepository = dataSource.getRepository(DictItem);
// 初始化语言字典
const locales = [
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文', shortName: 'CN' },
{ name: 'en-us', title: 'English', titleCn: '英文', shortName: 'EN' },
];
for (const locale of locales) {
await this.createOrFindDict(dictRepository, locale);
}
// 添加示例翻译条目
const zhDict = await dictRepository.findOne({ where: { name: 'zh-cn' } });
const enDict = await dictRepository.findOne({ where: { name: 'en-us' } });
const translations = [
{ name: 'common-save', zh: '保存', en: 'Save' },
{ name: 'common-cancel', zh: '取消', en: 'Cancel' },
{ name: 'common-success', zh: '操作成功', en: 'Success' },
{ name: 'common-failure', zh: '操作失败', en: 'Failure' },
];
for (const t of translations) {
// 添加中文翻译
let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
if (!item) {
await dictItemRepository.save({ name: t.name, title: t.zh, titleCn: t.zh, shortName: t.zh.substring(0, 2).toUpperCase(), dict: zhDict });
}
// 添加英文翻译
item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
if (!item) {
await dictItemRepository.save({ name: t.name, title: t.en, titleCn: t.en, shortName: t.en.substring(0, 2).toUpperCase(), dict: enDict });
}
}
const brandDict = await this.createOrFindDict(dictRepository, { name: 'brand', title: '品牌', titleCn: '品牌', shortName: 'BR' });
const flavorDict = await this.createOrFindDict(dictRepository, { name: 'flavor', title: '口味', titleCn: '口味', shortName: 'FL' });
const strengthDict = await this.createOrFindDict(dictRepository, { name: 'strength', title: '强度', titleCn: '强度', shortName: 'ST' });
// 遍历品牌数据
await this.seedDictItems(dictItemRepository, brandDict, brandsData);
// 遍历口味数据
await this.seedDictItems(dictItemRepository, flavorDict, flavorsData);
// 遍历强度数据
await this.seedDictItems(dictItemRepository, strengthDict, strengthsData);
}
/**
*
* @param repo DictRepository
* @param dictInfo
* @returns Dict
*/
private async createOrFindDict(repo: any, dictInfo: { name: string; title: string; titleCn: string; shortName: string }): Promise<Dict> {
// 格式化 name
const formattedName = this.formatName(dictInfo.name);
let dict = await repo.findOne({ where: { name: formattedName } });
if (!dict) {
// 如果字典不存在,则使用格式化后的 name 创建新字典
dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn, shortName: dictInfo.shortName });
}
return dict;
}
/**
*
* @param repo DictItemRepository
* @param dict
* @param items
*/
private async seedDictItems(repo: any, dict: Dict, items: { name: string; title: string; titleCn: string; shortName: string }[]): Promise<void> {
for (const item of items) {
// 格式化 name
const formattedName = this.formatName(item.name);
const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
if (!existingItem) {
// 如果字典项不存在,则使用格式化后的 name 创建新字典项
await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, shortName: item.shortName, dict });
}
}
}
}
// 口味数据
const flavorsData = [
{ name: 'all-white', title: 'all white', titleCn: '全白', shortName: 'AL' },
{ name: 'amazing-apple-blackcurrant', title: 'amazing apple blackcurrant', titleCn: '惊艳苹果黑加仑', shortName: 'AM' },
{ name: 'apple-&-mint', title: 'apple & mint', titleCn: '苹果薄荷', shortName: 'AP' },
{ name: 'applemint', title: 'applemint', titleCn: '苹果薄荷混合', shortName: 'AP' },
{ name: 'apple-berry-ice', title: 'apple berry ice', titleCn: '苹果莓冰', shortName: 'AP' },
{ name: 'apple-bomb', title: 'apple bomb', titleCn: '苹果炸弹', shortName: 'AP' },
{ name: 'apple-kiwi-melon-ice', title: 'apple kiwi melon ice', titleCn: '苹果奇异瓜冰', shortName: 'AP' },
{ name: 'apple-mango-pear', title: 'apple mango pear', titleCn: '苹果芒果梨', shortName: 'AP' },
{ name: 'apple-melon-ice', title: 'apple melon ice', titleCn: '苹果瓜冰', shortName: 'AP' },
{ name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷', shortName: 'AP' },
{ name: 'apple-peach', title: 'apple peach', titleCn: '苹果桃子', shortName: 'AP' },
{ name: 'apple-peach-pear', title: 'apple peach pear', titleCn: '苹果桃梨', shortName: 'AP' },
{ name: 'apple-peach-strawww', title: 'apple peach strawww', titleCn: '苹果桃草莓', shortName: 'AP' },
{ name: 'apple-pom-passion-ice', title: 'apple pom passion ice', titleCn: '苹果石榴激情冰', shortName: 'AP' },
{ name: 'arctic-banana-glaze', title: 'arctic banana glaze', titleCn: '北极香蕉釉', shortName: 'AR' },
{ name: 'arctic-grapefruit', title: 'arctic grapefruit', titleCn: '北极葡萄柚', shortName: 'AR' },
{ name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' },
{ name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' },
{ name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' },
{ name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' },
{ name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' },
{ name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' },
{ name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' },
{ name: 'banana-ice', title: 'banana ice', titleCn: '香蕉冰', shortName: 'BA' },
{ name: 'banana-milkshake', title: 'banana milkshake', titleCn: '香蕉奶昔', shortName: 'BA' },
{ name: 'banana-pnck-dude', title: 'banana pnck dude', titleCn: '香蕉粉红小子', shortName: 'BA' },
{ name: 'banana-pomegranate-cherry-ice', title: 'banana pomegranate cherry ice', titleCn: '香蕉石榴樱桃冰', shortName: 'BA' },
{ name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' },
{ name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' },
{ name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' },
{ name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' },
{ name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' },
{ name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' },
{ name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' },
{ name: 'black', title: 'black', titleCn: '黑色', shortName: 'BL' },
{ name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' },
{ name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' },
{ name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' },
{ name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' },
{ name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' },
{ name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' },
{ name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' },
{ name: 'blackberry-raspberry-lemon', title: 'blackberry raspberry lemon', titleCn: '黑莓覆盆子柠檬', shortName: 'BL' },
{ name: 'blackcurrant-lychee-berries', title: 'blackcurrant lychee berries', titleCn: '黑加仑荔枝莓', shortName: 'BL' },
{ name: 'blackcurrant-pineapple-ice', title: 'blackcurrant pineapple ice', titleCn: '黑加仑菠萝冰', shortName: 'BL' },
{ name: 'blackcurrant-quench-ice', title: 'blackcurrant quench ice', titleCn: '黑加仑清爽冰', shortName: 'BL' },
{ name: 'blastin-banana-mango-iced', title: 'blastin banana mango iced', titleCn: '香蕉芒果爆炸冰', shortName: 'BL' },
{ name: 'blazin-banana-blackberry-iced', title: 'blazin banana blackberry iced', titleCn: '香蕉黑莓火焰冰', shortName: 'BL' },
{ name: 'blessed-blueberry-mint-iced', title: 'blessed blueberry mint iced', titleCn: '蓝莓薄荷冰', shortName: 'BL' },
{ name: 'bliss-iced', title: 'bliss iced', titleCn: '极乐冰', shortName: 'BL' },
{ name: 'blood-orange', title: 'blood orange', titleCn: '血橙', shortName: 'BL' },
{ name: 'blood-orange-ice', title: 'blood orange ice', titleCn: '血橙冰', shortName: 'BL' },
{ name: 'blue-dragon-fruit-peach', title: 'blue dragon fruit peach', titleCn: '蓝色龙果桃', shortName: 'BL' },
{ name: 'blue-lemon', title: 'blue lemon', titleCn: '蓝柠檬', shortName: 'BL' },
{ name: 'blue-raspberry', title: 'blue raspberry', titleCn: '蓝覆盆子', shortName: 'BL' },
{ name: 'blue-raspberry-apple', title: 'blue raspberry apple', titleCn: '蓝覆盆子苹果', shortName: 'BL' },
{ name: 'blue-raspberry-lemon', title: 'blue raspberry lemon', titleCn: '蓝覆盆子柠檬', shortName: 'BL' },
{ name: 'blue-raspberry-magic-cotton-ice', title: 'blue raspberry magic cotton ice', titleCn: '蓝覆盆子魔法棉花糖冰', shortName: 'BL' },
{ name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' },
{ name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' },
{ name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' },
{ name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' },
{ name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' },
{ name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' },
{ name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' },
{ name: 'blueberry', title: 'blueberry', titleCn: '蓝莓', shortName: 'BL' },
{ name: 'blueberry-banana', title: 'blueberry banana', titleCn: '蓝莓香蕉', shortName: 'BL' },
{ name: 'blueberry-cloudz', title: 'blueberry cloudz', titleCn: '蓝莓云', shortName: 'BL' },
{ name: 'blueberry-ice', title: 'blueberry ice', titleCn: '蓝莓冰', shortName: 'BL' },
{ name: 'blueberry-kiwi-ice', title: 'blueberry kiwi ice', titleCn: '蓝莓奇异果冰', shortName: 'BL' },
{ name: 'blueberry-lemon', title: 'blueberry lemon', titleCn: '蓝莓柠檬', shortName: 'BL' },
{ name: 'blueberry-lemon-ice', title: 'blueberry lemon ice', titleCn: '蓝莓柠檬冰', shortName: 'BL' },
{ name: 'blueberry-mint', title: 'blueberry mint', titleCn: '蓝莓薄荷', shortName: 'BL' },
{ name: 'blueberry-pear', title: 'blueberry pear', titleCn: '蓝莓梨', shortName: 'BL' },
{ name: 'blueberry-razz-cc', title: 'blueberry razz cc', titleCn: '蓝莓覆盆子混合', shortName: 'BL' },
{ name: 'blueberry-sour-raspberry', title: 'blueberry sour raspberry', titleCn: '蓝莓酸覆盆子', shortName: 'BL' },
{ name: 'blueberry-storm', title: 'blueberry storm', titleCn: '蓝莓风暴', shortName: 'BL' },
{ name: 'blueberry-swirl-ice', title: 'blueberry swirl ice', titleCn: '蓝莓漩涡冰', shortName: 'BL' },
{ name: 'blueberry-watermelon', title: 'blueberry watermelon', titleCn: '蓝莓西瓜', shortName: 'BL' },
{ name: 'bold-tobacco', title: 'bold tobacco', titleCn: '浓烈烟草', shortName: 'BO' },
{ name: 'bomb-blue-razz', title: 'bomb blue razz', titleCn: '蓝覆盆子炸弹', shortName: 'BO' },
{ name: 'boss-blueberry-iced', title: 'boss blueberry iced', titleCn: '老板蓝莓冰', shortName: 'BO' },
{ name: 'boss-blueberry-lced', title: 'boss blueberry lced', titleCn: '老板蓝莓冷饮', shortName: 'BO' },
{ name: 'bright-peppermint', title: 'bright peppermint', titleCn: '清爽薄荷', shortName: 'BR' },
{ name: 'bright-spearmint', title: 'bright spearmint', titleCn: '清爽留兰香', shortName: 'BR' },
{ name: 'brisky-classic-red', title: 'brisky classic red', titleCn: '经典红色烈酒', shortName: 'BR' },
{ name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' },
{ name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' },
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' },
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
{ name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' },
{ name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' },
{ name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' },
{ name: 'caribbean-spirit', title: 'caribbean spirit', titleCn: '加勒比风情', shortName: 'CA' },
{ name: 'caribbean-white', title: 'caribbean white', titleCn: '加勒比白', shortName: 'CA' },
{ name: 'cherry', title: 'cherry', titleCn: '樱桃', shortName: 'CH' },
{ name: 'cherry-blast-ice', title: 'cherry blast ice', titleCn: '樱桃爆炸冰', shortName: 'CH' },
{ name: 'cherry-classic-cola', title: 'cherry classic cola', titleCn: '樱桃经典可乐', shortName: 'CH' },
{ name: 'cherry-classic-red', title: 'cherry classic red', titleCn: '樱桃经典红', shortName: 'CH' },
{ name: 'cherry-cola-ice', title: 'cherry cola ice', titleCn: '樱桃可乐冰', shortName: 'CH' },
{ name: 'cherry-ice', title: 'cherry ice', titleCn: '樱桃冰', shortName: 'CH' },
{ name: 'cherry-lemon', title: 'cherry lemon', titleCn: '樱桃柠檬', shortName: 'CH' },
{ name: 'cherry-lime-classic', title: 'cherry lime classic', titleCn: '樱桃青柠经典', shortName: 'CH' },
{ name: 'cherry-lime-ice', title: 'cherry lime ice', titleCn: '樱桃青柠冰', shortName: 'CH' },
{ name: 'cherry-lychee', title: 'cherry lychee', titleCn: '樱桃荔枝', shortName: 'CH' },
{ name: 'cherry-peach-lemon', title: 'cherry peach lemon', titleCn: '樱桃桃子柠檬', shortName: 'CH' },
{ name: 'cherry-red-classic', title: 'cherry red classic', titleCn: '红樱桃经典', shortName: 'CH' },
{ name: 'cherry-strazz', title: 'cherry strazz', titleCn: '樱桃草莓', shortName: 'CH' },
{ name: 'cherry-watermelon', title: 'cherry watermelon', titleCn: '樱桃西瓜', shortName: 'CH' },
{ name: 'chill', title: 'chill', titleCn: '冰爽', shortName: 'CH' },
{ name: 'chilled-classic-red', title: 'chilled classic red', titleCn: '冰镇经典红', shortName: 'CH' },
{ name: 'chillin-coffee-iced', title: 'chillin coffee iced', titleCn: '冰镇咖啡', shortName: 'CH' },
{ name: 'chilly-jiggle-b', title: 'chilly jiggle b', titleCn: '清凉果冻 B', shortName: 'CH' },
{ name: 'churned-peanut', title: 'churned peanut', titleCn: '搅拌花生', shortName: 'CH' },
{ name: 'cinnamon', title: 'cinnamon', titleCn: '肉桂', shortName: 'CI' },
{ name: 'cinnamon-flame', title: 'cinnamon flame', titleCn: '肉桂火焰', shortName: 'CI' },
{ name: 'cinnamon-roll', title: 'cinnamon roll', titleCn: '肉桂卷', shortName: 'CI' },
{ name: 'circle-of-life', title: 'circle of life', titleCn: '生命循环', shortName: 'CI' },
{ name: 'citrus', title: 'citrus', titleCn: '柑橘', shortName: 'CI' },
{ name: 'citrus-burst-ice', title: 'citrus burst ice', titleCn: '柑橘爆发冰', shortName: 'CI' },
{ name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' },
{ name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' },
{ name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' },
{ name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' },
{ name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' },
{ name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' },
{ name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' },
{ name: 'classic-tobacco', title: 'classic tobacco', titleCn: '经典烟草', shortName: 'CL' },
{ name: 'classical-tobacco', title: 'classical tobacco', titleCn: '古典烟草', shortName: 'CL' },
{ name: 'coconut-ice', title: 'coconut ice', titleCn: '椰子冰', shortName: 'CO' },
{ name: 'coconut-water-ice', title: 'coconut water ice', titleCn: '椰子水冰', shortName: 'CO' },
{ name: 'coffee', title: 'coffee', titleCn: '咖啡', shortName: 'CO' },
{ name: 'coffee-stout', title: 'coffee stout', titleCn: '咖啡烈酒', shortName: 'CO' },
{ name: 'cola', title: 'cola', titleCn: '可乐', shortName: 'CO' },
{ name: 'cola-&-cherry', title: 'cola & cherry', titleCn: '可乐樱桃', shortName: 'CO' },
{ name: 'cola-&-vanilla', title: 'cola & vanilla', titleCn: '可乐香草', shortName: 'CO' },
{ name: 'cola-ice', title: 'cola ice', titleCn: '可乐冰', shortName: 'CO' },
{ name: 'cool-frost', title: 'cool frost', titleCn: '酷霜', shortName: 'CO' },
{ name: 'cool-mint', title: 'cool mint', titleCn: '酷薄荷', shortName: 'CO' },
{ name: 'cool-mint-ice', title: 'cool mint ice', titleCn: '酷薄荷冰', shortName: 'CO' },
{ name: 'cool-storm', title: 'cool storm', titleCn: '酷风暴', shortName: 'CO' },
{ name: 'cool-tropical', title: 'cool tropical', titleCn: '酷热带', shortName: 'CO' },
{ name: 'cool-watermelon', title: 'cool watermelon', titleCn: '酷西瓜', shortName: 'CO' },
{ name: 'cotton-clouds', title: 'cotton clouds', titleCn: '棉花云', shortName: 'CO' },
{ name: 'cranberry-blackcurrant', title: 'cranberry blackcurrant', titleCn: '蔓越莓黑加仑', shortName: 'CR' },
{ name: 'cranberry-lemon', title: 'cranberry lemon', titleCn: '蔓越莓柠檬', shortName: 'CR' },
{ name: 'cranberry-lemon-ice', title: 'cranberry lemon ice', titleCn: '蔓越莓柠檬冰', shortName: 'CR' },
{ name: 'creamy-maple', title: 'creamy maple', titleCn: '奶香枫糖', shortName: 'CR' },
{ name: 'creamy-vanilla', title: 'creamy vanilla', titleCn: '奶香香草', shortName: 'CR' },
{ name: 'crispy-peppermint', title: 'crispy peppermint', titleCn: '脆薄荷', shortName: 'CR' },
{ name: 'cuban-tobacco', title: 'cuban tobacco', titleCn: '古巴烟草', shortName: 'CU' },
{ name: 'cucumber-lime', title: 'cucumber lime', titleCn: '黄瓜青柠', shortName: 'CU' },
{ name: 'dark-blackcurrant', title: 'dark blackcurrant', titleCn: '深黑加仑', shortName: 'DA' },
{ name: 'dark-forest', title: 'dark forest', titleCn: '深林', shortName: 'DA' },
{ name: 'deep-freeze', title: 'deep freeze', titleCn: '极冻', shortName: 'DE' },
{ name: 'dope-double-kiwi-iced', title: 'dope double kiwi iced', titleCn: '双奇异果冰', shortName: 'DO' },
{ name: 'dope-double-kiwi-lced', title: 'dope double kiwi lced', titleCn: '双奇异果冷饮', shortName: 'DO' },
{ name: 'double-apple', title: 'double apple', titleCn: '双苹果', shortName: 'DO' },
{ name: 'double-apple-ice', title: 'double apple ice', titleCn: '双苹果冰', shortName: 'DO' },
{ name: 'double-berry-twist-ice', title: 'double berry twist ice', titleCn: '双浆果扭曲冰', shortName: 'DO' },
{ name: 'double-ice', title: 'double ice', titleCn: '双冰', shortName: 'DO' },
{ name: 'double-mango', title: 'double mango', titleCn: '双芒果', shortName: 'DO' },
{ name: 'double-mint', title: 'double mint', titleCn: '双薄荷', shortName: 'DO' },
{ name: 'double-mocha', title: 'double mocha', titleCn: '双摩卡', shortName: 'DO' },
{ name: 'double-shot-espresso', title: 'double shot espresso', titleCn: '双份浓缩咖啡', shortName: 'DO' },
{ name: 'dragon-berry-mango-ice', title: 'dragon berry mango ice', titleCn: '龙莓芒果冰', shortName: 'DR' },
{ name: 'dragon-fruit', title: 'dragon fruit', titleCn: '龙果', shortName: 'DR' },
{ name: 'dragon-fruit-lychee-ice', title: 'dragon fruit lychee ice', titleCn: '龙果荔枝冰', shortName: 'DR' },
{ name: 'dragon-fruit-strawberry-ice', title: 'dragon fruit strawberry ice', titleCn: '龙果草莓冰', shortName: 'DR' },
{ name: 'dragon-fruit-strawnana', title: 'dragon fruit strawnana', titleCn: '龙果香蕉', shortName: 'DR' },
{ name: 'dragon-melon-ice', title: 'dragon melon ice', titleCn: '龙瓜冰', shortName: 'DR' },
{ name: 'dragonfruit-lychee', title: 'dragonfruit lychee', titleCn: '龙果荔枝', shortName: 'DR' },
{ name: 'dreamy-dragonfruit-lychee-iced', title: 'dreamy dragonfruit lychee iced', titleCn: '梦幻龙果荔枝冰', shortName: 'DR' },
{ name: 'dub-dub', title: 'dub dub', titleCn: '双重', shortName: 'DU' },
{ name: 'durian', title: 'durian', titleCn: '榴莲', shortName: 'DU' },
{ name: 'electric-fruit-blast', title: 'electric fruit blast', titleCn: '电果爆炸', shortName: 'EL' },
{ name: 'electric-orange', title: 'electric orange', titleCn: '电橙', shortName: 'EL' },
{ name: 'energy-drink', title: 'energy drink', titleCn: '能量饮料', shortName: 'EN' },
{ name: 'epic-apple', title: 'epic apple', titleCn: '极致苹果', shortName: 'EP' },
{ name: 'epic-apple-peach', title: 'epic apple peach', titleCn: '极致苹果桃', shortName: 'EP' },
{ name: 'epic-banana', title: 'epic banana', titleCn: '极致香蕉', shortName: 'EP' },
{ name: 'epic-berry-swirl', title: 'epic berry swirl', titleCn: '极致浆果旋风', shortName: 'EP' },
{ name: 'epic-blue-razz', title: 'epic blue razz', titleCn: '极致蓝覆盆子', shortName: 'EP' },
{ name: 'epic-fruit-bomb', title: 'epic fruit bomb', titleCn: '极致水果炸弹', shortName: 'EP' },
{ name: 'epic-grape', title: 'epic grape', titleCn: '极致葡萄', shortName: 'EP' },
{ name: 'epic-honeydew-blackcurrant', title: 'epic honeydew blackcurrant', titleCn: '极致蜜瓜黑加仑', shortName: 'EP' },
{ name: 'epic-kiwi-mango', title: 'epic kiwi mango', titleCn: '极致奇异果芒果', shortName: 'EP' },
{ name: 'epic-peach-mango', title: 'epic peach mango', titleCn: '极致桃芒果', shortName: 'EP' },
{ name: 'epic-peppermint', title: 'epic peppermint', titleCn: '极致薄荷', shortName: 'EP' },
{ name: 'epic-sour-berries', title: 'epic sour berries', titleCn: '极致酸浆果', shortName: 'EP' },
{ name: 'epic-strawberry', title: 'epic strawberry', titleCn: '极致草莓', shortName: 'EP' },
{ name: 'epic-strawberry-watermelon', title: 'epic strawberry watermelon', titleCn: '极致草莓西瓜', shortName: 'EP' },
{ name: 'epic-watermelon-kiwi', title: 'epic watermelon kiwi', titleCn: '极致西瓜奇异果', shortName: 'EP' },
{ name: 'exotic-mango', title: 'exotic mango', titleCn: '异国芒果', shortName: 'EX' },
{ name: 'extreme-chill-mint', title: 'extreme chill mint', titleCn: '极寒薄荷', shortName: 'EX' },
{ name: 'extreme-cinnamon', title: 'extreme cinnamon', titleCn: '极寒肉桂', shortName: 'EX' },
{ name: 'extreme-mint', title: 'extreme mint', titleCn: '极寒薄荷', shortName: 'EX' },
{ name: 'extreme-mint-iced', title: 'extreme mint iced', titleCn: '极寒薄荷冰', shortName: 'EX' },
{ name: 'famous-fruit-ko-iced', title: 'famous fruit ko iced', titleCn: '知名水果 KO 冰', shortName: 'FA' },
{ name: 'famous-fruit-ko-lced', title: 'famous fruit ko lced', titleCn: '知名水果 KO 冷饮', shortName: 'FA' },
{ name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' },
{ name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' },
{ name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' },
{ name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' },
{ name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' },
{ name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' },
{ name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' },
{ name: 'freeze-mint', title: 'freeze mint', titleCn: '冰薄荷', shortName: 'FR' },
{ name: 'freeze-mint-salty', title: 'freeze mint salty', titleCn: '冰薄荷咸味', shortName: 'FR' },
{ name: 'freezing-peppermint', title: 'freezing peppermint', titleCn: '冰爽薄荷', shortName: 'FR' },
{ name: 'freezy-berry-peachy', title: 'freezy berry peachy', titleCn: '冰冻浆果桃', shortName: 'FR' },
{ name: 'fresh-fruit', title: 'fresh fruit', titleCn: '新鲜水果', shortName: 'FR' },
{ name: 'fresh-mint', title: 'fresh mint', titleCn: '新鲜薄荷', shortName: 'FR' },
{ name: 'fresh-mint-ice', title: 'fresh mint ice', titleCn: '新鲜薄荷冰', shortName: 'FR' },
{ name: 'froot-b', title: 'froot b', titleCn: '水果 B', shortName: 'FR' },
{ name: 'frost', title: 'frost', titleCn: '霜冻', shortName: 'FR' },
{ name: 'frost-mint', title: 'frost mint', titleCn: '霜薄荷', shortName: 'FR' },
{ name: 'frosted-strawberries', title: 'frosted strawberries', titleCn: '霜冻草莓', shortName: 'FR' },
{ name: 'frosty-grapefruit', title: 'frosty grapefruit', titleCn: '冰爽葡萄柚', shortName: 'FR' },
{ name: 'frozen-classical-ice', title: 'frozen classical ice', titleCn: '冷冻经典冰', shortName: 'FR' },
{ name: 'frozen-cloudberry', title: 'frozen cloudberry', titleCn: '冷冻云莓', shortName: 'FR' },
{ name: 'frozen-mint', title: 'frozen mint', titleCn: '冷冻薄荷', shortName: 'FR' },
{ name: 'frozen-pineapple', title: 'frozen pineapple', titleCn: '冷冻菠萝', shortName: 'FR' },
{ name: 'frozen-strawberry', title: 'frozen strawberry', titleCn: '冷冻草莓', shortName: 'FR' },
{ name: 'frozen-strawberrygb(gummy-bear)', title: 'frozen strawberrygb(gummy bear)', titleCn: '冷冻草莓软糖', shortName: 'FR' },
{ name: 'grapefruit-grape-gb(gummy-bear)', title: 'grapefruit grape gb(gummy bear)', titleCn: '葡萄柚葡萄软糖', shortName: 'GR' },
{ name: 'fruit-flash-ice', title: 'fruit flash ice', titleCn: '水果闪电冰', shortName: 'FR' },
{ name: 'fruity-explosion', title: 'fruity explosion', titleCn: '水果爆炸', shortName: 'FR' },
{ name: 'fuji-apple-ice', title: 'fuji apple ice', titleCn: '富士苹果冰', shortName: 'FU' },
{ name: 'fuji-ice', title: 'fuji ice', titleCn: '富士冰', shortName: 'FU' },
{ name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' },
{ name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' },
{ name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' },
{ name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖Gummy Bear', shortName: 'GB' },
{ name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' },
{ name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' },
{ name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' },
{ name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' },
{ name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' },
{ name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' },
{ name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D绿色露水', shortName: 'GN' },
{ name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' },
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
{ name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' },
{ name: 'grape-fury-ice', title: 'grape fury ice', titleCn: '葡萄狂怒冰', shortName: 'GR' },
{ name: 'grape-honeydew-ice', title: 'grape honeydew ice', titleCn: '葡萄蜜瓜冰', shortName: 'GR' },
{ name: 'grape-ice', title: 'grape ice', titleCn: '葡萄冰', shortName: 'GR' },
{ name: 'grape-pomegranate-ice', title: 'grape pomegranate ice', titleCn: '葡萄石榴冰', shortName: 'GR' },
{ name: 'grapefruit-grape', title: 'grapefruit grape', titleCn: '葡萄柚葡萄', shortName: 'GR' },
{ name: 'grapefruit-ice', title: 'grapefruit ice', titleCn: '葡萄柚冰', shortName: 'GR' },
{ name: 'grapes', title: 'grapes', titleCn: '葡萄', shortName: 'GR' },
{ name: 'grapplin-grape-sour-apple-iced', title: 'grapplin grape sour apple iced', titleCn: '葡萄酸苹果冰', shortName: 'GR' },
{ name: 'green-apple', title: 'green apple', titleCn: '青苹果', shortName: 'GR' },
{ name: 'green-apple-ice', title: 'green apple ice', titleCn: '青苹果冰', shortName: 'GR' },
{ name: 'green-grape-ice', title: 'green grape ice', titleCn: '青葡萄冰', shortName: 'GR' },
{ name: 'green-mango-ice', title: 'green mango ice', titleCn: '青芒果冰', shortName: 'GR' },
{ name: 'green-mint', title: 'green mint', titleCn: '青薄荷', shortName: 'GR' },
{ name: 'green-spearmint', title: 'green spearmint', titleCn: '青留兰香', shortName: 'GR' },
{ name: 'green-tea', title: 'green tea', titleCn: '绿茶', shortName: 'GR' },
{ name: 'groovy-grape', title: 'groovy grape', titleCn: '活力葡萄', shortName: 'GR' },
{ name: 'groovy-grape-passionfruit-iced', title: 'groovy grape passionfruit iced', titleCn: '活力葡萄激情果冰', shortName: 'GR' },
{ name: 'guava-ice', title: 'guava ice', titleCn: '番石榴冰', shortName: 'GU' },
{ name: 'guava-ice-t', title: 'guava ice t', titleCn: '番石榴冰 T', shortName: 'GU' },
{ name: 'guava-mango-peach', title: 'guava mango peach', titleCn: '番石榴芒果桃', shortName: 'GU' },
{ name: 'gusto-green-apple', title: 'gusto green apple', titleCn: '绿苹果狂热', shortName: 'GU' },
{ name: 'hakuna', title: 'hakuna', titleCn: '哈库纳', shortName: 'HA' },
{ name: 'harambae', title: 'harambae', titleCn: '哈兰贝', shortName: 'HA' },
{ name: 'harmony', title: 'harmony', titleCn: '和谐', shortName: 'HA' },
{ name: 'haven', title: 'haven', titleCn: '避风港', shortName: 'HA' },
{ name: 'haven-iced', title: 'haven iced', titleCn: '避风港冰', shortName: 'HA' },
{ name: 'hawaiian-blue', title: 'hawaiian blue', titleCn: '夏威夷蓝', shortName: 'HA' },
{ name: 'hawaiian-mist-ice', title: 'hawaiian mist ice', titleCn: '夏威夷薄雾冰', shortName: 'HA' },
{ name: 'hawaiian-storm', title: 'hawaiian storm', titleCn: '夏威夷风暴', shortName: 'HA' },
{ name: 'hip-honeydew-mango-iced', title: 'hip honeydew mango iced', titleCn: '蜜瓜芒果冰', shortName: 'HI' },
{ name: 'hokkaido-milk', title: 'hokkaido milk', titleCn: '北海道牛奶', shortName: 'HO' },
{ name: 'honeydew-blackcurrant', title: 'honeydew blackcurrant', titleCn: '蜜瓜黑加仑', shortName: 'HO' },
{ name: 'honeydew-mango-ice', title: 'honeydew mango ice', titleCn: '蜜瓜芒果冰', shortName: 'HO' },
{ name: 'hype', title: 'hype', titleCn: '狂热', shortName: 'HY' },
{ name: 'ice-blast', title: 'ice blast', titleCn: '冰爆', shortName: 'IC' },
{ name: 'ice-cool', title: 'ice cool', titleCn: '冰凉', shortName: 'IC' },
{ name: 'ice-cream', title: 'ice cream', titleCn: '冰淇淋', shortName: 'IC' },
{ name: 'ice-mint', title: 'ice mint', titleCn: '冰薄荷', shortName: 'IC' },
{ name: 'ice-wintergreen', title: 'ice wintergreen', titleCn: '冰冬青', shortName: 'IC' },
{ name: 'iced-americano', title: 'iced americano', titleCn: '冰美式', shortName: 'IC' },
{ name: 'icy-berries', title: 'icy berries', titleCn: '冰爽浆果', shortName: 'IC' },
{ name: 'icy-blackcurrant', title: 'icy blackcurrant', titleCn: '冰爽黑加仑', shortName: 'IC' },
{ name: 'icy-cherry', title: 'icy cherry', titleCn: '冰爽樱桃', shortName: 'IC' },
{ name: 'icy-mint', title: 'icy mint', titleCn: '冰爽薄荷', shortName: 'IC' },
{ name: 'icy-pink-clouds', title: 'icy pink clouds', titleCn: '冰粉云', shortName: 'IC' },
{ name: 'intense-blue-razz', title: 'intense blue razz', titleCn: '强烈蓝覆盆子', shortName: 'IN' },
{ name: 'intense-blueberry-lemon', title: 'intense blueberry lemon', titleCn: '强烈蓝莓柠檬', shortName: 'IN' },
{ name: 'intense-flavourless', title: 'intense flavourless', titleCn: '强烈无味', shortName: 'IN' },
{ name: 'intense-fruity-explosion', title: 'intense fruity explosion', titleCn: '强烈水果爆炸', shortName: 'IN' },
{ name: 'intense-juicy-peach', title: 'intense juicy peach', titleCn: '强烈多汁桃', shortName: 'IN' },
{ name: 'intense-red-apple', title: 'intense red apple', titleCn: '强烈红苹果', shortName: 'IN' },
{ name: 'intense-ripe-mango', title: 'intense ripe mango', titleCn: '强烈熟芒果', shortName: 'IN' },
{ name: 'intense-strawberry-watermelon', title: 'intense strawberry watermelon', titleCn: '强烈草莓西瓜', shortName: 'IN' },
{ name: 'intense-white-grape', title: 'intense white grape', titleCn: '强烈白葡萄', shortName: 'IN' },
{ name: 'intense-white-mint', title: 'intense white mint', titleCn: '强烈白薄荷', shortName: 'IN' },
{ name: 'jasmine-tea', title: 'jasmine tea', titleCn: '茉莉茶', shortName: 'JA' },
{ name: 'jiggly-b', title: 'jiggly b', titleCn: '果冻 B', shortName: 'JI' },
{ name: 'jiggly-sting', title: 'jiggly sting', titleCn: '果冻刺', shortName: 'JI' },
{ name: 'juicy-mango', title: 'juicy mango', titleCn: '多汁芒果', shortName: 'JU' },
{ name: 'juicy-peach', title: 'juicy peach', titleCn: '多汁桃', shortName: 'JU' },
{ name: 'juicy-peach-ice', title: 'juicy peach ice', titleCn: '多汁桃冰', shortName: 'JU' },
{ name: 'jungle-secrets', title: 'jungle secrets', titleCn: '丛林秘密', shortName: 'JU' },
{ name: 'kanzi', title: 'kanzi', titleCn: '甘之', shortName: 'KA' },
{ name: 'kewl-kiwi-passionfruit-iced', title: 'kewl kiwi passionfruit iced', titleCn: '酷奇奇', shortName: 'KE' },
{ name: 'kiwi-berry-ice', title: 'kiwi berry ice', titleCn: '奇异果浆果冰', shortName: 'KI' },
{ name: 'kiwi-dragon-berry', title: 'kiwi dragon berry', titleCn: '奇异果龙莓', shortName: 'KI' },
{ name: 'kiwi-green-t', title: 'kiwi green t', titleCn: '奇异果绿茶', shortName: 'KI' },
{ name: 'kiwi-guava-ice', title: 'kiwi guava ice', titleCn: '奇异果番石榴冰', shortName: 'KI' },
{ name: 'kiwi-guava-passionfruit-ice', title: 'kiwi guava passionfruit ice', titleCn: '奇异果番石榴激情果冰', shortName: 'KI' },
{ name: 'kiwi-passion-fruit-guava', title: 'kiwi passion fruit guava', titleCn: '奇异果激情果番石榴', shortName: 'KI' },
{ name: 'kyoho-grape', title: 'kyoho grape', titleCn: '巨峰葡萄', shortName: 'KY' },
{ name: 'kyoho-grape-ice', title: 'kyoho grape ice', titleCn: '巨峰葡萄冰', shortName: 'KY' },
{ name: 'lemon', title: 'lemon', titleCn: '柠檬', shortName: 'LE' },
{ name: 'lemon-berry', title: 'lemon berry', titleCn: '柠檬浆果', shortName: 'LE' },
{ name: 'lemon-blue-razz-ice', title: 'lemon blue razz ice', titleCn: '柠檬蓝覆盆子冰', shortName: 'LE' },
{ name: 'lemon-lime-cranberry', title: 'lemon lime cranberry', titleCn: '柠檬青柠蔓越莓', shortName: 'LE' },
{ name: 'lemon-lime-ice', title: 'lemon lime ice', titleCn: '柠檬青柠冰', shortName: 'LE' },
{ name: 'lemon-sprite', title: 'lemon sprite', titleCn: '柠檬汽水', shortName: 'LE' },
{ name: 'lemon-spritz', title: 'lemon spritz', titleCn: '柠檬气泡', shortName: 'LE' },
{ name: 'lemon-squeeze-ice', title: 'lemon squeeze ice', titleCn: '柠檬榨汁冰', shortName: 'LE' },
{ name: 'lemon-squeeze-iced', title: 'lemon squeeze iced', titleCn: '柠檬榨汁冷饮', shortName: 'LE' },
{ name: 'lemon-t', title: 'lemon t', titleCn: '柠檬 T', shortName: 'LE' },
{ name: 'lemon-tea-ice', title: 'lemon tea ice', titleCn: '柠檬茶冰', shortName: 'LE' },
{ name: 'lemon-twist-ice', title: 'lemon twist ice', titleCn: '柠檬扭转冰', shortName: 'LE' },
{ name: 'lemur', title: 'lemur', titleCn: '狐猴', shortName: 'LE' },
{ name: 'lime-berry-orange-ice', title: 'lime berry orange ice', titleCn: '青柠浆果橙冰', shortName: 'LI' },
{ name: 'lime-flame', title: 'lime flame', titleCn: '青柠火焰', shortName: 'LI' },
{ name: 'liquorice', title: 'liquorice', titleCn: '甘草', shortName: 'LI' },
{ name: 'lit-lychee-watermelon-iced', title: 'lit lychee watermelon iced', titleCn: '荔枝西瓜冰', shortName: 'LI' },
{ name: 'loco-cocoa-latte-iced', title: 'loco cocoa latte iced', titleCn: '可可拿铁冷饮', shortName: 'LO' },
{ name: 'lofty-liquorice', title: 'lofty liquorice', titleCn: '高挑甘草', shortName: 'LO' },
{ name: 'lush-ice', title: 'lush ice', titleCn: '冰爽浓郁', shortName: 'LU' },
{ name: 'lychee-ice', title: 'lychee ice', titleCn: '荔枝冰', shortName: 'LY' },
{ name: 'lychee-mango-ice', title: 'lychee mango ice', titleCn: '荔枝芒果冰', shortName: 'LY' },
{ name: 'lychee-mango-melon', title: 'lychee mango melon', titleCn: '荔枝芒果瓜', shortName: 'LY' },
{ name: 'lychee-melon-ice', title: 'lychee melon ice', titleCn: '荔枝瓜冰', shortName: 'LY' },
{ name: 'lychee-watermelon-strawberry', title: 'lychee watermelon strawberry', titleCn: '荔枝西瓜草莓', shortName: 'LY' },
{ name: 'mad-mango-peach', title: 'mad mango peach', titleCn: '疯狂芒果桃', shortName: 'MA' },
{ name: 'mangabeys', title: 'mangabeys', titleCn: '长臂猿', shortName: 'MA' },
{ name: 'mango', title: 'mango', titleCn: '芒果', shortName: 'MA' },
{ name: 'mango-berry', title: 'mango berry', titleCn: '芒果浆果', shortName: 'MA' },
{ name: 'mango-blueberry', title: 'mango blueberry', titleCn: '芒果蓝莓', shortName: 'MA' },
{ name: 'mango-dragon-fruit-lemon-ice', title: 'mango dragon fruit lemon ice', titleCn: '芒果龙果柠檬冰', shortName: 'MA' },
{ name: 'mango-flame', title: 'mango flame', titleCn: '芒果火焰', shortName: 'MA' },
{ name: 'mango-honeydew-ice', title: 'mango honeydew ice', titleCn: '芒果蜜瓜冰', shortName: 'MA' },
{ name: 'mango-ice', title: 'mango ice', titleCn: '芒果冰', shortName: 'MA' },
{ name: 'mango-madness', title: 'mango madness', titleCn: '芒果狂热', shortName: 'MA' },
{ name: 'mango-nectar-ice', title: 'mango nectar ice', titleCn: '芒果花蜜冰', shortName: 'MA' },
{ name: 'mango-on-ice', title: 'mango on ice', titleCn: '芒果冰镇', shortName: 'MA' },
{ name: 'mango-melon', title: 'mango melon', titleCn: '芒果瓜', shortName: 'MA' },
{ name: 'mango-peach', title: 'mango peach', titleCn: '芒果桃', shortName: 'MA' },
{ name: 'mango-peach-apricot-ice', title: 'mango peach apricot ice', titleCn: '芒果桃杏冰', shortName: 'MA' },
{ name: 'mango-peach-orange', title: 'mango peach orange', titleCn: '芒果桃橙', shortName: 'MA' },
{ name: 'mango-peach-tings', title: 'mango peach tings', titleCn: '芒果桃滋味', shortName: 'MA' },
{ name: 'mango-peach-watermelon', title: 'mango peach watermelon', titleCn: '芒果桃西瓜', shortName: 'MA' },
{ name: 'mango-pineapple', title: 'mango pineapple', titleCn: '芒果菠萝', shortName: 'MA' },
{ name: 'mango-pineapple-guava-ice', title: 'mango pineapple guava ice', titleCn: '芒果菠萝番石榴冰', shortName: 'MA' },
{ name: 'mango-pineapple-ice', title: 'mango pineapple ice', titleCn: '芒果菠萝冰', shortName: 'MA' },
{ name: 'mango-squared', title: 'mango squared', titleCn: '芒果平方', shortName: 'MA' },
{ name: 'matata', title: 'matata', titleCn: '马塔塔', shortName: 'MA' },
{ name: 'max-freeze', title: 'max freeze', titleCn: '极冻', shortName: 'MA' },
{ name: 'max-polar-mint', title: 'max polar mint', titleCn: '极地薄荷', shortName: 'MA' },
{ name: 'max-polarmint', title: 'max polarmint', titleCn: '极地薄荷', shortName: 'MA' },
{ name: 'mclaren-sweet-papaya', title: 'mclaren sweet papaya', titleCn: '迈凯轮甜木瓜', shortName: 'MC' },
{ name: 'mega-mixed-berries', title: 'mega mixed berries', titleCn: '超级混合浆果', shortName: 'ME' },
{ name: 'melon-&-mint', title: 'melon & mint', titleCn: '瓜与薄荷', shortName: 'ME' },
{ name: 'melon-ice', title: 'melon ice', titleCn: '瓜冰', shortName: 'ME' },
{ name: 'menthol', title: 'menthol', titleCn: '薄荷', shortName: 'ME' },
{ name: 'menthol-ice', title: 'menthol ice', titleCn: '薄荷冰', shortName: 'ME' },
{ name: 'mexican-mango-ice', title: 'mexican mango ice', titleCn: '墨西哥芒果冰', shortName: 'ME' },
{ name: 'miami-mint', title: 'miami mint', titleCn: '迈阿密薄荷', shortName: 'MI' },
{ name: 'mint', title: 'mint', titleCn: '薄荷', shortName: 'MI' },
{ name: 'mint-energy', title: 'mint energy', titleCn: '薄荷 能量', shortName: 'MI' },
{ name: 'mint-tobacco', title: 'mint tobacco', titleCn: '薄荷烟草', shortName: 'MI' },
{ name: 'mirage', title: 'mirage', titleCn: '海市蜃楼', shortName: 'MI' },
{ name: 'mix-berries', title: 'mix berries', titleCn: '混合浆果', shortName: 'MI' },
{ name: 'mixed-barries', title: 'mixed barries', titleCn: '混合浆果', shortName: 'MI' },
{ name: 'mixed-berry', title: 'mixed berry', titleCn: '混合浆果', shortName: 'MI' },
{ name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' },
{ name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' },
{ name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' },
{ name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' },
{ name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' },
{ name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' },
{ name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' },
{ name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' },
{ name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' },
{ name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' },
{ name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' },
{ name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' },
{ name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' },
{ name: 'orange-citrus', title: 'orange citrus', titleCn: '橙子柑橘', shortName: 'OR' },
{ name: 'orange-fizz-ice', title: 'orange fizz ice', titleCn: '橙子汽水冰', shortName: 'OR' },
{ name: 'orange-ft', title: 'orange ft', titleCn: '橙子 FT', shortName: 'OR' },
{ name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' },
{ name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' },
{ name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' },
{ name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P芬达', shortName: 'OR' },
{ name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' },
{ name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' },
{ name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' },
{ name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' },
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果Popn 桃浆果)', shortName: 'PA' },
{ name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' },
{ name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' },
{ name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' },
{ name: 'passion', title: 'passion', titleCn: '百香果', shortName: 'PA' },
{ name: 'passion-fruit', title: 'passion fruit', titleCn: '百香果冰', shortName: 'PA' },
{ name: 'passion-fruit-mango', title: 'passion fruit mango', titleCn: '百香果芒果', shortName: 'PA' },
{ name: 'passion-fruit-mango-lime', title: 'passion fruit mango lime', titleCn: '百香果芒果青柠', shortName: 'PA' },
{ name: 'passion-guava-grapefruit', title: 'passion guava grapefruit', titleCn: '百香果番石榴葡萄柚', shortName: 'PA' },
{ name: 'patas-pipe', title: 'patas pipe', titleCn: '帕塔烟斗', shortName: 'PA' },
{ name: 'peach', title: 'peach', titleCn: '桃子', shortName: 'PE' },
{ name: 'peach-&-mint', title: 'peach & mint', titleCn: '桃子薄荷', shortName: 'PE' },
{ name: 'peach-bellini', title: 'peach bellini', titleCn: '桃子贝里尼', shortName: 'PE' },
{ name: 'peach-berry', title: 'peach berry', titleCn: '桃子浆果', shortName: 'PE' },
{ name: 'peach-berry-ice', title: 'peach berry ice', titleCn: '桃子浆果冰', shortName: 'PE' },
{ name: 'peach-berry-lime-ice', title: 'peach berry lime ice', titleCn: '桃子浆果青柠冰', shortName: 'PE' },
{ name: 'peach-blossom', title: 'peach blossom', titleCn: '桃花', shortName: 'PE' },
{ name: 'peach-blue-raspberry', title: 'peach blue raspberry', titleCn: '桃子蓝莓覆盆子', shortName: 'PE' },
{ name: 'peach-blue-razz-ice', title: 'peach blue razz ice', titleCn: '桃子蓝覆盆子冰', shortName: 'PE' },
{ name: 'peach-blue-razz-mango-ice', title: 'peach blue razz mango ice', titleCn: '桃子蓝覆盆子芒果冰', shortName: 'PE' },
{ name: 'peach-blue-s', title: 'peach blue s', titleCn: '桃子蓝覆盆子 S', shortName: 'PE' },
{ name: 'peach-ice', title: 'peach ice', titleCn: '桃子冰', shortName: 'PE' },
{ name: 'peach-lychee-ice', title: 'peach lychee ice', titleCn: '桃荔枝冰', shortName: 'PE' },
{ name: 'peach-mango', title: 'peach mango', titleCn: '桃芒果', shortName: 'PE' },
{ name: 'peach-mango-ice', title: 'peach mango ice', titleCn: '桃芒果冰', shortName: 'PE' },
{ name: 'peach-mango-watermelon', title: 'peach mango watermelon', titleCn: '桃芒果西瓜', shortName: 'PE' },
{ name: 'peach-mango-watermelon-ice', title: 'peach mango watermelon ice', titleCn: '桃芒果西瓜冰', shortName: 'PE' },
{ name: 'peach-nectarine-ice', title: 'peach nectarine ice', titleCn: '桃子花蜜冰', shortName: 'PE' },
{ name: 'peach-passion-ice', title: 'peach passion ice', titleCn: '桃子桃冰', shortName: 'PE' },
{ name: 'peach-raspberry', title: 'peach raspberry', titleCn: '桃覆盆子', shortName: 'PE' },
{ name: 'peach-strawberry-ice', title: 'peach strawberry ice', titleCn: '桃草莓冰', shortName: 'PE' },
{ name: 'peach-strawberry-watermelon', title: 'peach strawberry watermelon', titleCn: '桃草莓西瓜', shortName: 'PE' },
{ name: 'peach-watermelon-ice', title: 'peach watermelon ice', titleCn: '桃西瓜冰', shortName: 'PE' },
{ name: 'peach-zing', title: 'peach zing', titleCn: '桃子滋味', shortName: 'PE' },
{ name: 'peaches-cream', title: 'peaches cream', titleCn: '桃子奶油', shortName: 'PE' },
{ name: 'peppered-mint', title: 'peppered mint', titleCn: '胡椒薄荷', shortName: 'PE' },
{ name: 'peppermint', title: 'peppermint', titleCn: '薄荷', shortName: 'PE' },
{ name: 'peppermint-salty', title: 'peppermint salty', titleCn: '薄荷咸味', shortName: 'PE' },
{ name: 'peppermint-storm', title: 'peppermint storm', titleCn: '薄荷风暴', shortName: 'PE' },
{ name: 'pina-blend', title: 'pina blend', titleCn: '菠萝混合', shortName: 'PI' },
{ name: 'pina-colada-ice', title: 'pina colada ice', titleCn: '菠萝椰子冰', shortName: 'PI' },
{ name: 'pineapple', title: 'pineapple', titleCn: '菠萝', shortName: 'PI' },
{ name: 'pineapple-blueberry-kiwi-ice', title: 'pineapple blueberry kiwi ice', titleCn: '菠萝蓝莓奇异果冰', shortName: 'PI' },
{ name: 'pineapple-citrus', title: 'pineapple citrus', titleCn: '菠萝柑橘', shortName: 'PI' },
{ name: 'pineapple-coconut', title: 'pineapple coconut', titleCn: '菠萝椰子', shortName: 'PI' },
{ name: 'pineapple-coconut-ice', title: 'pineapple coconut ice', titleCn: '菠萝椰子冰', shortName: 'PI' },
{ name: 'pineapple-ice', title: 'pineapple ice', titleCn: '菠萝冰', shortName: 'PI' },
{ name: 'pineapple-lemonade', title: 'pineapple lemonade', titleCn: '菠萝柠檬水', shortName: 'PI' },
{ name: 'pineapple-orange-cherry', title: 'pineapple orange cherry', titleCn: '菠萝橙樱桃', shortName: 'PI' },
{ name: 'pink-lemon', title: 'pink lemon', titleCn: '粉柠檬', shortName: 'PI' },
{ name: 'pink-lemon-ice', title: 'pink lemon ice', titleCn: '粉柠檬冰', shortName: 'PI' },
{ name: 'pink-lemonade', title: 'pink lemonade', titleCn: '粉红柠檬水', shortName: 'PI' },
{ name: 'pink-punch', title: 'pink punch', titleCn: '粉红拳', shortName: 'PI' },
{ name: 'polar-chill', title: 'polar chill', titleCn: '极地清凉', shortName: 'PO' },
{ name: 'polar-mint-max', title: 'polar mint max', titleCn: '极地薄荷', shortName: 'PO' },
{ name: 'pomegranate-ice', title: 'pomegranate ice', titleCn: '石榴冰', shortName: 'PO' },
{ name: 'poppin-strawkiwi', title: 'poppin strawkiwi', titleCn: '草莓猕猴', shortName: 'PO' },
{ name: 'prism-ice', title: 'prism ice', titleCn: '棱镜冰', shortName: 'PR' },
{ name: 'punch', title: 'punch', titleCn: '果汁', shortName: 'PU' },
{ name: 'punch-ice', title: 'punch ice', titleCn: '果汁冰', shortName: 'PU' },
{ name: 'pure-tobacco', title: 'pure tobacco', titleCn: '纯烟草', shortName: 'PU' },
{ name: 'puris', title: 'puris', titleCn: '纯味', shortName: 'PU' },
{ name: 'purple-grape', title: 'purple grape', titleCn: '紫葡萄', shortName: 'PU' },
{ name: 'quad-berry', title: 'quad berry', titleCn: '四重浆果', shortName: 'QU' },
{ name: 'queen-soko', title: 'queen soko', titleCn: '女王索科', shortName: 'QU' },
{ name: 'rad-razz-melon-iced', title: 'rad razz melon iced', titleCn: '疯狂覆盆子瓜冰', shortName: 'RA' },
{ name: 'ragin-razz-mango-iced', title: 'ragin razz mango iced', titleCn: '狂暴覆盆子芒果冰', shortName: 'RA' },
{ name: 'rainbow-candy', title: 'rainbow candy', titleCn: '彩虹糖', shortName: 'RA' },
{ name: 'raspberry-blast', title: 'raspberry blast', titleCn: '覆盆子爆炸', shortName: 'RA' },
{ name: 'raspberry-buzz-ice', title: 'raspberry buzz ice', titleCn: '覆盆子嗡嗡冰', shortName: 'RA' },
{ name: 'raspberry-dragon-fruit-ice', title: 'raspberry dragon fruit ice', titleCn: '覆盆子龙果冰', shortName: 'RA' },
{ name: 'raspberry-ice', title: 'raspberry ice', titleCn: '覆盆子冰', shortName: 'RA' },
{ name: 'raspberry-lemon', title: 'raspberry lemon', titleCn: '覆盆子柠檬', shortName: 'RA' },
{ name: 'raspberry-mango-ice', title: 'raspberry mango ice', titleCn: '覆盆子芒果冰', shortName: 'RA' },
{ name: 'raspberry-peach-mango-ice', title: 'raspberry peach mango ice', titleCn: '覆盆子桃芒果冰', shortName: 'RA' },
{ name: 'raspberry-pomegranate', title: 'raspberry pomegranate', titleCn: '覆盆子石榴', shortName: 'RA' },
{ name: 'raspberry-vanilla', title: 'raspberry vanilla', titleCn: '覆盆子香草', shortName: 'RA' },
{ name: 'raspberry-watermelon', title: 'raspberry watermelon', titleCn: '覆盆子西瓜', shortName: 'RA' },
{ name: 'raspberry-watermelon-ice', title: 'raspberry watermelon ice', titleCn: '覆盆子西瓜冰', shortName: 'RA' },
{ name: 'raspberry-zing', title: 'raspberry zing', titleCn: '覆盆子滋味', shortName: 'RA' },
{ name: 'razz-apple-ice', title: 'razz apple ice', titleCn: '覆盆子苹果冰', shortName: 'RA' },
{ name: 'razz-currant-ice', title: 'razz currant ice', titleCn: '红苹果冰', shortName: 'RA' },
{ name: 'red-apple-ice', title: 'red apple ice', titleCn: '红豆', shortName: 'RE' },
{ name: 'red-bean', title: 'red bean', titleCn: '红枣 ', shortName: 'RE' },
{ name: 'red-berry-cherry', title: 'red berry cherry', titleCn: '红浆果樱桃', shortName: 'RE' },
{ name: 'red-date-yg', title: 'red date yg', titleCn: '红枣 Y', shortName: 'RE' },
{ name: 'red-eye-espresso', title: 'red eye espresso', titleCn: '红眼浓缩咖啡', shortName: 'RE' },
{ name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' },
{ name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' },
{ name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' },
{ name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' },
{ name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' },
{ name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' },
{ name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' },
{ name: 'rose-grape', title: 'rose grape', titleCn: '玫瑰葡萄', shortName: 'RO' },
{ name: 'rosemary', title: 'rosemary', titleCn: '迷迭香', shortName: 'RO' },
{ name: 'royal-violet', title: 'royal violet', titleCn: '皇家紫罗兰', shortName: 'RO' },
{ name: 'ruby-berry', title: 'ruby berry', titleCn: '红宝石浆果', shortName: 'RU' },
{ name: 's-apple-ice', title: 's apple ice', titleCn: 'S 苹果冰', shortName: 'SA' },
{ name: 's-watermelon-peach', title: 's watermelon peach', titleCn: 'S 西瓜桃', shortName: 'SW' },
{ name: 'saimiri', title: 'saimiri', titleCn: '卷尾猴', shortName: 'SA' },
{ name: 'sakura-grap', title: 'sakura grap', titleCn: '樱花葡萄', shortName: 'SA' },
{ name: 'sakura-grape', title: 'sakura grape', titleCn: '樱花葡萄', shortName: 'SA' },
{ name: 'salt', title: 'salt', titleCn: '盐', shortName: 'SA' },
{ name: 'salted-caramel', title: 'salted caramel', titleCn: '咸焦糖', shortName: 'SA' },
{ name: 'salty-liquorice', title: 'salty liquorice', titleCn: '咸甘草', shortName: 'SA' },
{ name: 'sanctuary', title: 'sanctuary', titleCn: '避风港', shortName: 'SA' },
{ name: 'savage-strawberry-watermelon-iced', title: 'savage strawberry watermelon iced', titleCn: '狂野草莓西瓜冰', shortName: 'SA' },
{ name: 'shoku', title: 'shoku', titleCn: 'Shoku', shortName: 'SH' },
{ name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' },
{ name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' },
{ name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' },
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC彩虹糖', shortName: 'SK' },
{ name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS酸糖', shortName: 'SL' },
{ name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' },
{ name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' },
{ name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' },
{ name: 'smooth-strawberry', title: 'smooth strawberry', titleCn: '顺滑草莓', shortName: 'SM' },
{ name: 'smooth-tobacco', title: 'smooth tobacco', titleCn: '顺滑烟草', shortName: 'SM' },
{ name: 'snazzy-razz', title: 'snazzy razz', titleCn: '炫酷覆盆子', shortName: 'SN' },
{ name: 'snazzy-s-storm', title: 'snazzy s storm', titleCn: '炫酷风暴', shortName: 'SN' },
{ name: 'snazzy-strawberrry-citrus', title: 'snazzy strawberrry citrus', titleCn: '炫酷草莓柑橘', shortName: 'SN' },
{ name: 'snow-pear', title: 'snow pear', titleCn: '酸梨', shortName: 'SN' },
{ name: 'sour', title: 'sour', titleCn: '酸', shortName: 'SO' },
{ name: 'sour-apple', title: 'sour apple', titleCn: '酸苹果', shortName: 'SO' },
{ name: 'sour-blue-razz', title: 'sour blue razz', titleCn: '酸蓝覆盆子', shortName: 'SO' },
{ name: 'sour-cherry', title: 'sour cherry', titleCn: '酸樱桃', shortName: 'SO' },
{ name: 'sour-lime', title: 'sour lime', titleCn: '酸青柠', shortName: 'SO' },
{ name: 'sour-ruby', title: 'sour ruby', titleCn: '酸红宝石', shortName: 'SO' },
{ name: 'spearmint', title: 'spearmint', titleCn: '留兰香', shortName: 'SP' },
{ name: 'spearmint-blast-ice', title: 'spearmint blast ice', titleCn: '留兰香爆发冰', shortName: 'SP' },
{ name: 'star-coffee', title: 'star coffee', titleCn: '星辰咖啡', shortName: 'ST' },
{ name: 'straw-kiwi-melon-ice', title: 'straw kiwi melon ice', titleCn: '草莓奇异果瓜冰', shortName: 'ST' },
{ name: 'strawanna-ice', title: 'strawanna ice', titleCn: '草莓香蕉冰', shortName: 'ST' },
{ name: 'strawberry', title: 'strawberry', titleCn: '草莓', shortName: 'ST' },
{ name: 'strawberry-&-watermelon', title: 'strawberry & watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
{ name: 'strawberry-apple-grape', title: 'strawberry apple grape', titleCn: '草莓苹果葡萄', shortName: 'ST' },
{ name: 'strawberry-apricot-ice', title: 'strawberry apricot ice', titleCn: '草莓杏子冰', shortName: 'ST' },
{ name: 'strawberry-banana', title: 'strawberry banana', titleCn: '草莓香蕉', shortName: 'ST' },
{ name: 'strawberry-banana-ice', title: 'strawberry banana ice', titleCn: '草莓香蕉冰', shortName: 'ST' },
{ name: 'strawberry-banana-mango-ice', title: 'strawberry banana mango ice', titleCn: '草莓香蕉芒果冰', shortName: 'ST' },
{ name: 'strawberry-berry', title: 'strawberry berry', titleCn: '草莓浆果', shortName: 'ST' },
{ name: 'strawberry-burst-ice', title: 'strawberry burst ice', titleCn: '草莓爆发冰', shortName: 'ST' },
{ name: 'strawberry-cherry-lemon', title: 'strawberry cherry lemon', titleCn: '草莓樱桃柠檬', shortName: 'ST' },
{ name: 'strawberry-dragon-fruit', title: 'strawberry dragon fruit', titleCn: '草莓龙果', shortName: 'ST' },
{ name: 'strawberry-ft', title: 'strawberry ft', titleCn: '草莓 FT', shortName: 'ST' },
{ name: 'strawberry-grapefruit', title: 'strawberry grapefruit', titleCn: '草莓葡萄柚', shortName: 'ST' },
{ name: 'strawberry-ice', title: 'strawberry ice', titleCn: '草莓冰', shortName: 'ST' },
{ name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' },
{ name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' },
{ name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' },
{ name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' },
{ name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' },
{ name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' },
{ name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' },
{ name: 'strawberry-lemon', title: 'strawberry lemon', titleCn: '草莓柠檬', shortName: 'ST' },
{ name: 'strawberry-lime-ice', title: 'strawberry lime ice', titleCn: '草莓青柠冰', shortName: 'ST' },
{ name: 'strawberry-lychee-ice', title: 'strawberry lychee ice', titleCn: '草莓荔枝冰', shortName: 'ST' },
{ name: 'strawberry-mango-ice', title: 'strawberry mango ice', titleCn: '草莓芒果冰', shortName: 'ST' },
{ name: 'strawberry-mint', title: 'strawberry mint', titleCn: '草莓薄荷', shortName: 'ST' },
{ name: 'strawberry-orange', title: 'strawberry orange', titleCn: '草莓橙', shortName: 'ST' },
{ name: 'strawberry-peach-mint', title: 'strawberry peach mint', titleCn: '草莓桃薄荷', shortName: 'ST' },
{ name: 'strawberry-raspberry', title: 'strawberry raspberry', titleCn: '草莓覆盆子', shortName: 'ST' },
{ name: 'strawberry-twist-ice', title: 'strawberry twist ice', titleCn: '草莓扭转冰', shortName: 'ST' },
{ name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
{ name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' },
{ name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' },
{ name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' },
{ name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' },
{ name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' },
{ name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' },
{ name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' },
{ name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' },
{ name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' },
{ name: 'sweet-blackcurrant', title: 'sweet blackcurrant', titleCn: '甜黑加仑', shortName: 'SW' },
{ name: 'sweet-mint', title: 'sweet mint', titleCn: '甜薄荷', shortName: 'SW' },
{ name: 't-berries', title: 't berries', titleCn: 'T 浆果', shortName: 'TB' },
{ name: 'taste-of-gods-x', title: 'taste of gods x', titleCn: '神之味 X', shortName: 'TA' },
{ name: 'the-prophet', title: 'the prophet', titleCn: '先知', shortName: 'TH' },
{ name: 'tiki-punch-ice', title: 'tiki punch ice', titleCn: 'Tiki 冲击冰', shortName: 'TI' },
{ name: 'triple-berry', title: 'triple berry', titleCn: '三重浆果', shortName: 'TR' },
{ name: 'triple-berry-ice', title: 'triple berry ice', titleCn: '三重浆果冰', shortName: 'TR' },
{ name: 'triple-mango', title: 'triple mango', titleCn: '三重芒果', shortName: 'TR' },
{ name: "trippin'-triple-berry", title: "trippin' triple berry", titleCn: '三重浆果旋风', shortName: 'TR' },
{ name: 'tropical', title: 'tropical', titleCn: '热带', shortName: 'TR' },
{ name: 'tropical-burst-ice', title: 'tropical burst ice', titleCn: '热带爆发冰', shortName: 'TR' },
{ name: 'tropical-mango', title: 'tropical mango', titleCn: '热带芒果', shortName: 'TR' },
{ name: 'tropical-mango-ice', title: 'tropical mango ice', titleCn: '热带芒果冰', shortName: 'TR' },
{ name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' },
{ name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' },
{ name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' },
{ name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' },
{ name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' },
{ name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' },
{ name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' },
{ name: 'twisted-apple', title: 'twisted apple', titleCn: '扭苹果', shortName: 'TW' },
{ name: 'twisted-pineapple', title: 'twisted pineapple', titleCn: '扭菠萝', shortName: 'TW' },
{ name: 'ultra-fresh-mint', title: 'ultra fresh mint', titleCn: '极新鲜薄荷', shortName: 'UL' },
{ name: 'vanilla', title: 'vanilla', titleCn: '香草', shortName: 'VA' },
{ name: 'vanilla-classic', title: 'vanilla classic', titleCn: '香草经典', shortName: 'VA' },
{ name: 'vanilla-classic-cola', title: 'vanilla classic cola', titleCn: '香草经典可乐', shortName: 'VA' },
{ name: 'vanilla-classic-red', title: 'vanilla classic red', titleCn: '香草经典红', shortName: 'VA' },
{ name: 'vanilla-tobacco', title: 'vanilla tobacco', titleCn: '香草烟草', shortName: 'VA' },
{ name: 'vb-arctic-berry', title: 'vb arctic berry', titleCn: 'VB 北极浆果', shortName: 'VB' },
{ name: 'vb-arctic-mint', title: 'vb arctic mint', titleCn: 'VB 北极薄荷', shortName: 'VB' },
{ name: 'vb-spearmint-salty', title: 'vb spearmint salty', titleCn: 'VB 留兰香咸味', shortName: 'VB' },
{ name: 'vc-delight', title: 'vc delight', titleCn: 'VC 美味', shortName: 'VC' },
{ name: 'vintage', title: 'vintage', titleCn: '复古', shortName: 'VI' },
{ name: 'violet-licorice', title: 'violet licorice', titleCn: '紫罗兰甘草', shortName: 'VI' },
{ name: 'watermelon', title: 'watermelon', titleCn: '西瓜', shortName: 'WA' },
{ name: 'watermelon-bbg', title: 'watermelon bbg', titleCn: '西瓜 BBG', shortName: 'WA' },
{ name: 'watermelon-bubble-gum', title: 'watermelon bubble gum', titleCn: '西瓜泡泡糖', shortName: 'WA' },
{ name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' },
{ name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' },
{ name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' },
{ name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' },
{ name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' },
{ name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' },
{ name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' },
{ name: 'weekend-watermelon', title: 'weekend watermelon', titleCn: '周末西瓜', shortName: 'WE' },
{ name: 'weekend-watermelon-iced', title: 'weekend watermelon iced', titleCn: '周末西瓜冰', shortName: 'WE' },
{ name: 'white-grape', title: 'white grape', titleCn: '白葡萄', shortName: 'WH' },
{ name: 'white-grape-ice', title: 'white grape ice', titleCn: '白葡萄冰', shortName: 'WH' },
{ name: 'white-ice', title: 'white ice', titleCn: '白冰', shortName: 'WH' },
{ name: 'white-peach-ice', title: 'white peach ice', titleCn: '白桃冰', shortName: 'WH' },
{ name: 'white-peach-splash', title: 'white peach splash', titleCn: '白桃飞溅', shortName: 'WH' },
{ name: 'white-peach-yaklt', title: 'white peach yaklt', titleCn: '白桃益菌乳', shortName: 'WH' },
{ name: 'wicked-white-peach', title: 'wicked white peach', titleCn: '邪恶白桃', shortName: 'WI' },
{ name: 'wild-blue-raspberry', title: 'wild blue raspberry', titleCn: '野生蓝覆盆子', shortName: 'WI' },
{ name: 'wild-blueberry-ice', title: 'wild blueberry ice', titleCn: '野生蓝莓冰', shortName: 'WI' },
{ name: 'wild-cherry-cola', title: 'wild cherry cola', titleCn: '野樱桃可乐', shortName: 'WI' },
{ name: 'wild-dragonfruit-lychee', title: 'wild dragonfruit lychee', titleCn: '野生龙果荔枝', shortName: 'WI' },
{ name: 'wild-strawberry-banana', title: 'wild strawberry banana', titleCn: '野生草莓香蕉', shortName: 'WI' },
{ name: 'wild-strawberry-ice', title: 'wild strawberry ice', titleCn: '野生草莓冰', shortName: 'WI' },
{ name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' },
{ name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' },
{ name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' },
{ name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' },
{ name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' },
{ name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' },
{ name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' },
{ name: 'wintery-watermelon', title: 'wintery watermelon', titleCn: '冬季西瓜', shortName: 'WI' },
{ name: 'woke-watermelon-tropica-iced', title: 'woke watermelon tropica iced', titleCn: '觉醒西瓜热带冰', shortName: 'WO' },
{ name: 'wonder', title: 'wonder', titleCn: '奇迹', shortName: 'WO' },
{ name: 'x-freeze', title: 'x freeze', titleCn: 'X 冰冻', shortName: 'XF' },
{ name: 'zen', title: 'zen', titleCn: '禅', shortName: 'ZE' },
{ name: 'zest-flame', title: 'zest flame', titleCn: '清新火焰', shortName: 'ZE' },
{ name: 'zesty-elderflower', title: 'zesty elderflower', titleCn: '活力接骨木花', shortName: 'ZE' },
{ name: 'zingy-eucalyptus', title: 'zingy eucalyptus', titleCn: '清爽桉树', shortName: 'ZI' },
];
// Total flavors: 655
// 强度数据
const strengthsData = [
{ name: '1.5mg', title: '1.5mg', titleCn: '1.5毫克', shortName: '1.5' },
{ name: '2mg', title: '2mg', titleCn: '2毫克', shortName: '2MG' },
{ name: '3mg', title: '3mg', titleCn: '3毫克', shortName: '3MG' },
{ name: '3.5mg', title: '3.5mg', titleCn: '3.5毫克', shortName: '3.5' },
{ name: '4mg', title: '4mg', titleCn: '4毫克', shortName: '4MG' },
{ name: '5,2 mg', title: '5,2 mg', titleCn: '5,2毫克', shortName: '5,2' },
{ name: '5.6mg', title: '5.6mg', titleCn: '5.6毫克', shortName: '5.6' },
{ name: '6mg', title: '6mg', titleCn: '6毫克', shortName: '6MG' },
{ name: '6.5mg', title: '6.5mg', titleCn: '6.5毫克', shortName: '6.5' },
{ name: '8mg', title: '8mg', titleCn: '8毫克', shortName: '8MG' },
{ name: '9mg', title: '9mg', titleCn: '9毫克', shortName: '9MG' },
{ name: '10mg', title: '10mg', titleCn: '10毫克', shortName: '10M' },
{ name: '10,4 mg', title: '10,4 mg', titleCn: '10,4 毫克', shortName: '10,' },
{ name: '10,9mg', title: '10,9mg', titleCn: '10,9毫克', shortName: '10,' },
{ name: '11mg', title: '11mg', titleCn: '11毫克', shortName: '11M' },
{ name: '12mg', title: '12mg', titleCn: '12毫克', shortName: '12M' },
{ name: '12.5mg', title: '12.5mg', titleCn: '12.5毫克', shortName: '12.' },
{ name: '13.5mg', title: '13.5mg', titleCn: '13.5毫克', shortName: '13.' },
{ name: '14mg', title: '14mg', titleCn: '14毫克', shortName: '14M' },
{ name: '15mg', title: '15mg', titleCn: '15毫克', shortName: '15M' },
{ name: '16mg', title: '16mg', titleCn: '16毫克', shortName: '16M' },
{ name: '16.5mg', title: '16.5mg', titleCn: '16.5毫克', shortName: '16.' },
{ name: '16.6mg', title: '16.6mg', titleCn: '16.6毫克', shortName: '16.' },
{ name: '17mg', title: '17mg', titleCn: '17毫克', shortName: '17M' },
{ name: '18mg', title: '18mg', titleCn: '18毫克', shortName: '18M' },
{ name: '20mg', title: '20mg', titleCn: '20毫克', shortName: '20M' },
{ name: '30mg', title: '30mg', titleCn: '30毫克', shortName: '30M' },
{ name: 'extra-strong', title: 'extra strong', titleCn: '超强', shortName: 'EXT' },
{ name: 'low', title: 'low', titleCn: '低', shortName: 'LOW' },
{ name: 'max', title: 'max', titleCn: '最大', shortName: 'MAX' },
{ name: 'medium', title: 'medium', titleCn: '中等', shortName: 'MED' },
{ name: 'normal', title: 'normal', titleCn: '普通', shortName: 'NOR' },
{ name: 'strong', title: 'strong', titleCn: '强', shortName: 'STR' },
{ name: 'super-strong', title: 'super strong', titleCn: '特强', shortName: 'SUP' },
{ name: 'ultra-strong', title: 'ultra strong', titleCn: '极强', shortName: 'ULT' },
{ name: 'xx-strong', title: 'xx strong', titleCn: '超超强', shortName: 'XXS' },
{ name: 'x-intense', title: 'x intense', titleCn: '强', shortName: 'XIN' },
];
// Total strengths: 37
// 品牌数据
const brandsData = [
{ name: 'yoone', title: 'yoone', titleCn: '', shortName: 'YO' },
{ name: 'zyn', title: 'zyn', titleCn: '', shortName: 'ZY' },
{ name: 'on!', title: 'on!', titleCn: '', shortName: 'ON' },
{ name: 'alibarbar', title: 'alibarbar', titleCn: '', shortName: 'AL' },
{ name: 'iget-pro', title: 'iget pro', titleCn: '', shortName: 'IG' },
{ name: 'jux', title: 'jux', titleCn: '', shortName: 'JU' },
{ name: 'velo', title: 'velo', titleCn: '', shortName: 'VE' },
{ name: 'white-fox', title: 'white fox', titleCn: '', shortName: 'WH' },
{ name: 'zolt', title: 'zolt', titleCn: '', shortName: 'ZO' },
{ name: '77', title: '77', titleCn: '', shortName: '77' },
{ name: 'xqs', title: 'xqs', titleCn: '', shortName: 'XQ' },
{ name: 'zex', title: 'zex', titleCn: '', shortName: 'ZE' },
{ name: 'zonnic', title: 'zonnic', titleCn: '', shortName: 'ZO' },
{ name: 'lucy', title: 'Lucy', titleCn: '', shortName: 'LU' },
{ name: 'egp', title: 'EGP', titleCn: '', shortName: 'EG' },
{ name: 'bridge', title: 'Bridge', titleCn: '', shortName: 'BR' },
{ name: 'sesh', title: 'Sesh', titleCn: '', shortName: 'SE' },
{ name: 'pablo', title: 'Pablo', titleCn: '', shortName: 'PA' },
{ name: 'elfbar', title: 'elfbar', titleCn: '', shortName: 'EL' },
{ name: 'chacha', title: 'chacha', titleCn: '', shortName: 'CH' },
{ name: 'yoone-wave', title: 'yoone wave', titleCn: '', shortName: 'YO' },
{ name: 'yoone-e-liquid', title: 'yoone e-liquid', titleCn: '', shortName: 'YO' },
{ name: 'geek-bar', title: 'geek bar', titleCn: '', shortName: 'GE' },
{ name: 'iget-bar', title: 'iget bar', titleCn: '', shortName: 'IG' },
{ name: 'twelve-monkeys', title: 'twelve monkeys', titleCn: '', shortName: 'TW' },
{ name: 'z-pods', title: 'z pods', titleCn: '', shortName: 'ZP' },
{ name: 'yoone-y-pods', title: 'yoone y-pods', titleCn: '', shortName: 'YO' },
{ name: 'allo-e-liquid', title: 'allo e-liquid', titleCn: '', shortName: 'AL' },
{ name: 'allo-ultra', title: 'allo ultra', titleCn: '', shortName: 'AL' },
{ name: 'base-x', title: 'base x', titleCn: '', shortName: 'BA' },
{ name: 'breeze-pro', title: 'breeze pro', titleCn: '', shortName: 'BR' },
{ name: 'deu', title: 'deu', titleCn: '', shortName: 'DE' },
{ name: 'evo', title: 'evo', titleCn: '', shortName: 'EV' },
{ name: 'elf-bar', title: 'elf bar', titleCn: '', shortName: 'EL' },
{ name: 'feed', title: 'feed', titleCn: '', shortName: 'FE' },
{ name: 'flavour-beast', title: 'flavour beast', titleCn: '', shortName: 'FL' },
{ name: 'fog-formulas', title: 'fog formulas', titleCn: '', shortName: 'FO' },
{ name: 'fruitii', title: 'fruitii', titleCn: '', shortName: 'FR' },
{ name: 'gcore', title: 'gcore', titleCn: '', shortName: 'GC' },
{ name: 'gr1nds', title: 'gr1nds', titleCn: '', shortName: 'GR' },
{ name: 'hqd', title: 'hqd', titleCn: '', shortName: 'HQ' },
{ name: 'illusions', title: 'illusions', titleCn: '', shortName: 'IL' },
{ name: 'kraze', title: 'kraze', titleCn: '', shortName: 'KR' },
{ name: 'level-x', title: 'level x', titleCn: '', shortName: 'LE' },
{ name: 'lfgo-energy', title: 'lfgo energy', titleCn: '', shortName: 'LF' },
{ name: 'lost-mary', title: 'lost mary', titleCn: '', shortName: 'LO' },
{ name: 'mr-fog', title: 'mr fog', titleCn: '', shortName: 'MR' },
{ name: 'nicorette', title: 'nicorette', titleCn: '', shortName: 'NI' },
{ name: 'oxbar', title: 'oxbar', titleCn: '', shortName: 'OX' },
{ name: 'rabeats', title: 'rabeats', titleCn: '', shortName: 'RA' },
{ name: 'yoone-vapengin', title: 'yoone vapengin', titleCn: '', shortName: 'YO' },
{ name: 'sesh', title: 'sesh', titleCn: '', shortName: 'SE' },
{ name: 'spin', title: 'spin', titleCn: '', shortName: 'SP' },
{ name: 'stlth', title: 'stlth', titleCn: '', shortName: 'ST' },
{ name: 'tornado', title: 'tornado', titleCn: '', shortName: 'TO' },
{ name: 'uwell', title: 'uwell', titleCn: '', shortName: 'UW' },
{ name: 'vanza', title: 'vanza', titleCn: '', shortName: 'VA' },
{ name: 'vapgo', title: 'vapgo', titleCn: '', shortName: 'VA' },
{ name: 'vase', title: 'vase', titleCn: '', shortName: 'VA' },
{ name: 'vice-boost', title: 'vice boost', titleCn: '', shortName: 'VI' },
{ name: 'vozol-star', title: 'vozol star', titleCn: '', shortName: 'VO' },
{ name: 'zpods', title: 'zpods', titleCn: '', shortName: 'ZP' },
];
// Total brands: 62

View File

@ -0,0 +1,91 @@
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { Template } from '../../entity/template.entity';
/**
* @class TemplateSeeder
* @description ,.
*/
export default class TemplateSeeder implements Seeder {
/**
* @method run
* @description .,;,.
* @param {DataSource} dataSource - , repository.
* @param {SeederFactoryManager} factoryManager - Seeder .
*/
public async run(
dataSource: DataSource,
factoryManager: SeederFactoryManager
): Promise<any> {
// 获取 Template 实体的 repository
const templateRepository = dataSource.getRepository(Template);
const templates = [
{
name: 'product.sku',
value: "<%= [it.category.shortName].concat(it.attributes.map(a => a.shortName)).join('-') %>",
description: '产品SKU模板',
testData: JSON.stringify({
category: {
shortName: 'CAT',
},
attributes: [
{ shortName: 'BR' },
{ shortName: 'FL' },
{ shortName: '10MG' },
{ shortName: 'DRY' },
],
}),
},
{
name: 'product.title',
value: "<%= it.attributes.map(a => a.title).join(' ') %>",
description: '产品标题模板',
testData: JSON.stringify({
attributes: [
{ title: 'Brand' },
{ title: 'Flavor' },
{ title: '10mg' },
{ title: 'Dry' },
],
}),
},
{
name: 'site.product.sku',
value: '<%= it.site.skuPrefix %><%= it.product.sku %>',
description: '站点产品SKU模板',
testData: JSON.stringify({
site: {
skuPrefix: 'SITE-',
},
product: {
sku: 'PRODUCT-SKU-001',
},
}),
},
];
for (const t of templates) {
// 检查模板是否已存在
const existingTemplate = await templateRepository.findOne({
where: { name: t.name },
});
if (existingTemplate) {
// 如果存在,则更新
existingTemplate.value = t.value;
existingTemplate.description = t.description;
existingTemplate.testData = t.testData;
await templateRepository.save(existingTemplate);
} else {
// 如果不存在,则创建并保存
const template = new Template();
template.name = t.name;
template.value = t.value;
template.description = t.description;
template.testData = t.testData;
await templateRepository.save(template);
}
}
}
}

199
src/dto/api.dto.ts Normal file
View File

@ -0,0 +1,199 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
export class UnifiedPaginationDTO<T> {
// 分页DTO用于承载统一分页信息与列表数据
@ApiProperty({ description: '列表数据' })
items: T[];
@ApiProperty({ description: '总数', example: 100 })
total: number;
@ApiProperty({ description: '当前页', example: 1 })
page: number;
@ApiProperty({ description: '每页数量', example: 20 })
per_page: number;
@ApiProperty({ description: '总页数', example: 5 })
totalPages: number;
}
export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
// 统一查询参数DTO用于承载分页与筛选与排序参数
@ApiProperty({ description: '页码', example: 1, required: false })
page?: number;
@ApiProperty({ description: '每页数量', example: 20, required: false })
per_page?: number;
@ApiProperty({ description: '查询时间范围开始', example: '2023-01-01T00:00:00Z', required: false })
after?: string;
@ApiProperty({ description: '查询时间范围结束', example: '2023-01-01T23:59:59Z', required: false })
before?: string;
@ApiProperty({ description: '搜索关键词', required: false })
search?: string;
@ApiProperty({
description: '过滤条件对象',
type: 'any', // FIXME 这里是因为 openapit2ts 会将 where 变成 undefined 所以有嵌套对象时先不指定类型
required: false,
})
where?: Where;
@ApiProperty({
description: '排序对象,例如 { "sku": "desc" }',
type: 'any', // FIXME 这里是因为 openapit2ts 会将 where 变成 undefined 所以有嵌套对象时先不指定类型
required: false,
})
orderBy?: Record<string, 'asc' | 'desc'> | string;
}
/**
*
*/
export interface BatchErrorItem {
// 错误项标识可以是ID、邮箱等
identifier: string;
// 错误信息
error: string;
}
/**
*
*/
export interface BatchOperationResult {
// 总处理数量
total: number;
// 成功处理数量
processed: number;
// 创建数量
created?: number;
// 更新数量
updated?: number;
// 删除数量
deleted?: number;
// 跳过的数量(如数据已存在或无需处理)
skipped?: number;
// 错误列表
errors: BatchErrorItem[];
}
/**
*
*/
export class SyncOperationResult implements BatchOperationResult {
total: number;
processed: number;
created?: number;
updated?: number;
deleted?: number;
skipped?: number;
errors: BatchErrorItem[];
// 同步成功数量
synced: number;
}
/**
* DTO
*/
export class BatchErrorItemDTO {
@ApiProperty({ description: '错误项标识如ID、邮箱等', type: String })
@Rule(RuleType.string().required())
identifier: string;
@ApiProperty({ description: '错误信息', type: String })
@Rule(RuleType.string().required())
error: string;
}
/**
* DTO
*/
export class BatchOperationResultDTO {
@ApiProperty({ description: '总处理数量', type: Number })
total: number;
@ApiProperty({ description: '成功处理数量', type: Number })
processed: number;
@ApiProperty({ description: '创建数量', type: Number, required: false })
created?: number;
@ApiProperty({ description: '更新数量', type: Number, required: false })
updated?: number;
@ApiProperty({ description: '删除数量', type: Number, required: false })
deleted?: number;
@ApiProperty({ description: '跳过的数量', type: Number, required: false })
skipped?: number;
@ApiProperty({ description: '错误列表', type: [BatchErrorItemDTO] })
errors: BatchErrorItemDTO[];
}
/**
* DTO
*/
export class SyncOperationResultDTO extends BatchOperationResultDTO {
@ApiProperty({ description: '同步成功数量', type: Number })
synced: number;
}
/**
* DTO
*/
export class SyncParamsDTO {
@ApiProperty({ description: '页码', type: Number, required: false, default: 1 })
@Rule(RuleType.number().integer().min(1).optional())
page?: number = 1;
@ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 })
@Rule(RuleType.number().integer().min(1).max(1000).optional())
pageSize?: number = 100;
@ApiProperty({ description: '开始时间', type: String, required: false })
@Rule(RuleType.string().optional())
startDate?: string;
@ApiProperty({ description: '结束时间', type: String, required: false })
@Rule(RuleType.string().optional())
endDate?: string;
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
@Rule(RuleType.boolean().optional())
force?: boolean = false;
}
/**
* DTO
*/
export class BatchQueryDTO {
@ApiProperty({ description: 'ID列表', type: [String, Number] })
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required())
ids: Array<string | number>;
@ApiProperty({ description: '包含关联数据', type: Boolean, required: false, default: false })
@Rule(RuleType.boolean().optional())
includeRelations?: boolean = false;
}
/**
*
*/
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
@ApiProperty({ description: '操作成功的数据列表', type: Array })
data?: T[];
}
/**
*
*/
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
@ApiProperty({ description: '同步成功的数据列表', type: Array })
data?: T[];
}

29
src/dto/area.dto.ts Normal file
View File

@ -0,0 +1,29 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
export class CreateAreaDTO {
@ApiProperty({ description: '编码' })
@Rule(RuleType.string().required())
code: string;
}
export class UpdateAreaDTO {
@ApiProperty({ description: '编码', required: false })
@Rule(RuleType.string())
code?: string;
}
export class QueryAreaDTO {
@ApiProperty({ description: '当前页', required: false, default: 1 })
@Rule(RuleType.number().integer().min(1).default(1))
currentPage?: number;
@ApiProperty({ description: '每页数量', required: false, default: 10 })
@Rule(RuleType.number().integer().min(1).default(10))
pageSize?: number;
@ApiProperty({ description: '关键词(名称或编码)', required: false })
@Rule(RuleType.string())
keyword?: string;
}

210
src/dto/batch.dto.ts Normal file
View File

@ -0,0 +1,210 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
/**
*
*/
export interface BatchErrorItem {
// 错误项标识可以是ID、邮箱等
identifier: string;
// 错误信息
error: string;
}
/**
*
*/
export interface BatchOperationResult {
// 总处理数量
total: number;
// 成功处理数量
processed: number;
// 创建数量
created?: number;
// 更新数量
updated?: number;
// 删除数量
deleted?: number;
// 跳过的数量(如数据已存在或无需处理)
skipped?: number;
// 错误列表
errors: BatchErrorItem[];
}
/**
*
*/
export interface SyncOperationResult extends BatchOperationResult {
// 同步成功数量
synced: number;
}
/**
* DTO
*/
export class BatchErrorItemDTO {
@ApiProperty({ description: '错误项标识如ID、邮箱等', type: String })
@Rule(RuleType.string().required())
identifier: string;
@ApiProperty({ description: '错误信息', type: String })
@Rule(RuleType.string().required())
error: string;
}
/**
* DTO
*/
export class BatchOperationResultDTO {
@ApiProperty({ description: '总处理数量', type: Number })
total: number;
@ApiProperty({ description: '成功处理数量', type: Number })
processed: number;
@ApiProperty({ description: '创建数量', type: Number, required: false })
created?: number;
@ApiProperty({ description: '更新数量', type: Number, required: false })
updated?: number;
@ApiProperty({ description: '删除数量', type: Number, required: false })
deleted?: number;
@ApiProperty({ description: '跳过的数量', type: Number, required: false })
skipped?: number;
@ApiProperty({ description: '错误列表', type: [BatchErrorItemDTO] })
errors: BatchErrorItemDTO[];
}
/**
* DTO
*/
export class SyncOperationResultDTO extends BatchOperationResultDTO {
@ApiProperty({ description: '同步成功数量', type: Number })
synced: number;
}
/**
* DTO
*/
export class BatchCreateDTO<T = any> {
@ApiProperty({ description: '要创建的数据列表', type: Array })
@Rule(RuleType.array().required())
items: T[];
}
/**
* DTO
*/
export class BatchUpdateDTO<T = any> {
@ApiProperty({ description: '要更新的数据列表', type: Array })
@Rule(RuleType.array().required())
items: T[];
}
/**
* DTO
*/
export class BatchDeleteDTO {
@ApiProperty({ description: '要删除的ID列表', type: [String, Number] })
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required())
ids: Array<string | number>;
}
/**
* DTO
*/
export class BatchOperationDTO<T = any> {
@ApiProperty({ description: '要创建的数据列表', type: Array, required: false })
@Rule(RuleType.array().optional())
create?: T[];
@ApiProperty({ description: '要更新的数据列表', type: Array, required: false })
@Rule(RuleType.array().optional())
update?: T[];
@ApiProperty({ description: '要删除的ID列表', type: [String, Number], required: false })
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).optional())
delete?: Array<string | number>;
}
/**
* DTO
*/
export class PaginatedBatchOperationDTO<T = any> {
@ApiProperty({ description: '页码', type: Number, required: false, default: 1 })
@Rule(RuleType.number().integer().min(1).optional())
page?: number = 1;
@ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 })
@Rule(RuleType.number().integer().min(1).max(1000).optional())
pageSize?: number = 100;
@ApiProperty({ description: '要创建的数据列表', type: Array, required: false })
@Rule(RuleType.array().optional())
create?: T[];
@ApiProperty({ description: '要更新的数据列表', type: Array, required: false })
@Rule(RuleType.array().optional())
update?: T[];
@ApiProperty({ description: '要删除的ID列表', type: [String, Number], required: false })
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).optional())
delete?: Array<string | number>;
}
/**
* DTO
*/
export class SyncParamsDTO {
@ApiProperty({ description: '页码', type: Number, required: false, default: 1 })
@Rule(RuleType.number().integer().min(1).optional())
page?: number = 1;
@ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 })
@Rule(RuleType.number().integer().min(1).max(1000).optional())
pageSize?: number = 100;
@ApiProperty({ description: '开始时间', type: String, required: false })
@Rule(RuleType.string().optional())
startDate?: string;
@ApiProperty({ description: '结束时间', type: String, required: false })
@Rule(RuleType.string().optional())
endDate?: string;
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
@Rule(RuleType.boolean().optional())
force?: boolean = false;
}
/**
* DTO
*/
export class BatchQueryDTO {
@ApiProperty({ description: 'ID列表', type: [String, Number] })
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required())
ids: Array<string | number>;
@ApiProperty({ description: '包含关联数据', type: Boolean, required: false, default: false })
@Rule(RuleType.boolean().optional())
includeRelations?: boolean = false;
}
/**
*
*/
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
@ApiProperty({ description: '操作成功的数据列表', type: Array })
data?: T[];
}
/**
*
*/
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
@ApiProperty({ description: '同步成功的数据列表', type: Array })
data?: T[];
}

View File

@ -1,38 +1,364 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { UnifiedSearchParamsDTO } from './api.dto';
import { Customer } from '../entity/customer.entity';
export class QueryCustomerListDTO { // 客户基本信息DTO用于响应
@ApiProperty() export class CustomerDTO extends Customer{
current: string; @ApiProperty({ description: '客户ID' })
id: number;
@ApiProperty() @ApiProperty({ description: '站点ID', required: false })
pageSize: string; site_id: number;
@ApiProperty() @ApiProperty({ description: '原始ID', required: false })
origin_id: string;
@ApiProperty({ description: '站点创建时间', required: false })
site_created_at: Date;
@ApiProperty({ description: '站点更新时间', required: false })
site_updated_at: Date;
@ApiProperty({ description: '邮箱' })
email: string; email: string;
@ApiProperty() @ApiProperty({ description: '名字', required: false })
tags: string; first_name: string;
@ApiProperty() @ApiProperty({ description: '姓氏', required: false })
sorterKey: string; last_name: string;
@ApiProperty() @ApiProperty({ description: '全名', required: false })
sorterValue: string; fullname: string;
@ApiProperty() @ApiProperty({ description: '用户名', required: false })
state: string; username: string;
@ApiProperty() @ApiProperty({ description: '电话', required: false })
first_purchase_date: string; phone: string;
@ApiProperty() @ApiProperty({ description: '头像URL', required: false })
customerId: number; avatar: string;
@ApiProperty({ description: '账单信息', type: 'object', required: false })
billing: any;
@ApiProperty({ description: '配送信息', type: 'object', required: false })
shipping: any;
@ApiProperty({ description: '原始数据', type: 'object', required: false })
raw: any;
@ApiProperty({ description: '创建时间' })
created_at: Date;
@ApiProperty({ description: '更新时间' })
updated_at: Date;
@ApiProperty({ description: '评分' })
rate: number;
@ApiProperty({ description: '标签列表', type: [String], required: false })
tags: string[];
} }
export class CustomerTagDTO { // ====================== 单条操作 ======================
@ApiProperty()
// 创建客户请求DTO
export class CreateCustomerDTO {
@ApiProperty({ description: '站点ID' })
site_id: number;
@ApiProperty({ description: '原始ID', required: false })
origin_id?: string;
@ApiProperty({ description: '邮箱' })
email: string; email: string;
@ApiProperty() @ApiProperty({ description: '名字', required: false })
first_name?: string;
@ApiProperty({ description: '姓氏', required: false })
last_name?: string;
@ApiProperty({ description: '全名', required: false })
fullname?: string;
@ApiProperty({ description: '用户名', required: false })
username?: string;
@ApiProperty({ description: '电话', required: false })
phone?: string;
@ApiProperty({ description: '头像URL', required: false })
avatar?: string;
@ApiProperty({ description: '账单信息', type: 'object', required: false })
billing?: any;
@ApiProperty({ description: '配送信息', type: 'object', required: false })
shipping?: any;
@ApiProperty({ description: '原始数据', type: 'object', required: false })
raw?: any;
@ApiProperty({ description: '评分', required: false })
rate?: number;
@ApiProperty({ description: '标签列表', type: [String], required: false })
tags?: string[];
@ApiProperty({ description: '站点创建时间', required: false })
site_created_at?: Date;
@ApiProperty({ description: '站点更新时间', required: false })
site_updated_at?: Date;
}
// 更新客户请求DTO
export class UpdateCustomerDTO {
@ApiProperty({ description: '站点ID', required: false })
site_id?: number;
@ApiProperty({ description: '原始ID', required: false })
origin_id?: string;
@ApiProperty({ description: '邮箱', required: false })
email?: string;
@ApiProperty({ description: '名字', required: false })
first_name?: string;
@ApiProperty({ description: '姓氏', required: false })
last_name?: string;
@ApiProperty({ description: '全名', required: false })
fullname?: string;
@ApiProperty({ description: '用户名', required: false })
username?: string;
@ApiProperty({ description: '电话', required: false })
phone?: string;
@ApiProperty({ description: '头像URL', required: false })
avatar?: string;
@ApiProperty({ description: '账单信息', type: 'object', required: false })
billing?: any;
@ApiProperty({ description: '配送信息', type: 'object', required: false })
shipping?: any;
@ApiProperty({ description: '原始数据', type: 'object', required: false })
raw?: any;
@ApiProperty({ description: '评分', required: false })
rate?: number;
@ApiProperty({ description: '标签列表', type: [String], required: false })
tags?: string[];
}
// 查询单个客户响应DTO继承基本信息
export class GetCustomerDTO extends CustomerDTO {
// 可以添加额外的详细信息字段
}
// 客户统计信息DTO包含订单统计
export class CustomerStatisticDTO extends CustomerDTO {
@ApiProperty({ description: '创建日期' })
date_created: Date;
@ApiProperty({ description: '首次购买日期' })
first_purchase_date: Date;
@ApiProperty({ description: '最后购买日期' })
last_purchase_date: Date;
@ApiProperty({ description: '订单数量' })
orders: number;
@ApiProperty({ description: '总消费金额' })
total: number;
@ApiProperty({ description: 'Yoone订单数量', required: false })
yoone_orders?: number;
@ApiProperty({ description: 'Yoone总金额', required: false })
yoone_total?: number;
}
// 客户统计查询条件DTO
export class CustomerStatisticWhereDTO {
@ApiProperty({ description: '邮箱筛选', required: false })
email?: string;
@ApiProperty({ description: '标签筛选', required: false })
tags?: string;
@ApiProperty({ description: '首次购买日期筛选', required: false })
first_purchase_date?: string;
@ApiProperty({ description: '评分筛选', required: false })
rate?: number;
@ApiProperty({ description: '客户ID筛选', required: false })
customerId?: number;
}
// 客户统计查询参数DTO继承通用查询参数
export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO<CustomerStatisticWhereDTO>;
// 客户统计列表响应DTO
export class CustomerStatisticListResponseDTO {
@ApiProperty({ description: '客户统计列表', type: [CustomerStatisticDTO] })
items: CustomerStatisticDTO[];
@ApiProperty({ description: '总数', example: 100 })
total: number;
@ApiProperty({ description: '当前页', example: 1 })
current: number;
@ApiProperty({ description: '每页数量', example: 20 })
pageSize: number;
}
// ====================== 批量操作 ======================
// 批量创建客户请求DTO
export class BatchCreateCustomerDTO {
@ApiProperty({ description: '客户列表', type: [CreateCustomerDTO] })
customers: CreateCustomerDTO[];
}
// 单个客户更新项DTO
export class UpdateCustomerItemDTO {
@ApiProperty({ description: '客户ID' })
id: number;
@ApiProperty({ description: '更新字段', type: UpdateCustomerDTO })
update_data: Partial<Customer>;
}
// 批量更新客户请求DTO - 每个对象包含id和要更新的字段
export class BatchUpdateCustomerDTO {
@ApiProperty({ description: '客户更新列表', type: [UpdateCustomerItemDTO] })
customers: UpdateCustomerItemDTO[];
}
// 批量删除客户请求DTO
export class BatchDeleteCustomerDTO {
@ApiProperty({ description: '客户ID列表', type: [Number] })
ids: number[];
}
// ====================== 查询操作 ======================
// 客户查询条件DTO用于UnifiedSearchParamsDTO的where参数
export class CustomerWhereDTO {
@ApiProperty({ description: '邮箱筛选', required: false })
email?: string;
@ApiProperty({ description: '标签筛选', required: false })
tags?: string;
@ApiProperty({ description: '评分筛选', required: false })
rate?: number;
@ApiProperty({ description: '站点ID筛选', required: false })
site_id?: number;
@ApiProperty({ description: '客户ID筛选', required: false })
customerId?: number;
@ApiProperty({ description: '首次购买日期筛选', required: false })
first_purchase_date?: string;
@ApiProperty({ description: '角色筛选', required: false })
role?: string;
}
// 客户查询参数DTO继承通用查询参数
export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO<CustomerWhereDTO>;
// 客户列表响应DTO参考site-api.dto.ts中的分页格式
export class CustomerListResponseDTO {
@ApiProperty({ description: '客户列表', type: [CustomerDTO] })
items: CustomerDTO[];
@ApiProperty({ description: '总数', example: 100 })
total: number;
@ApiProperty({ description: '页码', example: 1 })
page: number;
@ApiProperty({ description: '每页数量', example: 20 })
per_page: number;
@ApiProperty({ description: '总页数', example: 5 })
total_pages: number;
}
// ====================== 客户标签相关 ======================
// 客户标签基本信息DTO
export class CustomerTagBasicDTO {
@ApiProperty({ description: '标签ID' })
id: number;
@ApiProperty({ description: '客户ID' })
customer_id: number;
@ApiProperty({ description: '标签名称' })
tag: string;
@ApiProperty({ description: '创建时间', required: false })
created_at?: string;
}
// 添加客户标签请求DTO
export class AddCustomerTagDTO {
@ApiProperty({ description: '客户ID' })
customer_id: number;
@ApiProperty({ description: '标签名称' })
tag: string; tag: string;
} }
// 批量添加客户标签请求DTO
export class BatchAddCustomerTagDTO {
@ApiProperty({ description: '客户ID' })
customer_id: number;
@ApiProperty({ description: '标签列表', type: [String] })
tags: string[];
}
// 删除客户标签请求DTO
export class DeleteCustomerTagDTO {
@ApiProperty({ description: '标签ID' })
tag_id: number;
}
// 批量删除客户标签请求DTO
export class BatchDeleteCustomerTagDTO {
@ApiProperty({ description: '标签ID列表', type: [Number] })
tag_ids: number[];
}
// ====================== 同步操作 ======================
// 同步客户数据请求DTO
export class SyncCustomersDTO {
@ApiProperty({ description: '站点ID' })
siteId: number;
@ApiProperty({ description: '查询参数支持where和orderBy', type: UnifiedSearchParamsDTO, required: false })
params?: UnifiedSearchParamsDTO<CustomerWhereDTO>;
}

62
src/dto/dict.dto.ts Normal file
View File

@ -0,0 +1,62 @@
import { Rule, RuleType } from '@midwayjs/validate';
// 创建字典的数据传输对象
export class CreateDictDTO {
@Rule(RuleType.string().required())
name: string; // 字典名称
@Rule(RuleType.string().required())
title: string; // 字典标题
}
// 更新字典的数据传输对象
export class UpdateDictDTO {
@Rule(RuleType.string())
name?: string; // 字典名称 (可选)
@Rule(RuleType.string())
title?: string; // 字典标题 (可选)
}
// 创建字典项的数据传输对象
export class CreateDictItemDTO {
@Rule(RuleType.string().required())
name: string; // 字典项名称
@Rule(RuleType.string().required())
title: string; // 字典项标题
@Rule(RuleType.string().allow('').allow(null))
titleCN?: string; // 字典项中文标题 (可选)
@Rule(RuleType.string().allow('').allow(null))
image?: string; // 图片 (可选)
@Rule(RuleType.string().allow('').allow(null))
shortName?: string; // 简称 (可选)
@Rule(RuleType.number().required())
dictId: number; // 所属字典的ID
}
// 更新字典项的数据传输对象
export class UpdateDictItemDTO {
@Rule(RuleType.string())
name?: string; // 字典项名称 (可选)
@Rule(RuleType.string())
title?: string; // 字典项标题 (可选)
@Rule(RuleType.string().allow('').allow(null))
titleCN?: string; // 字典项中文标题 (可选)
@Rule(RuleType.string().allow(null))
value?: string; // 字典项值 (可选)
@Rule(RuleType.string().allow('').allow(null))
image?: string; // 图片 (可选)
@Rule(RuleType.string().allow('').allow(null))
shortName?: string; // 简称 (可选)
}

View File

@ -8,7 +8,7 @@ export type PackagingType =
// | PackagingCourierPak // | PackagingCourierPak
// | PackagingEnvelope; // | PackagingEnvelope;
// 定义包装类型的枚举用于 API 文档描述 // 定义包装类型的枚举,用于 API 文档描述
export enum PackagingTypeEnum { export enum PackagingTypeEnum {
Pallet = 'pallet', Pallet = 'pallet',
Package = 'package', Package = 'package',

View File

@ -61,8 +61,8 @@ export class QueryOrderDTO {
externalOrderId: string; externalOrderId: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.number())
siteId: string; siteId: number;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string().allow('')) @Rule(RuleType.string().allow(''))
@ -91,6 +91,10 @@ export class QueryOrderDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
payment_method: string; payment_method: string;
@ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
@Rule(RuleType.bool().default(false))
isSubscriptionOnly?: boolean;
} }
export class QueryOrderSalesDTO { export class QueryOrderSalesDTO {
@ -111,19 +115,19 @@ export class QueryOrderSalesDTO {
pageSize: number; pageSize: number;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.number())
siteId: string; siteId: number;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
name: string; name: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.date().required()) @Rule(RuleType.date())
startDate: Date; startDate: Date;
@ApiProperty() @ApiProperty()
@Rule(RuleType.date().required()) @Rule(RuleType.date())
endDate: Date; endDate: Date;
} }
@ -141,3 +145,37 @@ export class CreateOrderNoteDTO {
@Rule(RuleType.string()) @Rule(RuleType.string())
content: string; content: string;
} }
export class QueryOrderItemDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.number().allow(''))
siteId: number;
@ApiProperty()
@Rule(RuleType.string().allow(''))
name: string; // 商品名称关键字
@ApiProperty()
@Rule(RuleType.string().allow(''))
externalProductId: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
externalVariationId: string;
@ApiProperty()
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date())
endDate: Date;
}

View File

@ -1,5 +1,31 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate'; import { Rule, RuleType } from '@midwayjs/validate';
import { UnifiedSearchParamsDTO } from './api.dto';
/**
* DTO
*/
export class AttributeInputDTO {
@ApiProperty({ description: '属性字典标识', example: 'brand' })
@Rule(RuleType.string())
dictName?: string;
@ApiProperty({ description: '属性值', example: 'ZYN' })
@Rule(RuleType.string())
value?: string;
@ApiProperty({ description: '属性ID', example: 1 })
@Rule(RuleType.number())
id?: number;
@ApiProperty({ description: '属性名称', example: 'ZYN' })
@Rule(RuleType.string())
name?: string;
@ApiProperty({ description: '属性显示名称', example: 'ZYN' })
@Rule(RuleType.string())
title?: string;
}
/** /**
* DTO * DTO
@ -13,162 +39,387 @@ export class CreateProductDTO {
@Rule(RuleType.string().required().empty({ message: '产品名称不能为空' })) @Rule(RuleType.string().required().empty({ message: '产品名称不能为空' }))
name: string; name: string;
@ApiProperty({ description: '产品中文名称', required: false })
@Rule(RuleType.string().allow('').optional())
nameCn?: string;
@ApiProperty({ example: '产品描述', description: '产品描述' }) @ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string()) @Rule(RuleType.string())
description: string; description: string;
@ApiProperty({ example: '1', description: '分类 ID' }) @ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
@Rule(RuleType.number()) @Rule(RuleType.string().optional())
categoryId: number; shortDescription?: string;
@ApiProperty() @ApiProperty({ description: '产品 SKU', required: false })
@Rule(RuleType.number())
strengthId: number;
@ApiProperty()
@Rule(RuleType.number())
flavorsId: number;
@ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
humidity: string; sku?: string;
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
@Rule(RuleType.number())
categoryId?: number;
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
@Rule(RuleType.array().items(RuleType.string()).optional())
siteSkus?: string[];
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
// 当 type 为 'single' 时必填,当 type 为 'bundle' 时可选
@ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(
RuleType.array()
.when('type', {
is: 'single',
then: RuleType.array().required(),
otherwise: RuleType.array().optional()
})
)
attributes?: AttributeInputDTO[];
// 商品价格
@ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number())
price?: number;
// 促销价格
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
@Rule(RuleType.number())
promotionPrice?: number;
// 商品类型(默认 single; bundle 需手动设置组成)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
@Rule(RuleType.string().valid('single', 'bundle').default('single'))
type?: string;
// 仅当 type 为 'bundle' 时,才需要提供 components
@ApiProperty({ description: '产品组成', type: 'array', required: false })
@Rule(
RuleType.array()
.items(
RuleType.object({
sku: RuleType.string().required(),
quantity: RuleType.number().required(),
})
)
.when('type', {
is: 'bundle',
then: RuleType.array().required(),
})
)
components?: { sku: string; quantity: number }[];
} }
/** /**
* DTO * DTO
*/ */
export class UpdateProductDTO extends CreateProductDTO { export class UpdateProductDTO {
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' }) @ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
@Rule(RuleType.string()) @Rule(RuleType.string())
name: string; name?: string;
@ApiProperty({ description: '产品中文名称', required: false })
@Rule(RuleType.string().allow('').optional())
nameCn?: string;
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string())
description?: string;
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
@Rule(RuleType.string().optional())
shortDescription?: string;
@ApiProperty({ description: '产品 SKU', required: false })
@Rule(RuleType.string())
sku?: string;
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
@Rule(RuleType.number())
categoryId?: number;
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
@Rule(RuleType.array().items(RuleType.string()).optional())
siteSkus?: string[];
// 商品价格
@ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number())
price?: number;
// 促销价格
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
@Rule(RuleType.number())
promotionPrice?: number;
// 属性更新(可选, 支持增量替换指定字典的属性项)
@ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(RuleType.array())
attributes?: AttributeInputDTO[];
// 商品类型(single 或 bundle)
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
@Rule(RuleType.string().valid('single', 'bundle'))
type?: string;
// 仅当 type 为 'bundle' 时,才需要提供 components
@ApiProperty({ description: '产品组成', type: 'array', required: false })
@Rule(
RuleType.array()
.items(
RuleType.object({
sku: RuleType.string().required(),
quantity: RuleType.number().required(),
})
)
.when('type', {
is: 'bundle',
then: RuleType.array().optional(),
})
)
components?: { sku: string; quantity: number }[];
}
/**
* DTO
*/
export class BatchUpdateProductDTO {
@ApiProperty({ description: '产品ID列表', type: 'array', required: true })
@Rule(RuleType.array().items(RuleType.number()).required().min(1))
ids: number[];
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称', required: false })
@Rule(RuleType.string().optional())
name?: string;
@ApiProperty({ description: '产品中文名称', required: false })
@Rule(RuleType.string().allow('').optional())
nameCn?: string;
@ApiProperty({ example: '产品描述', description: '产品描述', required: false })
@Rule(RuleType.string().optional())
description?: string;
@ApiProperty({ example: '产品简短描述', description: '产品简短描述', required: false })
@Rule(RuleType.string().optional())
shortDescription?: string;
@ApiProperty({ description: '产品 SKU', required: false })
@Rule(RuleType.string().optional())
sku?: string;
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
@Rule(RuleType.number().optional())
categoryId?: number;
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
@Rule(RuleType.array().items(RuleType.string()).optional())
siteSkus?: string[];
@ApiProperty({ description: '价格', example: 99.99, required: false })
@Rule(RuleType.number().optional())
price?: number;
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
@Rule(RuleType.number().optional())
promotionPrice?: number;
@ApiProperty({ description: '属性列表', type: 'array', required: false })
@Rule(RuleType.array().optional())
attributes?: AttributeInputDTO[];
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
@Rule(RuleType.string().valid('single', 'bundle').optional())
type?: string;
}
/**
* DTO
*/
export class BatchDeleteProductDTO {
@ApiProperty({ description: '产品ID列表', type: 'array', required: true })
@Rule(RuleType.array().items(RuleType.number()).required().min(1))
ids: number[];
}
/**
* DTO
*/
export class CreateCategoryAttributeDTO {
@ApiProperty({ description: '分类字典项ID', example: 1 })
@Rule(RuleType.number().required())
categoryItemId: number;
@ApiProperty({ description: '属性字典ID列表', example: [2, 3] })
@Rule(RuleType.array().items(RuleType.number()).required())
attributeDictIds: number[];
}
/**
*
*/
export interface ProductWhereFilter {
// 产品ID
id?: number;
// 产品ID列表
ids?: number[];
// SKU
sku?: string;
// SKU列表
skus?: string[];
// 产品名称
name?: string;
// 产品中文名称
nameCn?: string;
// 分类ID
categoryId?: number;
// 分类ID列表
categoryIds?: number[];
// 品牌ID
brandId?: number;
// 品牌ID列表
brandIds?: number[];
// 产品类型
type?: string;
// 价格最小值
minPrice?: number;
// 价格最大值
maxPrice?: number;
// 促销价格最小值
minPromotionPrice?: number;
// 促销价格最大值
maxPromotionPrice?: number;
// 创建时间范围开始
createdAtStart?: string;
// 创建时间范围结束
createdAtEnd?: string;
// 更新时间范围开始
updatedAtStart?: string;
// 更新时间范围结束
updatedAtEnd?: string;
}
/**
* DTO
*/
export class ProductWhereFilterDTO {
@ApiProperty({ description: '产品ID', example: 1 })
id?: number;
@ApiProperty({ description: '产品ID列表', example: [1, 2, 3] })
ids?: number[];
@ApiProperty({ description: 'SKU', example: 'ZYN-6MG-WINTERGREEN' })
sku?: string;
@ApiProperty({ description: 'SKU列表', example: ['ZYN-6MG-WINTERGREEN', 'ZYN-3MG-WINTERGREEN'] })
skus?: string[];
@ApiProperty({ description: '产品名称', example: 'ZYN 6MG WINTERGREEN' })
name?: string;
@ApiProperty({ description: '产品中文名称', example: 'ZYN 6毫克 冬清味' })
nameCn?: string;
@ApiProperty({ description: '分类ID', example: 1 })
categoryId?: number;
@ApiProperty({ description: '分类ID列表', example: [1, 2, 3] })
categoryIds?: number[];
@ApiProperty({ description: '品牌ID', example: 1 })
brandId?: number;
@ApiProperty({ description: '品牌ID列表', example: [1, 2, 3] })
brandIds?: number[];
@ApiProperty({ description: '产品类型', example: 'single', enum: ['single', 'bundle'] })
type?: string;
@ApiProperty({ description: '价格最小值', example: 99.99 })
minPrice?: number;
@ApiProperty({ description: '价格最大值', example: 199.99 })
maxPrice?: number;
@ApiProperty({ description: '促销价格最小值', example: 89.99 })
minPromotionPrice?: number;
@ApiProperty({ description: '促销价格最大值', example: 179.99 })
maxPromotionPrice?: number;
@ApiProperty({ description: '创建时间范围开始', example: '2023-01-01 00:00:00' })
createdAtStart?: string;
@ApiProperty({ description: '创建时间范围结束', example: '2023-12-31 23:59:59' })
createdAtEnd?: string;
@ApiProperty({ description: '更新时间范围开始', example: '2023-01-01 00:00:00' })
updatedAtStart?: string;
@ApiProperty({ description: '更新时间范围结束', example: '2023-12-31 23:59:59' })
updatedAtEnd?: string;
} }
/** /**
* DTO * DTO
* where条件
*/ */
export class QueryProductDTO { export class QueryProductDTO extends UnifiedSearchParamsDTO<ProductWhereFilter> {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ example: '1', description: '分类 ID' })
@Rule(RuleType.string())
categoryId: number;
} }
/** /**
* DTO * DTO
*/ */
export class CreateCategoryDTO { export class CreateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true }) @ApiProperty({ description: '分类显示名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' })) @Rule(RuleType.string().required())
title: string;
@ApiProperty({ description: '分类中文名称', required: false })
@Rule(RuleType.string().allow('').optional())
titleCN?: string;
@ApiProperty({ description: '分类唯一标识', required: true })
@Rule(RuleType.string().required())
name: string; name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' })) @ApiProperty({ description: '分类短名称,用于生成SKU', required: false })
unique_key: string; @Rule(RuleType.string().allow('').optional())
shortName?: string;
@ApiProperty({ description: '排序', required: false })
@Rule(RuleType.number().optional())
sort?: number;
} }
/** /**
* DTO * DTO
*/ */
export class UpdateCategoryDTO { export class UpdateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' }) @ApiProperty({ description: '分类显示名称', required: false })
@Rule(RuleType.string()) @Rule(RuleType.string().optional())
name: string; title?: string;
@ApiProperty({ description: '分类中文名称', required: false })
@Rule(RuleType.string().allow('').optional())
titleCN?: string;
@ApiProperty({ description: '分类唯一标识', required: false })
@Rule(RuleType.string().optional())
name?: string;
@ApiProperty({ description: '分类短名称,用于生成SKU', required: false })
@Rule(RuleType.string().allow('').optional())
shortName?: string;
@ApiProperty({ description: '排序', required: false })
@Rule(RuleType.number().optional())
sort?: number;
} }
/**
* DTO
*/
export class QueryCategoryDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class CreateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
export class UpdateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
export class QueryFlavorsDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class CreateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
export class UpdateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
export class QueryStrengthDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class SkuItemDTO {
@ApiProperty({ description: '产品 ID' })
productId: number;
@ApiProperty({ description: 'sku 编码' })
sku: string;
}
export class BatchSetSkuDTO {
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
skus: SkuItemDTO[];
}

View File

@ -1,7 +1,6 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Category } from '../entity/category.entity';
import { Order } from '../entity/order.entity'; import { Order } from '../entity/order.entity';
import { Product } from '../entity/product.entty'; import { Product } from '../entity/product.entity';
import { StockPoint } from '../entity/stock_point.entity'; import { StockPoint } from '../entity/stock_point.entity';
import { PaginatedWrapper } from '../utils/paginated-response.util'; import { PaginatedWrapper } from '../utils/paginated-response.util';
import { import {
@ -12,21 +11,23 @@ import { OrderStatusCountDTO } from './order.dto';
import { SiteConfig } from './site.dto'; import { SiteConfig } from './site.dto';
import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto'; import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto';
import { LoginResDTO } from './user.dto'; import { LoginResDTO } from './user.dto';
import { WpProductDTO } from './wp_product.dto';
import { OrderSale } from '../entity/order_sale.entity'; import { OrderSale } from '../entity/order_sale.entity';
import { Service } from '../entity/service.entity'; import { Service } from '../entity/service.entity';
import { RateDTO } from './freightcom.dto'; import { RateDTO } from './freightcom.dto';
import { ShippingAddress } from '../entity/shipping_address.entity'; import { ShippingAddress } from '../entity/shipping_address.entity';
import { OrderItem } from '../entity/order_item.entity'; import { OrderItem } from '../entity/order_item.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity'; import { OrderRefundItem } from '../entity/order_refund_item.entity';
import { OrderNote } from '../entity/order_note.entity'; import { OrderNote } from '../entity/order_note.entity';
import { PaymentMethodDTO } from './logistics.dto'; import { PaymentMethodDTO } from './logistics.dto';
import { Flavors } from '../entity/flavors.entity'; import { Subscription } from '../entity/subscription.entity';
import { Strength } from '../entity/strength.entity'; import { Dict } from '../entity/dict.entity';
import { SyncOperationResultDTO } from './api.dto';
export class BooleanRes extends SuccessWrapper(Boolean) {} export class BooleanRes extends SuccessWrapper(Boolean) {}
// 同步操作结果返回数据
export class SyncOperationResultRes extends SuccessWrapper(SyncOperationResultDTO) {}
//网站配置返回数据 //网站配置返回数据
export class WpSitesResponse extends SuccessArrayWrapper(SiteConfig) {} export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {}
//产品分页数据 //产品分页数据
export class ProductPaginatedResponse extends PaginatedWrapper(Product) {} export class ProductPaginatedResponse extends PaginatedWrapper(Product) {}
//产品分页返回数据 //产品分页返回数据
@ -34,27 +35,49 @@ export class ProductListRes extends SuccessWrapper(ProductPaginatedResponse) {}
//产品返回数据 //产品返回数据
export class ProductRes extends SuccessWrapper(Product) {} export class ProductRes extends SuccessWrapper(Product) {}
export class ProductsRes extends SuccessArrayWrapper(Product) {} export class ProductsRes extends SuccessArrayWrapper(Product) {}
//产品分类返分页数据 //产品品牌返分页数据
export class CategoryPaginatedResponse extends PaginatedWrapper(Category) {} export class BrandPaginatedResponse extends PaginatedWrapper(Dict) {}
export class FlavorsPaginatedResponse extends PaginatedWrapper(Flavors) {} //产品品牌返分页返回数据
export class StrengthPaginatedResponse extends PaginatedWrapper(Strength) {} export class ProductBrandListRes extends SuccessWrapper(
//产品分类返分页返回数据 BrandPaginatedResponse
export class ProductCatListRes extends SuccessWrapper(
CategoryPaginatedResponse
) {} ) {}
//产品分类返所有数据 //产品品牌返所有数据
export class ProductCatAllRes extends SuccessArrayWrapper(Category) {} export class ProductBrandAllRes extends SuccessArrayWrapper(Dict) {}
//产品分类返回数据 //产品品牌返回数据
export class ProductCatRes extends SuccessWrapper(Category) {} export class ProductBrandRes extends SuccessWrapper(Dict) {}
//产品分页数据 //产品口味返分页数据
export class WpProductPaginatedResponse extends PaginatedWrapper( export class FlavorsPaginatedResponse extends PaginatedWrapper(Dict) {}
WpProductDTO //产品口味返分页返回数据
export class ProductFlavorsListRes extends SuccessWrapper(
FlavorsPaginatedResponse
) {} ) {}
//产品分页返回数据 //产品口味返所有数据
export class WpProductListRes extends SuccessWrapper( export class ProductFlavorsAllRes extends SuccessArrayWrapper(Dict) {}
WpProductPaginatedResponse //产品口味返回数据
export class ProductFlavorsRes extends SuccessWrapper(Dict) {}
//产品规格返分页数据
export class StrengthPaginatedResponse extends PaginatedWrapper(Dict) {}
//产品规格返分页返回数据
export class ProductStrengthListRes extends SuccessWrapper(
StrengthPaginatedResponse
) {} ) {}
//产品规格返所有数据
export class ProductStrengthAllRes extends SuccessArrayWrapper(Dict) {}
//产品规格返返回数据
export class ProductStrengthRes extends SuccessWrapper(Dict) {}
// 产品尺寸返分页数据
export class SizePaginatedResponse extends PaginatedWrapper(Dict) {}
// 产品尺寸返分页返回数据
export class ProductSizeListRes extends SuccessWrapper(
SizePaginatedResponse
) {}
// 产品尺寸返所有数据
export class ProductSizeAllRes extends SuccessArrayWrapper(Dict) {}
// 产品尺寸返回数据
export class ProductSizeRes extends SuccessWrapper(Dict) {}
export class LoginRes extends SuccessWrapper(LoginResDTO) {} export class LoginRes extends SuccessWrapper(LoginResDTO) {}
export class StockPaginatedRespone extends PaginatedWrapper(StockDTO) {} export class StockPaginatedRespone extends PaginatedWrapper(StockDTO) {}
@ -117,3 +140,8 @@ export class OrderDetailRes extends SuccessWrapper(OrderDetail) {}
export class PaymentMethodListRes extends SuccessArrayWrapper( export class PaymentMethodListRes extends SuccessArrayWrapper(
PaymentMethodDTO PaymentMethodDTO
) {} ) {}
// 订阅分页数据(列表 + 总数等分页信息)
export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {}
// 订阅分页返回数据(统一成功包装)
export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {}

454
src/dto/shopyy.dto.ts Normal file
View File

@ -0,0 +1,454 @@
// Shopyy 平台原始数据类型定义
// 仅包含当前映射逻辑所需字段以保持简洁与类型安全
export interface ShopyyTag {
id?: number;
name?: string;
}
// 产品类型
export interface ShopyyProduct {
// 产品主键
id: number;
// 产品名称或标题
name?: string;
title?: string;
// 产品类型
product_type?: string | number;
// 产品状态数值 1为发布 其他为草稿
status: number;
// 变体信息
variant?: {
sku?: string;
price?: string;
};
// 价格
special_price?: string;
price?: string;
// 库存追踪标识 1表示跟踪
inventory_tracking?: number;
// 库存数量
inventory_quantity?: number;
// 图片列表
images?: Array<{
id?: number;
src: string;
alt?: string;
position?: string | number;
}>;
// 主图
image?: {
src: string;
file_name?: string;
alt?: string;
file_size?: number;
width?: number;
height?: number;
id?: number;
position?: number | string;
file_type?: string;
};
// 标签
tags?: ShopyyTag[];
// 变体列表
variants?: ShopyyVariant[];
// 分类集合
collections?: Array<{ id?: number; title?: string }>;
// 规格选项列表
options?: Array<{
id?: number;
position?: number | string;
option_name?: string;
values?: Array<{ option_value?: string; id?: number; position?: number }>;
}>;
// 发布与标识
published_at?: string;
handle?: string;
spu?: string;
// 创建与更新时间
created_at?: string | number;
updated_at?: string | number;
}
// 变体类型
export interface ShopyyVariant {
id: number;
sku?: string;
price?: string;
special_price?: string;
inventory_tracking?: number;
inventory_quantity?: number;
available?: number;
barcode?: string;
weight?: number;
image?: { src: string; id?: number; file_name?: string; alt?: string; position?: number | string };
position?: number | string;
sku_code?: string;
}
// 订单类型
export interface ShopyyOrder {
// 主键与外部ID
id?: number;
order_id?: number;
// 订单号
order_number?: string;
order_sn?: string;
// 状态
status?: number | string;
order_status?: number | string;
// 币种
currency_code?: string;
currency?: string;
// 金额
total_price?: string | number;
total_amount?: string | number;
current_total_price?: string | number;
current_subtotal_price?: string | number;
current_shipping_price?: string | number;
current_tax_price?: string | number;
current_coupon_price?: string | number;
current_payment_price?: string | number;
// 客户ID
customer_id?: number;
user_id?: number;
// 客户信息
customer_name?: string;
firstname?: string;
lastname?: string;
customer_email?: string;
email?: string;
// 地址字段
billing_address?: {
first_name?: string;
last_name?: string;
name?: string;
company?: string;
phone?: string;
address1?: string;
address2?: string;
city?: string;
province?: string;
zip?: string;
country_name?: string;
country_code?: string;
};
shipping_address?: {
first_name?: string;
last_name?: string;
name?: string;
company?: string;
phone?: string;
address1?: string;
address2?: string;
city?: string;
province?: string;
zip?: string;
country_name?: string;
country_code?: string;
} | string;
telephone?: string;
payment_address?: string;
payment_city?: string;
payment_zone?: string;
payment_postcode?: string;
payment_country?: string;
shipping_city?: string;
shipping_zone?: string;
shipping_postcode?: string;
shipping_country?: string;
// 订单项集合
products?: Array<{
id?: number;
name?: string;
product_title?: string;
product_id?: number;
quantity?: number;
price?: string | number;
sku?: string;
sku_code?: string;
}>;
// 支付方式
payment_method?: string;
payment_id?: number;
payment_cards?: Array<{
store_id?: number;
card_len?: number;
card_suffix?: number;
year?: number;
payment_status?: number;
created_at?: number;
month?: number;
updated_at?: number;
payment_id?: number;
payment_interface?: string;
card_prefix?: number;
id?: number;
order_id?: number;
card?: string;
transaction_no?: string;
}>;
fulfillments?: Array<{
// 物流回传状态
payment_tracking_status?: number;
// 备注
note?: string;
// 更新时间
updated_at?: number;
// 追踪接口编号
courier_code?: string;
// 物流公司 id
courier_id?: number;
// 创建时间
created_at?: number;
id?: number;
// 物流单号
tracking_number?: string;
// 物流公司名称
tracking_company?: string;
// 物流回传结果
payment_tracking_result?: string;
// 物流回传时间
payment_tracking_at?: number;
// 商品
products?: Array<{
// 订单商品表 id
order_product_id?: number;
// 数量
quantity?: number;
// 更新时间
updated_at?: number;
// 创建时间
created_at?: number;
// 发货商品表 id
id?: number }>;
}>;
shipping_zone_plans?: Array<{
shipping_price?: number | string;
updated_at?: number;
created_at?: number;
id?: number;
shipping_zone_name?: string;
shipping_zone_id?: number;
shipping_zone_plan_id?: number;
shipping_zone_plan_name?: string;
}>;
transaction?: {
note?: string;
amount?: number | string;
created_at?: number;
merchant_id?: string;
payment_type?: string;
merchant_account?: string;
updated_at?: number;
payment_id?: number;
admin_id?: number;
admin_name?: string;
id?: number;
payment_method?: string;
transaction_no?: string;
};
coupon_code?: string;
coupon_name?: string;
store_id?: number;
visitor_id?: string;
currency_rate?: string | number;
landing_page?: string;
note?: string;
admin_note?: string;
source_device?: string;
checkout_type?: string;
version?: string;
brand_id?: number;
tags?: string[];
financial_status?: number;
fulfillment_status?: number;
// 创建与更新时间可能为时间戳
created_at?: number | string;
date_added?: string;
updated_at?: number | string;
date_updated?: string;
last_modified?: string;
// 支付时间
pay_at?: number | null;
ip?: string;
utm_source?: string;
// 配送方式
shipping_lines?: Array<ShopyyShippingLineDTO>;
// 费用项
fee_lines?: Array<ShopyyFeeLineDTO>;
// 优惠券项
coupon_lines?: Array<ShopyyCouponLineDTO>;
}
export class ShopyyShippingLineDTO {
// 配送方式DTO用于承载统一配送方式数据
id?: string | number;
method_title?: string;
method_id?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}
export class ShopyyFeeLineDTO {
// 费用项DTO用于承载统一费用项数据
id?: string | number;
name?: string;
tax_class?: string;
tax_status?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}
export class ShopyyCouponLineDTO {
// 优惠券项DTO用于承载统一优惠券项数据
id?: string | number;
code?: string;
discount?: string;
discount_tax?: string;
meta_data?: any[];
}
// 客户类型
export interface ShopyyCustomer {
// 主键与兼容ID
id?: number;
customer_id?: number;
// 姓名
first_name?: string;
firstname?: string;
last_name?: string;
lastname?: string;
fullname?: string;
customer_name?: string;
// 联系信息
email?: string;
customer_email?: string;
contact?: string;
phone?: string;
// 地址集合
addresses?: any[];
default_address?: any;
// 国家
country?: { country_name?: string };
// 统计字段
orders_count?: number;
order_count?: number;
orders?: number;
total_spent?: number | string;
total_spend_amount?: number | string;
total_spend_money?: number | string;
// 创建与更新时间可能为时间戳
created_at?: number | string;
date_added?: string;
updated_at?: number | string;
date_updated?: string;
}
// 评论类型
export interface ShopyyReview {
// 主键ID
id: number;
// 产品ID
product_id: number;
// 客户ID
customer_id: number;
// 国家ID
country_id: number;
// IP地址
ip: string;
// 评分星级
star: number;
// 客户名称
customer_name: string;
// 客户邮箱
customer_email: string;
// 回复内容
reply_content: string;
// 评论内容
content: string;
// 状态 1表示正常
status: number;
// 是否包含图片 0表示不包含
is_image: number;
// 图片列表
images: any[];
// 更新时间戳
updated_at: number;
// 创建时间戳
created_at: number;
}
export interface ShopyyWebhookEvent {
id: number;
'event_name': string;
'event_code': string;
"event_decript": string;
isemail_event: number;
email_event_file: string;
email_event_status: number;
is_webhook: number;
is_script_event: number;
created_at: number;
updated_at: number;
}
export interface ShopyyWebhook {
id: number;
"webhook_name": string;
"url": string;
event_id: number;
event_name: string;
event_code: string;
}
// 发货相关DTO
// 批量履行
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
export class ShopyyFulfillmentDTO {
"order_number": string;
"tracking_company": string;
"tracking_number": string;
"courier_code": number;
"note": string;
"mode": "replace" | 'cover' | null// 模式 replace替换 cover (覆盖) 空(新增)
}
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
export class ShopyPartFulfillmentDTO {
order_number: string;
note: string;
tracking_company: string;
tracking_number: string;
courier_code: string;
products: ({
quantity: number,
order_product_id: string
})[]
}
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
export class ShopyyCancelFulfillmentDTO {
order_id: string;
fulfillment_id: string;
}
export class ShopyyBatchFulfillmentItemDTO {
fulfillments: ShopyPartFulfillmentDTO[]
}

987
src/dto/site-api.dto.ts Normal file
View File

@ -0,0 +1,987 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
UnifiedPaginationDTO,
} from './api.dto';
// export class UnifiedOrderWhere{
// []
// }
export enum OrderFulfillmentStatus {
// 未发货
PENDING,
// 部分发货
PARTIALLY_FULFILLED,
// 已发货
FULFILLED,
// 已取消
CANCELLED,
// 确认发货
CONFIRMED,
}
export class UnifiedTagDTO {
// 标签DTO用于承载统一标签数据
@ApiProperty({ description: '标签ID' })
id: number | string;
@ApiProperty({ description: '标签名称' })
name: string;
}
export class UnifiedCategoryDTO {
// 分类DTO用于承载统一分类数据
@ApiProperty({ description: '分类ID' })
id: number | string;
@ApiProperty({ description: '分类名称' })
name: string;
}
export class UnifiedImageDTO {
// 图片DTO用于承载统一图片数据
@ApiProperty({ description: '图片ID' })
id: number | string;
@ApiProperty({ description: '图片URL' })
src: string;
@ApiProperty({ description: '图片名称', required: false })
name?: string;
@ApiProperty({ description: '替代文本', required: false })
alt?: string;
}
export class UnifiedAddressDTO {
// 地址DTO用于承载统一地址数据
@ApiProperty({ description: '名', required: false })
first_name?: string;
@ApiProperty({ description: '姓', required: false })
last_name?: string;
@ApiProperty({ description: '全名', required: false })
fullname?: string;
@ApiProperty({ description: '公司', required: false })
company?: string;
@ApiProperty({ description: '地址1', required: false })
address_1?: string;
@ApiProperty({ description: '地址2', required: false })
address_2?: string;
@ApiProperty({ description: '城市', required: false })
city?: string;
@ApiProperty({ description: '省/州', required: false })
state?: string;
@ApiProperty({ description: '邮政编码', required: false })
postcode?: string;
@ApiProperty({ description: '国家', required: false })
country?: string;
@ApiProperty({ description: '邮箱', required: false })
email?: string;
@ApiProperty({ description: '电话', required: false })
phone?: string;
@ApiProperty({ description: '配送方式', required: false })
method_title?: string;
}
export class UnifiedOrderLineItemDTO {
// 订单项DTO用于承载统一订单项数据
@ApiProperty({ description: '订单项ID' })
id: number | string;
@ApiProperty({ description: '产品名称' })
name: string;
@ApiProperty({ description: '产品ID' })
product_id: number | string;
@ApiProperty({ description: '变体ID', required: false })
variation_id?: number | string;
@ApiProperty({ description: '数量' })
quantity: number;
@ApiProperty({ description: '总计' })
total: string;
@ApiProperty({ description: 'SKU' })
sku: string;
}
export class UnifiedProductAttributeDTO {
// 产品属性DTO用于承载统一产品属性数据
@ApiProperty({ description: '属性ID', required: false })
id?: number | string;
@ApiProperty({ description: '属性名称' })
name: string;
@ApiProperty({ description: '属性位置', example: 0, required: false })
position?: number;
@ApiProperty({ description: '对变体是否可见', example: true, required: false })
visible?: boolean;
@ApiProperty({ description: '是否为变体属性', example: true, required: false })
variation?: boolean;
@ApiProperty({ description: '属性选项', type: [String] })
options: string[];
@ApiProperty({ description: '变体属性值(单个值)', required: false })
option?: string;
}
export class UnifiedProductVariationDTO {
// 产品变体DTO用于承载统一产品变体数据
@ApiProperty({ description: '变体ID' })
id: number | string;
@ApiProperty({ description: '变体名称' })
name: string;
@ApiProperty({ description: '变体SKU' })
sku: string;
@ApiProperty({ description: '常规价格' })
regular_price: string;
@ApiProperty({ description: '销售价格' })
sale_price: string;
@ApiProperty({ description: '当前价格' })
price: string;
@ApiProperty({ description: '库存状态' })
stock_status: string;
@ApiProperty({ description: '库存数量' })
stock_quantity: number;
@ApiProperty({ description: '变体属性', type: () => [UnifiedProductAttributeDTO], required: false })
attributes?: UnifiedProductAttributeDTO[];
@ApiProperty({ description: '变体图片', type: () => UnifiedImageDTO, required: false })
image?: UnifiedImageDTO;
@ApiProperty({ description: '变体描述', required: false })
description?: string;
@ApiProperty({ description: '是否启用', required: false })
enabled?: boolean;
@ApiProperty({ description: '是否可下载', required: false })
downloadable?: boolean;
@ApiProperty({ description: '是否为虚拟商品', required: false })
virtual?: boolean;
@ApiProperty({ description: '管理库存', required: false })
manage_stock?: boolean;
@ApiProperty({ description: '重量', required: false })
weight?: string;
@ApiProperty({ description: '长度', required: false })
length?: string;
@ApiProperty({ description: '宽度', required: false })
width?: string;
@ApiProperty({ description: '高度', required: false })
height?: string;
@ApiProperty({ description: '运输类别', required: false })
shipping_class?: string;
@ApiProperty({ description: '税类别', required: false })
tax_class?: string;
@ApiProperty({ description: '菜单顺序', required: false })
menu_order?: number;
}
export class UnifiedProductDTO {
// 产品DTO用于承载统一产品数据
@ApiProperty({ description: '产品ID' })
id: string | number;
@ApiProperty({ description: '产品名称' })
name: string;
@ApiProperty({ description: '产品类型' })
type: string;
@ApiProperty({ description: '产品状态' })
status: string;
@ApiProperty({ description: '产品SKU' })
sku: string;
@ApiProperty({ description: '常规价格' })
regular_price: string;
@ApiProperty({ description: '销售价格' })
sale_price: string;
@ApiProperty({ description: '当前价格' })
price: string;
@ApiProperty({ description: '库存状态' })
stock_status: string;
@ApiProperty({ description: '库存数量' })
stock_quantity: number;
@ApiProperty({ description: '产品图片', type: () => [UnifiedImageDTO] })
images: UnifiedImageDTO[];
@ApiProperty({ description: '产品标签', type: () => [UnifiedTagDTO], required: false })
tags?: UnifiedTagDTO[];
@ApiProperty({ description: '产品分类', type: () => [UnifiedCategoryDTO], required: false })
categories?: UnifiedCategoryDTO[];
@ApiProperty({ description: '产品属性', type: () => [UnifiedProductAttributeDTO] })
attributes: UnifiedProductAttributeDTO[];
@ApiProperty({
description: '产品变体',
type: () => [UnifiedProductVariationDTO],
required: false,
})
variations?: UnifiedProductVariationDTO[];
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '更新时间' })
date_modified: string;
@ApiProperty({ description: '产品链接', required: false })
permalink?: string;
@ApiProperty({
description: '原始数据(保留备用)',
type: 'object',
required: false,
})
raw?: Record<string, any>;
@ApiProperty({
description: 'ERP产品信息',
type: 'object',
required: false,
})
erpProduct?: {
id: number;
sku: string;
name: string;
nameCn?: string;
category?: any;
attributes?: any[];
components?: any[];
price: number;
promotionPrice: number;
};
}
export class UnifiedOrderRefundDTO {
@ApiProperty({ description: '退款ID' })
id: number | string;
@ApiProperty({ description: '退款原因' })
reason: string;
@ApiProperty({ description: '退款金额' })
total: string;
}
export class UnifiedOrderDTO {
// 订单DTO用于承载统一订单数据
@ApiProperty({ description: '订单ID' })
id: string | number;
@ApiProperty({ description: '订单号' })
number: string;
@ApiProperty({ description: '订单状态' })
status: string;
@ApiProperty({ description: '财务状态',nullable: true })
financial_status?: string;
@ApiProperty({ description: '货币' })
currency: string;
@ApiProperty({ description: '货币符号' })
currency_symbol?: string;
@ApiProperty({ description: '总金额' })
total: string;
@ApiProperty({ description: '客户ID' })
customer_id: number;
@ApiProperty({ description: '客户姓名' })
customer_name: string;
@ApiProperty({ description: '客户邮箱' })
email: string;
@ApiProperty({ description: '客户邮箱' })
customer_email: string;
@ApiProperty({ description: '订单项(具体的商品)', type: () => [UnifiedOrderLineItemDTO] })
line_items: UnifiedOrderLineItemDTO[];
@ApiProperty({
description: '销售项(兼容前端)',
type: () => [UnifiedOrderLineItemDTO],
required: false,
})
sales?: UnifiedOrderLineItemDTO[];
@ApiProperty({ description: '账单地址', type: () => UnifiedAddressDTO })
billing: UnifiedAddressDTO;
@ApiProperty({ description: '收货地址', type: () => UnifiedAddressDTO })
shipping: UnifiedAddressDTO;
@ApiProperty({ description: '账单地址全称', required: false })
billing_full_address?: string;
@ApiProperty({ description: '收货地址全称', required: false })
shipping_full_address?: string;
@ApiProperty({ description: '支付方式' })
payment_method: string;
@ApiProperty({ description: '退款列表', type: () => [UnifiedOrderRefundDTO] })
refunds: UnifiedOrderRefundDTO[];
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '更新时间', required: false })
date_modified?: string;
@ApiProperty({ description: '原始数据', type: 'object', required: false })
raw?: Record<string, any>;
@ApiProperty({ description: '配送方式', type: () => [UnifiedShippingLineDTO], required: false })
shipping_lines?: UnifiedShippingLineDTO[];
@ApiProperty({ description: '费用项', type: () => [UnifiedFeeLineDTO], required: false })
fee_lines?: UnifiedFeeLineDTO[];
@ApiProperty({ description: '优惠券项', type: () => [UnifiedCouponLineDTO], required: false })
coupon_lines?: UnifiedCouponLineDTO[];
@ApiProperty({ description: '支付时间', required: false })
date_paid?: string | null;
@ApiProperty({ description: '客户IP地址', required: false })
customer_ip_address?: string;
@ApiProperty({ description: 'UTM来源', required: false })
utm_source?: string;
@ApiProperty({ description: '设备类型', required: false })
device_type?: string;
@ApiProperty({ description: '来源类型', required: false })
source_type?: string;
@ApiProperty({ description: '订单状态', required: false })
fulfillment_status?: OrderFulfillmentStatus;
// 物流信息
@ApiProperty({ description: '物流信息', type: () => [FulfillmentDTO], required: false })
fulfillments?: FulfillmentDTO[];
}
export class UnifiedShippingLineDTO {
// 配送方式DTO用于承载统一配送方式数据
@ApiProperty({ description: '配送方式ID' })
id?: string | number;
@ApiProperty({ description: '配送方式名称' })
method_title?: string;
@ApiProperty({ description: '配送方式实例ID' })
method_id?: string;
@ApiProperty({ description: '配送方式金额' })
total?: string;
@ApiProperty({ description: '配送方式税额' })
total_tax?: string;
@ApiProperty({ description: '配送方式税额详情' })
taxes?: any[];
@ApiProperty({ description: '配送方式元数据' })
meta_data?: any[];
}
export class UnifiedFeeLineDTO {
// 费用项DTO用于承载统一费用项数据
@ApiProperty({ description: '费用项ID' })
id?: string | number;
@ApiProperty({ description: '费用项名称' })
name?: string;
@ApiProperty({ description: '税率类' })
tax_class?: string;
@ApiProperty({ description: '税率状态' })
tax_status?: string;
@ApiProperty({ description: '总金额' })
total?: string;
@ApiProperty({ description: '总税额' })
total_tax?: string;
@ApiProperty({ description: '税额详情' })
taxes?: any[];
@ApiProperty({ description: '元数据' })
meta_data?: any[];
}
export class UnifiedCouponLineDTO {
// 优惠券项DTO用于承载统一优惠券项数据
@ApiProperty({ description: '优惠券项ID' })
id?: string | number;
@ApiProperty({ description: '优惠券项代码' })
code?: string;
@ApiProperty({ description: '优惠券项折扣' })
discount?: string;
@ApiProperty({ description: '优惠券项税额' })
discount_tax?: string;
@ApiProperty({ description: '优惠券项元数据' })
meta_data?: any[];
}
export class UnifiedCustomerDTO {
// 客户DTO用于承载统一客户数据
@ApiProperty({ description: '客户ID' })
id: string | number;
@ApiProperty({ description: '头像URL', required: false })
avatar?: string;
@ApiProperty({ description: '邮箱' })
email: string;
@ApiProperty({ description: '订单总数', required: false })
orders?: number;
@ApiProperty({ description: '总花费', required: false })
total_spend?: number;
@ApiProperty({ description: '创建时间', required: false })
date_created?: string;
@ApiProperty({ description: '更新时间', required: false })
date_modified?: string;
@ApiProperty({ description: '名', required: false })
first_name?: string;
@ApiProperty({ description: '姓', required: false })
last_name?: string;
@ApiProperty({ description: '名字', required: false })
fullname?: string;
@ApiProperty({ description: '用户名', required: false })
username?: string;
@ApiProperty({ description: '电话', required: false })
phone?: string;
@ApiProperty({
description: '账单地址',
type: () => UnifiedAddressDTO,
required: false,
})
billing?: UnifiedAddressDTO;
@ApiProperty({
description: '收货地址',
type: () => UnifiedAddressDTO,
required: false,
})
shipping?: UnifiedAddressDTO;
@ApiProperty({ description: '原始数据', type: 'object', required: false })
raw?: Record<string, any>;
}
export class UnifiedSubscriptionDTO {
// 订阅DTO用于承载统一订阅数据
@ApiProperty({ description: '订阅ID' })
id: string | number;
@ApiProperty({ description: '订阅状态' })
status: string;
@ApiProperty({ description: '客户ID' })
customer_id: number;
@ApiProperty({ description: '计费周期' })
billing_period: string;
@ApiProperty({ description: '计费间隔' })
billing_interval: number;
@ApiProperty({ description: '创建时间', required: false })
date_created?: string;
@ApiProperty({ description: '更新时间', required: false })
date_modified?: string;
@ApiProperty({ description: '开始时间' })
start_date: string;
@ApiProperty({ description: '下次支付时间' })
next_payment_date: string;
@ApiProperty({ description: '订单项', type: () => [UnifiedOrderLineItemDTO] })
line_items: UnifiedOrderLineItemDTO[];
@ApiProperty({ description: '原始数据', type: 'object', required: false })
raw?: Record<string, any>;
}
export class UnifiedMediaDTO {
// 媒体DTO用于承载统一媒体数据
@ApiProperty({ description: '媒体ID' })
id: number;
@ApiProperty({ description: '标题' })
title: string;
@ApiProperty({ description: '媒体类型' })
media_type: string;
@ApiProperty({ description: 'MIME类型' })
mime_type: string;
@ApiProperty({ description: '源URL' })
source_url: string;
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '更新时间', required: false })
date_modified?: string;
}
export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO<UnifiedProductDTO> {
// 产品分页DTO用于承载产品列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedProductDTO] })
items: UnifiedProductDTO[];
}
export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO<UnifiedOrderDTO> {
// 订单分页DTO用于承载订单列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedOrderDTO] })
items: UnifiedOrderDTO[];
}
export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO<UnifiedCustomerDTO> {
// 客户分页DTO用于承载客户列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedCustomerDTO] })
items: UnifiedCustomerDTO[];
}
export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO<UnifiedSubscriptionDTO> {
// 订阅分页DTO用于承载订阅列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedSubscriptionDTO] })
items: UnifiedSubscriptionDTO[];
}
export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO<UnifiedMediaDTO> {
// 媒体分页DTO用于承载媒体列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedMediaDTO] })
items: UnifiedMediaDTO[];
}
export class UnifiedReviewDTO {
// 评论DTO用于承载统一评论数据
@ApiProperty({ description: '评论ID' })
id: number | string;
@ApiProperty({ description: '产品ID' })
product_id: number | string;
@ApiProperty({ description: '评论者' })
author: string;
@ApiProperty({ description: '评论者邮箱' })
email: string;
@ApiProperty({ description: '评论内容' })
content: string;
@ApiProperty({ description: '评分' })
rating: number;
@ApiProperty({ description: '状态' })
status: string;
@ApiProperty({ description: '创建时间' })
date_created: string;
@ApiProperty({ description: '更新时间', required: false })
date_modified?: string;
@ApiProperty({ description: '原始数据', type: 'object', required: false })
raw?: Record<string, any>;
}
export class UnifiedReviewPaginationDTO extends UnifiedPaginationDTO<UnifiedReviewDTO> {
// 评论分页DTO用于承载评论列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedReviewDTO] })
items: UnifiedReviewDTO[];
}
export class CreateReviewDTO {
@ApiProperty({ description: '产品ID' })
product_id: number | string;
@ApiProperty({ description: '评论内容' })
review: string;
@ApiProperty({ description: '评论者' })
reviewer: string;
@ApiProperty({ description: '评论者邮箱' })
reviewer_email: string;
@ApiProperty({ description: '评分' })
rating: number;
}
export class UpdateReviewDTO {
@ApiProperty({ description: '评论内容', required: false })
review?: string;
@ApiProperty({ description: '评分', required: false })
rating?: number;
@ApiProperty({ description: '状态', required: false })
status?: string;
}
export class UploadMediaDTO {
@ApiProperty({ description: 'Base64 编码的文件内容' })
file: string;
@ApiProperty({ description: '文件名' })
filename: string;
}
export class UnifiedWebhookDTO {
// Webhook DTO用于承载统一webhook数据
@ApiProperty({ description: 'Webhook ID' })
id: number | string;
@ApiProperty({ description: '名称' })
name?: string;
@ApiProperty({ description: '状态' })
status: string;
@ApiProperty({ description: '主题/事件' })
topic: string;
@ApiProperty({ description: '目标URL' })
delivery_url: string;
@ApiProperty({ description: '秘密密钥' })
secret?: string;
@ApiProperty({ description: '创建时间' })
date_created?: string;
@ApiProperty({ description: '更新时间' })
date_modified?: string;
@ApiProperty({ description: '头部信息' })
headers?: Record<string, any>;
@ApiProperty({ description: 'API版本' })
api_version?: string;
}
export class UnifiedWebhookPaginationDTO extends UnifiedPaginationDTO<UnifiedWebhookDTO> {
// Webhook分页DTO用于承载webhook列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedWebhookDTO] })
items: UnifiedWebhookDTO[];
}
export class CreateWebhookDTO {
// 创建Webhook DTO
@ApiProperty({ description: '名称' })
name?: string;
@ApiProperty({ description: '主题/事件' })
topic: string;
@ApiProperty({ description: '目标URL' })
delivery_url: string;
@ApiProperty({ description: '秘密密钥' })
secret?: string;
@ApiProperty({ description: '头部信息' })
headers?: Record<string, any>;
@ApiProperty({ description: 'API版本' })
api_version?: string;
}
export class UpdateWebhookDTO {
// 更新Webhook DTO
@ApiProperty({ description: '名称', required: false })
name?: string;
@ApiProperty({ description: '状态', required: false })
status?: string;
@ApiProperty({ description: '主题/事件', required: false })
topic?: string;
@ApiProperty({ description: '目标URL', required: false })
delivery_url?: string;
@ApiProperty({ description: '秘密密钥', required: false })
secret?: string;
@ApiProperty({ description: '头部信息', required: false })
headers?: Record<string, any>;
@ApiProperty({ description: 'API版本', required: false })
api_version?: string;
}
export class FulfillmentItemDTO {
@ApiProperty({ description: '订单项ID' })
order_item_id: number;
@ApiProperty({ description: '数量' })
quantity: number;
}
export class FulfillmentDTO {
@ApiProperty({ description: '物流单号', required: false })
tracking_number?: string;
@ApiProperty({ description: '物流公司', required: false })
shipping_provider?: string;
@ApiProperty({ description: '发货方式', required: false })
shipping_method?: string;
@ApiProperty({ description: '状态', required: false })
status?: string;
@ApiProperty({ description: '创建时间', required: false })
date_created?: string;
@ApiProperty({ description: '发货商品项', type: () => [FulfillmentItemDTO], required: false })
items?: FulfillmentItemDTO[];
}
export class CancelFulfillmentDTO {
@ApiProperty({ description: '取消原因', required: false })
reason?: string;
@ApiProperty({ description: '发货单ID', required: false })
shipment_id?: string;
}
export class BatchFulfillmentItemDTO {
@ApiProperty({ description: '订单ID' })
order_id: string;
@ApiProperty({ description: '物流单号', required: false })
tracking_number?: string;
@ApiProperty({ description: '物流公司', required: false })
shipping_provider?: string;
@ApiProperty({ description: '发货方式', required: false })
shipping_method?: string;
@ApiProperty({ description: '发货商品项', type: () => [FulfillmentItemDTO], required: false })
items?: FulfillmentItemDTO[];
}
export class CreateVariationDTO {
// 创建产品变体DTO用于承载创建产品变体的请求数据
@ApiProperty({ description: '变体SKU', required: false })
sku?: string;
@ApiProperty({ description: '常规价格', required: false })
regular_price?: string;
@ApiProperty({ description: '销售价格', required: false })
sale_price?: string;
@ApiProperty({ description: '库存状态', required: false })
stock_status?: string;
@ApiProperty({ description: '库存数量', required: false })
stock_quantity?: number;
@ApiProperty({ description: '变体属性', type: () => [UnifiedProductAttributeDTO], required: false })
attributes?: UnifiedProductAttributeDTO[];
@ApiProperty({ description: '变体图片', type: () => UnifiedImageDTO, required: false })
image?: UnifiedImageDTO;
@ApiProperty({ description: '变体描述', required: false })
description?: string;
@ApiProperty({ description: '是否启用', required: false })
enabled?: boolean;
@ApiProperty({ description: '是否可下载', required: false })
downloadable?: boolean;
@ApiProperty({ description: '是否为虚拟商品', required: false })
virtual?: boolean;
@ApiProperty({ description: '管理库存', required: false })
manage_stock?: boolean;
@ApiProperty({ description: '重量', required: false })
weight?: string;
@ApiProperty({ description: '长度', required: false })
length?: string;
@ApiProperty({ description: '宽度', required: false })
width?: string;
@ApiProperty({ description: '高度', required: false })
height?: string;
@ApiProperty({ description: '运输类别', required: false })
shipping_class?: string;
@ApiProperty({ description: '税类别', required: false })
tax_class?: string;
@ApiProperty({ description: '菜单顺序', required: false })
menu_order?: number;
}
export class UpdateVariationDTO {
// 更新产品变体DTO用于承载更新产品变体的请求数据
@ApiProperty({ description: '变体SKU', required: false })
sku?: string;
@ApiProperty({ description: '常规价格', required: false })
regular_price?: string;
@ApiProperty({ description: '销售价格', required: false })
sale_price?: string;
@ApiProperty({ description: '库存状态', required: false })
stock_status?: string;
@ApiProperty({ description: '库存数量', required: false })
stock_quantity?: number;
@ApiProperty({ description: '变体属性', type: () => [UnifiedProductAttributeDTO], required: false })
attributes?: UnifiedProductAttributeDTO[];
@ApiProperty({ description: '变体图片', type: () => UnifiedImageDTO, required: false })
image?: UnifiedImageDTO;
@ApiProperty({ description: '变体描述', required: false })
description?: string;
@ApiProperty({ description: '是否启用', required: false })
enabled?: boolean;
@ApiProperty({ description: '是否可下载', required: false })
downloadable?: boolean;
@ApiProperty({ description: '是否为虚拟商品', required: false })
virtual?: boolean;
@ApiProperty({ description: '管理库存', required: false })
manage_stock?: boolean;
@ApiProperty({ description: '重量', required: false })
weight?: string;
@ApiProperty({ description: '长度', required: false })
length?: string;
@ApiProperty({ description: '宽度', required: false })
width?: string;
@ApiProperty({ description: '高度', required: false })
height?: string;
@ApiProperty({ description: '运输类别', required: false })
shipping_class?: string;
@ApiProperty({ description: '税类别', required: false })
tax_class?: string;
@ApiProperty({ description: '菜单顺序', required: false })
menu_order?: number;
}
export class UnifiedVariationPaginationDTO extends UnifiedPaginationDTO<UnifiedProductVariationDTO> {
// 产品变体分页DTO用于承载产品变体列表分页数据
@ApiProperty({ description: '列表数据', type: () => [UnifiedProductVariationDTO] })
items: UnifiedProductVariationDTO[];
}
export class BatchFulfillmentsDTO {
@ApiProperty({ description: '批量发货订单列表', type: () => [BatchFulfillmentItemDTO] })
orders: BatchFulfillmentItemDTO[];
}
// 订单跟踪号
export class UnifiedOrderTrackingDTO {
@ApiProperty({ description: '订单ID' })
order_id: string;
@ApiProperty({ description: '物流单号' })
tracking_number: string;
@ApiProperty({ description: '物流公司' })
shipping_provider: string;
@ApiProperty({ description: '发货方式' })
shipping_method?: string;
@ApiProperty({ description: '状态' })
status?: string;
@ApiProperty({ description: '创建时间' })
date_created?: string;
}

51
src/dto/site-sync.dto.ts Normal file
View File

@ -0,0 +1,51 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
/**
* SKU信息DTO
*/
export class ProductSiteSkuDTO {
@ApiProperty({ description: '产品ID', example: 1 })
@Rule(RuleType.number().required())
productId: number;
@ApiProperty({ description: '站点SKU',nullable:true, example: 'SKU-001' })
siteSku?: string;
}
/**
* DTO
*/
export class SyncProductToSiteDTO extends ProductSiteSkuDTO {
@ApiProperty({ description: '站点ID', example: 1 })
@Rule(RuleType.number().required())
siteId: number;
}
/**
* DTO
*/
export class SyncProductToSiteResultDTO {
@ApiProperty({ description: '同步状态', example: true })
success: boolean;
@ApiProperty({ description: '远程产品ID', example: '123', required: false })
remoteId?: string;
@ApiProperty({ description: '错误信息', required: false })
error?: string;
}
/**
* DTO
*/
export class BatchSyncProductToSiteDTO {
@ApiProperty({ description: '站点ID', example: 1 })
@Rule(RuleType.number().required())
siteId: number;
@ApiProperty({ description: '产品站点SKU列表', type: [ProductSiteSkuDTO] })
@Rule(RuleType.array().items(RuleType.object()).required().min(1))
data: ProductSiteSkuDTO[];
}

View File

@ -8,7 +8,7 @@ export class SiteConfig {
@ApiProperty({ description: '站点 URL' }) @ApiProperty({ description: '站点 URL' })
@Rule(RuleType.string()) @Rule(RuleType.string())
wpApiUrl: string; apiUrl: string;
@ApiProperty({ description: '站点 rest key' }) @ApiProperty({ description: '站点 rest key' })
@Rule(RuleType.string()) @Rule(RuleType.string())
@ -20,13 +20,145 @@ export class SiteConfig {
@ApiProperty({ description: '站点名' }) @ApiProperty({ description: '站点名' })
@Rule(RuleType.string()) @Rule(RuleType.string())
siteName: string; name: string;
@ApiProperty({ description: '站点邮箱' }) @ApiProperty({ description: '描述' })
@Rule(RuleType.string()) @Rule(RuleType.string().allow('').optional())
email?: string; description?: string;
@ApiProperty({ description: '站点邮箱密码' }) @ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
type: string;
@ApiProperty({ description: 'SKU 前缀' })
@Rule(RuleType.string()) @Rule(RuleType.string())
emailPswd?: string; skuPrefix: string;
}
export class CreateSiteDTO {
@ApiProperty({ description: '站点 API URL', required: false })
@Rule(RuleType.string().optional())
apiUrl?: string;
@ApiProperty({ description: '站点网站 URL', required: false })
@Rule(RuleType.string().optional())
websiteUrl?: string;
@ApiProperty({ description: '站点 REST Key', required: false })
@Rule(RuleType.string().optional())
consumerKey?: string;
@ApiProperty({ description: '站点 REST 秘钥', required: false })
@Rule(RuleType.string().optional())
consumerSecret?: string;
@ApiProperty({ description: '访问令牌', required: false })
@Rule(RuleType.string().optional())
token?: string;
@ApiProperty({ description: '站点名称' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ description: '站点描述', required: false })
@Rule(RuleType.string().allow('').optional())
description?: string;
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false })
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@ApiProperty({ description: 'SKU 前缀', required: false })
@Rule(RuleType.string().optional())
skuPrefix?: string;
// 区域
@ApiProperty({ description: '区域' })
@Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[];
// 绑定仓库
@ApiProperty({ description: '绑定仓库ID列表' })
@Rule(RuleType.array().items(RuleType.number()).optional())
stockPointIds?: number[];
}
export class UpdateSiteDTO {
@ApiProperty({ description: '站点 API URL', required: false })
@Rule(RuleType.string().optional())
apiUrl?: string;
@ApiProperty({ description: '站点 REST Key', required: false })
@Rule(RuleType.string().optional())
consumerKey?: string;
@ApiProperty({ description: '站点 REST 秘钥', required: false })
@Rule(RuleType.string().optional())
consumerSecret?: string;
@ApiProperty({ description: '访问令牌', required: false })
@Rule(RuleType.string().optional())
token?: string;
@ApiProperty({ description: '站点名称', required: false })
@Rule(RuleType.string().optional())
name?: string;
@ApiProperty({ description: '站点描述', required: false })
@Rule(RuleType.string().allow('').optional())
description?: string;
@ApiProperty({ description: '是否禁用', required: false })
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false })
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@ApiProperty({ description: 'SKU 前缀', required: false })
@Rule(RuleType.string().optional())
skuPrefix?: string;
// 区域
@ApiProperty({ description: '区域' })
@Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[];
// 绑定仓库
@ApiProperty({ description: '绑定仓库ID列表' })
@Rule(RuleType.array().items(RuleType.number()).optional())
stockPointIds?: number[];
@ApiProperty({ description: '站点网站URL', required: false })
@Rule(RuleType.string().optional())
websiteUrl?: string;
}
export class QuerySiteDTO {
@ApiProperty({ description: '当前页码', required: false })
@Rule(RuleType.number().optional())
current?: number;
@ApiProperty({ description: '每页数量', required: false })
@Rule(RuleType.number().optional())
pageSize?: number;
@ApiProperty({ description: '搜索关键词', required: false })
@Rule(RuleType.string().optional())
keyword?: string;
@ApiProperty({ description: '是否禁用', required: false })
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@ApiProperty({ description: '站点ID列表逗号分隔', required: false })
@Rule(RuleType.string().optional())
ids?: string;
}
export class DisableSiteDTO {
@ApiProperty({ description: '是否禁用' })
@Rule(RuleType.boolean())
disabled: boolean;
} }

View File

@ -16,8 +16,8 @@ export class OrderStatisticsParams {
keyword?: string; keyword?: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string().allow(null)) @Rule(RuleType.number().allow(null))
siteId?: string; siteId?: number;
@ApiProperty({ @ApiProperty({
enum: ['all', 'first_purchase', 'repeat_purchase'], enum: ['all', 'first_purchase', 'repeat_purchase'],
@ -33,4 +33,9 @@ export class OrderStatisticsParams {
@ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' }) @ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' })
@Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt')) @Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt'))
brand: string; brand: string;
@ApiProperty({ enum: ['day', 'week', 'month'], default: 'day' })
@Rule(RuleType.string().valid('day', 'week', 'month'))
grouping: string;
} }

View File

@ -20,7 +20,19 @@ export class QueryStockDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productName: string; name: string;
@ApiProperty()
@Rule(RuleType.string())
sku: string;
@ApiProperty({ description: '按库存点ID排序', required: false })
@Rule(RuleType.number().allow(null))
sortPointId?: number;
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }', required: false })
@Rule(RuleType.object().allow(null))
order?: Record<string, 'asc' | 'desc'>;
} }
export class QueryPointDTO { export class QueryPointDTO {
@ApiProperty({ example: '1', description: '页码' }) @ApiProperty({ example: '1', description: '页码' })
@ -46,11 +58,11 @@ export class QueryStockRecordDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productSku: string; sku: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productName: string; name: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
@ -84,7 +96,7 @@ export class QueryPurchaseOrderDTO {
export class StockDTO extends Stock { export class StockDTO extends Stock {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productName: string; name: string;
@ApiProperty({ @ApiProperty({
type: 'object', type: 'object',
@ -120,7 +132,7 @@ export class UpdateStockDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
productSku: string; sku: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.number()) @Rule(RuleType.number())
@ -155,6 +167,19 @@ export class CreateStockPointDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
contactPhone: string; contactPhone: string;
// 区域
@ApiProperty({ description: '区域' })
@Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[];
@ApiProperty({ description: '上游仓库点ID' })
@Rule(RuleType.number().optional())
upStreamStockPointId?: number;
@ApiProperty({ description: '上游名称' })
@Rule(RuleType.string().optional())
upStreamName?: string;
} }
export class UpdateStockPointDTO extends CreateStockPointDTO {} export class UpdateStockPointDTO extends CreateStockPointDTO {}

View File

@ -0,0 +1,36 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import { SubscriptionStatus } from '../enums/base.enum';
// 订阅列表查询参数(分页与筛选)
export class QuerySubscriptionDTO {
// 当前页码(从 1 开始)
@ApiProperty({ example: 1, description: '页码' })
@Rule(RuleType.number().default(1))
current: number;
// 每页数量
@ApiProperty({ example: 10, description: '每页大小' })
@Rule(RuleType.number().default(10))
pageSize: number;
// 站点 ID(可选)
@ApiProperty({ description: '站点ID' })
@Rule(RuleType.string().allow(''))
siteId: string;
// 订阅状态筛选(可选),支持枚举值
@ApiProperty({ description: '订阅状态', enum: SubscriptionStatus })
@Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow(''))
status: SubscriptionStatus | '';
// 客户邮箱(模糊匹配,可选)
@ApiProperty({ description: '客户邮箱' })
@Rule(RuleType.string().allow(''))
customer_email: string;
// 关键字(订阅ID,邮箱等,模糊匹配,可选)
@ApiProperty({ description: '关键字(订阅ID,邮箱等)' })
@Rule(RuleType.string().allow(''))
keyword: string;
}

40
src/dto/template.dto.ts Normal file
View File

@ -0,0 +1,40 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
export class CreateTemplateDTO {
@ApiProperty({ description: '模板名称', required: true })
@Rule(RuleType.string().required())
name: string;
@ApiProperty({ description: '模板内容', required: true })
@Rule(RuleType.string().required())
value: string;
@ApiProperty({ description: '测试数据JSON', required: false })
@Rule(RuleType.string().optional())
testData?: string;
}
export class UpdateTemplateDTO {
@ApiProperty({ description: '模板名称', required: true })
@Rule(RuleType.string().required())
name: string;
@ApiProperty({ description: '模板内容', required: true })
@Rule(RuleType.string().required())
value: string;
@ApiProperty({ description: '测试数据JSON', required: false })
@Rule(RuleType.string().optional())
testData?: string;
}
export class RenderTemplateDTO {
@ApiProperty({ description: '模板内容', required: true })
@Rule(RuleType.string().required())
template: string;
@ApiProperty({ description: '渲染数据', required: true })
@Rule(RuleType.object().required())
data: Record<string, any>;
}

659
src/dto/woocommerce.dto.ts Normal file
View File

@ -0,0 +1,659 @@
// WooCommerce 平台原始数据类型定义
// 仅包含当前映射逻辑所需字段以保持简洁与类型安全
// 产品类型
export interface WooProduct {
// 产品主键
id: number;
// 创建时间
date_created: string;
// 创建时间GMT
date_created_gmt: string;
// 更新时间
date_modified: string;
// 更新时间GMT
date_modified_gmt: string;
// 产品类型 simple grouped external variable
type: string;
// 产品状态 draft pending private publish
status: string;
// 是否为特色产品
featured: boolean;
// 目录可见性选项visible, catalog, search and hidden. Default is visible.
catalog_visibility: string;
// 常规价格
regular_price?: string;
// 促销价格
sale_price?: string;
// 当前价格
price?: string;
price_html?: string;
date_on_sale_from?: string; // Date the product is on sale from.
date_on_sale_from_gmt?: string; // Date the product is on sale from (GMT).
date_on_sale_to?: string; // Date the product is on sale to.
date_on_sale_to_gmt?: string; // Date the product is on sale to (GMT).
on_sale: boolean; // Whether the product is on sale.
purchasable: boolean; // Whether the product is purchasable.
total_sales: number; // Total sales for this product.
virtual: boolean; // Whether the product is virtual.
downloadable: boolean; // Whether the product is downloadable.
downloads: Array<{ id?: number; name?: string; file?: string }>; // Downloadable files for the product.
download_limit: number; // Download limit.
download_expiry: number; // Download expiry days.
external_url: string; // URL of the external product.
global_unique_id: string; // GTIN, UPC, EAN or ISBN - a unique identifier for each distinct product and service that can be purchased.
// 产品SKU
sku: string;
// 产品名称
name: string;
// 产品描述
description: string;
// 产品短描述
short_description: string;
// 产品永久链接
permalink: string;
// 产品URL路径
slug: string;
// 库存状态
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
// 库存数量
stock_quantity?: number;
// 是否管理库存
manage_stock?: boolean;
// 缺货预定设置 no notify yes
backorders?: 'no' | 'notify' | 'yes';
// 是否允许缺货预定 只读
backorders_allowed?: boolean;
// 是否处于缺货预定状态 只读
backordered?: boolean;
// 是否单独出售
sold_individually?: boolean;
// 重量
weight?: string;
// 尺寸
dimensions?: { length?: string; width?: string; height?: string };
// 是否需要运输 只读
shipping_required?: boolean;
// 运输是否计税 只读
shipping_taxable?: boolean;
// 运输类别 slug
shipping_class?: string;
// 运输类别ID 只读
shipping_class_id?: number;
// 图片列表
images?: Array<{ id: number; src: string; name?: string; alt?: string }>;
// 属性列表
attributes?: Array<{
id?: number;
name?: string;
position?: number;
visible?: boolean;
variation?: boolean;
options?: string[];
}>;
// 变体列表
variations?: number[];
// 默认变体属性
default_attributes?: Array<{ id?: number; name?: string; option?: string }>;
// 允许评论
reviews_allowed?: boolean;
// 平均评分 只读
average_rating?: string;
// 评分数量 只读
rating_count?: number;
// 相关产品ID列表 只读
related_ids?: number[];
// 追加销售产品ID列表
upsell_ids?: number[];
// 交叉销售产品ID列表
cross_sell_ids?: number[];
// 父产品ID
parent_id?: number;
// 购买备注
purchase_note?: string;
// 分类列表
categories?: Array<{ id: number; name?: string; slug?: string }>;
// 标签列表
tags?: Array<{ id: number; name?: string; slug?: string }>;
// 菜单排序
menu_order?: number;
// 元数据
meta_data?: Array<{ id?: number; key: string; value: any }>;
}
export interface WooVariation {
// 变体主键
id: number;
// 创建时间
date_created: string;
// 创建时间GMT
date_created_gmt: string;
// 更新时间
date_modified: string;
// 更新时间GMT
date_modified_gmt: string;
// 变体描述
description: string;
// 变体SKU
sku: string;
// 常规价格
regular_price?: string;
// 促销价格
sale_price?: string;
// 当前价格
price?: string;
// 价格HTML
price_html?: string;
// 促销开始日期
date_on_sale_from?: string;
// 促销开始日期GMT
date_on_sale_from_gmt?: string;
// 促销结束日期
date_on_sale_to?: string;
// 促销结束日期GMT
date_on_sale_to_gmt?: string;
// 是否在促销中
on_sale: boolean;
// 是否可购买
purchasable: boolean;
// 总销量
total_sales: number;
// 是否为虚拟商品
virtual: boolean;
// 是否可下载
downloadable: boolean;
// 下载文件
downloads: Array<{ id?: number; name?: string; file?: string }>;
// 下载限制
download_limit: number;
// 下载过期天数
download_expiry: number;
// 库存状态
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
// 库存数量
stock_quantity?: number;
// 是否管理库存
manage_stock?: boolean;
// 缺货预定设置
backorders?: 'no' | 'notify' | 'yes';
// 是否允许缺货预定
backorders_allowed?: boolean;
// 是否处于缺货预定状态
backordered?: boolean;
// 是否单独出售
sold_individually?: boolean;
// 重量
weight?: string;
// 尺寸
dimensions?: { length?: string; width?: string; height?: string };
// 是否需要运输
shipping_required?: boolean;
// 运输是否计税
shipping_taxable?: boolean;
// 运输类别
shipping_class?: string;
// 运输类别ID
shipping_class_id?: number;
// 变体图片
image?: { id: number; src: string; name?: string; alt?: string };
// 变体属性列表
attributes?: Array<{
id?: number;
name?: string;
option?: string;
}>;
// 菜单排序
menu_order?: number;
// 元数据
meta_data?: Array<{ id?: number; key: string; value: any }>;
// 父产品ID
parent_id?: number;
// 变体名称
name?: string;
// 是否启用
status?: string;
}
// 订单类型
export interface WooOrder {
// 订单主键
id: number;
// 父订单ID
parent_id?: number;
// 订单号
number: string;
// 订单键 只读
order_key?: string;
// 创建来源
created_via?: string;
// WooCommerce版本 只读
version?: string;
// 状态
status: string;
// 币种
currency: string;
// 价格是否含税 只读
prices_include_tax?: boolean;
// 总金额
total: string;
// 总税额 只读
total_tax?: string;
// 折扣总额 只读
discount_total?: string;
// 折扣税额 只读
discount_tax?: string;
// 运费总额 只读
shipping_total?: string;
// 运费税额 只读
shipping_tax?: string;
// 购物车税额 只读
cart_tax?: string;
// 客户ID
customer_id: number;
// 客户IP 只读
customer_ip_address?: string;
// 客户UA 只读
customer_user_agent?: string;
// 客户备注
customer_note?: string;
// 账单信息
billing?: {
first_name?: string;
last_name?: string;
email?: string;
company?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
phone?: string;
fullname?: string;
};
// 收货信息
shipping?: {
first_name?: string;
last_name?: string;
company?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
phone?: string;
fullname?: string;
};
// 订单项
line_items?: Array<{
product_id?: number;
variation_id?: number;
quantity?: number;
subtotal?: string;
subtotal_tax?: string;
total?: string;
total_tax?: string;
name?: string;
sku?: string;
price?: number;
meta_data?: Array<{ key: string; value: any }>;
[key: string]: any;
}>;
// 税费行 只读
tax_lines?: Array<{
id?: number;
rate_code?: string;
rate_id?: number;
label?: string;
tax_total?: string;
shipping_tax_total?: string;
compound?: boolean;
meta_data?: any[];
}>;
// 物流费用行
shipping_lines?: Array<{
id?: number;
method_title?: string;
method_id?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}>;
// 手续费行
fee_lines?: Array<{
id?: number;
name?: string;
tax_class?: string;
tax_status?: string;
total?: string;
total_tax?: string;
taxes?: any[];
meta_data?: any[];
}>;
// 优惠券行
coupon_lines?: Array<{
id?: number;
code?: string;
discount?: string;
discount_tax?: string;
meta_data?: any[];
}>;
// 退款列表 只读
refunds?: Array<WooOrderRefund>;
// 支付方式标题
payment_method_title?: string;
// 支付方式ID
payment_method?: string;
// 交易ID
transaction_id?: string;
// 已支付时间
date_paid?: string;
date_paid_gmt?: string;
// 完成时间
date_completed?: string;
date_completed_gmt?: string;
// 购物车hash 只读
cart_hash?: string;
// 设置为已支付 写入专用
set_paid?: boolean;
// 元数据
meta_data?: Array<{ id?: number; key: string; value: any }>;
// 创建与更新时间
date_created: string;
date_created_gmt?: string;
date_modified?: string;
date_modified_gmt?: string;
// 物流追踪信息
fulfillments?: Array<{
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id?: number;
quantity?: number;
}>;
}>;
}
export interface WooOrderRefund {
id?: number;
reason?: string;
total?: string;
}
// 订阅类型
export interface WooSubscription {
// 订阅主键
id: number;
// 订阅状态
status: string;
// 客户ID
customer_id: number;
// 计费周期
billing_period?: string;
// 计费间隔
billing_interval?: number;
// 开始时间
start_date?: string;
// 下次支付时间
next_payment_date?: string;
// 订阅项
line_items?: any[];
// 创建时间
date_created?: string;
// 更新时间
date_modified?: string;
}
// WordPress 媒体类型
export interface WpMedia {
// 媒体主键
id: number;
// 标题可能为字符串或包含rendered的对象
title?: { rendered?: string } | string;
// 媒体类型
media_type?: string;
// MIME类型
mime_type?: string;
// 源地址
source_url?: string;
// 创建时间兼容date字段
date_created?: string;
date?: string;
// 更新时间兼容modified字段
date_modified?: string;
modified?: string;
}
// 评论类型
export interface WooReview {
// 评论ID
id: number;
// 评论内容
review: string;
// 评分
rating: number;
// 评论者
reviewer: string;
// 评论者邮箱
reviewer_email: string;
// 状态
status: string;
// 产品ID
product_id: number;
// 创建日期
date_created: string;
// 更新日期
}
// 客户类型
export interface WooCustomer {
// 客户主键
id: number;
// 头像URL
avatar_url?: string;
// 邮箱
email: string;
// 订单总数
orders?: number;
// 总花费
total_spent?: number | string;
// 名
first_name?: string;
// 姓
last_name?: string;
// 用户名
username?: string;
// 角色 只读
role?: string;
// 密码 写入专用
password?: string;
// 账单信息
billing?: {
first_name?: string;
last_name?: string;
email?: string;
company?: string;
phone?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
};
// 收货信息
shipping?: {
first_name?: string;
last_name?: string;
company?: string;
phone?: string;
address_1?: string;
address_2?: string;
city?: string;
state?: string;
postcode?: string;
country?: string;
};
// 是否为付费客户 只读
is_paying_customer?: boolean;
// 元数据
meta_data?: Array<{ id?: number; key: string; value: any }>;
// 创建时间
date_created?: string;
date_created_gmt?: string;
// 更新时间
date_modified?: string;
date_modified_gmt?: string;
}
// Webhook类型
export interface WooWebhook {
id: number;
name: string;
status: string;
topic: string;
resource: string;
event: string;
hooks: string;
delivery_url: string;
secret: string;
date_created: string;
date_created_gmt: string;
date_modified: string;
date_modified_gmt: string;
api_version: string;
meta_data?: Array<{ id?: number; key: string; value: any }>;
}
export interface WooOrderSearchParams {
context: WooContext;
page: number;
per_page: number;
search: string;
after: string;
before: string;
modified_after: string;
modified_before: string;
date_are_gmt: boolean;
exclude: string[];
include: string[];
offset: number;
order: string;
orderby: string;
parant: string[];
status: (WooOrderStatusSearchParams)[];
customer: number;
product: number;
dp: number;
created_via: string;
}
export enum WooOrderStatusSearchParams {
pending,
processing,
"on-hold",
completed,
cancelled,
refunded,
failed,
trash,
any
}
export interface WooProductSearchParams extends ListParams {
slug: string;
status: string[];
include_status: string;
exclude_status: string;
type: string;
include_types: string;
exclude_types: string;
sku: string;
featured: boolean;
category: string;
tag: string;
shipping_class: string;
attribute: string;
attribute_term: string;
tax_class: string;
on_sale: boolean;
min_price: string;
max_price: string;
stock_status: string;
virtual: boolean;
downloadable: boolean;
}
export interface ListParams {
context: WooContext;
page: number;
per_page: number;
search: string;
search_fields: any[];
after: string;
before: string;
modified_after: string;
modified_before: string;
date_are_gmt: boolean;
exclude: string[];
include: string[];
offset: number;
order: string;
orderby: string;
parant: string[];
parent_exclude: string[];
}
export enum WooContext {
view,
edit
}
export enum WooProductStatusSearchParams {
any,
draft,
pending,
private,
publish
}
// 发货相关DTO
export class WooShipOrderItemDTO {
order_item_id: number;
quantity: number;
}
export class WooShipOrderDTO {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
items?: WooShipOrderItemDTO[];
}
export class WooCancelShipOrderDTO {
reason?: string;
shipment_id?: string;
}
export class WooBatchShipOrderItemDTO {
order_id: string;
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
items?: WooShipOrderItemDTO[];
}
export class WooBatchShipOrdersDTO {
orders: WooBatchShipOrderItemDTO[];
}

View File

@ -1,98 +0,0 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Variation } from '../entity/variation.entity';
import { WpProduct } from '../entity/wp_product.entity';
import { Rule, RuleType } from '@midwayjs/validate';
import { ProductStatus } from '../enums/base.enum';
export class VariationDTO extends Variation {}
export class WpProductDTO extends WpProduct {
@ApiProperty({ description: '变体列表', type: VariationDTO, isArray: true })
variations?: VariationDTO[];
}
export class UpdateVariationDTO {
@ApiProperty({ description: '产品名称' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ description: 'SKU' })
@Rule(RuleType.string().allow(''))
sku: string;
@ApiProperty({ description: '常规价格', type: Number })
@Rule(RuleType.number())
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Rule(RuleType.number())
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Rule(RuleType.boolean())
on_sale: boolean; // 是否促销中
}
export class UpdateWpProductDTO {
@ApiProperty({ description: '变体名称' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ description: 'SKU' })
@Rule(RuleType.string().allow(''))
sku: string;
@ApiProperty({ description: '常规价格', type: Number })
@Rule(RuleType.number())
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Rule(RuleType.number())
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Rule(RuleType.boolean())
on_sale: boolean; // 是否促销中
}
export class QueryWpProductDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty({ example: 'ZYN', description: '产品名' })
@Rule(RuleType.string())
name?: string;
@ApiProperty({ example: '1', description: '站点ID' })
@Rule(RuleType.string())
siteId?: string;
@ApiProperty({ description: '产品状态', enum: ProductStatus })
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
status?: ProductStatus;
}
export class SetConstitutionDTO {
@ApiProperty({ type: Boolean })
@Rule(RuleType.boolean())
isProduct: boolean;
@ApiProperty({
description: '构成成分',
type: 'array',
items: {
type: 'object',
properties: {
sku: { type: 'string' },
quantity: { type: 'number' },
},
},
})
@Rule(RuleType.array())
constitution: { sku: string; quantity: number }[] | null;
}

25
src/entity/area.entity.ts Normal file
View File

@ -0,0 +1,25 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
import { Site } from './site.entity';
import { StockPoint } from './stock_point.entity';
@Entity('area')
export class Area {
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ description: '名称' })
@Column()
name: string;
@ApiProperty({ description: '编码' })
@Column({ unique: true })
code: string;
@ManyToMany(() => Site, site => site.areas)
sites: Site[];
@ManyToMany(() => StockPoint, stockPoint => stockPoint.areas)
stockPoints: StockPoint[];
}

View File

@ -1,53 +1,43 @@
import { import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Product } from './product.entity';
import { CategoryAttribute } from './category_attribute.entity';
@Entity() @Entity('category')
export class Category { export class Category {
@ApiProperty({ @ApiProperty({ description: 'ID' })
example: '1',
description: '分类 ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ApiProperty({ @ApiProperty({ description: '分类显示名称' })
example: '分类名称',
description: '分类名称',
type: 'string',
required: true,
})
@Column() @Column()
title: string;
@ApiProperty({ description: '分类中文名称' })
@Column({ nullable: true })
titleCN: string;
@ApiProperty({ description: '分类唯一标识' })
@Column({ unique: true })
name: string; name: string;
// 分类短名称, 用于生成SKU
@ApiProperty({ description: '分类短名称' })
@Column({ nullable: true })
shortName: string;
@ApiProperty({ @ApiProperty({ description: '排序' })
description: '唯一识别key', @Column({ default: 0 })
type: 'string', sort: number;
required: true,
}) @OneToMany(() => Product, product => product.category)
@Column() products: Product[];
unique_key: string;
@OneToMany(() => CategoryAttribute, categoryAttribute => categoryAttribute.category)
attributes: CategoryAttribute[];
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn() @UpdateDateColumn()
updatedAt: Date; updatedAt: Date;
} }

View File

@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Category } from './category.entity';
import { Dict } from './dict.entity';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class CategoryAttribute {
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ description: '分类' })
@ManyToOne(() => Category, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'category_id' })
category: Category;
@ApiProperty({ description: '关联的属性字典' })
@ManyToOne(() => Dict, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'attribute_dict_id' })
attributeDict: Dict;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,13 +1,58 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('customer') @Entity('customer')
export class Customer { export class Customer {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@Column({ nullable: true })
site_id: number;
@Column({ nullable: true })
origin_id: string;
@Column({ unique: true }) @Column({ unique: true })
email: string; email: string;
@Column({ nullable: true })
first_name: string;
@Column({ nullable: true })
last_name: string;
@Column({ nullable: true })
fullname: string;
@Column({ nullable: true })
username: string;
@Column({ nullable: true })
phone: string;
@Column({ nullable: true })
avatar: string;
@Column({ type: 'json', nullable: true })
billing: any;
@Column({ type: 'json', nullable: true })
shipping: any;
@Column({ type: 'json', nullable: true })
raw: any;
@Column({ default: 0}) @Column({ default: 0})
rate: number; rate: number;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
@Column({ nullable: true })
site_created_at: Date;
@Column({ nullable: true })
site_updated_at: Date;
} }

42
src/entity/dict.entity.ts Normal file
View File

@ -0,0 +1,42 @@
/**
* @description
* @author ZKS
* @date 2025-11-27
*/
import { DictItem } from './dict_item.entity';
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class Dict {
// 主键
@PrimaryGeneratedColumn()
id: number;
@Column({comment: '字典显示名称'})
title: string;
// 字典名称
@Column({ unique: true, comment: '字典名称' })
name: string;
// 字典项
@OneToMany(() => DictItem, item => item.dict)
items: DictItem[];
// 是否可删除
@Column({ default: true, comment: '是否可删除' })
deletable: boolean;
// 创建时间
@CreateDateColumn()
createdAt: Date;
// 更新时间
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,70 @@
/**
* @description
* @author ZKS
* @date 2025-11-27
*/
import { Dict } from './dict.entity';
import { Product } from './product.entity';
import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
@Index(['name', 'dict'], { unique: true })
export class DictItem {
// 主键
@PrimaryGeneratedColumn()
id: number;
// 字典项名称
@Column({ comment: '字典项显示名称' })
title: string;
// 目前没有单独做国际化, 所以这里先添加 titleCN 用来标注
@Column({ comment: '字典项中文名称', nullable: true })
titleCN: string;
// 唯一标识
@Column({ comment: '字典唯一标识名称' })
name: string;
@Column({ nullable: true, comment: '简称' })
shortName: string;
@Column({ nullable: true, comment: '字典项描述' })
description?: string
// 字典项值
@Column({ nullable: true, comment: '字典项值' })
value?: string;
@Column({ nullable: true, comment: '图片' })
image: string;
// 排序
@Column({ default: 0, comment: '排序' })
sort: number;
// 属于哪个字典
@ManyToOne(() => Dict, dict => dict.items)
@JoinColumn({ name: 'dict_id' })
dict: Dict;
// 关联的产品
@ManyToMany(() => Product, product => product.attributes)
products: Product[];
// 创建时间
@CreateDateColumn()
createdAt: Date;
// 更新时间
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,43 +0,0 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Flavors {
@ApiProperty()
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
name: string;
@ApiProperty({
description: '唯一识别key',
type: 'string',
required: true,
})
@Column()
unique_key: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -24,9 +24,9 @@ export class Order {
id: number; id: number;
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; // 来源站点唯一标识 siteId: number; // 来源站点唯一标识
@ApiProperty() @ApiProperty()
@Column() @Column()
@ -62,14 +62,14 @@ export class Order {
currency: string; currency: string;
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
currency_symbol: string; currency_symbol?: string;
@ApiProperty() @ApiProperty()
@Column({ default: false }) @Column({ default: false })
@Expose() @Expose()
prices_include_tax: boolean; prices_include_tax?: boolean;
@ApiProperty() @ApiProperty()
@Column({ type: 'timestamp', nullable: true }) @Column({ type: 'timestamp', nullable: true })
@ -178,7 +178,7 @@ export class Order {
@ApiProperty() @ApiProperty()
@Column({ @Column({
type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT
nullable: true, // 可选是否允许为 NULL nullable: true, // 可选:是否允许为 NULL
}) })
@Expose() @Expose()
customer_note: string; customer_note: string;

View File

@ -22,9 +22,9 @@ export class OrderCoupon {
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; // 来源站点唯一标识 siteId: number; // 站点ID
@ApiProperty() @ApiProperty()
@Column() @Column()

View File

@ -22,9 +22,9 @@ export class OrderFee {
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; siteId: number; // 站点ID
@ApiProperty() @ApiProperty()
@Column() @Column()

View File

@ -0,0 +1,86 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_fulfillment')
@Exclude()
export class OrderFulfillment {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column({ name: 'order_id', nullable: true })
@Expose()
order_id: number; // 订单 ID
@ApiProperty()
@Column({ name: 'site_id', nullable: true })
@Expose()
site_id: number; // 站点ID
@ApiProperty()
@Column({ name: 'external_order_id', nullable: true })
@Expose()
external_order_id: string; // 外部订单 ID
@ApiProperty()
@Column({ name: 'external_fulfillment_id', nullable: true })
@Expose()
external_fulfillment_id: string; // 外部履约 ID
@ApiProperty()
@Column({ name: 'tracking_number' })
@Expose()
tracking_number: string; // 物流单号
@ApiProperty()
@Column({ name: 'shipping_provider', nullable: true })
@Expose()
shipping_provider: string; // 物流公司
@ApiProperty()
@Column({ name: 'shipping_method', nullable: true })
@Expose()
shipping_method: string; // 发货方式
@ApiProperty()
@Column({ nullable: true })
@Expose()
status: string; // 状态
@ApiProperty()
@Column({ name: 'date_created', type: 'timestamp', nullable: true })
@Expose()
date_created: Date; // 创建时间
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
items: any[]; // 发货商品项
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn({ name: 'created_at' })
@Expose()
created_at: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn({ name: 'updated_at' })
@Expose()
updated_at: Date;
}

View File

@ -22,9 +22,9 @@ export class OrderItem {
name: string; name: string;
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; // 来源站点唯一标识 siteId: number; // 来源站点唯一标识
@ApiProperty() @ApiProperty()
@Column() @Column()
@ -76,16 +76,61 @@ export class OrderItem {
@Expose() @Expose()
total_tax: number; total_tax: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
tax_class?: string; // 税类(来自 line_items.tax_class)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
taxes?: any[]; // 税明细(来自 line_items.taxes,数组)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data?: any[]; // 行项目元数据(包含订阅相关键值)
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
sku?: string; sku?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供)
@ApiProperty() @ApiProperty()
@Column('decimal', { precision: 10, scale: 2 }) @Column('decimal', { precision: 10, scale: 2 })
@Expose() @Expose()
price: number; price: number;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src)
@ApiProperty()
@Column({ nullable: true })
@Expose()
parent_name?: string; // 父商品名称(组合/捆绑时可能使用)
@ApiProperty()
@Column({ nullable: true })
@Expose()
bundled_by?: string; // 捆绑来源标识(bundled_by)
@ApiProperty()
@Column({ nullable: true })
@Expose()
bundled_item_title?: string; // 捆绑项标题
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
bundled_items?: any[]; // 捆绑项列表(数组)
@ApiProperty({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
description: '创建时间', description: '创建时间',

View File

@ -11,9 +11,9 @@ import {
} from 'typeorm'; } from 'typeorm';
import { Order } from './order.entity'; import { Order } from './order.entity';
@Entity('order_sale_original') @Entity('order_item_original')
@Exclude() @Exclude()
export class OrderSaleOriginal { export class OrderItemOriginal {
@ApiProperty() @ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
@Expose() @Expose()
@ -27,9 +27,9 @@ export class OrderSaleOriginal {
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; // 来源站点唯一标识 siteId: number; // 站点ID
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })

View File

@ -22,9 +22,9 @@ export class OrderRefund {
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; // 来源站点唯一标识 siteId: number; // 站点ID
@ApiProperty() @ApiProperty()
@Column() @Column()

View File

@ -22,9 +22,9 @@ export class OrderRefundItem {
refundId: number; // 订单 refund ID refundId: number; // 订单 refund ID
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; // 来源站点唯一标识 siteId: number; // 站点ID
@ApiProperty() @ApiProperty()
@Column() @Column()

View File

@ -9,10 +9,14 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
// import { Product } from './product.entity';
@Entity('order_sale') @Entity('order_sale')
@Exclude() @Exclude()
export class OrderSale { export class OrderSale {
// @ManyToOne(() => Product, { onDelete: 'CASCADE' })
// @JoinColumn({ name: 'productId' })
// product: Product;
@ApiProperty() @ApiProperty()
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
@Expose() @Expose()
@ -24,9 +28,9 @@ export class OrderSale {
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; // 来源站点唯一标识 siteId: number; // 来源站点唯一标识
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })

View File

@ -22,9 +22,9 @@ export class OrderShipping {
orderId: number; // 订单 ID orderId: number; // 订单 ID
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
siteId: string; siteId: number; // 站点ID
@ApiProperty() @ApiProperty()
@Column() @Column()
@ -47,9 +47,9 @@ export class OrderShipping {
method_id: string; method_id: string;
@ApiProperty() @ApiProperty()
@Column() @Column({ nullable: true })
@Expose() @Expose()
instance_id: string; instance_id?: string;
@ApiProperty() @ApiProperty()
@Column('decimal', { precision: 10, scale: 2 }) @Column('decimal', { precision: 10, scale: 2 })

View File

@ -0,0 +1,121 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
ManyToMany,
JoinTable,
OneToMany,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { DictItem } from './dict_item.entity';
import { ProductStockComponent } from './product_stock_component.entity';
import { Category } from './category.entity';
@Entity('product')
export class Product {
@ApiProperty({
example: '1',
description: 'ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ description: 'sku'})
@Column({ unique: true })
sku: string;
// 类型 主要用来区分混装和单品 单品死
@ApiProperty({ description: '类型' })
@Column({ length: 16, default: 'single' })
type: string;
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
description: '产品名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty({ description: '产品中文名称' })
@Column({ default: '' })
nameCn: string;
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
@Column({ nullable: true })
shortDescription?: string;
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Column({ nullable: true })
description?: string;
// 商品价格
@ApiProperty({ description: '价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
price: number;
// 促销价格
@ApiProperty({ description: '促销价格', example: 99.99 })
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number;
// 分类关联
@ManyToOne(() => Category, category => category.products)
@JoinColumn({ name: 'categoryId' })
category: Category;
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
cascade: true,
})
@JoinTable({
name: 'product_attributes_dict_item',
joinColumn: {
name: 'productId',
referencedColumnName: 'id'
},
inverseJoinColumn: {
name: 'dictItemId',
referencedColumnName: 'id'
}
})
attributes: DictItem[];
// 产品的库存组成,一对多关系(使用独立表)
@ApiProperty({ description: '库存组成', type: ProductStockComponent, isArray: true })
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
components: ProductStockComponent[];
@ApiProperty({ description: '站点 SKU 列表', type: 'string', isArray: true })
@Column({ type: 'simple-array' ,nullable:true})
siteSkus: string[];
// 来源
@ApiProperty({ description: '来源', example: '1' })
@Column({ default: 0 })
source: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -1,73 +0,0 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Product {
@ApiProperty({
example: '1',
description: 'ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
description: '产品名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty()
@Column({ default: ''})
nameCn: string;
@ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
@Column({ nullable: true })
description?: string;
@ApiProperty({ example: '1', description: '分类 ID', type: 'number' })
@Column()
categoryId: number;
@ApiProperty()
@Column()
flavorsId: number;
@ApiProperty()
@Column()
strengthId: number;
@ApiProperty()
@Column()
humidity: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Column({ nullable: true })
sku?: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,34 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { Product } from './product.entity';
@Entity('product_stock_component')
export class ProductStockComponent {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
productId: number;
@ApiProperty({ description: '组件所关联的 SKU', type: 'string' })
@Column({ type: 'varchar', length: 64 })
sku: string;
@ApiProperty({ type: Number, description: '组成数量' })
@Column({ type: 'int', default: 1 })
quantity: number;
// 多对一,组件隶属于一个产品
@ManyToOne(() => Product, (product) => product.components, { onDelete: 'CASCADE' })
product: Product;
@ApiProperty({ description: '创建时间' })
@CreateDateColumn()
createdAt: Date;
@ApiProperty({ description: '更新时间' })
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -10,11 +10,11 @@ export class PurchaseOrderItem {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productName: string; name: string;
@ApiProperty({ type: Number }) @ApiProperty({ type: Number })
@Column() @Column()

70
src/entity/site.entity.ts Normal file
View File

@ -0,0 +1,70 @@
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Area } from './area.entity';
import { StockPoint } from './stock_point.entity';
@Entity('site')
export class Site {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 255, nullable: true })
apiUrl: string;
@Column({ name: 'website_url', length: 255, nullable: true })
websiteUrl?: string;
@Column({ name: 'webhook_url', length: 255, nullable: true })
webhookUrl?: string;
@Column({ length: 255, nullable: true })
consumerKey?: string;
@Column({ length: 255, nullable: true })
consumerSecret?: string;
@Column({ nullable: true })
token?: string;
@Column({ length: 255, unique: true })
name: string;
@Column({ length: 255, nullable: true })
description?: string;
@Column({ length: 32, default: 'woocommerce' })
type: string; // 平台类型:woocommerce | shopyy
@Column({ length: 64, nullable: true })
skuPrefix: string;
@Column({ default: false })
isDisabled: boolean;
@ManyToMany(() => Area, { cascade: true })
@JoinTable({
name: 'site_areas_area',
joinColumn: {
name: 'siteId',
referencedColumnName: 'id'
},
inverseJoinColumn: {
name: 'areaId',
referencedColumnName: 'id'
}
})
areas: Area[];
@ManyToMany(() => StockPoint, stockPoint => stockPoint.sites)
@JoinTable({
name: 'site_stock_points_stock_point',
joinColumn: {
name: 'siteId',
referencedColumnName: 'id'
},
inverseJoinColumn: {
name: 'stockPointId',
referencedColumnName: 'id'
}
})
stockPoints: StockPoint[];
}

View File

@ -20,7 +20,7 @@ export class Stock {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: Number }) @ApiProperty({ type: Number })
@Column() @Column()

View File

@ -8,8 +8,12 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
OneToMany, OneToMany,
ManyToMany,
JoinTable,
} from 'typeorm'; } from 'typeorm';
import { Shipment } from './shipment.entity'; import { Shipment } from './shipment.entity';
import { Area } from './area.entity';
import { Site } from './site.entity';
@Entity('stock_point') @Entity('stock_point')
export class StockPoint extends BaseEntity { export class StockPoint extends BaseEntity {
@ -51,7 +55,7 @@ export class StockPoint extends BaseEntity {
@Column({ default: 'uniuni' }) @Column({ default: 'uniuni' })
upStreamName: string; upStreamName: string;
@Column() @Column({ default: 0 })
upStreamStockPointId: number; upStreamStockPointId: number;
@ApiProperty({ @ApiProperty({
@ -72,4 +76,21 @@ export class StockPoint extends BaseEntity {
@DeleteDateColumn() @DeleteDateColumn()
deletedAt: Date; // 软删除时间 deletedAt: Date; // 软删除时间
@ManyToMany(() => Area)
@JoinTable({
name: 'stock_point_areas_area',
joinColumn: {
name: 'stockPointId',
referencedColumnName: 'id'
},
inverseJoinColumn: {
name: 'areaId',
referencedColumnName: 'id'
}
})
areas: Area[];
@ManyToMany(() => Site, site => site.stockPoints)
sites: Site[];
} }

View File

@ -20,7 +20,7 @@ export class StockRecord {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: StockRecordOperationType }) @ApiProperty({ type: StockRecordOperationType })
@Column({ type: 'enum', enum: StockRecordOperationType }) @Column({ type: 'enum', enum: StockRecordOperationType })

View File

@ -1,43 +0,0 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Strength {
@ApiProperty()
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
name: string;
@ApiProperty({
description: '唯一识别key',
type: 'string',
required: true,
})
@Column()
unique_key: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,128 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { SubscriptionStatus } from '../enums/base.enum';
@Entity('subscription')
@Exclude()
export class Subscription {
// 本地主键,自增 ID
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
// 站点唯一标识,用于区分不同来源站点
@ApiProperty({ description: '来源站点唯一标识' })
@Column({ nullable: true })
@Expose()
siteId: number;
// WooCommerce 订阅的原始 ID(字符串化),用于幂等更新
@ApiProperty({ description: 'WooCommerce 订阅 ID' })
@Column()
@Expose()
externalSubscriptionId: string;
// 订阅状态(active/cancelled/on-hold 等)
@ApiProperty({ type: SubscriptionStatus })
@Column({ type: 'enum', enum: SubscriptionStatus })
@Expose()
status: SubscriptionStatus;
// 货币代码,例如 USD/CAD
@ApiProperty()
@Column({ default: '' })
@Expose()
currency: string;
// 总金额,保留两位小数
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total: number;
// 计费周期(day/week/month/year)
@ApiProperty({ description: '计费周期 e.g. day/week/month/year' })
@Column({ default: '' })
@Expose()
billing_period: string;
// 计费周期间隔(例如 1/3/12)
@ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' })
@Column({ type: 'int', default: 0 })
@Expose()
billing_interval: number;
// 客户 ID(WooCommerce 用户 ID)
@ApiProperty()
@Column({ type: 'int', default: 0 })
@Expose()
customer_id: number;
// 客户邮箱(从 billing.email 或 customer_email 提取)
@ApiProperty()
@Column({ default: '' })
@Expose()
customer_email: string;
// 父订单/订阅 ID(如有)
@ApiProperty({ description: '父订单/父订阅ID(如有)' })
@Column({ type: 'int', default: 0 })
@Expose()
parent_id: number;
// 订阅开始时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
start_date: Date;
// 试用结束时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
trial_end: Date;
// 下次支付时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
next_payment_date: Date;
// 订阅结束时间
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
end_date: Date;
// 商品项(订阅行项目)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
line_items: any[];
// 额外元数据(键值对)
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: any[];
// 创建时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true })
@CreateDateColumn()
@Expose()
createdAt: Date;
// 更新时间(数据库自动生成)
@ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true })
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,54 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('template')
export class Template {
@ApiProperty({ type: 'number' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: 'string' })
@Column({ unique: true })
name: string;
@ApiProperty({ type: 'string' })
@Column('text')
value: string;
@ApiProperty({ nullable: true ,name:"描述"})
@Column('text',{nullable: true,comment: "描述"})
description?: string;
@ApiProperty({ type: 'string', nullable: true, description: '测试数据JSON' })
@Column('text', { nullable: true, comment: '测试数据JSON' })
testData?: string;
@ApiProperty({
example: true,
description: '是否可删除',
required: true,
})
@Column({ default: true })
deletable: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -9,11 +9,11 @@ export class TransferItem {
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productSku: string; sku: string;
@ApiProperty({ type: String }) @ApiProperty({ type: String })
@Column() @Column()
productName: string; name: string;
@ApiProperty({ type: Number }) @ApiProperty({ type: Number })
@Column() @Column()

View File

@ -15,10 +15,14 @@ export class User {
password: string; password: string;
// @Column() // 默认角色为管理员 // @Column() // 默认角色为管理员
// roleId: number; // 角色 (如admin, editor, viewer) // roleId: number; // 角色 (如:admin, editor, viewer)
@Column({ type: 'simple-array', nullable: true }) @Column({ type: 'simple-array', nullable: true })
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit']) permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
// 邮箱字段,可选且唯一
@Column({ unique: true, nullable: true })
email?: string;
@Column({ default: false }) @Column({ default: false })
isSuper: boolean; // 超级管理员 isSuper: boolean; // 超级管理员
@ -28,4 +32,8 @@ export class User {
@Column({ default: true }) @Column({ default: true })
isActive: boolean; // 用户是否启用 isActive: boolean; // 用户是否启用
// 备注字段(可选)
@Column({ nullable: true })
remark?: string;
} }

View File

@ -1,119 +0,0 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
Unique,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity('variation')
@Unique(['siteId', 'externalProductId', 'externalVariationId']) // 确保变体的唯一性
export class Variation {
@ApiProperty({
example: '1',
description: 'ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: '1',
description: 'wp网站ID',
type: 'string',
required: true,
})
@Column()
siteId: string; // 来源站点唯一标识
@ApiProperty({
example: '1',
description: 'wp产品ID',
type: 'string',
required: true,
})
@Column()
externalProductId: string; // WooCommerce 产品 ID
@ApiProperty({
example: '1',
description: 'wp变体ID',
type: 'string',
required: true,
})
@Column()
externalVariationId: string; // WooCommerce 变体 ID
@ApiProperty({
example: '1',
description: '对应WP产品表的ID',
type: 'number',
required: true,
})
@Column()
productId: number; // 对应WP产品表的 ID
@ApiProperty({ description: 'sku', type: 'string' })
@Column({ nullable: true })
sku?: string; // sku 编码
@ApiProperty({
description: '变体名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty({ description: '常规价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Column({ nullable: true, type: Boolean })
on_sale: boolean; // 是否促销中
@ApiProperty({ description: '是否删除', type: Boolean })
@Column({ nullable: true, type: Boolean , default: false })
on_delete: boolean; // 是否删除
@Column({ type: 'json', nullable: true })
attributes: Record<string, any>; // 变体的属性
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
@ApiProperty({
description: '变体构成成分',
type: 'array',
items: {
type: 'object',
properties: {
sku: { type: 'string' },
quantity: { type: 'number' },
},
},
})
@Column('json', { nullable: true, comment: '变体构成成分' })
constitution: { sku: string; quantity: number }[] | null;
}

View File

@ -1,125 +0,0 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Unique,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { ProductStatus, ProductStockStatus, ProductType } from '../enums/base.enum';
@Entity('wp_product')
@Unique(['siteId', 'externalProductId']) // 确保产品的唯一性
export class WpProduct {
@ApiProperty({
example: '1',
description: 'ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: '1',
description: 'wp网站ID',
type: 'string',
required: true,
})
@Column()
siteId: string;
@ApiProperty({
example: '1',
description: 'wp产品ID',
type: 'string',
required: true,
})
@Column()
externalProductId: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Column({ nullable: true })
sku?: string;
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
description: '产品名称',
type: 'string',
required: true,
})
@Column()
name: string;
@ApiProperty({ description: '产品状态', enum: ProductStatus })
@Column({ type: 'enum', enum: ProductStatus })
status: ProductStatus;
@ApiProperty({ description: '上下架状态', enum: ProductStockStatus })
@Column({
name: 'stock_status',
type: 'enum',
enum: ProductStockStatus,
default: ProductStockStatus.INSTOCK
})
stockStatus: ProductStockStatus;
@ApiProperty({ description: '常规价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
regular_price: number; // 常规价格
@ApiProperty({ description: '销售价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
sale_price: number; // 销售价格
@ApiProperty({ description: '是否促销中', type: Boolean })
@Column({ nullable: true, type: Boolean })
on_sale: boolean; // 是否促销中
@ApiProperty({ description: '是否删除', type: Boolean })
@Column({ nullable: true, type: Boolean , default: false })
on_delete: boolean; // 是否删除
@ApiProperty({
description: '产品类型',
enum: ProductType,
})
@Column({ type: 'enum', enum: ProductType })
type: ProductType;
@Column({ type: 'json', nullable: true })
metadata: Record<string, any>; // 产品的其他扩展字段
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
@ApiProperty({
description: '产品构成成分',
type: 'array',
items: {
type: 'object',
properties: {
sku: { type: 'string' },
quantity: { type: 'number' },
},
},
})
@Column('json', { nullable: true, comment: '产品构成成分' })
constitution: { sku: string; quantity: number }[] | null;
}

View File

@ -42,6 +42,8 @@ export enum OrderStatus {
REFUNDED = 'refunded', // 已退款 REFUNDED = 'refunded', // 已退款
FAILED = 'failed', // 失败订单 FAILED = 'failed', // 失败订单
DRAFT = 'draft', // 草稿 DRAFT = 'draft', // 草稿
AUTO_DRAFT = 'auto-draft', // 自动草稿TODO:不知道为什么出现)
// TRASH = 'trash', // TRASH = 'trash',
// refund 也就是退款相关的状态 // refund 也就是退款相关的状态
RETURN_REQUESTED = 'return-requested', // 已申请退款 RETURN_REQUESTED = 'return-requested', // 已申请退款
@ -70,5 +72,17 @@ export enum ShipmentType {
} }
export enum staticValue { export enum staticValue {
// 万能验证码
STATIC_CAPTCHA = 'yoone2025!@YOONE0923' STATIC_CAPTCHA = 'yoone2025!@YOONE0923'
} }
// WooCommerce Subscription status
// Reference: https://woocommerce.com/document/subscriptions/statuses/
export enum SubscriptionStatus {
ACTIVE = 'active', // 活跃
PENDING = 'pending', // 待处理/待激活
ON_HOLD = 'on-hold', // 暂停
CANCELLED = 'cancelled', // 已取消
EXPIRED = 'expired', // 已过期
PENDING_CANCELLATION = 'pending-cancel', // 待取消
}

View File

@ -5,15 +5,6 @@ export interface IUserOptions {
uid: number; uid: number;
} }
export interface WpSite {
id: string;
wpApiUrl: string;
consumerKey: string;
consumerSecret: string;
siteName: string;
email: string;
emailPswd: string;
}
export interface PaginationParams { export interface PaginationParams {
current?: number; // 当前页码 current?: number; // 当前页码

View File

@ -0,0 +1,287 @@
// src/interface/platform.interface.ts
/**
*
*
*/
export interface IPlatformService {
/**
*
* @param site
* @returns
*/
getProducts(site: any): Promise<any[]>;
/**
*
* @param site
* @param id ID
* @returns
*/
getProduct(site: any, id: number): Promise<any>;
/**
*
* @param site
* @param productId ID
* @returns
*/
getVariations(site: any, productId: number): Promise<any[]>;
/**
*
* @param site
* @param productId ID
* @param variationId ID
* @returns
*/
getVariation(site: any, productId: number, variationId: number): Promise<any>;
/**
*
* @param siteId ID
* @returns
*/
getOrders(siteId: number, params: Record<string, any>): Promise<any[]>;
/**
*
* @param siteId ID
* @param orderId ID
* @returns
*/
getOrder(siteId: number, orderId: string): Promise<any>;
/**
*
* @param siteId ID
* @returns
*/
getSubscriptions?(siteId: number): Promise<any[]>;
/**
*
* @param site
* @returns
*/
getCustomers(site: any): Promise<any[]>;
/**
*
* @param site
* @param id ID
* @returns
*/
getCustomer(site: any, id: number): Promise<any>;
/**
*
* @param site
* @param data
* @returns
*/
createProduct(site: any, data: any): Promise<any>;
/**
*
* @param site
* @param productId ID
* @param data
* @returns
*/
updateProduct(site: any, productId: string, data: any): Promise<boolean>;
/**
*
* @param site
* @param productId ID
* @param status
* @param stockStatus
* @returns
*/
updateProductStatus(site: any, productId: string, status: string, stockStatus: string): Promise<boolean>;
/**
*
* @param site
* @param productId ID
* @param variationId ID
* @param data
* @returns
*/
updateVariation(site: any, productId: string, variationId: string, data: any): Promise<any>;
/**
*
* @param site
* @param orderId ID
* @param data
* @returns
*/
updateOrder(site: any, orderId: string, data: Record<string, any>): Promise<boolean>;
/**
*
* @param site
* @param orderId ID
* @param data
* @returns
*/
createFulfillment(site: any, orderId: string, data: any): Promise<any>;
/**
*
* @param site
* @param orderId ID
* @param fulfillmentId ID
* @returns
*/
deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean>;
/**
*
* @param site
* @param data
* @returns
*/
batchProcessProducts(site: any, data: { create?: any[]; update?: any[]; delete?: any[] }): Promise<any>;
/**
* api
* @param site
* @returns api
*/
getApiClient(site: any): any;
/**
*
* @param site
* @returns
*/
getCustomers(site: any): Promise<any[]>;
/**
*
* @param site
* @param id ID
* @returns
*/
getCustomer(site: any, id: number): Promise<any>;
/**
*
* @param site
* @returns
*/
getReviews(site: any): Promise<any[]>;
/**
*
* @param site
* @param data
* @returns
*/
createReview(site: any, data: any): Promise<any>;
/**
*
* @param site
* @param reviewId ID
* @param data
* @returns
*/
updateReview(site: any, reviewId: number, data: any): Promise<any>;
/**
*
* @param site
* @param reviewId ID
* @returns
*/
deleteReview(site: any, reviewId: number): Promise<boolean>;
/**
*
* @param site
* @param resource
* @param params
* @param namespace API命名空间
* @returns
*/
fetchResourcePaged<T>(site: any, resource: string, params: Record<string, any>, namespace?: any): Promise<{ items: T[]; total: number; totalPages: number; page: number; per_page: number }>;
/**
*
* @param site
* @param params
* @returns
*/
fetchMediaPaged(site: any, params: Record<string, any>): Promise<{ items: any[]; total: number; totalPages: number; page: number; per_page: number }>;
/**
*
* @param siteId ID
* @param mediaId ID
* @param force
* @returns
*/
deleteMedia(siteId: number, mediaId: number, force?: boolean): Promise<any>;
/**
*
* @param siteId ID
* @param mediaId ID
* @param data
* @returns
*/
updateMedia(siteId: number, mediaId: number, data: any): Promise<any>;
/**
* WebP格式
* @param siteId ID
* @param mediaIds ID列表
* @returns
*/
convertMediaToWebp(siteId: number, mediaIds: Array<number | string>): Promise<{ converted: any[]; failed: Array<{ id: number | string; error: string }> }>;
/**
* webhook列表
* @param site
* @param params
* @returns webhook列表
*/
getWebhooks(site: any, params: any): Promise<any>;
/**
* webhook
* @param site
* @param webhookId webhook ID
* @returns webhook详情
*/
getWebhook(site: any, webhookId: string | number): Promise<any>;
/**
* webhook
* @param site
* @param data webhook数据
* @returns
*/
createWebhook(site: any, data: any): Promise<any>;
/**
* webhook
* @param site
* @param webhookId webhook ID
* @param data
* @returns
*/
updateWebhook(site: any, webhookId: string | number, data: any): Promise<any>;
/**
* webhook
* @param site
* @param webhookId webhook ID
* @returns
*/
deleteWebhook(site: any, webhookId: string | number): Promise<boolean>;
}

View File

@ -0,0 +1,270 @@
import {
CreateReviewDTO,
UpdateReviewDTO,
UnifiedMediaDTO,
UnifiedOrderDTO,
UnifiedProductDTO,
UnifiedReviewDTO,
UnifiedSubscriptionDTO,
UnifiedCustomerDTO,
UnifiedWebhookDTO,
UnifiedWebhookPaginationDTO,
CreateWebhookDTO,
UpdateWebhookDTO,
CreateVariationDTO,
UpdateVariationDTO,
UnifiedProductVariationDTO,
UnifiedVariationPaginationDTO,
} from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
export interface ISiteAdapter {
/**
*
*/
getProducts(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedProductDTO>>;
/**
*
*/
getAllProducts(params?: UnifiedSearchParamsDTO): Promise<UnifiedProductDTO[]>;
/**
*
*/
getProduct(id: string | number): Promise<UnifiedProductDTO>;
/**
*
*/
getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>;
/**
*
*/
getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]>;
/**
*
*/
getOrder(id: string | number): Promise<UnifiedOrderDTO>;
/**
*
*/
getSubscriptions(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>>;
/**
*
*/
getAllSubscriptions(params?: UnifiedSearchParamsDTO): Promise<UnifiedSubscriptionDTO[]>;
/**
*
*/
getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>>;
/**
*
*/
getAllMedia(params?: UnifiedSearchParamsDTO): Promise<UnifiedMediaDTO[]>;
/**
*
*/
createMedia(file: any): Promise<UnifiedMediaDTO>;
/**
*
*/
getReviews(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedReviewDTO>>;
/**
*
*/
getAllReviews(params?: UnifiedSearchParamsDTO): Promise<UnifiedReviewDTO[]>;
/**
*
*/
createReview(data: CreateReviewDTO): Promise<UnifiedReviewDTO>;
/**
*
*/
updateReview(id: number, data: UpdateReviewDTO): Promise<UnifiedReviewDTO>;
/**
*
*/
deleteReview(id: number): Promise<boolean>;
/**
*
*/
createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO>;
/**
*
*/
updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean>;
/**
*
*/
deleteProduct(id: string | number): Promise<boolean>;
/**
*
*/
getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<UnifiedVariationPaginationDTO>;
/**
*
*/
getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]>;
/**
*
*/
getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO>;
/**
*
*/
createVariation(productId: string | number, data: CreateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise<UnifiedProductVariationDTO>;
/**
*
*/
deleteVariation(productId: string | number, variationId: string | number): Promise<boolean>;
/**
*
*/
getOrderNotes(orderId: string | number): Promise<any[]>;
/**
*
*/
createOrderNote(orderId: string | number, data: any): Promise<any>;
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
deleteOrder(id: string | number): Promise<boolean>;
batchProcessOrders?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]>;
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
deleteCustomer(id: string | number): Promise<boolean>;
batchProcessCustomers?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
/**
* webhooks列表
*/
getWebhooks(params: UnifiedSearchParamsDTO): Promise<UnifiedWebhookPaginationDTO>;
/**
* webhooks
*/
getAllWebhooks(params?: UnifiedSearchParamsDTO): Promise<UnifiedWebhookDTO[]>;
/**
* webhook
*/
getWebhook(id: string | number): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
createWebhook(data: CreateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
updateWebhook(id: string | number, data: UpdateWebhookDTO): Promise<UnifiedWebhookDTO>;
/**
* webhook
*/
deleteWebhook(id: string | number): Promise<boolean>;
/**
*
*/
getLinks(): Promise<Array<{title: string, url: string}>>;
/**
*
*/
fulfillOrder(orderId: string | number, data: {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any>;
/**
*
*/
cancelFulfillment(orderId: string | number, data: {
reason?: string;
shipment_id?: string;
}): Promise<any>;
/**
*
*/
getOrderFulfillments(orderId: string | number): Promise<any[]>;
/**
*
*/
createOrderFulfillment(orderId: string | number, data: {
tracking_number: string;
shipping_provider: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any>;
/**
*
*/
updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
tracking_number?: string;
shipping_provider?: string;
shipping_method?: string;
status?: string;
date_created?: string;
items?: Array<{
order_item_id: number;
quantity: number;
}>;
}): Promise<any>;
/**
*
*/
deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean>;
}

View File

@ -1,15 +1 @@
import { FORMAT, ILogger, Logger } from '@midwayjs/core'; export {}
import { IJob, Job } from '@midwayjs/cron';
@Job({
cronTime: FORMAT.CRONTAB.EVERY_DAY,
runOnInit: true,
})
export class SyncProductJob implements IJob {
@Logger()
logger: ILogger;
onTick() {
}
onComplete?(result: any) {}
}

0
src/main.ts Normal file
View File

View File

@ -21,8 +21,16 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
whiteList = [ whiteList = [
'/user/login', '/user/login',
'/webhook/woocommerce', '/webhook/woocommerce',
'/webhook/shoppy',
'/logistics/getTrackingNumber', '/logistics/getTrackingNumber',
'/logistics/getListByTrackingId', '/logistics/getListByTrackingId',
'/product/categories/all',
'/product/category/1/attributes',
'/product/category/2/attributes',
'/product/category/3/attributes',
'/product/category/4/attributes',
'/product/list',
'/dict/items',
]; ];
match(ctx: Context) { match(ctx: Context) {

View File

@ -0,0 +1,140 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
import * as qs from 'qs';
@Middleware()
export class QueryNormalizeMiddleware implements IMiddleware<Context, NextFunction> {
// 数值与布尔转换函数,用于将字符串转换为合适的类型
private toPrimitive(value: any): any {
const s = String(value);
if (s === 'true') return true;
if (s === 'false') return false;
const n = Number(s);
return Number.isFinite(n) && s !== '' ? n : value;
}
// 深度遍历对象并对字符串进行trim
private trimDeep(input: any): any {
if (input === null || input === undefined) return input;
if (typeof input === 'string') return input.trim();
if (Array.isArray(input)) return input.map(v => this.trimDeep(v));
if (typeof input === 'object') {
const out: Record<string, any> = {};
for (const key of Object.keys(input)) {
out[key] = this.trimDeep((input as any)[key]);
}
return out;
}
return input;
}
// 将路径数组对应的值赋到对象中,支持构建嵌套结构与数组
private assignByPath(target: Record<string, any>, path: string[], value: any): void {
let cur: any = target;
for (let i = 0; i < path.length; i++) {
const key = path[i];
const isLast = i === path.length - 1;
if (isLast) {
if (key === '') {
if (!Array.isArray(cur)) return;
cur.push(value);
} else {
if (cur[key] === undefined) cur[key] = value;
else if (Array.isArray(cur[key])) cur[key].push(value);
else cur[key] = [cur[key], value];
}
} else {
if (!cur[key] || typeof cur[key] !== 'object') cur[key] = {};
cur = cur[key];
}
}
}
// 解析可能为 JSON 字符串或鍵值串的输入为对象
private parseLooseObject(input: any): Record<string, any> {
if (!input) return {};
if (typeof input === 'object') return input as Record<string, any>;
const str = String(input).trim();
try {
if (str.startsWith('{') || str.startsWith('[')) {
const json = JSON.parse(str);
if (json && typeof json === 'object') return json as Record<string, any>;
}
} catch {}
const obj: Record<string, any> = {};
const pairs = str.split(/[&;,]/).map(s => s.trim()).filter(Boolean);
for (const pair of pairs) {
const idxEq = pair.indexOf('=');
const idxColon = pair.indexOf(':');
const idx = idxEq >= 0 ? idxEq : idxColon;
if (idx < 0) continue;
const key = decodeURIComponent(pair.slice(0, idx)).trim();
const valueRaw = decodeURIComponent(pair.slice(idx + 1)).trim();
obj[key] = this.toPrimitive(valueRaw);
}
return obj;
}
resolve() {
return async (ctx: Context, next: NextFunction) => {
const raw = String((ctx.request as any).querystring || '');
const parsed = qs.parse(raw, { allowDots: true, depth: 10, ignoreQueryPrefix: false, comma: true });
const query = { ...(ctx.request.query || {}), ...(parsed as any) } as Record<string, any>;
const trimmedTop: Record<string, any> = {};
for (const k of Object.keys(query)) {
const v = (query as any)[k];
trimmedTop[k] = typeof v === 'string' ? String(v).trim() : v;
}
Object.assign(query, trimmedTop);
// 解析 where 对象,支持 JSON 字符串与括号或点号语法
const hasWhereInput = (query as any).where !== undefined;
let whereObj: Record<string, any> = this.parseLooseObject((query as any).where);
for (const k of Object.keys(query)) {
if (k === 'where') continue;
if (k.startsWith('where[') || k.startsWith('where.')) {
const pathStr = k.replace(/^where\.?/, '').replace(/\]/g, '').replace(/\[/g, '.');
const path = pathStr.split('.');
const val = this.toPrimitive((query as any)[k]);
this.assignByPath(whereObj, path, val);
}
}
const hasWhereBracketKeys = Object.keys(query).some(k => k.startsWith('where[') || k.startsWith('where.'));
if (hasWhereInput || hasWhereBracketKeys) (query as any).where = this.trimDeep(whereObj);
// 解析 order 对象,支持 JSON 字符串与括号或点号语法
const hasOrderInput = (query as any).order !== undefined;
let orderObj: Record<string, any> = this.parseLooseObject((query as any).order);
for (const k of Object.keys(query)) {
if (k === 'order') continue;
if (k.startsWith('order[') || k.startsWith('order.')) {
const pathStr = k.replace(/^order\.?/, '').replace(/\]/g, '').replace(/\[/g, '.');
const path = pathStr.split('.');
const val = this.toPrimitive((query as any)[k]);
this.assignByPath(orderObj, path, val);
}
}
const hasOrderBracketKeys = Object.keys(query).some(k => k.startsWith('order[') || k.startsWith('order.'));
if (hasOrderInput || hasOrderBracketKeys) (query as any).order = this.trimDeep(orderObj);
// 将常见分页参数转换为数字类型
if (query.page !== undefined) (query as any).page = Number(query.page);
if ((query as any).page_size !== undefined) (query as any).page_size = Number((query as any).page_size);
if ((query as any).per_page !== undefined) (query as any).per_page = Number((query as any).per_page);
if ((query as any).customer_id !== undefined) (query as any).customer_id = Number((query as any).customer_id);
ctx.request.query = query as any;
(ctx as any).query = query as any;
return await next();
};
}
static getName(): string {
return 'queryNormalize';
}
static getPriority(): number {
// 优先级靠前,优先处理查询参数
return -1;
}
}

View File

@ -7,7 +7,7 @@ export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
return async (ctx: Context, next: NextFunction) => { return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑 // 控制器前执行的逻辑
const startTime = Date.now(); const startTime = Date.now();
// 执行下一个 Web 中间件最后执行到控制器 // 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值 // 这里可以拿到下一个中间件或者控制器的返回值
const result = await next(); const result = await next();
// 控制器之后执行的逻辑 // 控制器之后执行的逻辑

View File

@ -0,0 +1,80 @@
import { Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Like, Repository } from 'typeorm';
import { Area } from '../entity/area.entity';
import { CreateAreaDTO, QueryAreaDTO, UpdateAreaDTO } from '../dto/area.dto';
import * as countries from 'i18n-iso-countries';
@Provide()
export class AreaService {
@InjectEntityModel(Area)
areaRepository: Repository<Area>;
constructor() {
// 在服务初始化时注册中文语言包
countries.registerLocale(require('i18n-iso-countries/langs/zh.json'));
}
async getAreaList(query: QueryAreaDTO) {
const { currentPage = 1, pageSize = 10, keyword = '' } = query;
const [list, total] = await this.areaRepository.findAndCount({
where: [{ name: Like(`%${keyword}%`) }, { code: Like(`%${keyword}%`) }],
skip: (currentPage - 1) * pageSize,
take: pageSize,
});
return { list, total };
}
async getAreaById(id: number) {
return this.areaRepository.findOne({ where: { id } });
}
async createArea(createAreaDTO: CreateAreaDTO) {
// 根据 code 获取国家中文名称
const name = countries.getName(createAreaDTO.code, 'zh', {
select: 'official',
});
// 如果找不到对应的国家,则抛出错误
if (!name) {
throw new Error(`无效的国家代码: ${createAreaDTO.code}`);
}
const area = new Area();
area.name = name;
area.code = createAreaDTO.code;
return this.areaRepository.save(area);
}
async updateArea(id: number, updateAreaDTO: UpdateAreaDTO) {
const area = await this.getAreaById(id);
if (!area) {
return null;
}
// 如果 code 发生变化,则更新 name
if (updateAreaDTO.code && updateAreaDTO.code !== area.code) {
const name = countries.getName(updateAreaDTO.code, 'zh', {
select: 'official',
});
if (!name) {
throw new Error(`无效的国家代码: ${updateAreaDTO.code}`);
}
area.name = name;
area.code = updateAreaDTO.code;
}
return this.areaRepository.save(area);
}
async deleteArea(id: number) {
const area = await this.getAreaById(id);
if (!area) {
return false;
}
await this.areaRepository.remove(area);
return true;
}
}

View File

@ -57,7 +57,7 @@ export class CanadaPostService {
return builder.buildObject(xmlObj); return builder.buildObject(xmlObj);
} }
// 默认直接构建(用于 createShipment 这类已有完整结构) // 默认直接构建(用于 createShipment 这类已有完整结构)
return builder.buildObject(data); return builder.buildObject(data);
} }

Some files were not shown because too many files have changed in this diff Show More