Compare commits

..

8 Commits

Author SHA1 Message Date
tikkhun 3545633f9e feat(模板): 添加模板管理功能及相关服务
实现模板的增删改查功能,包括模板实体、DTO、服务和控制器
添加模板渲染服务用于动态生成产品SKU
在product服务中集成模板服务用于SKU生成
添加模板数据填充器初始化默认模板
2025-11-27 19:04:03 +08:00
tikkhun 0809840507 feat(字典): 重构字典模块并实现产品属性关联
重构字典模块,支持字典项与产品的多对多关联
添加字典项导入导出功能,支持XLSX模板下载
优化产品管理,使用字典项作为产品属性
新增字典项排序和值字段
修改数据源配置,添加字典种子数据
2025-11-27 18:45:30 +08:00
tikkhun 889f00bde8 feat(字典): 添加字典和字典项的管理功能
实现字典和字典项的完整CRUD功能,包括:
- 字典的创建、查询、更新和删除
- 字典项的创建、查询、更新和删除
- 支持按条件查询字典和字典项
- 使用DTO进行参数校验
2025-11-27 16:15:36 +08:00
tikkhun 46bdcf0c69 refactor(service): 统一字典项关联字段命名和使用方式
将字典项关联字段从`dict_id`改为`dict`对象引用,简化关联查询
修复product.service.ts中flavors查询的错误条件
2025-11-27 15:42:00 +08:00
tikkhun d9800f341f refactor(实体): 重构产品相关实体及字典系统
- 将分类、口味、规格实体重构为字典系统
- 新增dict和dict_item实体实现通用字典管理
- 修改product实体字段从categoryId改为brandId
- 修复order_coupon和order_refund_item实体文件名拼写错误
- 更新typeorm配置和种子数据初始化逻辑
- 调整相关DTO和控制器接口适配新字典系统
- 更新package.json依赖版本和脚本
2025-11-27 15:32:45 +08:00
tikkhun 3c1da145d3 feat(db): 添加字典数据种子文件及初始化逻辑
添加品牌、口味和规格的字典数据种子文件
实现数据库连接和字典数据初始化的逻辑
2025-11-27 11:00:35 +08:00
tikkhun 543b015f72 refactor(service): 使用 Partial 类型更新变异参数类型 2025-11-27 10:25:14 +08:00
tikkhun 17dda81a98 feat(产品实体): 扩展产品实体字段以支持完整电商功能
添加多个产品相关字段,包括库存管理、价格促销、税务信息、产品分类等
完善字段注释以明确各字段用途和可选值
2025-11-27 10:24:20 +08:00
33 changed files with 3233 additions and 486 deletions

1125
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,14 +17,18 @@
"@midwayjs/typeorm": "^3.20.0",
"@midwayjs/validate": "^3.20.2",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"axios": "^1.7.9",
"axios": "^1.13.2",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"csv-parse": "^6.1.0",
"dayjs": "^1.11.13",
"mysql2": "^3.11.5",
"nodemailer": "^7.0.5",
"npm-check-updates": "^19.1.2",
"swagger-ui-dist": "^5.18.2",
"typeorm": "^0.3.20",
"typeorm": "^0.3.27",
"typeorm-extension": "^3.7.2",
"xlsx": "^0.18.5",
"xml2js": "^0.6.2"
},
"engines": {
@ -36,10 +40,13 @@
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js",
"test": "cross-env NODE_ENV=unittest jest",
"cov": "jest --coverage",
"lint": "mwts check",
"lint:fix": "mwts fix",
"lint": "mwtsc check",
"lint:fix": "mwtsc fix",
"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"
},
"repository": {
"type": "git",
@ -51,6 +58,7 @@
"@midwayjs/mock": "^3.20.11",
"cross-env": "^10.1.0",
"mwtsc": "^1.15.2",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
}
}

View File

@ -65,12 +65,18 @@ importers:
nodemailer:
specifier: ^7.0.5
version: 7.0.10
npm-check-updates:
specifier: ^19.1.2
version: 19.1.2
swagger-ui-dist:
specifier: ^5.18.2
version: 5.30.2
typeorm:
specifier: ^0.3.20
version: 0.3.27(mysql2@3.15.3)(reflect-metadata@0.2.2)
xlsx:
specifier: ^0.18.5
version: 0.18.5
xml2js:
specifier: ^0.6.2
version: 0.6.2
@ -84,6 +90,9 @@ importers:
mwtsc:
specifier: ^1.15.2
version: 1.15.2
tsx:
specifier: ^4.20.6
version: 4.20.6
typescript:
specifier: ^5.9.3
version: 5.9.3
@ -97,6 +106,162 @@ packages:
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.12':
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.12':
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.12':
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.12':
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.12':
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.12':
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.12':
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.12':
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.12':
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.12':
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.12':
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.12':
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.12':
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.12':
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.12':
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.12':
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.12':
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.12':
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.12':
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.12':
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.12':
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.12':
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.12':
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.12':
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.12':
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@hapi/bourne@3.0.0':
resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==}
@ -314,6 +479,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@ -415,6 +584,10 @@ packages:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@ -446,6 +619,10 @@ packages:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -488,6 +665,11 @@ packages:
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
create-hash@1.2.0:
resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==}
@ -606,6 +788,11 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@ -651,6 +838,10 @@ packages:
formidable@2.1.5:
resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@ -961,6 +1152,11 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
npm-check-updates@19.1.2:
resolution: {integrity: sha512-FNeFCVgPOj0fz89hOpGtxP2rnnRHR7hD2E8qNU8SMWfkyDZXA/xpgjsL3UMLSo3F/K13QvJDnbxPngulNDDo/g==}
engines: {node: '>=20.0.0', npm: '>=8.12.1'}
hasBin: true
oauth-1.0a@2.2.6:
resolution: {integrity: sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==}
@ -1160,6 +1356,10 @@ packages:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
@ -1228,6 +1428,11 @@ packages:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
tsx@4.20.6:
resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==}
engines: {node: '>=18.0.0'}
hasBin: true
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -1327,6 +1532,14 @@ packages:
engines: {node: '>= 8'}
hasBin: true
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@ -1338,6 +1551,11 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
@ -1373,6 +1591,84 @@ snapshots:
'@epic-web/invariant@1.0.0': {}
'@esbuild/aix-ppc64@0.25.12':
optional: true
'@esbuild/android-arm64@0.25.12':
optional: true
'@esbuild/android-arm@0.25.12':
optional: true
'@esbuild/android-x64@0.25.12':
optional: true
'@esbuild/darwin-arm64@0.25.12':
optional: true
'@esbuild/darwin-x64@0.25.12':
optional: true
'@esbuild/freebsd-arm64@0.25.12':
optional: true
'@esbuild/freebsd-x64@0.25.12':
optional: true
'@esbuild/linux-arm64@0.25.12':
optional: true
'@esbuild/linux-arm@0.25.12':
optional: true
'@esbuild/linux-ia32@0.25.12':
optional: true
'@esbuild/linux-loong64@0.25.12':
optional: true
'@esbuild/linux-mips64el@0.25.12':
optional: true
'@esbuild/linux-ppc64@0.25.12':
optional: true
'@esbuild/linux-riscv64@0.25.12':
optional: true
'@esbuild/linux-s390x@0.25.12':
optional: true
'@esbuild/linux-x64@0.25.12':
optional: true
'@esbuild/netbsd-arm64@0.25.12':
optional: true
'@esbuild/netbsd-x64@0.25.12':
optional: true
'@esbuild/openbsd-arm64@0.25.12':
optional: true
'@esbuild/openbsd-x64@0.25.12':
optional: true
'@esbuild/openharmony-arm64@0.25.12':
optional: true
'@esbuild/sunos-x64@0.25.12':
optional: true
'@esbuild/win32-arm64@0.25.12':
optional: true
'@esbuild/win32-ia32@0.25.12':
optional: true
'@esbuild/win32-x64@0.25.12':
optional: true
'@hapi/bourne@3.0.0': {}
'@hapi/hoek@9.3.0': {}
@ -1645,6 +1941,8 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
adler-32@1.3.1: {}
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
@ -1735,6 +2033,11 @@ snapshots:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@ -1779,6 +2082,8 @@ snapshots:
co@4.6.0: {}
codepage@1.15.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -1812,6 +2117,8 @@ snapshots:
core-util-is@1.0.3: {}
crc-32@1.2.2: {}
create-hash@1.2.0:
dependencies:
cipher-base: 1.0.7
@ -1919,6 +2226,35 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
'@esbuild/android-arm': 0.25.12
'@esbuild/android-arm64': 0.25.12
'@esbuild/android-x64': 0.25.12
'@esbuild/darwin-arm64': 0.25.12
'@esbuild/darwin-x64': 0.25.12
'@esbuild/freebsd-arm64': 0.25.12
'@esbuild/freebsd-x64': 0.25.12
'@esbuild/linux-arm': 0.25.12
'@esbuild/linux-arm64': 0.25.12
'@esbuild/linux-ia32': 0.25.12
'@esbuild/linux-loong64': 0.25.12
'@esbuild/linux-mips64el': 0.25.12
'@esbuild/linux-ppc64': 0.25.12
'@esbuild/linux-riscv64': 0.25.12
'@esbuild/linux-s390x': 0.25.12
'@esbuild/linux-x64': 0.25.12
'@esbuild/netbsd-arm64': 0.25.12
'@esbuild/netbsd-x64': 0.25.12
'@esbuild/openbsd-arm64': 0.25.12
'@esbuild/openbsd-x64': 0.25.12
'@esbuild/openharmony-arm64': 0.25.12
'@esbuild/sunos-x64': 0.25.12
'@esbuild/win32-arm64': 0.25.12
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
escalade@3.2.0: {}
escape-html@1.0.3: {}
@ -1967,6 +2303,8 @@ snapshots:
once: 1.4.0
qs: 6.14.0
frac@1.1.2: {}
fresh@0.5.2: {}
fsevents@2.3.3:
@ -2313,6 +2651,8 @@ snapshots:
normalize-path@3.0.0: {}
npm-check-updates@19.1.2: {}
oauth-1.0a@2.2.6: {}
object-inspect@1.13.4: {}
@ -2494,6 +2834,10 @@ snapshots:
sqlstring@2.3.3: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
statuses@1.5.0: {}
statuses@2.0.1: {}
@ -2582,6 +2926,13 @@ snapshots:
tsscmp@1.0.6: {}
tsx@4.20.6:
dependencies:
esbuild: 0.25.12
get-tsconfig: 4.13.0
optionalDependencies:
fsevents: 2.3.3
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
@ -2647,6 +2998,10 @@ snapshots:
dependencies:
isexe: 2.0.0
wmf@1.0.2: {}
word@0.3.0: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@ -2661,6 +3016,16 @@ snapshots:
wrappy@1.0.2: {}
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xml2js@0.6.2:
dependencies:
sax: 1.4.3

View File

@ -1,6 +1,5 @@
import { MidwayConfig } from '@midwayjs/core';
import { Product } from '../entity/product.entity';
import { Category } from '../entity/category.entity';
import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity';
import { User } from '../entity/user.entity';
@ -11,10 +10,10 @@ import { StockPoint } from '../entity/stock_point.entity';
import { StockRecord } from '../entity/stock_record.entity';
import { Order } from '../entity/order.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 { 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 { OrderSaleOriginal } from '../entity/order_item_original.entity';
import { OrderShipping } from '../entity/order_shipping.entity';
@ -26,14 +25,16 @@ import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity';
import { Transfer } from '../entity/transfer.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 { Customer } from '../entity/customer.entity';
import { DeviceWhitelist } from '../entity/device_whitelist';
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 DictSeeder from '../db/seeds/dict.seeder';
export default {
// use for cookie sign key, should change to your own and keep security
@ -42,9 +43,6 @@ export default {
default: {
entities: [
Product,
Category,
Strength,
Flavors,
WpProduct,
Variation,
User,
@ -76,9 +74,13 @@ export default {
AuthCode,
Subscription,
Site,
Dict,
DictItem,
Template
],
synchronize: true,
logging: false,
seeders: [DictSeeder],
},
dataSource: {
default: {

View File

@ -0,0 +1,171 @@
import { Inject, Controller, Get, Post, Put, Del, Query, Body, Param, Files, 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';
/**
*
* @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) {
// 调用服务层方法
return this.dictService.createDict(createDictDTO);
}
/**
*
* @param id ID
* @param updateDictDTO
*/
@Put('/:id')
@Validate()
async updateDict(@Param('id') id: number, @Body() updateDictDTO: UpdateDictDTO) {
// 调用服务层方法
return this.dictService.updateDict(id, updateDictDTO);
}
/**
*
* @param id ID
*/
@Del('/:id')
async deleteDict(@Param('id') id: number) {
// 调用服务层方法
return this.dictService.deleteDict(id);
}
/**
*
* @param files
* @param body ID
*/
@Post('/item/import')
@Validate()
async importDictItems(@Files() files: any, @Body() body: { dictId: number }) {
// 获取第一个文件
const file = files[0];
// 调用服务层方法
const result = await this.dictService.importDictItemsFromXLSX(file.data, body.dictId);
// 返回结果
return 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();
}
/**
*
* @param dictId ID ()
*/
@Get('/items')
async getDictItems(@Query('dictId') dictId?: number) {
// 调用服务层方法
return this.dictService.getDictItems(dictId);
}
/**
*
* @param createDictItemDTO
*/
@Post('/item')
@Validate()
async createDictItem(@Body() createDictItemDTO: CreateDictItemDTO) {
// 调用服务层方法
return this.dictService.createDictItem(createDictItemDTO);
}
/**
*
* @param id ID
* @param updateDictItemDTO
*/
@Put('/item/:id')
@Validate()
async updateDictItem(@Param('id') id: number, @Body() updateDictItemDTO: UpdateDictItemDTO) {
// 调用服务层方法
return this.dictService.updateDictItem(id, updateDictItemDTO);
}
/**
*
* @param id ID
*/
@Del('/item/:id')
async deleteDictItem(@Param('id') id: number) {
// 调用服务层方法
return this.dictService.deleteDictItem(id);
}
}

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

@ -13,15 +13,15 @@ import { ProductService } from '../service/product.service';
import { errorResponse, successResponse } from '../utils/response.util';
import {
BatchSetSkuDTO,
CreateCategoryDTO,
CreateBrandDTO,
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
QueryCategoryDTO,
QueryBrandDTO,
QueryFlavorsDTO,
QueryProductDTO,
QueryStrengthDTO,
UpdateCategoryDTO,
UpdateBrandDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
@ -29,8 +29,8 @@ import {
import { ApiOkResponse } from '@midwayjs/swagger';
import {
BooleanRes,
ProductCatListRes,
ProductCatRes,
ProductBrandListRes,
ProductBrandRes,
ProductListRes,
ProductRes,
ProductsRes,
@ -79,12 +79,12 @@ export class ProductController {
async getProductList(
@Query() query: QueryProductDTO
): Promise<ProductListRes> {
const { current = 1, pageSize = 10, name, categoryId } = query;
const { current = 1, pageSize = 10, name, brandId } = query;
try {
const data = await this.productService.getProductList(
{ current, pageSize },
name,
categoryId
brandId
);
return successResponse(data);
} catch (error) {
@ -138,8 +138,6 @@ export class ProductController {
}
}
@ApiOkResponse({
type: BooleanRes,
})
@ -154,13 +152,13 @@ export class ProductController {
}
@ApiOkResponse({
type: ProductCatListRes,
type: ProductBrandListRes,
})
@Get('/categories')
async getCategories(@Query() query: QueryCategoryDTO) {
@Get('/brands')
async getBrands(@Query() query: QueryBrandDTO) {
const { current = 1, pageSize = 10, name } = query;
try {
let data = await this.productService.getCategoryList(
let data = await this.productService.getBrandList(
{ current, pageSize },
name
);
@ -171,10 +169,10 @@ export class ProductController {
}
@ApiOkResponse()
@Get('/categorieAll')
async getCategorieAll() {
@Get('/brandAll')
async getBrandAll() {
try {
let data = await this.productService.getCategoryAll();
let data = await this.productService.getBrandAll();
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -182,18 +180,19 @@ export class ProductController {
}
@ApiOkResponse({
type: ProductCatRes,
type: ProductBrandRes,
})
@Post('/category')
async createCategory(@Body() categoryData: CreateCategoryDTO) {
@Post('/brand')
async createBrand(@Body() brandData: CreateBrandDTO) {
try {
const hasCategory = await this.productService.hasCategory(
categoryData.name
const hasBrand = await this.productService.hasAttribute(
'brand',
brandData.name
);
if (hasCategory) {
return errorResponse('分类已存在');
if (hasBrand) {
return errorResponse('品牌已存在');
}
let data = await this.productService.createCategory(categoryData);
let data = await this.productService.createBrand(brandData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -201,21 +200,23 @@ export class ProductController {
}
@ApiOkResponse({
type: ProductCatRes,
type: ProductBrandRes,
})
@Put('/category/:id')
async updateCategory(
@Put('/brand/:id')
async updateBrand(
@Param('id') id: number,
@Body() categoryData: UpdateCategoryDTO
@Body() brandData: UpdateBrandDTO
) {
try {
const hasCategory = await this.productService.hasCategory(
categoryData.name
const hasBrand = await this.productService.hasAttribute(
'brand',
brandData.name,
id
);
if (hasCategory) {
return errorResponse('分类已存在');
if (hasBrand) {
return errorResponse('品牌已存在');
}
const data = this.productService.updateCategory(id, categoryData);
const data = this.productService.updateBrand(id, brandData);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -225,12 +226,12 @@ export class ProductController {
@ApiOkResponse({
type: BooleanRes,
})
@Del('/category/:id')
async deleteCategory(@Param('id') id: number) {
@Del('/brand/:id')
async deleteBrand(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInCategory(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除');
const data = await this.productService.deleteCategory(id);
const hasProducts = await this.productService.hasProductsInAttribute(id);
if (hasProducts) throw new Error('该品牌下有商品,无法删除');
const data = await this.productService.deleteBrand(id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || error);
@ -281,9 +282,9 @@ export class ProductController {
@Post('/flavors')
async createFlavors(@Body() flavorsData: CreateFlavorsDTO) {
try {
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name);
if (hasFlavors) {
return errorResponse('分类已存在');
return errorResponse('口味已存在');
}
let data = await this.productService.createFlavors(flavorsData);
return successResponse(data);
@ -299,9 +300,9 @@ export class ProductController {
@Body() flavorsData: UpdateFlavorsDTO
) {
try {
const hasFlavors = await this.productService.hasFlavors(flavorsData.name);
const hasFlavors = await this.productService.hasAttribute('flavor', flavorsData.name, id);
if (hasFlavors) {
return errorResponse('分类已存在');
return errorResponse('口味已存在');
}
const data = this.productService.updateFlavors(id, flavorsData);
return successResponse(data);
@ -316,8 +317,8 @@ export class ProductController {
@Del('/flavors/:id')
async deleteFlavors(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInFlavors(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除');
const hasProducts = await this.productService.hasProductsInAttribute(id);
if (hasProducts) throw new Error('该口味下有商品,无法删除');
const data = await this.productService.deleteFlavors(id);
return successResponse(data);
} catch (error) {
@ -355,11 +356,12 @@ export class ProductController {
@Post('/strength')
async createStrength(@Body() strengthData: CreateStrengthDTO) {
try {
const hasStrength = await this.productService.hasStrength(
const hasStrength = await this.productService.hasAttribute(
'strength',
strengthData.name
);
if (hasStrength) {
return errorResponse('分类已存在');
return errorResponse('规格已存在');
}
let data = await this.productService.createStrength(strengthData);
return successResponse(data);
@ -375,11 +377,13 @@ export class ProductController {
@Body() strengthData: UpdateStrengthDTO
) {
try {
const hasStrength = await this.productService.hasStrength(
strengthData.name
const hasStrength = await this.productService.hasAttribute(
'strength',
strengthData.name,
id
);
if (hasStrength) {
return errorResponse('分类已存在');
return errorResponse('规格已存在');
}
const data = this.productService.updateStrength(id, strengthData);
return successResponse(data);
@ -394,8 +398,8 @@ export class ProductController {
@Del('/strength/:id')
async deleteStrength(@Param('id') id: number) {
try {
const hasProducts = await this.productService.hasProductsInStrength(id);
if (hasProducts) throw new Error('该分类下有商品,无法删除');
const hasProducts = await this.productService.hasProductsInAttribute(id);
if (hasProducts) throw new Error('该规格下有商品,无法删除');
const data = await this.productService.deleteStrength(id);
return successResponse(data);
} catch (error) {

View File

@ -0,0 +1,115 @@
import { Inject, Controller, Get, Post, Put, Del, Body, Param } from '@midwayjs/core';
import { TemplateService } from '../service/template.service';
import { successResponse, errorResponse } from '../utils/response.util';
import { CreateTemplateDTO, UpdateTemplateDTO } 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('/')
async getTemplateList() {
try {
// 调用服务层获取列表
const data = await this.templateService.getTemplateList();
// 返回成功响应
return successResponse(data);
} catch (error) {
// 返回错误响应
return errorResponse(error.message);
}
}
/**
* @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);
}
}
}

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

@ -0,0 +1,88 @@
import { DataSource, DataSourceOptions } from 'typeorm';
import { SeederOptions } from 'typeorm-extension';
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 { PurchaseOrder } from '../entity/purchase_order.entity';
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
import { Stock } from '../entity/stock.entity';
import { StockPoint } from '../entity/stock_point.entity';
import { StockRecord } from '../entity/stock_record.entity';
import { Order } from '../entity/order.entity';
import { OrderItem } from '../entity/order_item.entity';
import { OrderCoupon } from '../entity/order_coupon.entity';
import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity';
import { OrderRefundItem } from '../entity/order_refund_item.entity';
import { OrderSale } from '../entity/order_sale.entity';
import { OrderSaleOriginal } from '../entity/order_item_original.entity';
import { OrderShipping } from '../entity/order_shipping.entity';
import { Service } from '../entity/service.entity';
import { ShippingAddress } from '../entity/shipping_address.entity';
import { OrderNote } from '../entity/order_note.entity';
import { OrderShipment } from '../entity/order_shipment.entity';
import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity';
import { Transfer } from '../entity/transfer.entity';
import { TransferItem } from '../entity/transfer_item.entity';
import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity';
import { DeviceWhitelist } from '../entity/device_whitelist';
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';
const options: DataSourceOptions & SeederOptions = {
type: 'mysql',
host: 'localhost',
port: 23306,
username: 'root',
password: '12345678',
database: 'inventory',
synchronize: false,
logging: true,
entities: [
Product,
WpProduct,
Variation,
User,
PurchaseOrder,
PurchaseOrderItem,
Stock,
StockPoint,
StockRecord,
Order,
OrderItem,
OrderCoupon,
OrderFee,
OrderRefund,
OrderRefundItem,
OrderSale,
OrderSaleOriginal,
OrderShipment,
ShipmentItem,
Shipment,
OrderShipping,
Service,
ShippingAddress,
OrderNote,
Transfer,
TransferItem,
CustomerTag,
Customer,
DeviceWhitelist,
AuthCode,
Subscription,
Site,
Dict,
DictItem,
],
migrations: ['src/migration/*.ts'],
seeds: ['src/db/seeds/**/*.ts'],
};
export const AppDataSource = new DataSource(options);

View File

@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ProductDictItemManyToMany1764238434984 implements MigrationInterface {
name = 'ProductDictItemManyToMany1764238434984'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`product_attributes_dict_item\` (\`productId\` int NOT NULL, \`dictItemId\` int NOT NULL, INDEX \`IDX_592cdbdaebfec346c202ffb82c\` (\`productId\`), INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` (\`dictItemId\`), PRIMARY KEY (\`productId\`, \`dictItemId\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`brandId\``);
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`flavorsId\``);
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`strengthId\``);
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`humidity\``);
await queryRunner.query(`ALTER TABLE \`product\` ADD \`sku\` varchar(255) NOT NULL`);
await queryRunner.query(`ALTER TABLE \`product\` ADD UNIQUE INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\` (\`sku\`)`);
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_592cdbdaebfec346c202ffb82ca\` FOREIGN KEY (\`productId\`) REFERENCES \`product\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_406c1da5b6de45fecb7967c3ec0\` FOREIGN KEY (\`dictItemId\`) REFERENCES \`dict_item\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_406c1da5b6de45fecb7967c3ec0\``);
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_592cdbdaebfec346c202ffb82ca\``);
await queryRunner.query(`ALTER TABLE \`product\` DROP INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\``);
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`sku\``);
await queryRunner.query(`ALTER TABLE \`product\` ADD \`humidity\` varchar(255) NOT NULL`);
await queryRunner.query(`ALTER TABLE \`product\` ADD \`strengthId\` int NOT NULL`);
await queryRunner.query(`ALTER TABLE \`product\` ADD \`flavorsId\` int NOT NULL`);
await queryRunner.query(`ALTER TABLE \`product\` ADD \`brandId\` int NOT NULL`);
await queryRunner.query(`DROP INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` ON \`product_attributes_dict_item\``);
await queryRunner.query(`DROP INDEX \`IDX_592cdbdaebfec346c202ffb82c\` ON \`product_attributes_dict_item\``);
await queryRunner.query(`DROP TABLE \`product_attributes_dict_item\``);
}
}

View File

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

@ -0,0 +1,153 @@
import { Seeder, SeederFactoryManager } 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 {
public async run(
dataSource: DataSource,
factoryManager: SeederFactoryManager
): Promise<any> {
const dictRepository = dataSource.getRepository(Dict);
const dictItemRepository = dataSource.getRepository(DictItem);
const flavorsData = [
{ id: 1, title: 'Bellini Mini', name: 'Bellini Mini' },
{ id: 2, title: 'Max Polarmint', name: 'Max Polarmint' },
{ id: 3, title: 'Blueberry', name: 'Blueberry' },
{ id: 4, title: 'Citrus', name: 'Citrus' },
{ id: 5, title: 'Wintergreen', name: 'Wintergreen' },
{ id: 6, title: 'COOL MINT', name: 'COOL MINT' },
{ id: 7, title: 'JUICY PEACH', name: 'JUICY PEACH' },
{ id: 8, title: 'ORANGE', name: 'ORANGE' },
{ id: 9, title: 'PEPPERMINT', name: 'PEPPERMINT' },
{ id: 10, title: 'SPEARMINT', name: 'SPEARMINT' },
{ id: 11, title: 'STRAWBERRY', name: 'STRAWBERRY' },
{ id: 12, title: 'WATERMELON', name: 'WATERMELON' },
{ id: 13, title: 'COFFEE', name: 'COFFEE' },
{ id: 14, title: 'LEMONADE', name: 'LEMONADE' },
{ id: 15, title: 'apple mint', name: 'apple mint' },
{ id: 16, title: 'PEACH', name: 'PEACH' },
{ id: 17, title: 'Mango', name: 'Mango' },
{ id: 18, title: 'ICE WINTERGREEN', name: 'ICE WINTERGREEN' },
{ id: 19, title: 'Pink Lemonade', name: 'Pink Lemonade' },
{ id: 20, title: 'Blackcherry', name: 'Blackcherry' },
{ id: 21, title: 'fresh mint', name: 'fresh mint' },
{ id: 22, title: 'Strawberry Lychee', name: 'Strawberry Lychee' },
{ id: 23, title: 'Passion Fruit', name: 'Passion Fruit' },
{ id: 24, title: 'Banana lce', name: 'Banana lce' },
{ id: 25, title: 'Bubblegum', name: 'Bubblegum' },
{ id: 26, title: 'Mango lce', name: 'Mango lce' },
{ id: 27, title: 'Grape lce', name: 'Grape lce' },
];
const brandsData = [
{ id: 1, title: 'Yoone', name: 'YOONE' },
{ id: 2, title: 'White Fox', name: 'WHITE_FOX' },
{ id: 3, title: 'ZYN', name: 'ZYN' },
{ id: 4, title: 'Zonnic', name: 'ZONNIC' },
{ id: 5, title: 'Zolt', name: 'ZOLT' },
{ id: 6, title: 'Velo', name: 'VELO' },
{ id: 7, title: 'Lucy', name: 'LUCY' },
{ id: 8, title: 'EGP', name: 'EGP' },
{ id: 9, title: 'Bridge', name: 'BRIDGE' },
{ id: 10, title: 'ZEX', name: 'ZEX' },
{ id: 11, title: 'Sesh', name: 'Sesh' },
{ id: 12, title: 'Pablo', name: 'Pablo' },
];
const strengthsData = [
{ id: 1, title: '3MG', name: '3MG' },
{ id: 2, title: '9MG', name: '9MG' },
{ id: 3, title: '2MG', name: '2MG' },
{ id: 4, title: '4MG', name: '4MG' },
{ id: 5, title: '12MG', name: '12MG' },
{ id: 6, title: '18MG', name: '18MG' },
{ id: 7, title: '6MG', name: '6MG' },
{ id: 8, title: '16.5MG', name: '16.5MG' },
{ id: 9, title: '6.5MG', name: '6.5MG' },
{ id: 10, title: '30MG', name: '30MG' },
];
// 在插入新数据前,不清空旧数据,改为如果不存在则创建
// await dictItemRepository.query('DELETE FROM `dict_item`');
// await dictRepository.query('DELETE FROM `dict`');
// // 重置自增 ID
// await dictItemRepository.query('ALTER TABLE `dict_item` AUTO_INCREMENT = 1');
// await dictRepository.query('ALTER TABLE `dict` AUTO_INCREMENT = 1');
// 初始化语言字典
const locales = [
{ name: 'zh-CN', title: '简体中文' },
{ name: 'en-US', title: 'English' },
];
for (const locale of locales) {
let dict = await dictRepository.findOne({ where: { name: locale.name } });
if (!dict) {
dict = await dictRepository.save(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, 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, dict: enDict });
}
}
const brandDict = await dictRepository.save({ title: '品牌', name: 'brand' });
const flavorDict = await dictRepository.save({ title: '口味', name: 'flavor' });
const strengthDict = await dictRepository.save({ title: '强度', name: 'strength' });
// 遍历品牌数据
for (const brand of brandsData) {
// 保存字典项,并关联到品牌字典
await dictItemRepository.save({
title: brand.title,
name: brand.name,
dict: brandDict,
});
}
// 遍历口味数据
for (const flavor of flavorsData) {
// 保存字典项,并关联到口味字典
await dictItemRepository.save({
title: flavor.title,
name: flavor.name,
dict: flavorDict,
});
}
// 遍历强度数据
for (const strength of strengthsData) {
// 保存字典项,并关联到强度字典
await dictItemRepository.save({
title: strength.title,
name: strength.name,
dict: strengthDict,
});
}
}
}

View File

@ -0,0 +1,36 @@
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 product_sku
* @param {DataSource} dataSource - repository
* @param {SeederFactoryManager} factoryManager - Seeder
*/
public async run(
dataSource: DataSource,
factoryManager: SeederFactoryManager
): Promise<any> {
// 获取 Template 实体的 repository
const templateRepository = dataSource.getRepository(Template);
// 检查名为 'product_sku' 的模板是否已存在
const existingTemplate = await templateRepository.findOne({
where: { name: 'product_sku' },
});
// 如果模板不存在,则创建并保存
if (!existingTemplate) {
const template = new Template();
template.name = 'product_sku';
template.value = '{{brand}}-{{flavor}}-{{strength}}-{{humidity}}';
await templateRepository.save(template);
}
}
}

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

@ -0,0 +1,40 @@
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.number().required())
dictId: number; // 所属字典的ID
}
// 更新字典项的数据传输对象
export class UpdateDictItemDTO {
@Rule(RuleType.string())
name?: string; // 字典项名称 (可选)
@Rule(RuleType.string())
title?: string; // 字典项标题 (可选)
}

View File

@ -1,6 +1,16 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
class DictItemDTO {
@ApiProperty({ description: '显示名称', required: false })
@Rule(RuleType.string())
title?: string;
@ApiProperty({ description: '唯一标识', required: true })
@Rule(RuleType.string().required())
name: string;
}
/**
* DTO
*/
@ -17,17 +27,36 @@ export class CreateProductDTO {
@Rule(RuleType.string())
description: string;
@ApiProperty({ example: '1', description: '分类 ID' })
@Rule(RuleType.number())
categoryId: number;
@ApiProperty({ description: '产品 SKU', required: false })
@Rule(RuleType.string())
sku?: string;
@ApiProperty()
@Rule(RuleType.number())
strengthId: number;
@ApiProperty({ description: '品牌', type: DictItemDTO })
@Rule(
RuleType.object().keys({
title: RuleType.string().required(),
name: RuleType.string(),
})
)
brand: DictItemDTO;
@ApiProperty()
@Rule(RuleType.number())
flavorsId: number;
@ApiProperty({ description: '规格', type: DictItemDTO })
@Rule(
RuleType.object().keys({
title: RuleType.string().required(),
name: RuleType.string(),
})
)
strength: DictItemDTO;
@ApiProperty({ description: '口味', type: DictItemDTO })
@Rule(
RuleType.object().keys({
title: RuleType.string().required(),
name: RuleType.string(),
})
)
flavor: DictItemDTO;
@ApiProperty()
@Rule(RuleType.string())
@ -59,36 +88,41 @@ export class QueryProductDTO {
@Rule(RuleType.string())
name: string;
@ApiProperty({ example: '1', description: '分类 ID' })
@ApiProperty({ example: '1', description: '品牌 ID' })
@Rule(RuleType.string())
categoryId: number;
brandId: number;
}
/**
* DTO
* DTO
*/
export class CreateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
export class CreateBrandDTO {
@ApiProperty({ example: 'ZYN', description: '品牌名称', required: true })
@Rule(RuleType.string().required().empty({ message: '品牌名称不能为空' }))
title: string;
@ApiProperty({ example: 'ZYN', description: '品牌唯一标识', required: true })
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
name: string;
}
/**
* DTO
* DTO
*/
export class UpdateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
export class UpdateBrandDTO {
@ApiProperty({ example: 'ZYN', description: '品牌名称' })
@Rule(RuleType.string())
title: string;
@ApiProperty({ example: 'ZYN', description: '品牌唯一标识' })
@Rule(RuleType.string())
name: string;
}
/**
* DTO
* DTO
*/
export class QueryCategoryDTO {
export class QueryBrandDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ -98,21 +132,30 @@ export class QueryCategoryDTO {
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
@Rule(RuleType.string().required())
name: string; // 搜索关键字(支持模糊查询)
}
export class CreateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@ApiProperty({ example: 'WINTERGREEN', description: '口味名称', required: true })
@Rule(RuleType.string().required().empty({ message: '口味名称不能为空' }))
title: string;
@ApiProperty({
example: 'WINTERGREEN',
description: '口味唯一标识',
required: true,
})
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
name: string;
}
export class UpdateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@ApiProperty({ example: 'WINTERGREEN', description: '口味名称' })
@Rule(RuleType.string())
title: string;
@ApiProperty({ example: 'WINTERGREEN', description: '口味唯一标识' })
@Rule(RuleType.string())
name: string;
}
@ -132,16 +175,21 @@ export class QueryFlavorsDTO {
}
export class CreateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@ApiProperty({ example: '6MG', description: '规格名称', required: false })
@Rule(RuleType.string())
title?: string;
@ApiProperty({ example: '6MG', description: '规格唯一标识', required: true })
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
name: string;
}
export class UpdateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@ApiProperty({ example: '6MG', description: '规格名称' })
@Rule(RuleType.string())
title: string;
@ApiProperty({ example: '6MG', description: '规格唯一标识' })
@Rule(RuleType.string())
name: string;
}
@ -155,7 +203,7 @@ export class QueryStrengthDTO {
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@ApiProperty({ example: 'YOONE', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}

View File

@ -1,5 +1,4 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Category } from '../entity/category.entity';
import { Order } from '../entity/order.entity';
import { Product } from '../entity/product.entity';
import { StockPoint } from '../entity/stock_point.entity';
@ -18,12 +17,11 @@ import { Service } from '../entity/service.entity';
import { RateDTO } from './freightcom.dto';
import { ShippingAddress } from '../entity/shipping_address.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 { PaymentMethodDTO } from './logistics.dto';
import { Flavors } from '../entity/flavors.entity';
import { Strength } from '../entity/strength.entity';
import { Subscription } from '../entity/subscription.entity';
import { Dict } from '../entity/dict.entity';
export class BooleanRes extends SuccessWrapper(Boolean) {}
//网站配置返回数据
@ -35,18 +33,38 @@ export class ProductListRes extends SuccessWrapper(ProductPaginatedResponse) {}
//产品返回数据
export class ProductRes extends SuccessWrapper(Product) {}
export class ProductsRes extends SuccessArrayWrapper(Product) {}
//产品分类返分页数据
export class CategoryPaginatedResponse extends PaginatedWrapper(Category) {}
export class FlavorsPaginatedResponse extends PaginatedWrapper(Flavors) {}
export class StrengthPaginatedResponse extends PaginatedWrapper(Strength) {}
//产品分类返分页返回数据
export class ProductCatListRes extends SuccessWrapper(
CategoryPaginatedResponse
//产品品牌返分页数据
export class BrandPaginatedResponse extends PaginatedWrapper(Dict) {}
//产品品牌返分页返回数据
export class ProductBrandListRes extends SuccessWrapper(
BrandPaginatedResponse
) {}
//产品分类返所有数据
export class ProductCatAllRes extends SuccessArrayWrapper(Category) {}
//产品分类返回数据
export class ProductCatRes extends SuccessWrapper(Category) {}
//产品品牌返所有数据
export class ProductBrandAllRes extends SuccessArrayWrapper(Dict) {}
//产品品牌返回数据
export class ProductBrandRes extends SuccessWrapper(Dict) {}
//产品口味返分页数据
export class FlavorsPaginatedResponse extends PaginatedWrapper(Dict) {}
//产品口味返分页返回数据
export class ProductFlavorsListRes extends SuccessWrapper(
FlavorsPaginatedResponse
) {}
//产品口味返所有数据
export class ProductFlavorsAllRes extends SuccessArrayWrapper(Dict) {}
//产品口味返回数据
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 WpProductPaginatedResponse extends PaginatedWrapper(

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

@ -0,0 +1,22 @@
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;
}
export class UpdateTemplateDTO {
@ApiProperty({ description: '模板名称', required: true })
@Rule(RuleType.string().required())
name: string;
@ApiProperty({ description: '模板内容', required: true })
@Rule(RuleType.string().required())
value: string;
}

View File

@ -1,53 +0,0 @@
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Category {
@ApiProperty({
example: '1',
description: '分类 ID',
type: 'number',
required: true,
})
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({
example: '分类名称',
description: '分类名称',
type: 'string',
required: true,
})
@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;
}

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,57 @@
/**
* @description
* @author ZKS
* @date 2025-11-27
*/
import { Dict } from './dict.entity';
import { Product } from './product.entity';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class DictItem {
// 主键
@PrimaryGeneratedColumn()
id: number;
// 字典项名称
@Column({ comment: '字典项显示名称' })
title: string;
// 唯一标识
@Column({ unique: true, comment: '字典唯一标识名称' })
name: string;
// 字典项值
@Column({ nullable: true, comment: '字典项值' })
value?: 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

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

View File

@ -4,8 +4,11 @@ import {
CreateDateColumn,
UpdateDateColumn,
Entity,
ManyToMany,
JoinTable,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
import { DictItem } from './dict_item.entity';
@Entity()
export class Product {
@ -31,30 +34,19 @@ export class Product {
@Column({ default: '' })
nameCn: string;
@ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Column({ nullable: true })
description?: string;
@ApiProperty({ example: '1', description: '分类 ID', type: 'number' })
@Column()
categoryId: number;
@ApiProperty({ description: 'sku'})
@Column({ unique: true })
sku: string;
@ApiProperty({ description: '口味ID' })
@Column()
flavorsId: number;
@ApiProperty({ description: '尼古丁强度ID' })
@Column()
strengthId: number;
@ApiProperty({ description: '湿度' })
@Column()
humidity: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Column({ nullable: true })
sku?: string;
@ManyToMany(() => DictItem, {
cascade: true,
})
@JoinTable()
attributes: DictItem[];
@ApiProperty({
example: '2022-12-12 11:11:11',

View File

@ -2,27 +2,27 @@ import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('site')
export class Site {
@PrimaryGeneratedColumn({ type: 'int' })
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 255, nullable: true })
@Column({ length: 255, nullable: true })
apiUrl: string;
@Column({ type: 'varchar', length: 255, nullable: true })
@Column({ length: 255, nullable: true })
consumerKey: string;
@Column({ type: 'varchar', length: 255, nullable: true })
@Column({ length: 255, nullable: true })
consumerSecret: string;
@Column({ type: 'varchar', length: 255, unique: true })
@Column({ length: 255, unique: true })
siteName: string;
@Column({ type: 'varchar', length: 32, default: 'woocommerce' })
@Column({ length: 32, default: 'woocommerce' })
type: string; // 平台类型woocommerce | shopyy
@Column({ type: 'varchar', length: 64, nullable: true })
@Column({ length: 64, nullable: true })
skuPrefix: string;
@Column({ type: 'tinyint', default: 0 })
isDisabled: number;
@Column({ default: false })
isDisabled: boolean;
}

View File

@ -1,29 +1,29 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger';
@Entity()
export class Strength {
@ApiProperty()
@Entity('template')
export class Template {
@ApiProperty({ type: 'number' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
@ApiProperty({ type: 'string' })
@Column({ unique: true })
name: string;
@ApiProperty({
description: '唯一识别key',
type: 'string',
required: true,
})
@Column()
unique_key: string;
@ApiProperty({ type: 'string' })
@Column('text')
value: string;
@ApiProperty({ nullable: true ,name:"描述"})
@Column('text',{nullable: true,comment: "描述"})
description?: string;
@ApiProperty({
example: '2022-12-12 11:11:11',

View File

@ -53,42 +53,150 @@ export class WpProduct {
name: string;
@ApiProperty({ description: '产品状态', enum: ProductStatus })
@Column({ type: 'enum', enum: ProductStatus })
@Column({ type: 'enum', enum: ProductStatus, comment: '产品状态: draft, pending, private, publish' })
status: ProductStatus;
@ApiProperty({ description: '是否为特色产品', type: 'boolean' })
@Column({ default: false, comment: '是否为特色产品' })
featured: boolean;
@ApiProperty({ description: '目录可见性', type: 'string' })
@Column({ default: 'visible', comment: '目录可见性: visible, catalog, search, hidden' })
catalog_visibility: string;
@ApiProperty({ description: '产品描述', type: 'string' })
@Column({ type: 'text', nullable: true, comment: '产品描述' })
description: string;
@ApiProperty({ description: '产品短描述', type: 'string' })
@Column({ type: 'text', nullable: true, comment: '产品短描述' })
short_description: string;
@ApiProperty({ description: '上下架状态', enum: ProductStockStatus })
@Column({
name: 'stock_status',
type: 'enum',
enum: ProductStockStatus,
default: ProductStockStatus.INSTOCK
default: ProductStockStatus.INSTOCK,
comment: '库存状态: instock, outofstock, onbackorder',
})
stockStatus: ProductStockStatus;
@ApiProperty({ description: '库存数量', type: 'number' })
@Column({ type: 'int', nullable: true, comment: '库存数量' })
stock_quantity: number;
@ApiProperty({ description: '允许缺货下单', type: 'string' })
@Column({ nullable: true, comment: '允许缺货下单: no, notify, yes' })
backorders: string;
@ApiProperty({ description: '是否单独出售', type: 'boolean' })
@Column({ default: false, comment: '是否单独出售' })
sold_individually: boolean;
@ApiProperty({ description: '常规价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
regular_price: number; // 常规价格
@Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '常规价格' })
regular_price: number;
@ApiProperty({ description: '销售价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true })
sale_price: number; // 销售价格
@Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '销售价格' })
sale_price: number;
@ApiProperty({ description: '促销开始日期', type: 'datetime' })
@Column({ type: 'datetime', nullable: true, comment: '促销开始日期' })
date_on_sale_from: Date| null;
@ApiProperty({ description: '促销结束日期', type: 'datetime' })
@Column({ type: 'datetime', nullable: true, comment: '促销结束日期' })
date_on_sale_to: Date|null;
@ApiProperty({ description: '是否促销中', type: Boolean })
@Column({ nullable: true, type: Boolean })
on_sale: boolean; // 是否促销中
@Column({ nullable: true, type: 'boolean', comment: '是否促销中' })
on_sale: boolean;
@ApiProperty({ description: '是否删除', type: Boolean })
@Column({ nullable: true, type: Boolean , default: false })
on_delete: boolean; // 是否删除
@ApiProperty({ description: '税务状态', type: 'string' })
@Column({ default: 'taxable', comment: '税务状态: taxable, shipping, none' })
tax_status: string;
@ApiProperty({ description: '税类', type: 'string' })
@Column({ nullable: true, comment: '税类' })
tax_class: string;
@ApiProperty({
description: '产品类型',
enum: ProductType,
})
@Column({ type: 'enum', enum: ProductType })
@ApiProperty({ description: '重量(g)', type: 'number' })
@Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '重量(g)' })
weight: number;
@ApiProperty({ description: '尺寸(长宽高)', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '尺寸' })
dimensions: { length: string; width: string; height: string };
@ApiProperty({ description: '允许评论', type: 'boolean' })
@Column({ default: true, comment: '允许客户评论' })
reviews_allowed: boolean;
@ApiProperty({ description: '购买备注', type: 'string' })
@Column({ nullable: true, comment: '购买备注' })
purchase_note: string;
@ApiProperty({ description: '菜单排序', type: 'number' })
@Column({ default: 0, comment: '菜单排序' })
menu_order: number;
@ApiProperty({ description: '产品类型', enum: ProductType })
@Column({ type: 'enum', enum: ProductType, comment: '产品类型: simple, grouped, external, variable' })
type: ProductType;
@ApiProperty({ description: '父产品ID', type: 'number' })
@Column({ default: 0, comment: '父产品ID' })
parent_id: number;
@ApiProperty({ description: '外部产品URL', type: 'string' })
@Column({ type: 'text', nullable: true, comment: '外部产品URL' })
external_url: string;
@ApiProperty({ description: '外部产品按钮文本', type: 'string' })
@Column({ nullable: true, comment: '外部产品按钮文本' })
button_text: string;
@ApiProperty({ description: '分组产品', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '分组产品' })
grouped_products: number[];
@ApiProperty({ description: '追加销售', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '追加销售' })
upsell_ids: number[];
@ApiProperty({ description: '交叉销售', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '交叉销售' })
cross_sell_ids: number[];
@ApiProperty({ description: '分类', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '分类' })
categories: { id: number; name: string; slug: string }[];
@ApiProperty({ description: '标签', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '标签' })
tags: { id: number; name: string; slug: string }[];
@ApiProperty({ description: '图片', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '图片' })
images: { id: number; src: string; name: string; alt: string }[];
@ApiProperty({ description: '产品属性', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '产品属性' })
attributes: { id: number; name: string; position: number; visible: boolean; variation: boolean; options: string[] }[];
@ApiProperty({ description: '默认属性', type: 'json' })
@Column({ type: 'json', nullable: true, comment: '默认属性' })
default_attributes: { id: number; name: string; option: string }[];
@ApiProperty({ description: 'GTIN', type: 'string' })
@Column({ nullable: true, comment: 'GTIN, UPC, EAN, or ISBN' })
gtin: string;
@ApiProperty({ description: '是否删除', type: 'boolean' })
@Column({ nullable: true, type: 'boolean', default: false, comment: '是否删除' })
on_delete: boolean;
@Column({ type: 'json', nullable: true })
metadata: Record<string, any>; // 产品的其他扩展字段

161
src/service/dict.service.ts Normal file
View File

@ -0,0 +1,161 @@
import { Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository, Like } from 'typeorm';
import { Dict } from '../entity/dict.entity';
import { DictItem } from '../entity/dict_item.entity';
import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto';
import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
import * as xlsx from 'xlsx';
@Provide()
export class DictService {
@InjectEntityModel(Dict)
dictModel: Repository<Dict>;
@InjectEntityModel(DictItem)
dictItemModel: Repository<DictItem>;
// 生成并返回字典的XLSX模板
getDictXLSXTemplate() {
// 定义表头
const headers = ['name', 'title'];
// 创建一个新的工作表
const ws = xlsx.utils.aoa_to_sheet([headers]);
// 创建一个新的工作簿
const wb = xlsx.utils.book_new();
// 将工作表添加到工作簿
xlsx.utils.book_append_sheet(wb, ws, 'Dicts');
// 将工作簿写入缓冲区
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
}
// 从XLSX文件导入字典
async importDictsFromXLSX(buffer: Buffer) {
// 读取缓冲区中的工作簿
const wb = xlsx.read(buffer, { type: 'buffer' });
// 获取第一个工作表的名称
const wsname = wb.SheetNames[0];
// 获取第一个工作表
const ws = wb.Sheets[wsname];
// 将工作表转换为JSON对象数组
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title'] }).slice(1);
// 创建要保存的字典实体数组
const dicts = data.map((row: any) => {
const dict = new Dict();
dict.name = row.name;
dict.title = row.title;
return dict;
});
// 保存字典实体数组到数据库
await this.dictModel.save(dicts);
// 返回成功导入的记录数
return { success: true, count: dicts.length };
}
// 生成并返回字典项的XLSX模板
getDictItemXLSXTemplate() {
const headers = ['name', 'title', 'value', 'sort'];
const ws = xlsx.utils.aoa_to_sheet([headers]);
const wb = xlsx.utils.book_new();
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
}
// 从XLSX文件导入字典项
async importDictItemsFromXLSX(buffer: Buffer, dictId: number) {
const dict = await this.dictModel.findOneBy({ id: dictId });
if (!dict) {
throw new Error('指定的字典不存在');
}
const wb = xlsx.read(buffer, { type: 'buffer' });
const wsname = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'value', 'sort'] }).slice(1);
const items = data.map((row: any) => {
const item = new DictItem();
item.name = row.name;
item.title = row.title;
item.value = row.value;
item.sort = row.sort || 0;
item.dict = dict;
return item;
});
await this.dictItemModel.save(items);
return { success: true, count: items.length };
}
getDict(where: { name?: string; id?: number; }, relations: string[]) {
if (!where.name && !where.id) {
throw new Error('必须提供 name 或 id');
}
return this.dictModel.findOne({ where, relations });
}
// 获取字典列表,支持按标题搜索
async getDicts(options: { title?: string; name?: string; }) {
const where = {
title: options.title ? Like(`%${options.title}%`) : undefined,
name: options.name ? Like(`%${options.name}%`) : undefined,
}
return this.dictModel.find({ where });
}
// 创建新字典
async createDict(createDictDTO: CreateDictDTO) {
const dict = new Dict();
dict.name = createDictDTO.name;
dict.title = createDictDTO.title;
return this.dictModel.save(dict);
}
// 更新字典
async updateDict(id: number, updateDictDTO: UpdateDictDTO) {
await this.dictModel.update(id, updateDictDTO);
return this.dictModel.findOneBy({ id });
}
// 删除字典及其所有字典项
async deleteDict(id: number) {
// 首先删除该字典下的所有字典项
await this.dictItemModel.delete({ dict: { id } });
// 然后删除字典本身
const result = await this.dictModel.delete(id);
return result.affected > 0;
}
// 获取字典项列表,支持按 dictId 过滤
async getDictItems(dictId?: number) {
// 如果提供了 dictId则只返回该字典下的项
if (dictId) {
return this.dictItemModel.find({ where: { dict: { id: dictId } } });
}
// 否则,返回所有字典项
return this.dictItemModel.find();
}
// 创建新字典项
async createDictItem(createDictItemDTO: CreateDictItemDTO) {
const dict = await this.dictModel.findOneBy({ id: createDictItemDTO.dictId });
if (!dict) {
throw new Error('指定的字典不存在');
}
const item = new DictItem();
item.name = createDictItemDTO.name;
item.title = createDictItemDTO.title;
item.dict = dict;
return this.dictItemModel.save(item);
}
// 更新字典项
async updateDictItem(id: number, updateDictItemDTO: UpdateDictItemDTO) {
await this.dictItemModel.update(id, updateDictItemDTO);
return this.dictItemModel.findOneBy({ id });
}
// 删除字典项
async deleteDictItem(id: number) {
const result = await this.dictItemModel.delete(id);
return result.affected > 0;
}
}

View File

@ -12,8 +12,8 @@ import { WpProduct } from '../entity/wp_product.entity';
import { Product } from '../entity/product.entity';
import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity';
import { OrderCoupon } from '../entity/order_copon.entity';
import { OrderRefundItem } from '../entity/order_refund_item.entity';
import { OrderCoupon } from '../entity/order_coupon.entity';
import { OrderShipping } from '../entity/order_shipping.entity';
import { Shipment } from '../entity/shipment.entity';
import { Customer } from '../entity/customer.entity';

View File

@ -1,21 +1,20 @@
import { Provide } from '@midwayjs/core';
import { Inject, Provide } from '@midwayjs/core';
import { In, Like, Not, Repository } from 'typeorm';
import { Product } from '../entity/product.entity';
import { Category } from '../entity/category.entity';
import { paginate } from '../utils/paginate.util';
import { PaginationParams } from '../interface';
import {
CreateCategoryDTO,
CreateBrandDTO,
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
UpdateCategoryDTO,
UpdateBrandDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
} from '../dto/product.dto';
import {
CategoryPaginatedResponse,
BrandPaginatedResponse,
FlavorsPaginatedResponse,
ProductPaginatedResponse,
StrengthPaginatedResponse,
@ -23,22 +22,27 @@ import {
import { InjectEntityModel } from '@midwayjs/typeorm';
import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity';
import { Strength } from '../entity/strength.entity';
import { Flavors } from '../entity/flavors.entity';
import { Dict } from '../entity/dict.entity';
import { DictItem } from '../entity/dict_item.entity';
import { Context } from '@midwayjs/koa';
import { TemplateService } from './template.service';
@Provide()
export class ProductService {
@Inject()
ctx: Context;
@Inject()
templateService: TemplateService;
@InjectEntityModel(Product)
productModel: Repository<Product>;
@InjectEntityModel(Category)
categoryModel: Repository<Category>;
@InjectEntityModel(Dict)
dictModel: Repository<Dict>;
@InjectEntityModel(Strength)
strengthModel: Repository<Strength>;
@InjectEntityModel(Flavors)
flavorsModel: Repository<Flavors>;
@InjectEntityModel(DictItem)
dictItemModel: Repository<DictItem>;
@InjectEntityModel(WpProduct)
wpProductModel: Repository<WpProduct>;
@ -111,39 +115,39 @@ export class ProductService {
async getProductList(
pagination: PaginationParams,
name?: string,
categoryId?: number
brandId?: number
): Promise<ProductPaginatedResponse> {
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
const qb = this.productModel
.createQueryBuilder('product')
.leftJoin(Category, 'category', 'category.id = product.categoryId')
.leftJoin(Strength, 'strength', 'strength.id = product.strengthId')
.leftJoin(Flavors, 'flavors', 'flavors.id = product.flavorsId')
.select([
'product.id as id',
'product.name as name',
'product.nameCn as nameCn',
'product.description as description',
'product.humidity as humidity',
'product.sku as sku',
'product.createdAt as createdAt',
'product.updatedAt as updatedAt',
'category.name AS categoryName',
'strength.name AS strengthName',
'flavors.name AS flavorsName',
]);
.leftJoinAndSelect('product.attributes', 'attribute')
.leftJoinAndSelect('attribute.dict', 'dict');
// 模糊搜索 name支持多个关键词
nameFilter.forEach((word, index) => {
qb.andWhere(`product.name LIKE :name${index}`, {
[`name${index}`]: `%${word}%`,
});
});
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
if (nameFilter.length > 0) {
const nameConditions = nameFilter
.map((word, index) => `product.name LIKE :name${index}`)
.join(' AND ');
const nameParams = nameFilter.reduce(
(params, word, index) => ({ ...params, [`name${index}`]: `%${word}%` }),
{}
);
qb.where(`(${nameConditions})`, nameParams);
}
// 分类过滤
if (categoryId) {
qb.andWhere('product.categoryId = :categoryId', { categoryId });
// 品牌过滤
if (brandId) {
qb.andWhere(qb => {
const subQuery = qb
.subQuery()
.select('product_attributes_dict_item.productId')
.from('product_attributes_dict_item', 'product_attributes_dict_item')
.where('product_attributes_dict_item.dictItemId = :brandId', {
brandId,
})
.getQuery();
return 'product.id IN ' + subQuery;
});
}
// 分页
@ -151,46 +155,120 @@ export class ProductService {
pagination.pageSize
);
// 执行查询
const items = await qb.getRawMany();
const total = await qb.getCount();
const [items, total] = await qb.getManyAndCount();
// 格式化返回的数据
const formattedItems = items.map(product => {
const getAttributeTitle = (dictName: string) =>
product.attributes.find(a => a.dict.name === dictName)?.title || null;
return {
items,
id: product.id,
name: product.name,
nameCn: product.nameCn,
description: product.description,
humidity: getAttributeTitle('humidity'),
sku: product.sku,
createdAt: product.createdAt,
updatedAt: product.updatedAt,
brandName: getAttributeTitle('brand'),
flavorsName: getAttributeTitle('flavor'),
strengthName: getAttributeTitle('strength'),
};
});
return {
items: formattedItems,
total,
...pagination,
};
}
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const { name, description, categoryId, strengthId, flavorsId, humidity } =
createProductDTO;
const isExit = await this.productModel.findOne({
where: {
categoryId,
strengthId,
flavorsId,
humidity,
},
async getOrCreateDictItem(
dictName: string,
itemTitle: string,
itemName?: string
): Promise<DictItem> {
// 查找字典
const dict = await this.dictModel.findOne({ where: { name: dictName } });
if (!dict) {
throw new Error(`字典 '${dictName}' 不存在`);
}
// 查找字典项
let item = await this.dictItemModel.findOne({
where: { title: itemTitle, dict: { id: dict.id } },
});
// 如果字典项不存在,则创建
if (!item) {
item = new DictItem();
item.title = itemTitle;
item.name = itemName || itemTitle; // 如果没有提供 name则使用 title
item.dict = dict;
await this.dictItemModel.save(item);
}
return item;
}
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
const { name, description, brand, flavor, strength, humidity, sku } =
createProductDTO;
// 获取或创建品牌、口味、规格
const brandItem = await this.getOrCreateDictItem(
'brand',
brand.title,
brand.name
);
const flavorItem = await this.getOrCreateDictItem(
'flavor',
flavor.title,
flavor.name
);
const strengthItem = await this.getOrCreateDictItem(
'strength',
strength.title,
strength.name
);
const humidityItem = await this.getOrCreateDictItem('humidity', humidity);
// 检查具有完全相同属性组合的产品是否已存在
const attributesToMatch = [brandItem, flavorItem, strengthItem, humidityItem];
const qb = this.productModel.createQueryBuilder('product');
attributesToMatch.forEach((attr, index) => {
qb.innerJoin(
'product.attributes',
`attr${index}`,
`attr${index}.id = :attrId${index}`,
{ [`attrId${index}`]: attr.id }
);
});
const isExit = await qb.getOne();
if (isExit) throw new Error('产品已存在');
// 创建新产品实例
const product = new Product();
product.name = name;
product.description = description;
product.categoryId = categoryId;
product.strengthId = strengthId;
product.flavorsId = flavorsId;
product.humidity = humidity;
const categoryKey = (
await this.categoryModel.findOne({ where: { id: categoryId } })
).unique_key;
const strengthKey = (
await this.strengthModel.findOne({ where: { id: strengthId } })
).unique_key;
const flavorsKey = (
await this.flavorsModel.findOne({ where: { id: flavorsId } })
).unique_key;
product.sku = `${categoryKey}-${flavorsKey}-${strengthKey}-${humidity}`;
product.attributes = attributesToMatch;
// 如果用户提供了 sku则直接使用否则通过模板引擎生成
if (sku) {
product.sku = sku;
} else {
// 生成 SKU
product.sku = await this.templateService.render('product_sku', {
brand: brandItem.name,
flavor: flavorItem.name,
strength: strengthItem.name,
humidity: humidityItem.name,
});
}
// 保存产品
return await this.productModel.save(product);
}
@ -257,191 +335,271 @@ export class ProductService {
return result.affected > 0; // `affected` 表示删除的行数
}
async hasProductsInCategory(categoryId: number): Promise<boolean> {
const count = await this.productModel.count({
where: { categoryId },
});
return count > 0;
}
async hasCategory(name: string, id?: string): Promise<boolean> {
const where: any = { name };
async hasAttribute(
dictName: string,
title: string,
id?: number
): Promise<boolean> {
const dict = await this.dictModel.findOne({
where: { name: dictName },
});
if (!dict) {
return false;
}
const where: any = { title, dict: { id: dict.id } };
if (id) where.id = Not(id);
const count = await this.categoryModel.count({
const count = await this.dictItemModel.count({
where,
});
return count > 0;
}
async getCategoryList(
async hasProductsInAttribute(attributeId: number): Promise<boolean> {
const count = await this.productModel
.createQueryBuilder('product')
.innerJoin('product.attributes', 'attribute')
.where('attribute.id = :attributeId', { attributeId })
.getCount();
return count > 0;
}
async getBrandList(
pagination: PaginationParams,
name?: string
): Promise<CategoryPaginatedResponse> {
const where: any = {};
if (name) {
where.name = Like(`%${name}%`);
}
return await paginate(this.categoryModel, { pagination, where });
title?: string
): Promise<BrandPaginatedResponse> {
// 查找 'brand' 字典
const brandDict = await this.dictModel.findOne({
where: { name: 'brand' },
});
// 如果字典不存在,则返回空
if (!brandDict) {
return {
items: [],
total: 0,
...pagination,
};
}
async getCategoryAll(): Promise<CategoryPaginatedResponse> {
return await this.categoryModel.find();
// 设置查询条件
const where: any = { dict: { id: brandDict.id } };
if (title) {
where.title = Like(`%${title}%`);
}
async createCategory(
createCategoryDTO: CreateCategoryDTO
): Promise<Category> {
const { name, unique_key } = createCategoryDTO;
const category = new Category();
category.name = name;
category.unique_key = unique_key;
return await this.categoryModel.save(category);
// 分页查询
return await paginate(this.dictItemModel, { pagination, where });
}
async updateCategory(id: number, updateCategory: UpdateCategoryDTO) {
// 确认产品是否存在
const category = await this.categoryModel.findOneBy({ id });
if (!category) {
throw new Error(`产品分类 ID ${id} 不存在`);
}
// 更新产品
await this.categoryModel.update(id, updateCategory);
// 返回更新后的产品
return await this.categoryModel.findOneBy({ id });
async getBrandAll(): Promise<BrandPaginatedResponse> {
// 查找 'brand' 字典
const brandDict = await this.dictModel.findOne({
where: { name: 'brand' },
});
// 如果字典不存在,则返回空数组
if (!brandDict) {
return [];
}
async deleteCategory(id: number): Promise<boolean> {
// 检查产品是否存在
const category = await this.categoryModel.findOneBy({ id });
if (!category) {
throw new Error(`产品分类 ID ${id} 不存在`);
// 返回所有品牌
return this.dictItemModel.find({ where: { dict: { id: brandDict.id } } });
}
// 删除产品
const result = await this.categoryModel.delete(id);
async createBrand(createBrandDTO: CreateBrandDTO): Promise<DictItem> {
const { title, name } = createBrandDTO;
// 查找 'brand' 字典
const brandDict = await this.dictModel.findOne({
where: { name: 'brand' },
});
// 如果字典不存在,则抛出错误
if (!brandDict) {
throw new Error('品牌字典不存在');
}
// 创建新的品牌实例
const brand = new DictItem();
brand.title = title;
brand.name = name;
brand.dict = brandDict;
// 保存到数据库
return await this.dictItemModel.save(brand);
}
async updateBrand(id: number, updateBrand: UpdateBrandDTO) {
// 确认品牌是否存在
const brand = await this.dictItemModel.findOneBy({ id });
if (!brand) {
throw new Error(`品牌 ID ${id} 不存在`);
}
// 更新品牌
await this.dictItemModel.update(id, updateBrand);
// 返回更新后的品牌
return await this.dictItemModel.findOneBy({ id });
}
async deleteBrand(id: number): Promise<boolean> {
// 检查品牌是否存在
const brand = await this.dictItemModel.findOneBy({ id });
if (!brand) {
throw new Error(`品牌 ID ${id} 不存在`);
}
// 删除品牌
const result = await this.dictItemModel.delete(id);
return result.affected > 0; // `affected` 表示删除的行数
}
async hasProductsInFlavors(flavorsId: number): Promise<boolean> {
const count = await this.productModel.count({
where: { flavorsId },
});
return count > 0;
}
async hasFlavors(name: string, id?: string): Promise<boolean> {
const where: any = { name };
if (id) where.id = Not(id);
const count = await this.flavorsModel.count({
where,
});
return count > 0;
}
async getFlavorsList(
pagination: PaginationParams,
name?: string
title?: string
): Promise<FlavorsPaginatedResponse> {
const where: any = {};
if (name) {
where.name = Like(`%${name}%`);
const flavorsDict = await this.dictModel.findOne({
where: { name: 'flavor' },
});
if (!flavorsDict) {
return {
items: [],
total: 0,
...pagination,
};
}
return await paginate(this.flavorsModel, { pagination, where });
const where: any = { dict: { id: flavorsDict.id } };
if (title) {
where.title = Like(`%${title}%`);
}
return await paginate(this.dictItemModel, { pagination, where });
}
async getFlavorsAll(): Promise<FlavorsPaginatedResponse> {
return await this.flavorsModel.find();
const flavorsDict = await this.dictModel.findOne({
where: { name: 'flavor' },
});
if (!flavorsDict) {
return [];
}
return this.dictItemModel.find({ where: { id: flavorsDict.id } });
}
async createFlavors(createFlavorsDTO: CreateFlavorsDTO): Promise<Flavors> {
const { name, unique_key } = createFlavorsDTO;
const flavors = new Flavors();
async createFlavors(createFlavorsDTO: CreateFlavorsDTO): Promise<DictItem> {
const { title, name } = createFlavorsDTO;
const flavorsDict = await this.dictModel.findOne({
where: { name: 'flavor' },
});
if (!flavorsDict) {
throw new Error('口味字典不存在');
}
const flavors = new DictItem();
flavors.title = title;
flavors.name = name;
flavors.unique_key = unique_key;
return await this.flavorsModel.save(flavors);
flavors.dict = flavorsDict;
return await this.dictItemModel.save(flavors);
}
async updateFlavors(id: number, updateFlavors: UpdateFlavorsDTO) {
// 确认产品是否存在
const flavors = await this.flavorsModel.findOneBy({ id });
const flavors = await this.dictItemModel.findOneBy({ id });
if (!flavors) {
throw new Error(`口味 ID ${id} 不存在`);
}
// 更新产品
await this.flavorsModel.update(id, updateFlavors);
// 返回更新后的产品
return await this.flavorsModel.findOneBy({ id });
await this.dictItemModel.update(id, updateFlavors);
return await this.dictItemModel.findOneBy({ id });
}
async deleteFlavors(id: number): Promise<boolean> {
// 检查产品是否存在
const flavors = await this.flavorsModel.findOneBy({ id });
const flavors = await this.dictItemModel.findOneBy({ id });
if (!flavors) {
throw new Error(`口味 ID ${id} 不存在`);
}
// 删除产品
const result = await this.flavorsModel.delete(id);
return result.affected > 0; // `affected` 表示删除的行数
}
async hasProductsInStrength(strengthId: number): Promise<boolean> {
const count = await this.productModel.count({
where: { strengthId },
});
return count > 0;
const result = await this.dictItemModel.delete(id);
return result.affected > 0;
}
async hasStrength(name: string, id?: string): Promise<boolean> {
const where: any = { name };
async hasStrength(title: string, id?: string): Promise<boolean> {
const strengthDict = await this.dictModel.findOne({
where: { name: 'strength' },
});
if (!strengthDict) {
return false;
}
const where: any = { title, dict: { id: strengthDict.id } };
if (id) where.id = Not(id);
const count = await this.strengthModel.count({
const count = await this.dictItemModel.count({
where,
});
return count > 0;
}
async getStrengthList(
pagination: PaginationParams,
name?: string
title?: string
): Promise<StrengthPaginatedResponse> {
const where: any = {};
if (name) {
where.name = Like(`%${name}%`);
const strengthDict = await this.dictModel.findOne({
where: { name: 'strength' },
});
if (!strengthDict) {
return {
items: [],
total: 0,
...pagination,
};
}
return await paginate(this.strengthModel, { pagination, where });
const where: any = { dict: { id: strengthDict.id } };
if (title) {
where.title = Like(`%${title}%`);
}
return await paginate(this.dictItemModel, { pagination, where });
}
async getStrengthAll(): Promise<StrengthPaginatedResponse> {
return await this.strengthModel.find();
const strengthDict = await this.dictModel.findOne({
where: { name: 'strength' },
});
if (!strengthDict) {
return [];
}
return this.dictItemModel.find({ where: { dict: { id: strengthDict.id } } });
}
async createStrength(
createStrengthDTO: CreateStrengthDTO
): Promise<Strength> {
const { name, unique_key } = createStrengthDTO;
const strength = new Strength();
async createStrength(createStrengthDTO: CreateStrengthDTO): Promise<DictItem> {
const { title, name } = createStrengthDTO;
const strengthDict = await this.dictModel.findOne({
where: { name: 'strength' },
});
if (!strengthDict) {
throw new Error('规格字典不存在');
}
const strength = new DictItem();
strength.title = title;
strength.name = name;
strength.unique_key = unique_key;
return await this.strengthModel.save(strength);
strength.dict = strengthDict;
return await this.dictItemModel.save(strength);
}
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) {
// 确认产品是否存在
const strength = await this.strengthModel.findOneBy({ id });
const strength = await this.dictItemModel.findOneBy({ id });
if (!strength) {
throw new Error(`口味 ID ${id} 不存在`);
throw new Error(`规格 ID ${id} 不存在`);
}
// 更新产品
await this.strengthModel.update(id, updateStrength);
// 返回更新后的产品
return await this.strengthModel.findOneBy({ id });
await this.dictItemModel.update(id, updateStrength);
return await this.dictItemModel.findOneBy({ id });
}
async deleteStrength(id: number): Promise<boolean> {
// 检查产品是否存在
const strength = await this.strengthModel.findOneBy({ id });
const strength = await this.dictItemModel.findOneBy({ id });
if (!strength) {
throw new Error(`口味 ID ${id} 不存在`);
throw new Error(`规格 ID ${id} 不存在`);
}
// 删除产品
const result = await this.flavorsModel.delete(id);
return result.affected > 0; // `affected` 表示删除的行数
const result = await this.dictItemModel.delete(id);
return result.affected > 0;
}
async batchSetSku(skus: { productId: number; sku: string }[]) {

View File

@ -90,7 +90,7 @@ export class SiteService {
async disable(id: string | number, disabled: boolean) {
// 设置站点禁用状态true -> 1, false -> 0
await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled ? 1 : 0 });
await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled });
return true;
}
}

View File

@ -0,0 +1,112 @@
import { Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { Template } from '../entity/template.entity';
import { CreateTemplateDTO, UpdateTemplateDTO } from '../dto/template.dto';
/**
* @service TemplateService
*/
@Provide()
export class TemplateService {
// 注入 Template 实体模型
@InjectEntityModel(Template)
templateModel: Repository<Template>;
/**
*
* @returns {Promise<Template[]>}
*/
async getTemplateList(): Promise<Template[]> {
// 使用 find 方法查询所有模板
return this.templateModel.find();
}
/**
*
* @param {string} name -
* @returns {Promise<Template>}
*/
async getTemplateByName(name: string): Promise<Template> {
// 使用 findOne 方法按名称查询模板
return this.templateModel.findOne({ where: { name } });
}
/**
*
* @param {CreateTemplateDTO} templateData -
* @returns {Promise<Template>}
*/
async createTemplate(templateData: CreateTemplateDTO): Promise<Template> {
// 创建一个新的模板实例
const template = new Template();
// 设置模板的名称和值
template.name = templateData.name;
template.value = templateData.value;
// 保存新模板到数据库
return this.templateModel.save(template);
}
/**
* ID
* @param {number} id - ID
* @param {UpdateTemplateDTO} templateData -
* @returns {Promise<Template>}
*/
async updateTemplate(
id: number,
templateData: UpdateTemplateDTO
): Promise<Template> {
// 首先根据 ID 查找模板
const template = await this.templateModel.findOneBy({ id });
// 如果模板不存在,则抛出错误
if (!template) {
throw new Error(`模板 ID ${id} 不存在`);
}
// 更新模板的名称和值
template.name = templateData.name;
template.value = templateData.value;
// 保存更新后的模板到数据库
return this.templateModel.save(template);
}
/**
* ID
* @param {number} id - ID
* @returns {Promise<boolean>} true false
*/
async deleteTemplate(id: number): Promise<boolean> {
// 执行删除操作
const result = await this.templateModel.delete(id);
// 如果影响的行数大于 0则表示删除成功
return result.affected > 0;
}
/**
*
* @param {string} name -
* @param {Record<string, any>} data -
* @returns {Promise<string>}
*/
async render(name: string, data: Record<string, any>): Promise<string> {
// 根据名称获取模板
const template = await this.getTemplateByName(name);
// 如果模板不存在,则抛出错误
if (!template) {
throw new Error(`模板 '${name}' 不存在`);
}
// 获取模板的原始内容
let rendered = template.value;
// 遍历数据对象,替换模板中的占位符
for (const key in data) {
// 创建一个正则表达式来匹配 {{key}}
const regex = new RegExp(`{{${key}}}`, 'g');
// 执行替换操作
rendered = rendered.replace(regex, data[key]);
}
// 返回渲染后的字符串
return rendered;
}
}

View File

@ -320,7 +320,7 @@ export class WPService {
site: any,
productId: string,
variationId: string,
data: UpdateVariationDTO
data: Partial<UpdateVariationDTO>
): Promise<Boolean> {
const { regular_price, sale_price, ...params } = data;
return await this.updateData(