From 037df8008088993e0f812feb5163fa957f5d57cb Mon Sep 17 00:00:00 2001 From: cll <931958862@qq.com> Date: Thu, 22 May 2025 15:15:43 +0800 Subject: [PATCH] init --- .editorconfig | 11 + .eslintrc.json | 7 + .gitignore | 15 + .prettierrc.js | 3 + bootstrap.js | 2 + jest.config.js | 6 + package.json | 59 ++ src/config/config.default.ts | 101 ++ src/config/config.local.ts | 52 + src/config/config.unittest.ts | 7 + src/configuration.ts | 78 ++ src/controller/api.controller.ts | 8 + src/controller/logistics.controller.ts | 227 +++++ src/controller/order.controller.ts | 197 ++++ src/controller/product.controller.ts | 387 +++++++ src/controller/site.controller.ts | 25 + src/controller/statistics.controller.ts | 79 ++ src/controller/stock.controller.ts | 262 +++++ src/controller/user.controller.ts | 72 ++ src/controller/webhook.controller.ts | 138 +++ src/controller/wp_product.controller.ts | 161 +++ src/decorator/user.decorator.ts | 8 + src/dto/freightcom.dto.ts | 303 ++++++ src/dto/logistics.dto.ts | 63 ++ src/dto/order.dto.ts | 131 +++ src/dto/product.dto.ts | 174 ++++ src/dto/reponse.dto.ts | 119 +++ src/dto/site.dto.ts | 32 + src/dto/statistics.dto.ts | 36 + src/dto/stock.dto.ts | 173 ++++ src/dto/user.dto.ts | 26 + src/dto/wp_product.dto.ts | 98 ++ src/entity/category.entity.ts | 53 + src/entity/flavors.entity.ts | 43 + src/entity/order.entity.ts | 267 +++++ src/entity/order_copon.entity.ts | 86 ++ src/entity/order_fee.entity.ts | 86 ++ src/entity/order_item.entity.ts | 106 ++ src/entity/order_note.entity.ts | 49 + src/entity/order_refund.entity.ts | 86 ++ src/entity/order_retund_item.entity.ts | 111 ++ src/entity/order_sale.entity.ts | 74 ++ src/entity/order_shipment.entity.ts | 21 + src/entity/order_shipping.entity.ts | 81 ++ src/entity/product.entty.ts | 69 ++ src/entity/purchase_order.entity.ts | 61 ++ src/entity/purchase_order_item.entity.ts | 30 + src/entity/service.entity.ts | 51 + src/entity/shipment.entity.ts | 104 ++ src/entity/shipment_item.entity.ts | 61 ++ src/entity/shipping_address.entity.ts | 67 ++ src/entity/stock.entity.ts | 44 + src/entity/stock_point.entity.ts | 64 ++ src/entity/stock_record.entity.ts | 48 + src/entity/strength.entity.ts | 43 + src/entity/transfer.entity.ts | 67 ++ src/entity/transfer_item.entity.ts | 25 + src/entity/user.entity.ts | 31 + src/entity/variation.entity.ts | 115 +++ src/entity/wp_product.entity.ts | 110 ++ src/enums/base.enum.ts | 55 + src/filter/default.filter.ts | 13 + src/filter/notfound.filter.ts | 10 + src/interface.ts | 23 + src/job/sync_products.job.ts | 15 + src/job/sync_shipment.job.ts | 22 + src/middleware/auth.middleware.ts | 68 ++ src/middleware/report.middleware.ts | 27 + src/service/canadaPost.service.ts | 171 ++++ src/service/freightcom.service.ts | 142 +++ src/service/logistics.service.ts | 553 ++++++++++ src/service/order.service.ts | 1189 ++++++++++++++++++++++ src/service/product.service.ts | 424 ++++++++ src/service/statistics.service.ts | 904 ++++++++++++++++ src/service/stock.service.ts | 557 ++++++++++ src/service/user.service.ts | 79 ++ src/service/wp.service.ts | 279 +++++ src/service/wp_product.service.ts | 447 ++++++++ src/utils/helper.util.ts | 9 + src/utils/paginate.util.ts | 77 ++ src/utils/paginated-response.util.ts | 22 + src/utils/response-wrapper.util.ts | 40 + src/utils/response.util.ts | 50 + test/controller/api.test.ts | 20 + tsconfig.json | 22 + 85 files changed, 10331 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierrc.js create mode 100644 bootstrap.js create mode 100644 jest.config.js create mode 100644 package.json create mode 100644 src/config/config.default.ts create mode 100644 src/config/config.local.ts create mode 100644 src/config/config.unittest.ts create mode 100644 src/configuration.ts create mode 100644 src/controller/api.controller.ts create mode 100644 src/controller/logistics.controller.ts create mode 100644 src/controller/order.controller.ts create mode 100644 src/controller/product.controller.ts create mode 100644 src/controller/site.controller.ts create mode 100644 src/controller/statistics.controller.ts create mode 100644 src/controller/stock.controller.ts create mode 100644 src/controller/user.controller.ts create mode 100644 src/controller/webhook.controller.ts create mode 100644 src/controller/wp_product.controller.ts create mode 100644 src/decorator/user.decorator.ts create mode 100644 src/dto/freightcom.dto.ts create mode 100644 src/dto/logistics.dto.ts create mode 100644 src/dto/order.dto.ts create mode 100644 src/dto/product.dto.ts create mode 100644 src/dto/reponse.dto.ts create mode 100644 src/dto/site.dto.ts create mode 100644 src/dto/statistics.dto.ts create mode 100644 src/dto/stock.dto.ts create mode 100644 src/dto/user.dto.ts create mode 100644 src/dto/wp_product.dto.ts create mode 100644 src/entity/category.entity.ts create mode 100644 src/entity/flavors.entity.ts create mode 100644 src/entity/order.entity.ts create mode 100644 src/entity/order_copon.entity.ts create mode 100644 src/entity/order_fee.entity.ts create mode 100644 src/entity/order_item.entity.ts create mode 100644 src/entity/order_note.entity.ts create mode 100644 src/entity/order_refund.entity.ts create mode 100644 src/entity/order_retund_item.entity.ts create mode 100644 src/entity/order_sale.entity.ts create mode 100644 src/entity/order_shipment.entity.ts create mode 100644 src/entity/order_shipping.entity.ts create mode 100644 src/entity/product.entty.ts create mode 100644 src/entity/purchase_order.entity.ts create mode 100644 src/entity/purchase_order_item.entity.ts create mode 100644 src/entity/service.entity.ts create mode 100644 src/entity/shipment.entity.ts create mode 100644 src/entity/shipment_item.entity.ts create mode 100644 src/entity/shipping_address.entity.ts create mode 100644 src/entity/stock.entity.ts create mode 100644 src/entity/stock_point.entity.ts create mode 100644 src/entity/stock_record.entity.ts create mode 100644 src/entity/strength.entity.ts create mode 100644 src/entity/transfer.entity.ts create mode 100644 src/entity/transfer_item.entity.ts create mode 100644 src/entity/user.entity.ts create mode 100644 src/entity/variation.entity.ts create mode 100644 src/entity/wp_product.entity.ts create mode 100644 src/enums/base.enum.ts create mode 100644 src/filter/default.filter.ts create mode 100644 src/filter/notfound.filter.ts create mode 100644 src/interface.ts create mode 100644 src/job/sync_products.job.ts create mode 100644 src/job/sync_shipment.job.ts create mode 100644 src/middleware/auth.middleware.ts create mode 100644 src/middleware/report.middleware.ts create mode 100644 src/service/canadaPost.service.ts create mode 100644 src/service/freightcom.service.ts create mode 100644 src/service/logistics.service.ts create mode 100644 src/service/order.service.ts create mode 100644 src/service/product.service.ts create mode 100644 src/service/statistics.service.ts create mode 100644 src/service/stock.service.ts create mode 100644 src/service/user.service.ts create mode 100644 src/service/wp.service.ts create mode 100644 src/service/wp_product.service.ts create mode 100644 src/utils/helper.util.ts create mode 100644 src/utils/paginate.util.ts create mode 100644 src/utils/paginated-response.util.ts create mode 100644 src/utils/response-wrapper.util.ts create mode 100644 src/utils/response.util.ts create mode 100644 test/controller/api.test.ts create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4c7f8a8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# 🎨 editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..8d20e22 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "extends": "./node_modules/mwts/", + "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"], + "env": { + "jest": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c492891 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +logs/ +npm-debug.log +yarn-error.log +node_modules/ +coverage/ +dist/ +.idea/ +run/ +.DS_Store +*.sw* +*.un~ +.tsbuildinfo +.tsbuildinfo.* +yarn.lock +**/config.prod.ts \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..b964930 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('mwts/.prettierrc.json') +} diff --git a/bootstrap.js b/bootstrap.js new file mode 100644 index 0000000..e0c9aa3 --- /dev/null +++ b/bootstrap.js @@ -0,0 +1,2 @@ +const { Bootstrap } = require('@midwayjs/bootstrap') +Bootstrap.run() diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..c5bd388 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: ['/test/fixtures'], + coveragePathIgnorePatterns: ['/test/'], +}; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3310d10 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "my-midway-project", + "version": "1.0.0", + "description": "", + "private": true, + "dependencies": { + "@midwayjs/bootstrap": "^3.20.0", + "@midwayjs/core": "^3.20.0", + "@midwayjs/cron": "^3.20.0", + "@midwayjs/cross-domain": "^3.20.2", + "@midwayjs/decorator": "^3.20.0", + "@midwayjs/info": "^3.20.2", + "@midwayjs/jwt": "^3.20.2", + "@midwayjs/koa": "^3.20.2", + "@midwayjs/logger": "^3.1.0", + "@midwayjs/swagger": "^3.20.2", + "@midwayjs/typeorm": "^3.20.0", + "@midwayjs/validate": "^3.20.2", + "axios": "^1.7.9", + "bcryptjs": "^2.4.3", + "class-transformer": "^0.5.1", + "dayjs": "^1.11.13", + "mysql2": "^3.11.5", + "swagger-ui-dist": "^5.18.2", + "typeorm": "^0.3.20", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "@midwayjs/mock": "^3.20.0", + "@types/jest": "^29.2.0", + "@types/node": "14", + "cross-env": "^6.0.0", + "jest": "^29.2.2", + "mwts": "^1.3.0", + "mwtsc": "^1.4.0", + "ts-jest": "^29.0.3", + "typescript": "~4.8.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "scripts": { + "start": "cross-env NODE_ENV=prod pm2 start ./bootstrap.js --name yoone -i 4", + "prod": "cross-env NODE_ENV=prod node ./bootstrap.js ", + "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", + "ci": "npm run cov", + "build": "mwtsc --cleanOutDir" + }, + "repository": { + "type": "git", + "url": "" + }, + "author": "anonymous", + "license": "MIT" +} diff --git a/src/config/config.default.ts b/src/config/config.default.ts new file mode 100644 index 0000000..f4e6d34 --- /dev/null +++ b/src/config/config.default.ts @@ -0,0 +1,101 @@ +import { MidwayConfig } from '@midwayjs/core'; +import { Product } from '../entity/product.entty'; +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'; +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_copon.entity'; +import { OrderFee } from '../entity/order_fee.entity'; +import { OrderRefund } from '../entity/order_refund.entity'; +import { OrderRefundItem } from '../entity/order_retund_item.entity'; +import { OrderSale } from '../entity/order_sale.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 { Strength } from '../entity/strength.entity'; +import { Flavors } from '../entity/flavors.entity'; + +export default { + // use for cookie sign key, should change to your own and keep security + keys: '1733728588817_720', + typeorm: { + default: { + entities: [ + Product, + Category, + Strength, + Flavors, + WpProduct, + Variation, + User, + PurchaseOrder, + PurchaseOrderItem, + Stock, + StockPoint, + StockRecord, + Order, + OrderItem, + OrderCoupon, + OrderFee, + OrderRefund, + OrderRefundItem, + OrderSale, + OrderShipment, + ShipmentItem, + Shipment, + OrderShipping, + Service, + ShippingAddress, + OrderNote, + Transfer, + TransferItem, + ], + synchronize: true, + logging: false, + }, + dataSource: { + default: { + type: 'mysql', + host: 'localhost', + port: 3306, + username: 'root', + password: 'root', + database: 'inventory', + }, + }, + }, + // cors: { + // origin: '*', // 允许所有来源跨域请求 + // allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 + // allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头 + // credentials: true, // 允许携带凭据(cookies等) + // }, + // jwt: { + // secret: 'YOONE2024!@abc', + // expiresIn: '7d', + // }, + // wpSite: [ + // { + // id: '2', + // wpApiUrl: 'http://localhost:10004', + // consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652', + // consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38', + // siteName: 'Local', + // email: 'tom@yoonevape.com', + // emailPswd: '', + // }, + // ], +} as MidwayConfig; diff --git a/src/config/config.local.ts b/src/config/config.local.ts new file mode 100644 index 0000000..fde05be --- /dev/null +++ b/src/config/config.local.ts @@ -0,0 +1,52 @@ +import { MidwayConfig } from '@midwayjs/core'; +export default { + koa: { + port: 7001, + }, + typeorm: { + dataSource: { + default: { + host: '127.0.0.1', + username: 'root', + password: '123456', + }, + }, + }, + cors: { + origin: '*', // 允许所有来源跨域请求 + allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 + allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头 + credentials: true, // 允许携带凭据(cookies等) + }, + jwt: { + secret: 'YOONE2024!@abc', + expiresIn: '7d', + }, + wpSite: [ + { + id: '-1', + siteName: 'Admin', + email: 'tom@yoonevape.com', + }, + { + id: '2', + wpApiUrl: 'http://localhost:10004', + consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652', + consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38', + siteName: 'Local', + email: 'tom@yoonevape.com', + emailPswd: 'lulin91.', + }, + ], + freightcom: { + url: 'https://customer-external-api.ssd-test.freightcom.com', + token: '6zGj1qPTL1jIkbLmgaiYc6SwHUIXJ2t25htUF8uuFYiCg8ILCY6xnBEbvrX1p79L', + }, + canadaPost: { + url: 'https://ct.soa-gw.canadapost.ca', + username: '65d23d3a75d7baf7', + password: '56443bb98b68dfdd60f52e', + customerNumber: '0006122480', + contractId: '0044168528', + }, +} as MidwayConfig; diff --git a/src/config/config.unittest.ts b/src/config/config.unittest.ts new file mode 100644 index 0000000..423dfcd --- /dev/null +++ b/src/config/config.unittest.ts @@ -0,0 +1,7 @@ +import { MidwayConfig } from '@midwayjs/core'; + +export default { + koa: { + port: null, + }, +} as MidwayConfig; diff --git a/src/configuration.ts b/src/configuration.ts new file mode 100644 index 0000000..8205c4b --- /dev/null +++ b/src/configuration.ts @@ -0,0 +1,78 @@ +import { + Configuration, + App, + Inject, + MidwayDecoratorService, +} from '@midwayjs/core'; +import * as koa from '@midwayjs/koa'; +import * as validate from '@midwayjs/validate'; +import * as info from '@midwayjs/info'; +import * as orm from '@midwayjs/typeorm'; +import { join } from 'path'; +// import { DefaultErrorFilter } from './filter/default.filter'; +// import { NotFoundFilter } from './filter/notfound.filter'; +import { ReportMiddleware } from './middleware/report.middleware'; +import * as swagger from '@midwayjs/swagger'; +import * as crossDomain from '@midwayjs/cross-domain'; +import * as cron from '@midwayjs/cron'; +import * as jwt from '@midwayjs/jwt'; +import { USER_KEY } from './decorator/user.decorator'; +import { AuthMiddleware } from './middleware/auth.middleware'; + +@Configuration({ + imports: [ + koa, + validate, + { + component: info, + enabledEnvironment: ['local', 'prod'], + }, + orm, + swagger, + crossDomain, + cron, + jwt, + ], + importConfigs: [join(__dirname, './config')], +}) +export class MainConfiguration { + @App('koa') + app: koa.Application; + + @Inject() + decoratorService: MidwayDecoratorService; + + @Inject() + jwtService: jwt.JwtService; // 注入 JwtService 实例 + + async onReady() { + // add middleware + this.app.useMiddleware([ReportMiddleware, AuthMiddleware]); + // add filter + // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]); + + this.decoratorService.registerParameterHandler( + USER_KEY, + async ( + options + ): Promise<{ + id: number; + userName: string; + }> => { + const ctx = options.originArgs[0]; + const token = ctx.headers['authorization']?.split(' ')[1]; + const config = ctx.app.getConfig('jwt'); // 动态获取配置项 + if (!token) { + ctx.throw(401, 'Token not found'); + } + + try { + const decoded: any = this.jwtService.verify(token, config.secret); // 替换为你的密钥 + return decoded; + } catch (error) { + ctx.throw(401, 'Invalid token'); + } + } + ); + } +} diff --git a/src/controller/api.controller.ts b/src/controller/api.controller.ts new file mode 100644 index 0000000..cb10a70 --- /dev/null +++ b/src/controller/api.controller.ts @@ -0,0 +1,8 @@ +import { Inject, Controller } from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; + +@Controller('/') +export class APIController { + @Inject() + ctx: Context; +} diff --git a/src/controller/logistics.controller.ts b/src/controller/logistics.controller.ts new file mode 100644 index 0000000..4e2435b --- /dev/null +++ b/src/controller/logistics.controller.ts @@ -0,0 +1,227 @@ +import { + Inject, + Controller, + Post, + Body, + Get, + Put, + Param, + Del, + Query, +} from '@midwayjs/core'; +import { Context } from '@midwayjs/koa'; +import { ApiOkResponse } from '@midwayjs/swagger'; +import { + BooleanRes, + RateLitRes, + ServiceListRes, + ShippingAddressListRes, +} from '../dto/reponse.dto'; +import { FreightcomService } from '../service/freightcom.service'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { LogisticsService } from '../service/logistics.service'; +import { ShippingDetailsDTO } from '../dto/freightcom.dto'; +import { ShippingAddress } from '../entity/shipping_address.entity'; +import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto'; +import { User } from '../decorator/user.decorator'; + +@Controller('/logistics') +export class LogisticsController { + @Inject() + ctx: Context; + + @Inject() + freightcomService: FreightcomService; + + @Inject() + logisticsService: LogisticsService; + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/syncServices') + async syncServices() { + try { + await this.freightcomService.syncServices(); + return successResponse(true); + } catch (error) { + return errorResponse('同步失败'); + } + } + + @ApiOkResponse({ + description: '服务商列表', + type: ServiceListRes, + }) + @Get('/getServiceList') + async getServiceList( + @Query() + param: QueryServiceDTO + ) { + try { + const data = await this.logisticsService.getServiceList(param); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/toggleActive') + async toggleActive(@Body() body: { id: string; isActive: boolean }) { + try { + await this.logisticsService.toggleServiceActive(body.id, body.isActive); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ + type: RateLitRes, + }) + @Post('/getRateList') + async getRateList(@Body() details: ShippingDetailsDTO) { + try { + const rates = await this.logisticsService.getRateList(details); + return successResponse(rates); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/createShippingAddress') + async createShippingAddress(@Body() shippingAddress: ShippingAddress) { + try { + await this.logisticsService.createShippingAddress(shippingAddress); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Put('/updateShippingAddress/:id') + async updateShippingAddress( + @Body() shippingAddress: ShippingAddress, + @Param('id') id: number + ) { + try { + await this.logisticsService.updateShippingAddress(id, shippingAddress); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } + + @ApiOkResponse({ + type: ShippingAddressListRes, + }) + @Get('/getShippingAddressList') + async getShippingAddressList() { + try { + const data = await this.logisticsService.getShippingAddressList(); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Del('/delShippingAddress/:id') + async delShippingAddress(@Param('id') id: number) { + try { + const boolen = await this.logisticsService.delShippingAddress(id); + return successResponse(boolen); + } catch (error) { + return errorResponse(error?.message || '删除失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/createShipment/:orderId') + async createShipment( + @Param('orderId') orderId: number, + @Body() data: ShipmentBookDTO, + @User() user + ) { + try { + await this.logisticsService.createShipment(orderId, data, user.id); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Post('/getPaymentMethods') + async getpaymentmethods() { + try { + const data = await this.freightcomService.getPaymentMethods(); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Del('/shipment/:id') + async delShipment(@Param('id') id: string, @User() user) { + try { + const data = await this.logisticsService.delShipment(id, user.id); + return successResponse(data); + } catch (error) { + console.log(error); + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/getTrackingNumber') + async getTrackingNumber(@Query('number') number: string) { + try { + return successResponse( + await this.logisticsService.getTrackingNumber(number) + ); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/getListByTrackingId') + async getListByTrackingId(@Query('shipment_id') shipment_id: string) { + try { + return successResponse( + await this.logisticsService.getListByTrackingId(shipment_id) + ); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ + description: '物流列表', + }) + @Get('/list') + async getList( + @Query() + param: Record + ) { + try { + const data = await this.logisticsService.getList(param); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } +} diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts new file mode 100644 index 0000000..1c09f3e --- /dev/null +++ b/src/controller/order.controller.ts @@ -0,0 +1,197 @@ +import { + Body, + Controller, + Del, + Get, + Inject, + Param, + Post, + Put, + Query, +} from '@midwayjs/core'; +import { ApiOkResponse } from '@midwayjs/swagger'; +import { + BooleanRes, + OrderDetailRes, + OrderListRes, + OrderSaleListRes, +} from '../dto/reponse.dto'; +import { OrderService } from '../service/order.service'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { + CreateOrderNoteDTO, + QueryOrderDTO, + QueryOrderSalesDTO, +} from '../dto/order.dto'; +import { User } from '../decorator/user.decorator'; +import { ErpOrderStatus } from '../enums/base.enum'; + +@Controller('/order') +export class OrderController { + @Inject() + orderService: OrderService; + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/syncOrder/:siteId') + async syncOrder(@Param('siteId') siteId: string) { + try { + await this.orderService.syncOrders(siteId); + return successResponse(true); + } catch (error) { + console.log(error); + return errorResponse('同步失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/syncOrder/:siteId/order/:orderId') + async syncOrderById( + @Param('siteId') siteId: string, + @Param('orderId') orderId: string + ) { + try { + await this.orderService.syncOrderById(siteId, orderId); + return successResponse(true); + } catch (error) { + console.log(error); + return errorResponse('同步失败'); + } + } + + @ApiOkResponse({ + type: OrderListRes, + }) + @Get('/getOrders') + async getOrders( + @Query() + param: QueryOrderDTO + ) { + try { + const count = await this.orderService.getOrderStatus(param); + const data = await this.orderService.getOrders(param); + return successResponse({ + ...data, + count, + }); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ + type: OrderSaleListRes, + }) + @Get('/getOrderSales') + async getOrderSales(@Query() param: QueryOrderSalesDTO) { + try { + if (param.isSource) + return successResponse(await this.orderService.getOrderItems(param)); + return successResponse(await this.orderService.getOrderSales(param)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ + type: OrderDetailRes, + }) + @Get('/:orderId') + async getOrderDetail(@Param('orderId') orderId: number) { + try { + return successResponse(await this.orderService.getOrderDetail(orderId)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Del('/:id') + async delOrder(@Param('id') id: number) { + try { + return successResponse(await this.orderService.delOrder(id)); + } catch (error) { + return errorResponse(error?.message || '删除失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/createNote') + async createNote(@Body() data: CreateOrderNoteDTO, @User() user) { + try { + return successResponse(await this.orderService.createNote(user.id, data)); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Post('/getOrderByNumber') + async getOrderByNumber(@Body('number') number: string) { + try { + return successResponse(await this.orderService.getOrderByNumber(number)); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Post('/order/cancel/:id') + async cancelOrder(@Param('id') id: number) { + try { + return successResponse(await this.orderService.cancelOrder(id)); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Post('/order/refund/:id') + async refundOrder(@Param('id') id: number) { + try { + return successResponse(await this.orderService.refundOrder(id)); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Post('/order/completed/:id') + async completedOrder(@Param('id') id: number) { + try { + return successResponse(await this.orderService.completedOrder(id)); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Put('/order/status/:id') + async changeStatus( + @Param('id') id: number, + @Body('status') status: ErpOrderStatus + ) { + try { + return successResponse(await this.orderService.changeStatus(id, status)); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Post('/order/create') + async createOrder(@Body() data: Record) { + try { + return successResponse(await this.orderService.createOrder(data)); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } +} diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts new file mode 100644 index 0000000..7e85072 --- /dev/null +++ b/src/controller/product.controller.ts @@ -0,0 +1,387 @@ +import { + Inject, + Post, + Put, + Get, + Body, + Param, + Del, + Query, + Controller, +} from '@midwayjs/core'; +import { ProductService } from '../service/product.service'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { + BatchSetSkuDTO, + CreateCategoryDTO, + CreateFlavorsDTO, + CreateProductDTO, + CreateStrengthDTO, + QueryCategoryDTO, + QueryFlavorsDTO, + QueryProductDTO, + QueryStrengthDTO, + UpdateCategoryDTO, + UpdateFlavorsDTO, + UpdateProductDTO, + UpdateStrengthDTO, +} from '../dto/product.dto'; +import { ApiOkResponse } from '@midwayjs/swagger'; +import { + BooleanRes, + ProductCatListRes, + ProductCatRes, + ProductListRes, + ProductRes, + ProductsRes, +} from '../dto/reponse.dto'; + +@Controller('/product') +export class ProductController { + @Inject() + productService: ProductService; + ProductRes; + + @ApiOkResponse({ + description: '通过name搜索产品', + type: ProductsRes, + }) + @Get('/search') + async searchProducts(@Query('name') name: string) { + try { + // 调用服务获取产品数据 + const products = await this.productService.findProductsByName(name); + return successResponse(products); + } catch (error) { + return errorResponse(error.message || '获取数据失败'); + } + } + + @ApiOkResponse({ + type: ProductRes, + }) + @Get('/sku/:sku') + async productBySku(@Param('sku') sku: string) { + try { + // 调用服务获取产品数据 + const product = await this.productService.findProductBySku(sku); + return successResponse(product); + } catch (error) { + return errorResponse(error.message || '获取数据失败'); + } + } + + @ApiOkResponse({ + description: '成功返回产品列表', + type: ProductListRes, + }) + @Get('/list') + async getProductList( + @Query() query: QueryProductDTO + ): Promise { + const { current = 1, pageSize = 10, name, categoryId } = query; + try { + const data = await this.productService.getProductList( + { current, pageSize }, + name, + categoryId + ); + return successResponse(data); + } catch (error) { + console.log(error); + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: ProductRes, + }) + @Post('/') + async createProduct(@Body() productData: CreateProductDTO) { + try { + const data = this.productService.createProduct(productData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: ProductRes, + }) + @Put('/:id') + async updateProduct( + @Param('id') id: number, + @Body() productData: UpdateProductDTO + ) { + try { + const data = this.productService.updateProduct(id, productData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Del('/:id') + async deleteProduct(@Param('id') id: number) { + try { + const data = await this.productService.deleteProduct(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: ProductCatListRes, + }) + @Get('/categories') + async getCategories(@Query() query: QueryCategoryDTO) { + const { current = 1, pageSize = 10, name } = query; + try { + let data = await this.productService.getCategoryList( + { current, pageSize }, + name + ); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Get('/categorieAll') + async getCategorieAll() { + try { + let data = await this.productService.getCategoryAll(); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: ProductCatRes, + }) + @Post('/category') + async createCategory(@Body() categoryData: CreateCategoryDTO) { + try { + const hasCategory = await this.productService.hasCategory( + categoryData.name + ); + if (hasCategory) { + return errorResponse('分类已存在'); + } + let data = await this.productService.createCategory(categoryData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: ProductCatRes, + }) + @Put('/category/:id') + async updateCategory( + @Param('id') id: number, + @Body() categoryData: UpdateCategoryDTO + ) { + try { + const hasCategory = await this.productService.hasCategory( + categoryData.name + ); + if (hasCategory) { + return errorResponse('分类已存在'); + } + const data = this.productService.updateCategory(id, categoryData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Del('/category/:id') + async deleteCategory(@Param('id') id: number) { + try { + const hasProducts = await this.productService.hasProductsInCategory(id); + if (hasProducts) throw new Error('该分类下有商品,无法删除'); + const data = await this.productService.deleteCategory(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @Post('/batchSetSku') + @ApiOkResponse({ + description: '批量设置 sku 的响应结果', + type: BooleanRes, + }) + async batchSetSku(@Body() body: BatchSetSkuDTO) { + try { + const result = await this.productService.batchSetSku(body.skus); + return successResponse(result, '批量设置 sku 成功'); + } catch (error) { + return errorResponse(error.message, 400); + } + } + + @ApiOkResponse() + @Get('/flavorsAll') + async getFlavorsAll() { + try { + let data = await this.productService.getFlavorsAll(); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Get('/flavors') + async getFlavors(@Query() query: QueryFlavorsDTO) { + const { current = 1, pageSize = 10, name } = query; + try { + let data = await this.productService.getFlavorsList( + { current, pageSize }, + name + ); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Post('/flavors') + async createFlavors(@Body() flavorsData: CreateFlavorsDTO) { + try { + const hasFlavors = await this.productService.hasFlavors(flavorsData.name); + if (hasFlavors) { + return errorResponse('分类已存在'); + } + let data = await this.productService.createFlavors(flavorsData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Put('/flavors/:id') + async updateFlavors( + @Param('id') id: number, + @Body() flavorsData: UpdateFlavorsDTO + ) { + try { + const hasFlavors = await this.productService.hasFlavors(flavorsData.name); + if (hasFlavors) { + return errorResponse('分类已存在'); + } + const data = this.productService.updateFlavors(id, flavorsData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Del('/flavors/:id') + async deleteFlavors(@Param('id') id: number) { + try { + const hasProducts = await this.productService.hasProductsInFlavors(id); + if (hasProducts) throw new Error('该分类下有商品,无法删除'); + const data = await this.productService.deleteFlavors(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Get('/strengthAll') + async getStrengthAll() { + try { + let data = await this.productService.getStrengthAll(); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Get('/strength') + async getStrength(@Query() query: QueryStrengthDTO) { + const { current = 1, pageSize = 10, name } = query; + try { + let data = await this.productService.getStrengthList( + { current, pageSize }, + name + ); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Post('/strength') + async createStrength(@Body() strengthData: CreateStrengthDTO) { + try { + const hasStrength = await this.productService.hasStrength( + strengthData.name + ); + if (hasStrength) { + return errorResponse('分类已存在'); + } + let data = await this.productService.createStrength(strengthData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse() + @Put('/strength/:id') + async updateStrength( + @Param('id') id: number, + @Body() strengthData: UpdateStrengthDTO + ) { + try { + const hasStrength = await this.productService.hasStrength( + strengthData.name + ); + if (hasStrength) { + return errorResponse('分类已存在'); + } + const data = this.productService.updateStrength(id, strengthData); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Del('/strength/:id') + async deleteStrength(@Param('id') id: number) { + try { + const hasProducts = await this.productService.hasProductsInStrength(id); + if (hasProducts) throw new Error('该分类下有商品,无法删除'); + const data = await this.productService.deleteStrength(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || error); + } + } +} diff --git a/src/controller/site.controller.ts b/src/controller/site.controller.ts new file mode 100644 index 0000000..c84c7c2 --- /dev/null +++ b/src/controller/site.controller.ts @@ -0,0 +1,25 @@ +import { Config, Controller, Get } from '@midwayjs/core'; +import { ApiOkResponse } from '@midwayjs/swagger'; +import { WpSitesResponse } from '../dto/reponse.dto'; +import { successResponse } from '../utils/response.util'; +import { WpSite } from '../interface'; + +@Controller('/site') +export class SiteController { + @Config('wpSite') + sites: WpSite[]; + + @ApiOkResponse({ + description: '关联网站', + type: WpSitesResponse, + }) + @Get('/all') + async all() { + return successResponse( + this.sites.map(v => ({ + id: v.id, + siteName: v.siteName, + })) + ); + } +} diff --git a/src/controller/statistics.controller.ts b/src/controller/statistics.controller.ts new file mode 100644 index 0000000..1e41bbf --- /dev/null +++ b/src/controller/statistics.controller.ts @@ -0,0 +1,79 @@ +import { Body, Controller, Inject, Post } from '@midwayjs/core'; +import { StatisticsService } from '../service/statistics.service'; +import { OrderStatisticsParams } from '../dto/statistics.dto'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { ApiOkResponse } from '@midwayjs/swagger'; + +@Controller('/statistics') +export class StatisticsController { + @Inject() + statisticsService: StatisticsService; + + @ApiOkResponse() + @Post('/order') + async getOrderStatistics(@Body() params: OrderStatisticsParams) { + try { + return successResponse( + await this.statisticsService.getOrderStatistics(params) + ); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/orderByDate') + async getOrderByDate(@Body('date') date: string) { + try { + return successResponse(await this.statisticsService.getOrderByDate(date)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/orderByEmail') + async getOrderByEmail(@Body('email') email: string) { + try { + return successResponse( + await this.statisticsService.getOrderByEmail(email) + ); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/getCustomerOrders') + async getCustomerOrders(@Body('month') month) { + try { + return successResponse( + await this.statisticsService.getCustomerOrders(month) + ); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/stockForecast') + async stockForecast(@Body() params) { + try { + return successResponse( + await this.statisticsService.stockForecast(params) + ); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Post('/restocking') + async restocking(@Body() params) { + try { + return successResponse(await this.statisticsService.restocking(params)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } +} diff --git a/src/controller/stock.controller.ts b/src/controller/stock.controller.ts new file mode 100644 index 0000000..9db7768 --- /dev/null +++ b/src/controller/stock.controller.ts @@ -0,0 +1,262 @@ +import { + Controller, + Post, + Get, + Body, + Inject, + Del, + Param, + Query, + Put, +} from '@midwayjs/core'; +import { StockService } from '../service/stock.service'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { + CreatePurchaseOrderDTO, + CreateStockPointDTO, + QueryPointDTO, + QueryPurchaseOrderDTO, + QueryStockDTO, + QueryStockRecordDTO, + UpdatePurchaseOrderDTO, + UpdateStockDTO, + UpdateStockPointDTO, +} from '../dto/stock.dto'; +import { ApiOkResponse } from '@midwayjs/swagger'; +import { + BooleanRes, + PurchaseOrderListRes, + StockListRes, + StockPointAllRespone, + StockPointListRes, + StockRecordListRes, +} from '../dto/reponse.dto'; +import { User } from '../decorator/user.decorator'; + +@Controller('/stock') +export class StockController { + @Inject() + private readonly stockService: StockService; + + @ApiOkResponse({ type: BooleanRes, description: '创建库存点' }) + @Post('/stock-point') + async createStockPoint(@Body() body: CreateStockPointDTO) { + try { + const data = await this.stockService.createStockPoint(body); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取库存列表失败'); + } + } + + @ApiOkResponse({ type: BooleanRes, description: '创建库存点' }) + @Put('/stock-point/:id') + async updateStockPoint( + @Param('id') id: number, + @Body() body: UpdateStockPointDTO + ) { + try { + const data = await this.stockService.updateStockPoint(id, body); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取库存列表失败'); + } + } + + @ApiOkResponse({ type: StockPointListRes, description: '获取库存点列表' }) + @Get('/stock-point') + async getStockPoints(@Query() query: QueryPointDTO) { + try { + const data = await this.stockService.getStockPoints(query); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取库存列表失败'); + } + } + + @ApiOkResponse({ type: StockPointAllRespone, description: '获取所有库存' }) + @Get('/stock-point/all') + async getAllStockPoints() { + try { + const data = await this.stockService.getAllStockPoints(); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse({ type: BooleanRes, description: '删除库存点' }) + @Del('/stock-point/:id') + async delStockPoints(@Param('id') id: number) { + try { + await this.stockService.delStockPoints(id); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '删除库存点失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Post('/purchase-order') + async createPurchaseOrder(@Body() body: CreatePurchaseOrderDTO) { + try { + const data = await this.stockService.createPurchaseOrder(body); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Put('/purchase-order/:id') + async updatePurchaseOrder( + @Param('id') id: number, + @Body() body: UpdatePurchaseOrderDTO + ) { + try { + const data = await this.stockService.updatePurchaseOrder(id, body); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } + + @ApiOkResponse({ type: PurchaseOrderListRes, description: '获取采购列表' }) + @Get('/purchase-order') + async getPurchaseOrders(@Query() query: QueryPurchaseOrderDTO) { + try { + const data = await this.stockService.getPurchaseOrders(query); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取库存记录列表失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Del('/purchase-order/:id') + async delPurchaseOrder(@Param('id') id: number) { + try { + const data = await this.stockService.delPurchaseOrder(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '删除失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Post('/purchase-order/:id') + async receivePurchaseOrder(@Param('id') id: number, @User() user) { + try { + const data = await this.stockService.receivePurchaseOrder(id, user.id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } + + @ApiOkResponse({ type: StockListRes, description: '获取库存列表' }) + @Get('/') + async getStocks(@Query() query: QueryStockDTO) { + try { + const data = await this.stockService.getStocks(query); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取库存列表失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + description: '更新库存(入库、出库、调整)', + }) + @Post('/update') + async updateStock(@Body() body: UpdateStockDTO) { + try { + await this.stockService.updateStock(body); + return successResponse(true); + } catch (error) { + return errorResponse(error?.message || '更新库存失败'); + } + } + + @ApiOkResponse({ type: StockRecordListRes, description: '获取库存记录列表' }) + @Get('/records') + async getStockRecords(@Query() query: QueryStockRecordDTO) { + try { + const data = await this.stockService.getStockRecords(query); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取库存记录列表失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Post('/transfer') + async createTransfer(@Body() body: Record, @User() user) { + try { + const data = await this.stockService.createTransfer(body, user.id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '创建失败'); + } + } + + @ApiOkResponse() + @Get('/transfer') + async getTransfers(@Query() query: Record) { + try { + const data = await this.stockService.getTransfers(query); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '获取调拨列表失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Post('/cancelTransfer/:id') + async cancelTransfer(@Param('id') id: number, @User() user) { + try { + const data = await this.stockService.cancelTransfer(id, user.id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '删除失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Post('/receiveTransfer/:id') + async receiveTransfer(@Param('id') id: number, @User() user) { + try { + const data = await this.stockService.receiveTransfer(id, user.id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Post('/lostTransfer/:id') + async lostTransfer(@Param('id') id: number) { + try { + const data = await this.stockService.lostTransfer(id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } + + @ApiOkResponse({ type: BooleanRes }) + @Put('/receiveTransfer/:id') + async updateTransfer( + @Param('id') id: number, + @Body() body: Record, + @User() user + ) { + try { + const data = await this.stockService.updateTransfer(id, body, user.id); + return successResponse(data); + } catch (error) { + return errorResponse(error?.message || '更新失败'); + } + } +} diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts new file mode 100644 index 0000000..9bce0d5 --- /dev/null +++ b/src/controller/user.controller.ts @@ -0,0 +1,72 @@ +// src/controller/user.controller.ts +import { Controller, Post, Get, Body, Query } from '@midwayjs/core'; +import { Inject } from '@midwayjs/decorator'; +import { UserService } from '../service/user.service'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { ApiOkResponse } from '@midwayjs/swagger'; +import { BooleanRes, LoginRes } from '../dto/reponse.dto'; +import { LoginDTO } from '../dto/user.dto'; +import { User } from '../decorator/user.decorator'; + +@Controller('/user') +export class UserController { + @Inject() + userService: UserService; + + @ApiOkResponse({ + type: LoginRes, + }) + @Post('/login') + async login(@Body() body: LoginDTO) { + const { username, password } = body; + try { + const result = await this.userService.login(username, password); + return successResponse(result, '登录成功'); + } catch (error) { + return errorResponse(error?.message || '登录失败'); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/logout') + async logout() { + // 可选:在这里处理服务端缓存的 token 或 session + + return successResponse(true); + } + + @Post('/add') + async addUser(@Body() body: { username: string; password: string }) { + const { username, password } = body; + try { + await this.userService.addUser(username, password); + return successResponse(true); + } catch (error) { + console.log(error); + return errorResponse('添加用户失败'); + } + } + + @Get('/list') + async listUsers(@Query() query: { current: number; pageSize: number }) { + const { current = 1, pageSize = 10 } = query; + return successResponse(await this.userService.listUsers(current, pageSize)); + } + + @Post('/toggleActive') + async toggleActive(@Body() body: { userId: number; isActive: boolean }) { + return this.userService.toggleUserActive(body.userId, body.isActive); + } + + @ApiOkResponse() + @Get() + async getUser(@User() user) { + try { + return successResponse(await this.userService.getUser(user.id)); + } catch (error) { + return errorResponse('获取失败'); + } + } +} diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts new file mode 100644 index 0000000..1374678 --- /dev/null +++ b/src/controller/webhook.controller.ts @@ -0,0 +1,138 @@ +import { Config, HttpStatus, Inject } from '@midwayjs/core'; +import { + Controller, + Post, + Body, + Headers, + Get, + Query, +} from '@midwayjs/decorator'; +import { Context } from '@midwayjs/koa'; +import * as crypto from 'crypto'; +import { WpProductService } from '../service/wp_product.service'; +import { WPService } from '../service/wp.service'; +import { OrderService } from '../service/order.service'; +import { WpSite } from '../interface'; + +@Controller('/webhook') +export class WebhookController { + private secret = 'YOONE24kd$kjcdjflddd'; + + @Inject() + private readonly wpProductService: WpProductService; + + @Inject() + private readonly wpApiService: WPService; + + @Inject() + private readonly orderService: OrderService; + + @Inject() + ctx: Context; + + @Config('wpSite') + sites: WpSite[]; + + @Get('/') + async test() { + return 'webhook'; + } + + @Post('/woocommerce') + async handleWooWebhook( + @Body() body: any, + @Query('siteId') siteId: string, + @Headers() header: any + ) { + const signature = header['x-wc-webhook-signature']; + const topic = header['x-wc-webhook-topic']; + const source = header['x-wc-webhook-source']; + let site = this.sites.find(item => item.id === siteId); + + if (!site || !source.includes(site.wpApiUrl)) { + console.log('domain not match'); + return { + code: HttpStatus.BAD_REQUEST, + success: false, + message: 'domain not match', + }; + } + + if (!signature) { + return { + code: HttpStatus.BAD_REQUEST, + success: false, + message: 'Signature missing', + }; + } + const rawBody = this.ctx.request.rawBody; + const hash = crypto + .createHmac('sha256', this.secret) + .update(rawBody) + .digest('base64'); + try { + if (hash === signature) { + switch (topic) { + case 'product.created': + case 'product.updated': + const site = await this.wpProductService.geSite(siteId); + // 变体更新 + if (body.type === 'variation') { + const variation = await this.wpApiService.getVariation( + site, + body.parent_id, + body.id + ); + this.wpProductService.syncVariation( + siteId, + body.parent_id, + variation + ); + break; + } + const variations = + body.type === 'variable' + ? await this.wpApiService.getVariations(site, body.id) + : []; + await this.wpProductService.syncProductAndVariations( + site.id, + body, + variations + ); + break; + case 'product.deleted': + await this.wpProductService.delWpProduct(site.id, body.id); + break; + case 'order.created': + case 'order.updated': + await this.orderService.syncSingleOrder(siteId, body); + break; + case 'order.deleted': + break; + case 'customer.created': + break; + case 'customer.updated': + break; + case 'customer.deleted': + break; + default: + console.log('Unhandled event:', body.event); + } + + return { + code: 200, + success: true, + message: 'Webhook processed successfully', + }; + } else { + return { + code: 403, + success: false, + message: 'Webhook verification failed', + }; + } + } catch (error) { + console.log(error); + } + } +} diff --git a/src/controller/wp_product.controller.ts b/src/controller/wp_product.controller.ts new file mode 100644 index 0000000..04d64bf --- /dev/null +++ b/src/controller/wp_product.controller.ts @@ -0,0 +1,161 @@ +import { + Controller, + Param, + Post, + Inject, + Get, + Query, + Put, + Body, +} from '@midwayjs/core'; +import { WpProductService } from '../service/wp_product.service'; +import { errorResponse, successResponse } from '../utils/response.util'; +import { ApiOkResponse } from '@midwayjs/swagger'; +import { BooleanRes, WpProductListRes } from '../dto/reponse.dto'; +import { + QueryWpProductDTO, + SetConstitutionDTO, + UpdateVariationDTO, + UpdateWpProductDTO, +} from '../dto/wp_product.dto'; +import { WPService } from '../service/wp.service'; + +@Controller('/wp_product') +export class WpProductController { + @Inject() + private readonly wpProductService: WpProductService; + + @Inject() + private readonly wpApiService: WPService; + + @ApiOkResponse({ + type: BooleanRes, + }) + @Post('/sync/:siteId') + async syncProducts(@Param('siteId') siteId: string) { + try { + await this.wpProductService.syncSite(siteId); + return successResponse(true); + } catch (error) { + console.log(error); + return errorResponse('同步失败'); + } + } + + @ApiOkResponse({ + type: WpProductListRes, + }) + @Get('/list') + async getWpProducts(@Query() query: QueryWpProductDTO) { + try { + const data = await this.wpProductService.getProductList(query); + return successResponse(data); + } catch (error) { + return errorResponse(error.message); + } + } + + @ApiOkResponse({ + type: BooleanRes, + }) + @Put('/:id/constitution') + async setConstitution( + @Param('id') id: number, + @Body() + body: SetConstitutionDTO + ) { + const { isProduct, constitution } = body; + try { + await this.wpProductService.setConstitution(id, isProduct, constitution); + return successResponse(true); + } catch (error) { + return errorResponse(error.message); + } + } + + /** + * 更新产品接口 + * @param productId 产品 ID + * @param body 更新数据 + */ + @ApiOkResponse({ + type: BooleanRes, + }) + @Put('/siteId/:siteId/products/:productId') + async updateProduct( + @Param('siteId') siteId: string, + @Param('productId') productId: string, + @Body() body: UpdateWpProductDTO + ) { + try { + const isDuplicate = await this.wpProductService.isSkuDuplicate( + body.sku, + siteId, + productId + ); + if (isDuplicate) { + return errorResponse('SKU已存在'); + } + const site = await this.wpProductService.geSite(siteId); + const result = await this.wpApiService.updateProduct( + site, + productId, + body + ); + if (result) { + this.wpProductService.updateWpProduct(siteId, productId, body); + return successResponse(result, '产品更新成功'); + } + return errorResponse('产品更新失败'); + } catch (error) { + console.error('更新产品失败:', error); + return errorResponse(error.message || '产品更新失败'); + } + } + + /** + * 更新变体接口 + * @param productId 产品 ID + * @param variationId 变体 ID + * @param body 更新数据 + */ + @Put('/siteId/:siteId/products/:productId/variations/:variationId') + async updateVariation( + @Param('siteId') siteId: string, + @Param('productId') productId: string, + @Param('variationId') variationId: string, + @Body() body: UpdateVariationDTO + ) { + try { + const isDuplicate = await this.wpProductService.isSkuDuplicate( + body.sku, + siteId, + productId, + variationId + ); + if (isDuplicate) { + return errorResponse('SKU已存在'); + } + const site = await this.wpProductService.geSite(siteId); + const result = await this.wpApiService.updateVariation( + site, + productId, + variationId, + body + ); + if (result) { + this.wpProductService.updateWpProductVaritation( + siteId, + productId, + variationId, + body + ); + return successResponse(result, '产品变体更新成功'); + } + return errorResponse('变体更新失败'); + } catch (error) { + console.error('更新变体失败:', error); + return errorResponse(error.message || '产品变体更新失败'); + } + } +} diff --git a/src/decorator/user.decorator.ts b/src/decorator/user.decorator.ts new file mode 100644 index 0000000..fa88fb7 --- /dev/null +++ b/src/decorator/user.decorator.ts @@ -0,0 +1,8 @@ +import { createCustomParamDecorator } from '@midwayjs/core'; + +export const USER_KEY = 'USER_KEY'; + +// 定义装饰器 +export function User(): ParameterDecorator { + return createCustomParamDecorator(USER_KEY, {}); +} diff --git a/src/dto/freightcom.dto.ts b/src/dto/freightcom.dto.ts new file mode 100644 index 0000000..81ee8cb --- /dev/null +++ b/src/dto/freightcom.dto.ts @@ -0,0 +1,303 @@ +import { ApiProperty } from '@midwayjs/swagger'; +import { Rule, RuleType } from '@midwayjs/validate'; + +// 定义包装类型的联合类型 +export type PackagingType = + // | PackagingPallet + PackagingPackage; +// | PackagingCourierPak +// | PackagingEnvelope; + +// 定义包装类型的枚举,用于 API 文档描述 +export enum PackagingTypeEnum { + Pallet = 'pallet', + Package = 'package', + CourierPak = 'courier-pak', + Envelope = 'envelope', +} + +export class Address { + @ApiProperty() + @Rule(RuleType.string()) + address_line_1: string; + + @ApiProperty() + @Rule(RuleType.string()) + city: string; + + @ApiProperty() + @Rule(RuleType.string()) + region: string; + + @ApiProperty() + @Rule(RuleType.string()) + country: string; + + @ApiProperty() + @Rule(RuleType.string()) + postal_code: string; +} + +export class PhoneNumber { + @ApiProperty() + @Rule(RuleType.string()) + number: string; + + @ApiProperty() + @Rule(RuleType.string()) + extension: string; +} + +export class Location { + @ApiProperty() + @Rule(RuleType.string()) + name: string; + + @ApiProperty({ type: Address }) + @Rule(RuleType.object
()) + address: Address; + + @ApiProperty({ type: PhoneNumber }) + @Rule(RuleType.object()) + phone_number: PhoneNumber; + + @ApiProperty() + @Rule(RuleType.array()) + email_addresses: string[]; + + contact_name?: string; +} + +export class Time { + @ApiProperty() + @Rule(RuleType.string()) + hour: string; + + @ApiProperty() + @Rule(RuleType.string()) + minute: string; +} + +export enum SignatureRequirementEnum { + NOTREQUIRED = 'not-required', + REQUIRED = 'required', + ADULTREQUIRED = 'adult-required', +} + +export class Destination extends Location { + @ApiProperty({ type: Time }) + @Rule(RuleType.object