zksu
/
API
forked from yoone/API
1
0
Fork 0
This commit is contained in:
cll 2025-05-22 15:15:43 +08:00
parent a5e9fd7a63
commit 037df80080
85 changed files with 10331 additions and 0 deletions

11
.editorconfig Normal file
View File

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

7
.eslintrc.json Normal file
View File

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

15
.gitignore vendored Normal file
View File

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

3
.prettierrc.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

2
bootstrap.js vendored Normal file
View File

@ -0,0 +1,2 @@
const { Bootstrap } = require('@midwayjs/bootstrap')
Bootstrap.run()

6
jest.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
};

59
package.json Normal file
View File

@ -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"
}

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,7 @@
import { MidwayConfig } from '@midwayjs/core';
export default {
koa: {
port: null,
},
} as MidwayConfig;

78
src/configuration.ts Normal file
View File

@ -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');
}
}
);
}
}

View File

@ -0,0 +1,8 @@
import { Inject, Controller } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class APIController {
@Inject()
ctx: Context;
}

View File

@ -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<string, any>
) {
try {
const data = await this.logisticsService.getList(param);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
}

View File

@ -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<string, any>) {
try {
return successResponse(await this.orderService.createOrder(data));
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
}

View File

@ -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<ProductListRes> {
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);
}
}
}

View File

@ -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,
}))
);
}
}

View File

@ -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 || '获取失败');
}
}
}

View File

@ -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<string, any>, @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<string, any>) {
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<string, any>,
@User() user
) {
try {
const data = await this.stockService.updateTransfer(id, body, user.id);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
}

View File

@ -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('获取失败');
}
}
}

View File

@ -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);
}
}
}

View File

@ -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 || '产品变体更新失败');
}
}
}

View File

@ -0,0 +1,8 @@
import { createCustomParamDecorator } from '@midwayjs/core';
export const USER_KEY = 'USER_KEY';
// 定义装饰器
export function User(): ParameterDecorator {
return createCustomParamDecorator(USER_KEY, {});
}

303
src/dto/freightcom.dto.ts Normal file
View File

@ -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: Address;
@ApiProperty({ type: PhoneNumber })
@Rule(RuleType.object<PhoneNumber>())
phone_number: PhoneNumber;
@ApiProperty()
@Rule(RuleType.array<string>())
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<Time>())
ready_at: Time;
@ApiProperty({ type: Time })
@Rule(RuleType.object<Time>())
ready_until: Time;
@ApiProperty({ type: SignatureRequirementEnum })
@Rule(RuleType.string().valid(...Object.values(SignatureRequirementEnum)))
SignatureRequirementEnum: SignatureRequirementEnum;
}
export class Date {
@ApiProperty()
@Rule(RuleType.string())
year: string;
@ApiProperty()
@Rule(RuleType.string())
month: string;
@ApiProperty()
@Rule(RuleType.string())
day: string;
}
export enum UnitEnum {
KG = 'kg',
LB = 'lb',
G = 'g',
OZ = 'oz',
}
export class Cubid {
@ApiProperty()
@Rule(RuleType.number())
w: number;
@ApiProperty()
@Rule(RuleType.number())
h: number;
@ApiProperty()
@Rule(RuleType.number())
l: number;
@ApiProperty()
@Rule(RuleType.string())
unit: string;
}
export class Weight {
@ApiProperty({ enum: UnitEnum })
@Rule(RuleType.string().valid(...Object.values(UnitEnum)))
unit: UnitEnum;
@ApiProperty()
@Rule(RuleType.number())
value: number;
}
export class Measurements {
@ApiProperty({ type: Cubid })
cuboid: Cubid;
@ApiProperty({ type: Cubid })
weight: Weight;
}
export class Pallets {
@ApiProperty({ type: Measurements })
@Rule(RuleType.object<Measurements>())
measurements: Measurements;
@ApiProperty()
@Rule(RuleType.string())
description: string;
@ApiProperty()
@Rule(RuleType.string())
freight_class: string;
}
export class PackagingPallet {
@ApiProperty()
@Rule(RuleType.string())
pallet_type: string;
@ApiProperty({ type: Pallets })
@Rule(RuleType.object<Pallets>())
pallets: Pallets;
}
export class Package {
@ApiProperty({ type: Measurements })
@Rule(RuleType.object<Measurements>())
measurements: Measurements;
@ApiProperty()
@Rule(RuleType.string())
description: string;
}
export class PackagingPackage {
@ApiProperty({ type: Package, isArray: true })
@Rule(RuleType.array<Package>())
packages: Package[];
}
export class PackagingCourierPak {
@ApiProperty({ type: Package, isArray: true })
@Rule(RuleType.array<Package>())
courier_paks: Package[];
}
export class PackagingEnvelope {
// 添加必要的属性和装饰器
@ApiProperty()
@Rule(RuleType.boolean())
includes_return_label?: boolean;
}
// export enum InsuranceTypeEnum {
// INTERNAL = 'internal',
// CARRIER = 'carrier',
// }
// export class Insurance {
// @ApiProperty({ enum: InsuranceTypeEnum })
// @Rule(RuleType.string().valid(...Object.values(InsuranceTypeEnum)))
// type: InsuranceTypeEnum;
// }
export class ShippingDetailsDTO {
@ApiProperty({ type: Location })
@Rule(RuleType.object<Location>())
origin: Location;
@ApiProperty({ type: Destination })
@Rule(RuleType.object<Destination>())
destination: Destination;
@ApiProperty({ type: Date })
@Rule(RuleType.object<Date>())
expected_ship_date: Date;
@ApiProperty({ enum: PackagingTypeEnum })
@Rule(RuleType.string().valid(...Object.values(PackagingTypeEnum)))
packaging_type: PackagingTypeEnum;
@ApiProperty({
type: () => [
// PackagingPallet,
PackagingPackage,
// PackagingCourierPak,
// PackagingEnvelope,
],
})
@Rule(RuleType.object<PackagingType>())
packaging_properties: PackagingType;
// @ApiProperty({ type: Insurance })
// @Rule(RuleType.object<Insurance>())
// insurance?: Insurance;
@ApiProperty()
reference_codes: string[];
}
export class Money {
@ApiProperty()
currency: string;
@ApiProperty()
value: string;
}
export class Surcharges {
@ApiProperty()
type: string;
@ApiProperty({ type: Money })
amount: Money;
}
export class RateDTO {
@ApiProperty()
carrier_name: string;
@ApiProperty()
service_name: string;
@ApiProperty()
service_id: string;
@ApiProperty({ type: Date })
valid_until: Date;
@ApiProperty({ type: Money })
total: Money;
@ApiProperty({ type: Money })
base: Money;
@ApiProperty({ type: Surcharges, isArray: true })
surcharges: Surcharges[];
@ApiProperty({ type: Money, isArray: true })
taxes: Money[];
@ApiProperty()
transit_time_days: number;
@ApiProperty()
transit_time_not_available: boolean;
}

63
src/dto/logistics.dto.ts Normal file
View File

@ -0,0 +1,63 @@
import { ApiProperty } from '@midwayjs/swagger';
import { ShippingDetailsDTO } from './freightcom.dto';
import { Rule, RuleType } from '@midwayjs/validate';
import { OrderSale } from '../entity/order_sale.entity';
import { ShipmentType } from '../enums/base.enum';
export class ShipmentBookDTO {
@ApiProperty({ type: OrderSale, isArray: true })
@Rule(RuleType.array<OrderSale>())
sales: OrderSale[];
@ApiProperty()
@Rule(RuleType.string())
payment_method_id: string;
@ApiProperty()
@Rule(RuleType.string())
service_id: string;
@ApiProperty()
@Rule(RuleType.string())
service_type: ShipmentType;
@ApiProperty({ type: ShippingDetailsDTO })
@Rule(RuleType.object<ShippingDetailsDTO>())
details: ShippingDetailsDTO;
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty({ type: 'number', isArray: true })
@Rule(RuleType.array<number>().default([]))
orderIds?: number[];
}
export class PaymentMethodDTO {
@ApiProperty()
id: string;
@ApiProperty()
type: string;
@ApiProperty()
label: string;
}
export class QueryServiceDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string())
carrier_name: string;
@ApiProperty()
@Rule(RuleType.bool())
isActive: boolean;
}

131
src/dto/order.dto.ts Normal file
View File

@ -0,0 +1,131 @@
import { ApiProperty } from '@midwayjs/swagger';
import { ErpOrderStatus } from '../enums/base.enum';
import { Rule, RuleType } from '@midwayjs/validate';
import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity';
export class OrderAddress {
@ApiProperty()
first_name: string;
@ApiProperty()
last_name: string;
@ApiProperty()
company: string;
@ApiProperty()
address_1: string;
@ApiProperty()
address_2: string;
@ApiProperty()
city: string;
@ApiProperty()
state: string;
@ApiProperty()
postcode: string;
@ApiProperty()
country: string;
@ApiProperty()
email: string;
@ApiProperty()
phone: string;
}
export class OrderStatusCountDTO {
@ApiProperty()
status: ErpOrderStatus;
@ApiProperty()
count: number;
}
export class QueryOrderDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string())
externalOrderId: string;
@ApiProperty()
@Rule(RuleType.string())
siteId: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
customer_email: string;
@ApiProperty()
@Rule(RuleType.string().allow(null))
keyword: string;
@ApiProperty()
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date())
endDate: Date;
@ApiProperty({ type: 'enum', enum: ErpOrderStatus })
@Rule(RuleType.string().valid(...Object.values(ErpOrderStatus)))
status: ErpOrderStatus;
}
export class QueryOrderSalesDTO {
@ApiProperty()
@Rule(RuleType.bool().default(false))
isSource: boolean;
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string())
siteId: string;
@ApiProperty()
@Rule(RuleType.string())
name: string;
@ApiProperty()
@Rule(RuleType.date().required())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date().required())
endDate: Date;
}
export class Tracking extends Shipment {
@ApiProperty({ type: ShipmentItem, isArray: true })
products?: ShipmentItem[];
}
export class CreateOrderNoteDTO {
@ApiProperty()
@Rule(RuleType.number())
orderId: number;
@ApiProperty()
@Rule(RuleType.string())
content: string;
}

174
src/dto/product.dto.ts Normal file
View File

@ -0,0 +1,174 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
/**
* DTO
*/
export class CreateProductDTO {
@ApiProperty({
example: 'ZYN 6MG WINTERGREEN',
description: '产品名称',
required: true,
})
@Rule(RuleType.string().required().empty({ message: '产品名称不能为空' }))
name: string;
@ApiProperty({ example: '产品描述', description: '产品描述' })
@Rule(RuleType.string())
description: string;
@ApiProperty({ example: '1', description: '分类 ID' })
@Rule(RuleType.number())
categoryId: number;
@ApiProperty()
@Rule(RuleType.number())
strengthId: number;
@ApiProperty()
@Rule(RuleType.number())
flavorsId: number;
@ApiProperty()
@Rule(RuleType.string())
humidity: string;
}
/**
* DTO
*/
export class UpdateProductDTO extends CreateProductDTO {
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称' })
@Rule(RuleType.string())
name: string;
}
/**
* DTO
*/
export class QueryProductDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string;
@ApiProperty({ example: '1', description: '分类 ID' })
@Rule(RuleType.string())
categoryId: number;
}
/**
* DTO
*/
export class CreateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
/**
* DTO
*/
export class UpdateCategoryDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
/**
* DTO
*/
export class QueryCategoryDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class CreateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
export class UpdateFlavorsDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
export class QueryFlavorsDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class CreateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称', required: true })
@Rule(RuleType.string().required().empty({ message: '分类名称不能为空' }))
name: string;
@Rule(RuleType.string().required().empty({ message: 'key不能为空' }))
unique_key: string;
}
export class UpdateStrengthDTO {
@ApiProperty({ example: 'ZYN', description: '分类名称' })
@Rule(RuleType.string())
name: string;
}
export class QueryStrengthDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty({ example: 'ZYN', description: '关键字' })
@Rule(RuleType.string())
name: string; // 搜索关键字(支持模糊查询)
}
export class SkuItemDTO {
@ApiProperty({ description: '产品 ID' })
productId: number;
@ApiProperty({ description: 'sku 编码' })
sku: string;
}
export class BatchSetSkuDTO {
@ApiProperty({ description: 'sku 数据列表', type: [SkuItemDTO] })
skus: SkuItemDTO[];
}

119
src/dto/reponse.dto.ts Normal file
View File

@ -0,0 +1,119 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Category } from '../entity/category.entity';
import { Order } from '../entity/order.entity';
import { Product } from '../entity/product.entty';
import { StockPoint } from '../entity/stock_point.entity';
import { PaginatedWrapper } from '../utils/paginated-response.util';
import {
SuccessArrayWrapper,
SuccessWrapper,
} from '../utils/response-wrapper.util';
import { OrderStatusCountDTO, Tracking } from './order.dto';
import { SiteConfig } from './site.dto';
import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto';
import { LoginResDTO } from './user.dto';
import { WpProductDTO } from './wp_product.dto';
import { OrderSale } from '../entity/order_sale.entity';
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 { OrderNote } from '../entity/order_note.entity';
import { PaymentMethodDTO } from './logistics.dto';
import { Flavors } from '../entity/flavors.entity';
import { Strength } from '../entity/strength.entity';
export class BooleanRes extends SuccessWrapper(Boolean) {}
//网站配置返回数据
export class WpSitesResponse extends SuccessArrayWrapper(SiteConfig) {}
//产品分页数据
export class ProductPaginatedResponse extends PaginatedWrapper(Product) {}
//产品分页返回数据
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 ProductCatAllRes extends SuccessArrayWrapper(Category) {}
//产品分类返回数据
export class ProductCatRes extends SuccessWrapper(Category) {}
//产品分页数据
export class WpProductPaginatedResponse extends PaginatedWrapper(
WpProductDTO
) {}
//产品分页返回数据
export class WpProductListRes extends SuccessWrapper(
WpProductPaginatedResponse
) {}
export class LoginRes extends SuccessWrapper(LoginResDTO) {}
export class StockPaginatedRespone extends PaginatedWrapper(StockDTO) {}
export class StockListRes extends SuccessWrapper(StockPaginatedRespone) {}
export class StockRecordPaginatedRespone extends PaginatedWrapper(
StockRecordDTO
) {}
export class StockRecordListRes extends SuccessWrapper(
StockRecordPaginatedRespone
) {}
export class StockPointAllRespone extends SuccessArrayWrapper(StockPoint) {}
export class StockPointPaginatedRespone extends PaginatedWrapper(StockPoint) {}
export class StockPointListRes extends SuccessWrapper(
StockPointPaginatedRespone
) {}
export class PurchaseOrderPaginatedRespone extends PaginatedWrapper(
PurchaseOrderDTO
) {}
export class PurchaseOrderListRes extends SuccessWrapper(
PurchaseOrderPaginatedRespone
) {}
export class OrderStatusCountRes extends SuccessArrayWrapper(
OrderStatusCountDTO
) {}
export class OrderPaginatedRespone extends PaginatedWrapper(Order) {
@ApiProperty({ type: OrderStatusCountDTO, isArray: true })
count: OrderStatusCountDTO;
}
export class OrderListRes extends SuccessWrapper(OrderPaginatedRespone) {}
export class OrderSaleDTO extends OrderSale {
@ApiProperty()
totalQuantity: number;
}
export class OrderSalePaginatedRespone extends PaginatedWrapper(OrderSaleDTO) {}
export class OrderSaleListRes extends SuccessWrapper(
OrderSalePaginatedRespone
) {}
export class ServiceListRes extends SuccessArrayWrapper(Service) {}
export class RateLitRes extends SuccessArrayWrapper(RateDTO) {}
export class ShippingAddressListRes extends SuccessArrayWrapper(
ShippingAddress
) {}
export class OrderDetail extends Order {
@ApiProperty({ type: OrderItem, isArray: true })
items: OrderItem[];
@ApiProperty({ type: OrderSale, isArray: true })
sales: OrderSale[];
@ApiProperty({ type: OrderRefundItem, isArray: true })
refundItems: OrderRefundItem[];
@ApiProperty({ type: Tracking, isArray: true })
trackings: Tracking[];
@ApiProperty({ type: OrderNote, isArray: true })
notes: OrderNote[];
}
export class OrderDetailRes extends SuccessWrapper(OrderDetail) {}
export class PaymentMethodListRes extends SuccessArrayWrapper(
PaymentMethodDTO
) {}

32
src/dto/site.dto.ts Normal file
View File

@ -0,0 +1,32 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
export class SiteConfig {
@ApiProperty({ example: '1', description: '站点 ID' })
@Rule(RuleType.string())
id: string;
@ApiProperty({ description: '站点 URL' })
@Rule(RuleType.string())
wpApiUrl: string;
@ApiProperty({ description: '站点 rest key' })
@Rule(RuleType.string())
consumerKey: string;
@ApiProperty({ description: '站点 rest 秘钥' })
@Rule(RuleType.string())
consumerSecret: string;
@ApiProperty({ description: '站点名' })
@Rule(RuleType.string())
siteName: string;
@ApiProperty({ description: '站点邮箱' })
@Rule(RuleType.string())
email?: string;
@ApiProperty({ description: '站点邮箱密码' })
@Rule(RuleType.string())
emailPswd?: string;
}

36
src/dto/statistics.dto.ts Normal file
View File

@ -0,0 +1,36 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import dayjs = require('dayjs');
export class OrderStatisticsParams {
@ApiProperty()
@Rule(RuleType.date().default(dayjs().subtract(1, 'month')))
startDate: Date;
@ApiProperty()
@Rule(RuleType.date().default(dayjs().subtract(1, 'month')))
endDate: Date;
@ApiProperty()
@Rule(RuleType.string().allow(null))
keyword?: string;
@ApiProperty()
@Rule(RuleType.string().allow(null))
siteId?: string;
@ApiProperty({
enum: ['all', 'first_purchase', 'repeat_purchase'],
default: 'all',
})
@Rule(RuleType.string().valid('all', 'first_purchase', 'repeat_purchase'))
purchaseType: string;
@ApiProperty({ enum: ['all', 'cpc', 'non_cpc'], default: 'all' })
@Rule(RuleType.string().valid('all', 'cpc', 'non_cpc'))
orderType: string;
@ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' })
@Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt'))
brand: string;
}

173
src/dto/stock.dto.ts Normal file
View File

@ -0,0 +1,173 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
import { Stock } from '../entity/stock.entity';
import {
PurchaseOrderStatus,
StockRecordOperationType,
} from '../enums/base.enum';
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
import { PurchaseOrder } from '../entity/purchase_order.entity';
import { StockRecord } from '../entity/stock_record.entity';
export class QueryStockDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty()
@Rule(RuleType.string())
productName: string;
}
export class QueryPointDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
}
export class QueryStockRecordDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty()
@Rule(RuleType.string())
productSku: string;
@ApiProperty()
@Rule(RuleType.string())
productName: string;
}
export class QueryPurchaseOrderDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number; // 页码
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number; // 每页大小
@ApiProperty()
@Rule(RuleType.string())
orderNumber?: string;
@ApiProperty()
@Rule(RuleType.number())
stockPointId?: number;
}
export class StockDTO extends Stock {
@ApiProperty()
@Rule(RuleType.string())
productName: string;
@ApiProperty({
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
quantity: { type: 'number' },
},
isArray: true,
})
@Rule(RuleType.array())
stockPoint: Array<{
id: number;
name: string;
quantity: number;
}>;
}
export class StockRecordDTO extends StockRecord {
@ApiProperty()
@Rule(RuleType.string())
productName: string;
}
export class PurchaseOrderDTO extends PurchaseOrder {
@ApiProperty({ type: PurchaseOrderItem, isArray: true })
@Rule(RuleType.array())
items: PurchaseOrderItem[];
}
export class UpdateStockDTO {
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty()
@Rule(RuleType.string())
productSku: string;
@ApiProperty()
@Rule(RuleType.number())
quantityChange: number;
@ApiProperty({ type: 'enum', enum: StockRecordOperationType })
@Rule(RuleType.string().valid(...Object.values(StockRecordOperationType)))
operationType: StockRecordOperationType;
@ApiProperty()
@Rule(RuleType.number())
operatorId: number;
@ApiProperty()
@Rule(RuleType.string().allow(''))
note: string;
}
export class CreateStockPointDTO {
@ApiProperty()
@Rule(RuleType.string())
name: string;
@ApiProperty()
@Rule(RuleType.string())
location: string;
@ApiProperty()
@Rule(RuleType.string())
contactPerson: string;
@ApiProperty()
@Rule(RuleType.string())
contactPhone: string;
}
export class UpdateStockPointDTO extends CreateStockPointDTO {}
export class CreatePurchaseOrderDTO {
@ApiProperty()
@Rule(RuleType.number())
stockPointId: number;
@ApiProperty()
@Rule(RuleType.date())
expectedArrivalTime: Date;
@ApiProperty({ type: 'enum', enum: PurchaseOrderStatus })
@Rule(RuleType.string().valid(...Object.values(PurchaseOrderStatus)))
status: PurchaseOrderStatus;
@ApiProperty()
@Rule(RuleType.string().allow(''))
note?: string;
@ApiProperty({
type: PurchaseOrderItem,
isArray: true,
})
@Rule(RuleType.array())
items: PurchaseOrderItem[];
}
export class UpdatePurchaseOrderDTO extends CreatePurchaseOrderDTO {}

26
src/dto/user.dto.ts Normal file
View File

@ -0,0 +1,26 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Rule, RuleType } from '@midwayjs/validate';
export class LoginResDTO {
@ApiProperty({ type: String })
token: string;
@ApiProperty({ type: Number })
userId: number;
@ApiProperty({ type: String })
username: string;
@ApiProperty({ type: String, isArray: true })
permissions: string[];
}
export class LoginDTO {
@ApiProperty({ type: String })
@Rule(RuleType.string())
username: string;
@ApiProperty({ type: String })
@Rule(RuleType.string())
password: string;
}

98
src/dto/wp_product.dto.ts Normal file
View File

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

View File

@ -0,0 +1,53 @@
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;
}

View File

@ -0,0 +1,43 @@
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;
}

267
src/entity/order.entity.ts Normal file
View File

@ -0,0 +1,267 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
BeforeInsert,
BeforeUpdate,
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { ErpOrderStatus, OrderStatus } from '../enums/base.enum';
import { OrderAddress } from '../dto/order.dto';
import { Exclude, Expose } from 'class-transformer';
@Entity('order')
@Exclude()
export class Order {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty({ type: OrderStatus })
@Column({ type: 'enum', enum: OrderStatus })
@Expose()
status: OrderStatus; // WooCommerce 订单 状态
@ApiProperty({ type: ErpOrderStatus })
@Column({
type: 'enum',
enum: ErpOrderStatus,
default: ErpOrderStatus.PENDING,
})
@Expose()
orderStatus: ErpOrderStatus; // 订单状态
@ApiProperty()
@Column()
@Expose()
currency: string;
@ApiProperty()
@Column()
@Expose()
currency_symbol: string;
@ApiProperty()
@Column({ default: false })
@Expose()
prices_include_tax: boolean;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_created: Date;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_modified: Date;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
discount_total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
discount_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
shipping_total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
shipping_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
cart_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, default: 0 })
@Expose()
total_tax: number;
@ApiProperty()
@Column({ default: 0 })
@Expose()
customer_id: number;
@ApiProperty()
@Column()
@Expose()
customer_email: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
order_key: string;
@ApiProperty({ type: OrderAddress })
@Column({ type: 'json', nullable: true })
@Expose()
billing: OrderAddress;
@ApiProperty({ type: OrderAddress })
@Column({ type: 'json', nullable: true })
@Expose()
shipping: OrderAddress;
@ApiProperty()
@Column({ default: '' })
@Expose()
payment_method: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
payment_method_title: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
transaction_id: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
customer_ip_address: string;
@ApiProperty()
@Column({
type: 'varchar',
length: 1024,
nullable: true,
})
@Expose()
customer_user_agent: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
created_via: string;
@ApiProperty()
@Column({
type: 'mediumtext', // 设置字段类型为 MEDIUMTEXT
nullable: true, // 可选:是否允许为 NULL
})
@Expose()
customer_note: string;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_completed: Date;
@ApiProperty()
@Column({ type: 'timestamp', nullable: true })
@Expose()
date_paid: Date;
@ApiProperty()
@Column({ default: '' })
@Expose()
cart_hash: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
number: string;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: any[];
@ApiProperty()
@Column({ default: '' })
@Expose()
payment_url: string;
@ApiProperty()
@Column({ default: false })
@Expose()
is_editable: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
needs_payment: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
needs_processing: boolean;
@ApiProperty()
@Column({ default: '' })
@Expose()
device_type: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
source_type: string;
@ApiProperty()
@Column({ default: '' })
@Expose()
utm_source: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
// 在插入或更新前处理用户代理字符串
@BeforeInsert()
@BeforeUpdate()
truncateUserAgent() {
const maxLength = 1024; // 根据数据库限制的实际长度
if (
this.customer_user_agent &&
this.customer_user_agent.length > maxLength
) {
this.customer_user_agent = this.customer_user_agent.substring(
0,
maxLength
);
}
}
}

View File

@ -0,0 +1,86 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_coupon')
@Exclude()
export class OrderCoupon {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderCouponId: string; // WooCommerce 订单coupon ID
@ApiProperty()
@Column()
@Expose()
code: string;
@ApiProperty()
@Column()
@Expose()
discount: string;
@ApiProperty()
@Column()
@Expose()
discount_tax: string;
@ApiProperty()
@Column()
@Expose()
discount_type: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
nominal_amount: number;
@ApiProperty()
@Column()
@Expose()
free_shipping: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,86 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_fee')
@Exclude()
export class OrderFee {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string;
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderFeeId: string; // WooCommerce 订单fee ID
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
tax_class: string;
@ApiProperty()
@Column()
@Expose()
tax_status: string;
@ApiProperty()
@Column()
@Expose()
amount: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total_tax: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,106 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_item')
@Exclude()
export class OrderItem {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty()
@Column()
@Expose()
externalProductId: string; // WooCommerce 产品 ID
@ApiProperty()
@Column()
@Expose()
externalVariationId: string; // WooCommerce 变体 ID
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
subtotal: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
subtotal_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2, nullable: true })
@Expose()
total_tax: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
sku?: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
price: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,49 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_note')
@Exclude()
export class OrderNote {
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
@Expose()
userId: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
content: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,86 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_refund')
@Exclude()
export class OrderRefund {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalRefundId: string; // WooCommerce refund ID
@ApiProperty()
@Column({ type: 'timestamp' })
@Expose()
date_created: Date;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
amount: number;
@ApiProperty()
@Column()
@Expose()
reason: string;
@ApiProperty()
@Column()
@Expose()
refunded_by: number;
@ApiProperty()
@Column()
@Expose()
refunded_payment: boolean;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
meta_data: [];
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,111 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_refund_item')
@Exclude()
export class OrderRefundItem {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
refundId: number; // 订单 refund ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalRefundId: string; // WooCommerce refund ID
@ApiProperty()
@Column()
@Expose()
externalRefundItemId: string; // WooCommerce refund item ID
@ApiProperty()
@Column()
@Expose()
externalProductId: string; // WooCommerce 产品 ID
@ApiProperty()
@Column()
@Expose()
externalVariationId: string; // WooCommerce 变体 ID
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column()
@Expose()
tax_class: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
subtotal: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
subtotal_tax: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total_tax: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
sku?: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
price: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,74 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_sale')
@Exclude()
export class OrderSale {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column()
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID
@ApiProperty()
@Column()
@Expose()
productId: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Expose()
@Column()
sku: string;
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty()
@Column({ default: false })
@Expose()
isPackage: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('order_shipment')
export class OrderShipment {
@ApiProperty()
@PrimaryGeneratedColumn()
id: number;
@ApiProperty()
@Column()
order_id: number;
@ApiProperty()
@Column()
shipment_id: string;
@ApiProperty()
@Column()
stockPointId: number;
}

View File

@ -0,0 +1,81 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('order_shipping')
@Exclude()
export class OrderShipping {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
orderId: number; // 订单 ID
@ApiProperty()
@Column()
@Expose()
siteId: string;
@ApiProperty()
@Column()
@Expose()
externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty()
@Column()
@Expose()
externalOrderShippingId: string; // WooCommerce 订单快递 ID
@ApiProperty()
@Column()
@Expose()
method_title: string;
@ApiProperty()
@Column()
@Expose()
method_id: string;
@ApiProperty()
@Column()
@Expose()
instance_id: string;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total: number;
@ApiProperty()
@Column('decimal', { precision: 10, scale: 2 })
@Expose()
total_tax: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

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

View File

@ -0,0 +1,61 @@
// src/entity/PurchaseOrder.ts
import { ApiProperty } from '@midwayjs/swagger';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { PurchaseOrderStatus } from '../enums/base.enum';
@Entity('purchase_order')
export class PurchaseOrder {
@ApiProperty({ type: 'number' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
stockPointId: number;
@ApiProperty({ type: 'string' })
@Column()
orderNumber: string;
@ApiProperty({ type: PurchaseOrderStatus })
@Column({
type: 'enum',
enum: PurchaseOrderStatus,
default: 'draft',
})
status: PurchaseOrderStatus;
@ApiProperty()
@Column({ nullable: true, type: 'text' })
note: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '预计时间',
required: true,
})
@Column({ type: 'timestamp', nullable: true })
expectedArrivalTime: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,30 @@
// src/entity/PurchaseOrderItem.ts
import { ApiProperty } from '@midwayjs/swagger';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('purchase_order_item')
export class PurchaseOrderItem {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: String })
@Column()
productName: string;
@ApiProperty({ type: Number })
@Column()
quantity: number;
@ApiProperty({ type: Number })
@Column({ type: 'decimal', precision: 10, scale: 2 })
price: number;
@ApiProperty({ type: Number })
@Column()
purchaseOrderId: number;
}

View File

@ -0,0 +1,51 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
@Exclude()
export class Service {
@ApiProperty()
@PrimaryColumn()
@Expose()
id?: string;
@ApiProperty()
@Column()
@Expose()
carrier_name: string;
@ApiProperty()
@Column()
@Expose()
service_name: string;
@ApiProperty()
@Column({ default: false })
@Expose()
isActive: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,104 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('shipment')
@Exclude()
export class Shipment {
@ApiProperty()
@PrimaryColumn()
@Expose()
id: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
tracking_provider?: string;
@ApiProperty()
@Column()
@Expose()
unique_id: string;
@Column({ nullable: true })
@Expose()
state?: string;
@Column({ nullable: true })
@Expose()
type?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
transaction_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
primary_tracking_number?: string;
@ApiProperty({ type: [String] })
@Column({ type: 'json', nullable: true })
@Expose()
tracking_numbers?: string[];
@ApiProperty()
@Column({ nullable: true })
@Expose()
tracking_url?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
return_tracking_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
bol_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
pickup_confirmation_number?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
customs_invoice_url?: string;
@ApiProperty({ type: Object })
@Column({ type: 'json', nullable: true })
@Expose()
rate?: Record<string, any>;
@ApiProperty()
@Column({ type: 'json', nullable: true })
@Expose()
labels?: Array<any>;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,61 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('shipment_item')
@Exclude()
export class ShipmentItem {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id: number;
@ApiProperty()
@Column()
@Expose()
shipment_id: string;
@ApiProperty()
@Column()
@Expose()
productId: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty({ description: 'sku', type: 'string' })
@Expose()
@Column()
sku: string;
@ApiProperty()
@Column()
@Expose()
quantity: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt: Date;
}

View File

@ -0,0 +1,67 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Address } from '../dto/freightcom.dto';
@Entity()
@Exclude()
export class ShippingAddress {
@ApiProperty()
@PrimaryGeneratedColumn()
@Expose()
id?: number;
@ApiProperty()
@Column()
@Expose()
name: string;
@ApiProperty()
@Column()
@Expose()
stockPointId: number;
@ApiProperty({ type: Address })
@Column({ type: 'json', nullable: true })
@Expose()
address: Address;
@ApiProperty()
@Column()
@Expose()
phone_number: string;
@ApiProperty()
@Column()
@Expose()
phone_number_extension: string;
@ApiProperty()
@Column()
@Expose()
phone_number_country: string;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
@Expose()
createdAt?: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
@Expose()
updatedAt?: Date;
}

View File

@ -0,0 +1,44 @@
// src/entity/Stock.ts
import { ApiProperty } from '@midwayjs/swagger';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('stock')
export class Stock {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
stockPointId: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: Number })
@Column()
quantity: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,64 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
BaseEntity,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('stock_point')
export class StockPoint extends BaseEntity {
@ApiProperty({ type: 'number' })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: 'string' })
@Column()
name: string;
@ApiProperty({ type: 'string' })
@Column()
location: string;
@ApiProperty({ type: 'string' })
@Column()
contactPerson: string;
@ApiProperty({ type: 'string' })
@Column()
contactPhone: string;
@ApiProperty()
@Column({ default: false })
ignore: boolean;
@ApiProperty()
@Column({ default: false })
inCanada: boolean;
@ApiProperty()
@Column({ default: false })
isB: boolean;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date; // 软删除时间
}

View File

@ -0,0 +1,48 @@
// src/entity/StockRecord.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
} from 'typeorm';
import { StockRecordOperationType } from '../enums/base.enum';
import { ApiProperty } from '@midwayjs/swagger';
@Entity('stock_record')
export class StockRecord {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: Number })
@Column()
stockPointId: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: StockRecordOperationType })
@Column({ type: 'enum', enum: StockRecordOperationType })
operationType: StockRecordOperationType;
@ApiProperty({ type: Number })
@Column()
quantityChange: number;
@ApiProperty()
@Column()
operatorId: number;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({ type: String })
@Column({ nullable: true })
note: string;
}

View File

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

View File

@ -0,0 +1,67 @@
import { ApiProperty } from '@midwayjs/swagger';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class Transfer {
@ApiProperty()
@PrimaryGeneratedColumn()
id?: number;
@ApiProperty({ type: 'string' })
@Column()
orderNumber: string;
@ApiProperty({ type: Number })
@Column()
sourceStockPointId: number;
@ApiProperty({ type: Number })
@Column()
destStockPointId: number;
@ApiProperty({})
@Column({ default: false })
isCancel: boolean;
@ApiProperty({})
@Column({ default: false })
isArrived: boolean;
@ApiProperty({})
@Column({ default: false })
isLost: boolean;
@ApiProperty()
@Column({ nullable: true, type: 'text' })
note: string;
@ApiProperty()
@Column({ nullable: true, type: Date })
sendAt: Date;
@ApiProperty()
@Column({ nullable: true, type: Date })
arriveAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '创建时间',
required: true,
})
@CreateDateColumn()
createdAt: Date;
@ApiProperty({
example: '2022-12-12 11:11:11',
description: '更新时间',
required: true,
})
@UpdateDateColumn()
updatedAt: Date;
}

View File

@ -0,0 +1,25 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('transfer_item')
export class TransferItem {
@ApiProperty({ type: Number })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ type: String })
@Column()
productSku: string;
@ApiProperty({ type: String })
@Column()
productName: string;
@ApiProperty({ type: Number })
@Column()
quantity: number;
@ApiProperty({ type: Number })
@Column()
transferId: number;
}

31
src/entity/user.entity.ts Normal file
View File

@ -0,0 +1,31 @@
// src/entity/user.entity.ts
import { Exclude } from 'class-transformer';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
@Exclude()
password: string;
// @Column() // 默认角色为管理员
// roleId: number; // 角色 (如admin, editor, viewer)
@Column({ type: 'simple-array', nullable: true })
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
@Column({ default: false })
isSuper: boolean; // 超级管理员
@Column({ default: false })
isAdmin: boolean; // 管理员
@Column({ default: true })
isActive: boolean; // 用户是否启用
}

View File

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

View File

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

55
src/enums/base.enum.ts Normal file
View File

@ -0,0 +1,55 @@
export enum ProductStatus {
PUBLISH = 'publish', // 已发布
DRAFT = 'draft', // 草稿
PENDING = 'pending', // 待审核
PRIVATE = 'private', // 私有
TRASH = 'trash', // 回收站
AUTO_DRAFT = 'auto-draft', // 自动草稿
FUTURE = 'future', // 定时发布
INHERIT = 'inherit', // 继承状态
}
export enum ProductType {
SIMPLE = 'simple',
VARIABLE = 'variable',
}
export enum PurchaseOrderStatus {
DRAFT = 'draft',
SUBMITTED = 'submitted',
RECEIVED = 'received',
}
export enum StockRecordOperationType {
IN = 'in',
OUT = 'out',
}
export enum OrderStatus {
PENDING = 'pending',
PROCESSING = 'processing',
COMPLETED = 'completed',
ON_HOLD = 'on-hold',
CANCEL = 'cancelled',
REFUNDED = 'refunded',
FAILED = 'failed',
DRAFT = 'draft',
}
export enum ErpOrderStatus {
PENDING = 'pending', // 待确认
PROCESSING = 'processing', //待发货
COMPLETED = 'completed', //已完成
CANCEL = 'cancelled', //已取消
REFUNDED = 'refunded', //已退款
FAILED = 'failed', //失败
AFTER_SALE_PROCESSING = 'after_sale_pending', // 售后处理中
PENDING_RESHIPMENT = 'pending_reshipment', // 待补发
PENDING_REFUND = 'pending_refund', // 待退款
}
export enum ShipmentType {
CANADAPOST = 'canadapost',
FREIGHTCOM = 'freightcom',
}

View File

@ -0,0 +1,13 @@
import { Catch } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Catch()
export class DefaultErrorFilter {
async catch(err: Error, ctx: Context) {
// 所有的未分类错误会到这里
return {
success: false,
message: err.message,
};
}
}

View File

@ -0,0 +1,10 @@
import { Catch, httpError, MidwayHttpError } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Catch(httpError.NotFoundError)
export class NotFoundFilter {
async catch(err: MidwayHttpError, ctx: Context) {
// 404 错误会到这里
ctx.redirect('/404.html');
}
}

23
src/interface.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* @description User-Service parameters
*/
export interface IUserOptions {
uid: number;
}
export interface WpSite {
id: string;
wpApiUrl: string;
consumerKey: string;
consumerSecret: string;
siteName: string;
email: string;
emailPswd: string;
}
export interface PaginationParams {
current?: number; // 当前页码
pageSize?: number; // 每页数量
sorter?: any;
filter?: any;
}

View File

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

View File

@ -0,0 +1,22 @@
import { FORMAT, ILogger, Inject, Logger } from '@midwayjs/core';
import { IJob, Job } from '@midwayjs/cron';
import { LogisticsService } from '../service/logistics.service';
@Job({
cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,
start: true,
runOnInit: true,
})
export class SyncShipmentJob implements IJob {
@Logger()
logger: ILogger;
@Inject()
logisticsService: LogisticsService;
onTick() {
this.logisticsService.syncShipmentStatus();
this.logisticsService.syncShipment();
}
onComplete?(result: any) {}
}

View File

@ -0,0 +1,68 @@
// src/middleware/auth.middleware.ts
import {
IMiddleware,
Middleware,
Inject,
NextFunction,
httpError,
} from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
import { JwtService } from '@midwayjs/jwt'; // 引入 JwtService 类型
@Middleware()
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
@Inject()
jwtService: JwtService; // 注入 JwtService 实例
// 白名单配置
whiteList = [
'/user/login',
'/webhook/woocommerce',
'/logistics/getTrackingNumber',
'/logistics/getListByTrackingId',
];
match(ctx: Context) {
return !this.isWhiteListed(ctx);
}
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 判断下有没有校验信息
if (!ctx.headers['authorization']) {
throw new httpError.UnauthorizedError();
}
// 从 header 上获取校验信息
const parts = ctx.get('authorization').trim().split(' ');
if (parts.length !== 2) {
throw new httpError.UnauthorizedError();
}
const [scheme, token] = parts;
if (/^Bearer$/i.test(scheme)) {
try {
//jwt.verify方法验证token是否有效
await this.jwtService.verify(token, {
complete: true,
});
} catch (error) {
throw new httpError.UnauthorizedError();
}
await next();
}
};
}
static getName(): string {
return 'authMiddleware';
}
static getPriority(): number {
return 0;
}
isWhiteListed(ctx: Context): boolean {
return this.whiteList.includes(ctx.path);
}
}

View File

@ -0,0 +1,27 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
ctx.logger.info(
`Report in "src/middleware/report.middleware.ts", rt = ${
Date.now() - startTime
}ms`
);
// 返回给上一个中间件的结果
return result;
};
}
static getName(): string {
return 'report';
}
}

View File

@ -0,0 +1,171 @@
import { Provide } from '@midwayjs/core';
import { Config } from '@midwayjs/decorator';
import axios from 'axios';
import { parseStringPromise, Builder } from 'xml2js';
import { ShipmentType } from '../enums/base.enum';
@Provide()
export class CanadaPostService {
@Config('canadaPost.url')
url;
@Config('canadaPost.username')
username;
@Config('canadaPost.password')
password;
@Config('canadaPost.customerNumber')
customerNumber;
@Config('canadaPost.contractId')
contractId;
private getAuth() {
return {
username: this.username,
password: this.password,
};
}
private getHeaders(accept = 'application/vnd.cpc.shipment-v8+xml') {
return {
Accept: accept,
'Content-Type': accept,
'Accept-language': 'en-CA',
};
}
private async parseXML(xml: string) {
return await parseStringPromise(xml, { explicitArray: false });
}
private buildXML(data: any, rootName?: string, namespace?: string): string {
const builder = new Builder({
headless: false,
xmldec: { version: '1.0', encoding: 'UTF-8' },
});
// 如果指定了根节点和命名空间
if (rootName && namespace) {
const xmlObj = {
[rootName]: {
$: { xmlns: namespace },
...data,
},
};
return builder.buildObject(xmlObj);
}
// 默认直接构建(用于 createShipment 这类已有完整结构)
return builder.buildObject(data);
}
private normalizeCanadaPostRates(rawData: any) {
const services = rawData['price-quotes']?.['price-quote'];
if (!services) return [];
const list = Array.isArray(services) ? services : [services];
return list.map(s => ({
carrier_name: 'Canada Post',
service_name: s['service-name'],
service_id: s['service-code'],
total: {
value: 100 * parseFloat(s['price-details']['due']),
currency: 'CAD',
},
transit_time_days: s['service-standard']?.['expected-transit-time'],
type: ShipmentType.CANADAPOST,
}));
}
/** 获取运费估价 */
async getRates(rateRequest: any) {
const xmlBody = this.buildXML(
rateRequest,
'mailing-scenario',
'http://www.canadapost.ca/ws/ship/rate-v4'
);
const url = `${this.url}/rs/ship/price`;
const res = await axios.post(url, xmlBody, {
auth: this.getAuth(),
headers: this.getHeaders('application/vnd.cpc.ship.rate-v4+xml'),
});
return this.normalizeCanadaPostRates(await this.parseXML(res.data));
}
private normalizeCanadaPostShipment(rawData: any) {
const shipment = rawData['shipment-info'];
return {
id: shipment['shipment-id'],
tracking_provider: 'Canada Post',
unique_id: shipment['shipment-id'],
state: 'waiting-for-transit',
primary_tracking_number: shipment['tracking-pin'],
tracking_url: `https://www.canadapost-postescanada.ca/track-reperage/en#/details/${shipment['tracking-pin']}`,
labels: [
{
url: shipment['links']['link']?.find(
link => link['$']['rel'] === 'label'
)?.['$']['href'],
},
],
};
}
/** 创建运单 */
async createShipment(shipmentRequest: any) {
const xmlBody = this.buildXML(
shipmentRequest,
'shipment',
'http://www.canadapost.ca/ws/shipment-v8'
);
const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment`;
const res = await axios.post(url, xmlBody, {
auth: this.getAuth(),
headers: this.getHeaders('application/vnd.cpc.shipment-v8+xml'),
});
return this.normalizeCanadaPostShipment(await this.parseXML(res.data));
}
/** 查询运单 */
async getShipment(pin: string) {
const url = `${this.url}/vis/track/pin/${pin}/summary`;
const res = await axios.get(url, {
auth: this.getAuth(),
headers: this.getHeaders('application/vnd.cpc.track-v2+xml'),
});
const shipment = await this.parseXML(res.data);
const eventType =
shipment['tracking-summary']['pin-summary']['event-type']?.toUpperCase();
return {
shipment: {
state:
eventType === 'INDUCTION'
? 'waiting-for-transit'
: eventType === 'DELIVERED'
? 'delivered'
: 'in-transit',
},
};
}
/** 取消运单 */
async cancelShipment(shipmentId: string) {
const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment/${shipmentId}`;
const res = await axios.delete(url, {
auth: this.getAuth(),
headers: this.getHeaders('application/vnd.cpc.shipment-v8+xml'),
});
console.log(res);
return res.status === 200 || res.status === 204;
}
}

View File

@ -0,0 +1,142 @@
import { Provide, Config, Inject, sleep } from '@midwayjs/decorator';
import axios, { AxiosRequestConfig } from 'axios';
import { ShippingDetailsDTO } from '../dto/freightcom.dto';
import { Service } from '../entity/service.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { instanceToPlain } from 'class-transformer';
import { LogisticsService } from './logistics.service';
import { ShipmentType } from '../enums/base.enum';
@Provide()
export class FreightcomService {
@Config('freightcom.url') private apiUrl: string;
@Config('freightcom.token') private token: string;
@Inject() logger;
@InjectEntityModel(Service)
serviceModel: Repository<Service>;
@Inject()
logisticsService: LogisticsService;
async syncServices() {
const config: AxiosRequestConfig = {
method: 'GET',
url: `${this.apiUrl}/services`,
headers: {
Authorization: `${this.token}`,
},
};
const response = await axios.request(config);
const services: Service[] = response.data;
return await this.serviceModel.upsert(services, ['id']);
}
generateCurlCommand(config) {
let curl = `curl -X ${config.method.toUpperCase()}`;
curl += ` '${config.url}'`;
if (config.headers) {
for (const key in config.headers) {
curl += ` -H '${key}: ${config.headers[key]}'`;
}
}
if (config.data) {
curl += ` --data '${JSON.stringify(config.data)}'`;
}
return curl;
}
// 请求费率估算
async getRateEstimate(shippingDetails: ShippingDetailsDTO) {
const services = await this.logisticsService.getActiveServices();
const response = await axios.request({
url: `${this.apiUrl}/rate`,
method: 'POST',
data: {
services,
details: instanceToPlain(shippingDetails),
},
headers: {
Authorization: `${this.token}`,
'Content-Type': 'application/json',
},
});
return response.data;
}
// 获取预估运费
async getRates(rate_id: string) {
const response = await axios.request({
url: `${this.apiUrl}/rate/${rate_id}`,
method: 'GET',
headers: {
Authorization: `${this.token}`,
},
});
return (
response.data?.rates?.map(v => ({
...v,
type: ShipmentType.FREIGHTCOM,
})) || []
);
}
// 创建运单
async createShipment(data) {
const response = await axios.request({
url: `${this.apiUrl}/shipment`,
method: 'POST',
data,
headers: {
Authorization: `${this.token}`,
},
});
return response.data;
}
// 查询运单详细信息
async getShipment(shipment_id: string) {
let { status, data } = await axios.request({
url: `${this.apiUrl}/shipment/${shipment_id}`,
method: 'GET',
headers: {
Authorization: `${this.token}`,
},
});
if (status === 400) {
throw new Error(data);
}
if (status === 202) {
await sleep(2000);
data = this.getShipment(shipment_id);
}
return data;
}
// 取消发货
async cancelShipment(shipment_id: string) {
const response = await axios.request({
url: `${this.apiUrl}/shipment/${shipment_id}`,
method: 'DELETE',
headers: {
Authorization: `${this.token}`,
},
});
return response.data;
}
//获取支付方式
async getPaymentMethods() {
const response = await axios.request({
url: `${this.apiUrl}/finance/payment-methods`,
method: 'GET',
headers: {
Authorization: `${this.token}`,
},
});
return response.data;
}
}

View File

@ -0,0 +1,553 @@
import { Config, Inject, Provide, sleep } from '@midwayjs/core';
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
import { Service } from '../entity/service.entity';
import { In, IsNull, Like, Repository } from 'typeorm';
import { ShippingAddress } from '../entity/shipping_address.entity';
// import { ShipmentBookDTO } from '../dto/logistics.dto';
import { Order } from '../entity/order.entity';
import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity';
import { OrderShipment } from '../entity/order_shipment.entity';
import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto';
import {
ErpOrderStatus,
OrderStatus,
ShipmentType,
StockRecordOperationType,
} from '../enums/base.enum';
import { generateUniqueId } from '../utils/helper.util';
import { FreightcomService } from './freightcom.service';
import { StockRecord } from '../entity/stock_record.entity';
import { Stock } from '../entity/stock.entity';
import { plainToClass } from 'class-transformer';
import { WPService } from './wp.service';
import { WpSite } from '../interface';
import { Product } from '../entity/product.entty';
import { ShippingDetailsDTO } from '../dto/freightcom.dto';
import { CanadaPostService } from './canadaPost.service';
import { OrderItem } from '../entity/order_item.entity';
@Provide()
export class LogisticsService {
@InjectEntityModel(Service)
serviceModel: Repository<Service>;
@InjectEntityModel(ShippingAddress)
shippingAddressModel: Repository<ShippingAddress>;
@InjectEntityModel(Stock)
stockModel: Repository<Stock>;
@InjectEntityModel(Order)
orderModel: Repository<Order>;
@InjectEntityModel(Shipment)
shipmentModel: Repository<Shipment>;
@InjectEntityModel(ShipmentItem)
shipmentItemModel: Repository<ShipmentItem>;
@InjectEntityModel(OrderShipment)
orderShipmentModel: Repository<OrderShipment>;
@InjectEntityModel(OrderItem)
orderItem: Repository<OrderItem>;
@Inject()
freightcomService: FreightcomService;
@Inject()
canadaPostService: CanadaPostService;
@Inject()
wpService: WPService;
@Inject()
dataSourceManager: TypeORMDataSourceManager;
@Config('wpSite')
sites: WpSite[];
geSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
async getServiceList(param: QueryServiceDTO) {
const { pageSize, current, carrier_name, isActive } = param;
const where: Record<string, any> = {};
if (carrier_name) where.carrier_name = Like(`%${carrier_name}%`);
if (isActive !== undefined) where.isActive = isActive;
const [items, total] = await this.serviceModel.findAndCount({
where,
skip: (current - 1) * pageSize,
take: pageSize,
});
return { items, total, current, pageSize };
}
async toggleServiceActive(id: string, isActive: boolean) {
const service = await this.serviceModel.findOne({ where: { id } });
if (!service) {
throw new Error('服务商不存在');
}
service.isActive = isActive;
return this.serviceModel.save(service);
}
async getActiveServices() {
const services = await this.serviceModel.find({
where: { isActive: true },
});
if (!services) {
return [];
}
return services.map(service => service.id);
}
async createShippingAddress(shippingAddress: ShippingAddress) {
return await this.shippingAddressModel.save(shippingAddress);
}
async updateShippingAddress(id: number, shippingAddress: ShippingAddress) {
const address = await this.shippingAddressModel.findOneBy({ id });
if (!address) {
throw new Error(`发货地址 ID ${id} 不存在`);
}
await this.shippingAddressModel.update(id, shippingAddress);
return await this.shippingAddressModel.findOneBy({ id });
}
async getShippingAddressList() {
return await this.shippingAddressModel.find();
}
async delShippingAddress(id: number) {
const address = await this.shippingAddressModel.findOneBy({ id });
if (!address) {
throw new Error(`发货地址 ID ${id} 不存在`);
}
const result = await this.shippingAddressModel.delete(id);
return result.affected > 0;
}
// async saveTracking(
// orderId: number,
// shipment: Record<string, any>,
// data: ShipmentBookDTO
// ) {
// const order = await this.orderModel.findOneBy({ id: orderId });
// const orderTracking = this.orderTrackingModel.save({
// orderId: String(orderId),
// siteId: order.siteId,
// externalOrderId: order.externalOrderId,
// });
// }
async getRateList(details: ShippingDetailsDTO) {
details.destination.address.country = 'CA';
details.origin.address.country = 'CA';
const { request_id } = await this.freightcomService.getRateEstimate(
details
);
const rateRequest = {
'customer-number': this.canadaPostService.customerNumber,
'parcel-characteristics': {
weight: details?.packaging_properties?.packages?.reduce(
(cur, next) => cur + (next?.measurements?.weight?.value || 0),
0
),
},
'origin-postal-code': details.origin.address.postal_code.replace(
/\s/g,
''
),
destination: {
domestic: {
'postal-code': details.destination.address.postal_code.replace(
/\s/g,
''
),
},
},
};
const canadaPostRates = await this.canadaPostService.getRates(rateRequest);
await sleep(3000);
const rates = await this.freightcomService.getRates(request_id);
return [...rates, ...canadaPostRates];
}
async createShipment(orderId: number, data: ShipmentBookDTO, userId: number) {
const order = await this.orderModel.findOneBy({ id: orderId });
if (!order) {
throw new Error('订单不存在');
}
if (
order.orderStatus !== ErpOrderStatus.PROCESSING &&
order.orderStatus !== ErpOrderStatus.PENDING_RESHIPMENT
) {
throw new Error('订单状态不正确 ');
}
for (const item of data?.sales) {
const stock = await this.stockModel.findOne({
where: {
stockPointId: data.stockPointId,
productSku: item.sku,
},
});
if (!stock || stock.quantity < item.quantity)
throw new Error(item.name + '库存不足');
}
let shipment: Shipment;
if (data.service_type === ShipmentType.FREIGHTCOM) {
const uuid = generateUniqueId();
data.details.reference_codes = [String(orderId)];
const { id } = await this.freightcomService.createShipment({
unique_id: uuid,
payment_method_id: data.payment_method_id,
service_id: data.service_id,
details: data.details,
});
const service = await this.serviceModel.findOneBy({
id: data.service_id,
});
shipment = {
id,
unique_id: uuid,
tracking_provider: service?.carrier_name || '',
};
} else if (data.service_type === ShipmentType.CANADAPOST) {
const shipmentRequest = {
'transmit-shipment': true,
'requested-shipping-point':
data.details.origin.address.postal_code.replace(/\s/g, ''),
'delivery-spec': {
'service-code': data.service_id,
sender: {
name: data.details.origin.name,
company: data.details.origin.name,
'contact-phone': data.details.origin.phone_number.number,
'address-details': {
'address-line-1': data.details.origin.address.address_line_1,
city: data.details.origin.address.city,
'prov-state': data.details.origin.address.region,
'postal-zip-code':
data.details.origin.address.postal_code.replace(/\s/g, ''),
'country-code': data.details.origin.address.country,
},
},
destination: {
name: data.details.destination.contact_name,
company: data.details.destination.name,
'address-details': {
'address-line-1': data.details.destination.address.address_line_1,
city: data.details.destination.address.city,
'prov-state': data.details.destination.address.region,
'postal-zip-code':
data.details.destination.address.postal_code.replace(/\s/g, ''),
'country-code': data.details.destination.address.country,
},
},
'parcel-characteristics': {
weight: data.details.packaging_properties.packages?.reduce(
(cur, next) => cur + (next?.measurements?.weight?.value || 0),
0
),
},
preferences: {
'show-packing-instructions': true,
},
'settlement-info': {
'contract-id': this.canadaPostService.contractId,
'intended-method-of-payment': 'CreditCard',
},
},
};
shipment = await this.canadaPostService.createShipment(shipmentRequest);
}
shipment.type = data.service_type;
const dataSource = this.dataSourceManager.getDataSource('default');
return dataSource.transaction(async manager => {
const productRepo = manager.getRepository(Product);
const shipmentRepo = manager.getRepository(Shipment);
const shipmentItemRepo = manager.getRepository(ShipmentItem);
const orderShipmentRepo = manager.getRepository(OrderShipment);
const stockRecordRepo = manager.getRepository(StockRecord);
const stockRepo = manager.getRepository(Stock);
const orderRepo = manager.getRepository(Order);
await shipmentRepo.save(shipment);
await this.getShipment(shipment.id);
const shipmentItems = [];
for (const item of data?.sales) {
const product = await productRepo.findOne({ where: { sku: item.sku } });
shipmentItems.push({
shipment_id: shipment.id,
productId: product.id,
name: product.name,
sku: item.sku,
quantity: item.quantity,
});
const stock = await stockRepo.findOne({
where: {
stockPointId: data.stockPointId,
productSku: item.sku,
},
});
stock.quantity -= item.quantity;
await stockRepo.save(stock);
await stockRecordRepo.save({
stockPointId: data.stockPointId,
productSku: item.sku,
operationType: StockRecordOperationType.OUT,
quantityChange: item.quantity,
operatorId: userId,
note: `订单${[orderId, ...data.orderIds].join(',')} 发货`,
});
}
await shipmentItemRepo.save(shipmentItems);
await orderShipmentRepo.save({
order_id: orderId,
shipment_id: shipment.id,
stockPointId: data.stockPointId,
});
for (const orderId of data?.orderIds) {
await orderShipmentRepo.save({
order_id: orderId,
shipment_id: shipment.id,
stockPointId: data.stockPointId,
});
const order = await orderRepo.findOneBy({ id: orderId });
order.orderStatus = ErpOrderStatus.COMPLETED;
order.status = OrderStatus.COMPLETED;
await orderRepo.save(order);
}
order.orderStatus = ErpOrderStatus.COMPLETED;
order.status = OrderStatus.COMPLETED;
await orderRepo.save(order);
});
}
async syncShipment() {
try {
const shipments = await this.shipmentModel.find({
where: {
primary_tracking_number: IsNull(),
},
});
if (!shipments) return;
for (const shipment of shipments) {
await this.getShipment(shipment.id);
}
} catch (error) {
console.log('syncShipment', error);
}
}
async syncShipmentStatus() {
const shipments = await this.shipmentModel.find({
where: {
state: In([
'draft',
'waiting-for-transit',
'waiting-for-scheduling',
'in-transit',
]),
},
});
if (!shipments) return;
for (const item of shipments) {
try {
let res;
if (item.type === ShipmentType.FREIGHTCOM) {
res = await this.freightcomService.getShipment(item.id);
} else if (item.type === ShipmentType.CANADAPOST) {
res = await this.canadaPostService.getShipment(
item.primary_tracking_number
);
res.shipment.id = item.id;
}
if (!res) return;
const shipment = plainToClass(Shipment, res.shipment);
this.shipmentModel.save(shipment);
} catch (error) {
console.log('syncShipmentStatus error');
}
}
}
async getShipment(id: string) {
const orderShipments = await this.orderShipmentModel.find({
where: { shipment_id: id },
});
if (!orderShipments || orderShipments.length === 0) return;
const oldShipment = await this.shipmentModel.findOneBy({ id });
let res;
if (oldShipment.type === ShipmentType.FREIGHTCOM) {
res = await this.freightcomService.getShipment(id);
} else if (oldShipment.type === ShipmentType.CANADAPOST) {
res = await this.canadaPostService.getShipment(
oldShipment.primary_tracking_number
);
res.shipment.id = oldShipment.id;
}
if (!res) return;
const shipment = plainToClass(Shipment, res.shipment);
await this.shipmentModel.save(shipment);
for (const orderShipment of orderShipments) {
const order = await this.orderModel.findOneBy({
id: orderShipment.order_id,
});
const site = this.geSite(order.siteId);
await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.COMPLETED,
});
await this.wpService.createShipment(site, order.externalOrderId, {
tracking_number: shipment.primary_tracking_number,
tracking_provider: shipment?.rate?.carrier_name,
});
}
}
async delShipment(id: string, userId: number) {
const shipment = await this.shipmentModel.findOneBy({ id });
if (!shipment) throw new Error('物流不存在');
if (shipment.type === ShipmentType.FREIGHTCOM) {
await this.freightcomService.cancelShipment(shipment.id);
} else if (shipment.type === ShipmentType.CANADAPOST) {
await this.canadaPostService.cancelShipment(shipment.id);
}
const dataSource = this.dataSourceManager.getDataSource('default');
return dataSource.transaction(async manager => {
const shipmentRepo = manager.getRepository(Shipment);
const shipmentItemRepo = manager.getRepository(ShipmentItem);
const orderShipmentRepo = manager.getRepository(OrderShipment);
const stockRecordRepo = manager.getRepository(StockRecord);
const stockRepo = manager.getRepository(Stock);
const orderRepo = manager.getRepository(Order);
const orderShipments = await orderShipmentRepo.findBy({
shipment_id: id,
});
const shipmentItems = await shipmentItemRepo.findBy({ shipment_id: id });
await shipmentRepo.delete({ id });
await shipmentItemRepo.delete({ shipment_id: id });
await orderShipmentRepo.delete({ shipment_id: id });
for (const item of shipmentItems) {
const stock = await stockRepo.findOne({
where: {
stockPointId: orderShipments[0].stockPointId,
productSku: item.sku,
},
});
stock.quantity += item.quantity;
await stockRepo.save(stock);
await stockRecordRepo.save({
stockPointId: orderShipments[0].stockPointId,
productSku: item.sku,
operationType: StockRecordOperationType.IN,
quantityChange: item.quantity,
operatorId: userId,
note: `订单${orderShipments.map(v => v.order_id).join(',')} 取消发货`,
});
}
await orderRepo.update(
{ id: In(orderShipments.map(v => v.order_id)) },
{
orderStatus: ErpOrderStatus.PENDING_RESHIPMENT,
}
);
});
}
async getTrackingNumber(number: string) {
return await this.shipmentModel.find({
where: {
primary_tracking_number: Like(`%${number}%`),
},
});
}
async getListByTrackingId(shipment_id: string) {
const shipmentItem = await this.shipmentItemModel.find({
where: {
shipment_id,
},
});
const orderShipment = await this.orderShipmentModel.find({
where: {
shipment_id,
},
});
const orderItem = await this.orderItem.find({
where: {
id: In(orderShipment.map(v => v.order_id)),
},
});
return {
shipmentItem,
orderItem,
};
}
async getList(param: Record<string, any>) {
const {
pageSize = 10,
current = 1,
primary_tracking_number,
stockPointId,
} = param;
console.log(pageSize, current, primary_tracking_number, stockPointId);
const offset = pageSize * (current - 1);
const values: any[] = [];
let whereClause = 'WHERE 1=1';
if (primary_tracking_number) {
whereClause += ' AND s.primary_tracking_number LIKE ?';
values.push(`%${primary_tracking_number}%`);
}
if (stockPointId) {
whereClause += ' AND os.stockPointId = ?';
values.push(stockPointId);
}
const sql = `
SELECT s.*, sp.name
FROM shipment s
LEFT JOIN order_shipment os ON s.id = os.shipment_id
LEFT JOIN stock_point sp ON os.stockPointId = sp.id
${whereClause}
GROUP BY s.id
ORDER BY s.createdAt DESC
LIMIT ?, ?
`;
values.push(offset, Number(pageSize));
const items = await this.serviceModel.query(sql, values);
// 单独计算总数
const countSql = `
SELECT COUNT(DISTINCT s.id) as total
FROM shipment s
LEFT JOIN order_shipment os ON s.id = os.shipment_id
${whereClause}
`;
const countResult = await this.serviceModel.query(
countSql,
values.slice(0, values.length - 2)
);
const total = countResult[0]?.total || 0;
return { items, total, current, pageSize };
}
}

1189
src/service/order.service.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,424 @@
import { Provide } from '@midwayjs/core';
import { And, In, IsNull, Like, Not, Repository } from 'typeorm';
import { Product } from '../entity/product.entty';
import { Category } from '../entity/category.entity';
import { paginate } from '../utils/paginate.util';
import { PaginationParams } from '../interface';
import {
CreateCategoryDTO,
CreateFlavorsDTO,
CreateProductDTO,
CreateStrengthDTO,
UpdateCategoryDTO,
UpdateFlavorsDTO,
UpdateProductDTO,
UpdateStrengthDTO,
} from '../dto/product.dto';
import {
CategoryPaginatedResponse,
FlavorsPaginatedResponse,
ProductPaginatedResponse,
StrengthPaginatedResponse,
} from '../dto/reponse.dto';
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';
@Provide()
export class ProductService {
@InjectEntityModel(Product)
productModel: Repository<Product>;
@InjectEntityModel(Category)
categoryModel: Repository<Category>;
@InjectEntityModel(Strength)
strengthModel: Repository<Strength>;
@InjectEntityModel(Flavors)
flavorsModel: Repository<Flavors>;
@InjectEntityModel(WpProduct)
wpProductModel: Repository<WpProduct>;
@InjectEntityModel(Variation)
variationModel: Repository<Variation>;
async findProductsByName(name: string): Promise<Product[]> {
const where: any = {};
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
if (nameFilter.length > 0) {
const nameConditions = nameFilter.map(word => Like(`%${word}%`));
where.name = And(...nameConditions);
}
where.sku = Not(IsNull());
// 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
return this.productModel.find({
where,
take: 50,
});
}
async findProductBySku(sku: string): Promise<Product> {
return this.productModel.findOne({
where: {
sku,
},
});
}
async getProductList(
pagination: PaginationParams,
name?: string,
categoryId?: 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.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',
]);
// 模糊搜索 name支持多个关键词
nameFilter.forEach((word, index) => {
qb.andWhere(`product.name LIKE :name${index}`, {
[`name${index}`]: `%${word}%`,
});
});
// 分类过滤
if (categoryId) {
qb.andWhere('product.categoryId = :categoryId', { categoryId });
}
// 分页
qb.skip((pagination.current - 1) * pagination.pageSize).take(
pagination.pageSize
);
// 执行查询
const items = await qb.getRawMany();
const total = await qb.getCount();
return {
items,
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,
},
});
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}`;
return await this.productModel.save(product);
}
async updateProduct(
id: number,
updateProductDTO: UpdateProductDTO
): Promise<Product> {
// 确认产品是否存在
const product = await this.productModel.findOneBy({ id });
if (!product) {
throw new Error(`产品 ID ${id} 不存在`);
}
// 更新产品
await this.productModel.update(id, updateProductDTO);
// 返回更新后的产品
return await this.productModel.findOneBy({ id });
}
async deleteProduct(id: number): Promise<boolean> {
// 检查产品是否存在
const product = await this.productModel.findOneBy({ id });
if (!product) {
throw new Error(`产品 ID ${id} 不存在`);
}
const productSku = product.sku;
// 查询 wp_product 表中是否存在与该 SKU 关联的产品
const wpProduct = await this.wpProductModel
.createQueryBuilder('wp_product')
.where('JSON_CONTAINS(wp_product.constitution, :sku)', {
sku: JSON.stringify({ sku: productSku }),
})
.getOne();
if (wpProduct) {
throw new Error('无法删除请先删除关联的WP产品');
}
const variation = await this.variationModel
.createQueryBuilder('variation')
.where('JSON_CONTAINS(variation.constitution, :sku)', {
sku: JSON.stringify({ sku: productSku }),
})
.getOne();
if (variation) {
console.log(variation);
throw new Error('无法删除请先删除关联的WP变体');
}
// 删除产品
const result = await this.productModel.delete(id);
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 };
if (id) where.id = Not(id);
const count = await this.categoryModel.count({
where,
});
return count > 0;
}
async getCategoryList(
pagination: PaginationParams,
name?: string
): Promise<CategoryPaginatedResponse> {
const where: any = {};
if (name) {
where.name = Like(`%${name}%`);
}
return await paginate(this.categoryModel, { pagination, where });
}
async getCategoryAll(): Promise<CategoryPaginatedResponse> {
return await this.categoryModel.find();
}
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);
}
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 deleteCategory(id: number): Promise<boolean> {
// 检查产品是否存在
const category = await this.categoryModel.findOneBy({ id });
if (!category) {
throw new Error(`产品分类 ID ${id} 不存在`);
}
// 删除产品
const result = await this.categoryModel.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
): Promise<FlavorsPaginatedResponse> {
const where: any = {};
if (name) {
where.name = Like(`%${name}%`);
}
return await paginate(this.flavorsModel, { pagination, where });
}
async getFlavorsAll(): Promise<FlavorsPaginatedResponse> {
return await this.flavorsModel.find();
}
async createFlavors(createFlavorsDTO: CreateFlavorsDTO): Promise<Flavors> {
const { name, unique_key } = createFlavorsDTO;
const flavors = new Flavors();
flavors.name = name;
flavors.unique_key = unique_key;
return await this.flavorsModel.save(flavors);
}
async updateFlavors(id: number, updateFlavors: UpdateFlavorsDTO) {
// 确认产品是否存在
const flavors = await this.flavorsModel.findOneBy({ id });
if (!flavors) {
throw new Error(`口味 ID ${id} 不存在`);
}
// 更新产品
await this.flavorsModel.update(id, updateFlavors);
// 返回更新后的产品
return await this.flavorsModel.findOneBy({ id });
}
async deleteFlavors(id: number): Promise<boolean> {
// 检查产品是否存在
const flavors = await this.flavorsModel.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;
}
async hasStrength(name: string, id?: string): Promise<boolean> {
const where: any = { name };
if (id) where.id = Not(id);
const count = await this.strengthModel.count({
where,
});
return count > 0;
}
async getStrengthList(
pagination: PaginationParams,
name?: string
): Promise<StrengthPaginatedResponse> {
const where: any = {};
if (name) {
where.name = Like(`%${name}%`);
}
return await paginate(this.strengthModel, { pagination, where });
}
async getStrengthAll(): Promise<StrengthPaginatedResponse> {
return await this.strengthModel.find();
}
async createStrength(
createStrengthDTO: CreateStrengthDTO
): Promise<Strength> {
const { name, unique_key } = createStrengthDTO;
const strength = new Strength();
strength.name = name;
strength.unique_key = unique_key;
return await this.strengthModel.save(strength);
}
async updateStrength(id: number, updateStrength: UpdateStrengthDTO) {
// 确认产品是否存在
const strength = await this.strengthModel.findOneBy({ id });
if (!strength) {
throw new Error(`口味 ID ${id} 不存在`);
}
// 更新产品
await this.strengthModel.update(id, updateStrength);
// 返回更新后的产品
return await this.strengthModel.findOneBy({ id });
}
async deleteStrength(id: number): Promise<boolean> {
// 检查产品是否存在
const strength = await this.strengthModel.findOneBy({ id });
if (!strength) {
throw new Error(`口味 ID ${id} 不存在`);
}
// 删除产品
const result = await this.flavorsModel.delete(id);
return result.affected > 0; // `affected` 表示删除的行数
}
async batchSetSku(skus: { productId: number; sku: string }[]) {
// 提取所有 sku
const skuList = skus.map(item => item.sku);
// 检查是否存在重复 sku
const existingProducts = await this.productModel.find({
where: { sku: In(skuList) },
});
if (existingProducts.length > 0) {
const existingSkus = existingProducts.map(product => product.sku);
throw new Error(`以下 SKU 已存在: ${existingSkus.join(', ')}`);
}
// 遍历检查产品 ID 是否存在,并更新 sku
for (const { productId, sku } of skus) {
const product = await this.productModel.findOne({
where: { id: productId },
});
if (!product) {
throw new Error(`产品 ID '${productId}' 不存在`);
}
product.sku = sku;
await this.productModel.save(product);
}
return `成功更新 ${skus.length} 个 sku`;
}
}

View File

@ -0,0 +1,904 @@
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Order } from '../entity/order.entity';
import { Provide } from '@midwayjs/core';
import { Repository } from 'typeorm';
import { OrderStatisticsParams } from '../dto/statistics.dto';
import { OrderItem } from '../entity/order_item.entity';
import dayjs = require('dayjs');
@Provide()
export class StatisticsService {
@InjectEntityModel(Order)
orderRepository: Repository<Order>;
@InjectEntityModel(OrderItem)
orderItemRepository: Repository<OrderItem>;
async getOrderStatistics(params: OrderStatisticsParams) {
const { startDate, endDate, siteId } = params;
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : [];
const start = dayjs(startDate).format('YYYY-MM-DD');
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
let sql = `
WITH first_order AS (
SELECT customer_email, MIN(date_paid) AS first_purchase_date
FROM \`order\`
GROUP BY customer_email
),
daily_orders AS (
SELECT
o.id AS order_id,
DATE(o.date_paid) AS order_date,
o.customer_email,
o.total,
o.source_type,
o.utm_source,
o.siteId,
CASE
WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase'
ELSE 'repeat_purchase'
END AS purchase_type,
CASE
WHEN o.source_type = 'utm' AND o.utm_source = 'google' THEN 'cpc'
ELSE 'non_cpc'
END AS order_type,
MAX(CASE WHEN oi.name LIKE '%zyn%' THEN 'zyn' ELSE 'non_zyn' END) AS zyn_type,
MAX(CASE WHEN oi.name LIKE '%yoone%' THEN 'yoone' ELSE 'non_yoone' END) AS yoone_type,
MAX(CASE WHEN oi.name LIKE '%zex%' THEN 'zex' ELSE 'non_zex' END) AS zex_type
FROM \`order\` o
LEFT JOIN first_order f ON o.customer_email = f.customer_email
LEFT JOIN order_item oi ON o.id = oi.orderId
WHERE o.date_paid IS NOT NULL AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
AND o.status IN('processing','completed')
`;
if (siteId) sql += ` AND o.siteId=${siteId}`;
sql += `
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
),
order_sales_summary AS (
SELECT
orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
FROM order_sale
GROUP BY orderId
),
order_items_summary AS (
SELECT
orderId,
SUM(CASE WHEN name LIKE '%zyn%' THEN total + total_tax ELSE 0 END) AS zyn_amount,
SUM(CASE WHEN name LIKE '%yoone%' THEN total + total_tax ELSE 0 END) AS yoone_amount,
SUM(CASE WHEN name LIKE '%zex%' THEN total + total_tax ELSE 0 END) AS zex_amount,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_G_amount,
SUM(CASE WHEN name LIKE '%yoone%' AND name NOT LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_S_amount,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN total + total_tax ELSE 0 END) AS yoone_3_amount,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN total + total_tax ELSE 0 END) AS yoone_6_amount,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN total + total_tax ELSE 0 END) AS yoone_9_amount,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN total + total_tax ELSE 0 END) AS yoone_12_amount,
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN total + total_tax ELSE 0 END) AS yoone_15_amount
FROM order_item
GROUP BY orderId
),
daily_totals AS (
SELECT order_date, SUM(total) AS total_amount,
SUM(CASE WHEN siteId = 1 THEN total ELSE 0 END) AS togo_total_amount,
SUM(CASE WHEN siteId = 2 THEN total ELSE 0 END) AS can_total_amount,
COUNT(DISTINCT order_id) AS total_orders,
COUNT(DISTINCT CASE WHEN siteId = 1 THEN order_id END) AS togo_total_orders,
COUNT(DISTINCT CASE WHEN siteId = 2 THEN order_id END) AS can_total_orders,
SUM(CASE WHEN purchase_type = 'first_purchase' THEN total ELSE 0 END) AS first_purchase_total,
SUM(CASE WHEN purchase_type = 'repeat_purchase' THEN total ELSE 0 END) AS repeat_purchase_total,
SUM(CASE WHEN order_type = 'cpc' THEN total ELSE 0 END) AS cpc_total,
SUM(CASE WHEN order_type = 'non_cpc' THEN total ELSE 0 END) AS non_cpc_total,
SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'cpc' THEN total ELSE 0 END) AS zyn_total,
SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zyn_total,
SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'cpc' THEN total ELSE 0 END) AS yoone_total,
SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_yoone_total,
SUM(CASE WHEN zex_type = 'zex' AND order_type = 'cpc' THEN total ELSE 0 END) AS zex_total,
SUM(CASE WHEN zex_type = 'zex' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zex_total,
SUM(CASE WHEN source_type = 'typein' AND purchase_type = 'first_purchase' THEN total ELSE 0 END) AS direct_first_total
FROM daily_orders
GROUP BY order_date
)
SELECT
d.order_date,
COUNT(DISTINCT CASE WHEN d.purchase_type = 'first_purchase' THEN d.order_id END) AS first_purchase_orders,
COUNT(DISTINCT CASE WHEN d.purchase_type = 'repeat_purchase' THEN d.order_id END) AS repeat_purchase_orders,
COUNT(DISTINCT CASE WHEN d.order_type = 'cpc' THEN d.order_id END) AS cpc_orders,
COUNT(DISTINCT CASE WHEN d.order_type = 'cpc' AND d.siteId = 1 THEN d.order_id END) AS togo_cpc_orders,
COUNT(DISTINCT CASE WHEN d.order_type = 'cpc' AND d.siteId = 2 THEN d.order_id END) AS can_cpc_orders,
COUNT(DISTINCT CASE WHEN d.order_type = 'non_cpc' THEN d.order_id END) AS non_cpc_orders,
COUNT(DISTINCT CASE WHEN d.order_type = 'non_cpc' AND d.siteId = 1 THEN d.order_id END) AS non_togo_cpc_orders,
COUNT(DISTINCT CASE WHEN d.order_type = 'non_cpc' AND d.siteId = 2 THEN d.order_id END) AS non_can_cpc_orders,
COUNT(DISTINCT CASE WHEN d.zyn_type = 'zyn' AND d.order_type = 'cpc' THEN d.order_id END) AS zyn_orders,
COUNT(DISTINCT CASE WHEN d.zyn_type = 'zyn' AND d.order_type = 'non_cpc' THEN d.order_id END) AS non_zyn_orders,
COUNT(DISTINCT CASE WHEN d.yoone_type = 'yoone' AND d.order_type = 'cpc' THEN d.order_id END) AS yoone_orders,
COUNT(DISTINCT CASE WHEN d.yoone_type = 'yoone' AND d.order_type = 'non_cpc' THEN d.order_id END) AS non_yoone_orders,
COUNT(DISTINCT CASE WHEN d.zex_type = 'zex' AND d.order_type = 'cpc' THEN d.order_id END) AS zex_orders,
COUNT(DISTINCT CASE WHEN d.zex_type = 'zex' AND d.order_type = 'non_cpc' THEN d.order_id END) AS non_zex_orders,
COUNT(DISTINCT CASE WHEN d.source_type = 'typein' AND d.purchase_type = 'first_purchase' THEN d.order_id END) AS direct_first_orders,
dt.total_orders,
dt.togo_total_orders,
dt.can_total_orders,
dt.total_amount,
dt.togo_total_amount,
dt.can_total_amount,
dt.first_purchase_total,
dt.repeat_purchase_total,
dt.cpc_total,
dt.non_cpc_total,
dt.zyn_total,
dt.non_zyn_total,
dt.yoone_total,
dt.non_yoone_total,
dt.zex_total,
dt.non_zex_total,
dt.direct_first_total,
COALESCE(SUM(os.zyn_quantity), 0) AS zyn_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.zyn_quantity ELSE 0 END) AS cpc_zyn_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.zyn_quantity ELSE 0 END) AS non_cpc_zyn_quantity,
COALESCE(SUM(os.yoone_quantity), 0) AS yoone_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_quantity ELSE 0 END) AS cpc_yoone_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_quantity ELSE 0 END) AS non_cpc_yoone_quantity,
COALESCE(SUM(os.yoone_G_quantity), 0) AS yoone_G_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_G_quantity ELSE 0 END) AS cpc_yoone_G_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_G_quantity ELSE 0 END) AS non_cpc_yoone_G_quantity,
COALESCE(SUM(os.yoone_S_quantity), 0) AS yoone_S_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_S_quantity ELSE 0 END) AS cpc_yoone_S_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_S_quantity ELSE 0 END) AS non_cpc_yoone_S_quantity,
COALESCE(SUM(os.yoone_3_quantity), 0) AS yoone_3_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_3_quantity ELSE 0 END) AS cpc_yoone_3_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_3_quantity ELSE 0 END) AS non_cpc_yoone_3_quantity,
COALESCE(SUM(os.yoone_6_quantity), 0) AS yoone_6_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_6_quantity ELSE 0 END) AS cpc_yoone_6_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_6_quantity ELSE 0 END) AS non_cpc_yoone_6_quantity,
COALESCE(SUM(os.yoone_9_quantity), 0) AS yoone_9_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_9_quantity ELSE 0 END) AS cpc_yoone_9_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_9_quantity ELSE 0 END) AS non_cpc_yoone_9_quantity,
COALESCE(SUM(os.yoone_12_quantity), 0) AS yoone_12_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_12_quantity ELSE 0 END) AS cpc_yoone_12_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_12_quantity ELSE 0 END) AS non_cpc_yoone_12_quantity,
COALESCE(SUM(os.yoone_15_quantity), 0) AS yoone_15_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.yoone_15_quantity ELSE 0 END) AS cpc_yoone_15_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.yoone_15_quantity ELSE 0 END) AS non_cpc_yoone_15_quantity,
COALESCE(SUM(os.zex_quantity), 0) AS zex_quantity,
SUM(CASE WHEN d.order_type = 'cpc' THEN os.zex_quantity ELSE 0 END) AS cpc_zex_quantity,
SUM(CASE WHEN d.order_type = 'non_cpc' THEN os.zex_quantity ELSE 0 END) AS non_cpc_zex_quantity,
COALESCE(SUM(oi.zyn_amount), 0) AS zyn_amount,
COALESCE(SUM(oi.yoone_amount), 0) AS yoone_amount,
COALESCE(SUM(oi.zex_amount), 0) AS zex_amount,
COALESCE(SUM(oi.yoone_G_amount), 0) AS yoone_G_amount,
COALESCE(SUM(oi.yoone_S_amount), 0) AS yoone_S_amount,
COALESCE(SUM(oi.yoone_3_amount), 0) AS yoone_3_amount,
COALESCE(SUM(oi.yoone_6_amount), 0) AS yoone_6_amount,
COALESCE(SUM(oi.yoone_9_amount), 0) AS yoone_9_amount,
COALESCE(SUM(oi.yoone_12_amount), 0) AS yoone_12_amount,
COALESCE(SUM(oi.yoone_15_amount), 0) AS yoone_15_amount,
ROUND(COALESCE(dt.total_amount / dt.total_orders,0), 2) AS avg_total_amount,
ROUND(COALESCE(dt.togo_total_amount / dt.togo_total_orders,0), 2) AS avg_togo_total_amount,
ROUND(COALESCE(dt.can_total_amount / dt.can_total_orders,0), 2) AS avg_can_total_amount
FROM daily_orders d
LEFT JOIN daily_totals dt ON d.order_date = dt.order_date
LEFT JOIN order_sales_summary os ON d.order_id = os.orderId
LEFT JOIN order_items_summary oi ON d.order_id = oi.orderId
GROUP BY
d.order_date,
dt.total_amount,
dt.togo_total_amount,
dt.can_total_amount,
dt.first_purchase_total,
dt.repeat_purchase_total,
dt.cpc_total,
dt.non_cpc_total,
dt.zyn_total,
dt.non_zyn_total,
dt.yoone_total,
dt.non_yoone_total,
dt.zex_total,
dt.non_zex_total,
dt.direct_first_total,
dt.total_orders,
dt.togo_total_orders,
dt.can_total_orders
ORDER BY d.order_date DESC;
`;
return this.orderRepository.query(sql);
}
// async getOrderStatistics(params: OrderStatisticsParams) {
// const {
// startDate,
// endDate,
// siteId,
// purchaseType = 'all',
// orderType = 'all',
// brand = 'all',
// } = params;
// const start = dayjs(startDate).format('YYYY-MM-DD');
// const end = dayjs(endDate).add(1, 'day').format('YYYY-MM-DD');
// // 条件拼接
// const siteFilter = siteId ? `AND o.siteId = '${siteId}'` : '';
// const purchaseTypeFilter =
// purchaseType && purchaseType !== 'all'
// ? `AND purchase_type = '${purchaseType}'`
// : '';
// const orderTypeFilter =
// orderType && orderType !== 'all' ? `AND order_type = '${orderType}'` : '';
// const brandFilter = brand !== 'all' ? `WHERE name LIKE '%${brand}%'` : '';
// const sql = `
// WITH first_order AS (
// SELECT customer_email, MIN(date_paid) AS first_purchase_date
// FROM \`order\`
// GROUP BY customer_email
// ),
// all_orders AS (
// SELECT
// o.id AS order_id,
// DATE(o.date_paid) AS order_date,
// o.customer_email,
// o.total,
// o.source_type,
// o.utm_source,
// CASE
// WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase'
// ELSE 'repeat_purchase'
// END AS purchase_type,
// CASE
// WHEN o.source_type = 'utm' AND o.utm_source = 'google' THEN 'cpc'
// ELSE 'non_cpc'
// END AS order_type
// FROM \`order\` o
// LEFT JOIN first_order f ON o.customer_email = f.customer_email
// WHERE o.date_paid IS NOT NULL
// AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
// AND o.status IN ('processing','completed')
// ${siteFilter}
// ),
// filtered_orders AS (
// SELECT ao.*
// FROM all_orders ao
// WHERE 1=1
// ${purchaseTypeFilter}
// ${orderTypeFilter}
// ${
// brand !== 'all'
// ? `
// AND EXISTS (
// SELECT 1 FROM order_item oi
// WHERE oi.orderId = ao.order_id
// AND oi.name LIKE '%${brand}%'
// )
// `
// : ''
// }
// ),
// brand_quantity AS (
// SELECT
// orderId,
// SUM(quantity) AS total_quantity
// FROM order_sale
// ${brandFilter}
// GROUP BY orderId
// )
// SELECT
// d.order_date,
// COUNT(DISTINCT d.order_id) AS total_orders,
// ROUND(SUM(d.total), 2) AS total_amount,
// ROUND(SUM(d.total) / COUNT(DISTINCT d.order_id), 2) AS avg_order_amount,
// COALESCE(SUM(bq.total_quantity), 0) AS total_quantity
// FROM filtered_orders d
// LEFT JOIN brand_quantity bq ON d.order_id = bq.orderId
// GROUP BY d.order_date
// ORDER BY d.order_date DESC
// `;
// return this.orderRepository.query(sql);
// }
async getOrderByDate(date: string) {
const startOfDay = new Date(`${date}T00:00:00`);
const endOfDay = new Date(`${date}T23:59:59`);
const sql = `
WITH first_order AS (
SELECT customer_email, MIN(date_paid) AS first_purchase_date
FROM \`order\`
GROUP BY customer_email
),
customer_stats AS (
SELECT
customer_email,
COUNT(o.id) AS order_count,
SUM(o.total) AS total_spent
FROM \`order\` o
WHERE o.status IN ('processing', 'completed')
GROUP BY customer_email
)
SELECT
o.*,
JSON_ARRAYAGG(
JSON_OBJECT('name', oi.name, 'quantity', oi.quantity)
) AS orderItems,
f.first_purchase_date,
CASE
WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase'
ELSE 'repeat_purchase'
END AS purchase_type,
cs.order_count,
cs.total_spent
FROM \`order\` o
LEFT JOIN first_order f ON o.customer_email = f.customer_email
LEFT JOIN order_item oi ON oi.orderId = o.id
LEFT JOIN customer_stats cs ON o.customer_email = cs.customer_email
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('processing', 'completed')
GROUP BY o.id, f.first_purchase_date, cs.order_count, cs.total_spent
`;
const orders = await this.orderRepository.query(sql, [
startOfDay,
endOfDay,
]);
return orders;
}
async getOrderByEmail(email: string) {
const sql = `
SELECT
o.*,
JSON_ARRAYAGG(
JSON_OBJECT('name', oi.name, 'quantity', oi.quantity, 'total', oi.total)
) AS orderItems
FROM \`order\` o
LEFT JOIN order_item oi ON oi.orderId = o.id
WHERE o.customer_email='${email}'
GROUP BY o.id
ORDER BY o.date_paid DESC;
`;
const orders = await this.orderRepository.query(sql, [email]);
return orders;
}
async getCustomerOrders(month) {
const timeWhere = month
? `AND o.date_paid BETWEEN '${dayjs(month[0])
.startOf('month')
.format('YYYY-MM-DD HH:mm:ss')}' AND '${dayjs(month[1])
.endOf('month')
.format('YYYY-MM-DD HH:mm:ss')}'`
: '';
const sql = `WITH orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
WHERE 1=1
${timeWhere}
GROUP BY o.customer_email
),
completed_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
WHERE o.status IN ('completed', 'processing')
${timeWhere}
GROUP BY o.customer_email
),
yoone_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%yoone%'
${timeWhere}
GROUP BY o.customer_email
),
zyn_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%zyn%'
${timeWhere}
GROUP BY o.customer_email
),
zex_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%zex%'
${timeWhere}
GROUP BY o.customer_email
),
yoone_3mg_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%3mg%'
AND os.name LIKE '%yoone%'
${timeWhere}
GROUP BY o.customer_email
),
yoone_6mg_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%6mg%'
AND os.name LIKE '%yoone%'
${timeWhere}
GROUP BY o.customer_email
),
yoone_9mg_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%9mg%'
AND os.name LIKE '%yoone%'
${timeWhere}
GROUP BY o.customer_email
),
yoone_12mg_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%12mg%'
AND os.name LIKE '%yoone%'
${timeWhere}
GROUP BY o.customer_email
),
yoone_15mg_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%15mg%'
AND os.name LIKE '%yoone%'
${timeWhere}
GROUP BY o.customer_email
),
zyn_3mg_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%3mg%'
AND os.name LIKE '%zyn%'
${timeWhere}
GROUP BY o.customer_email
),
zyn_6mg_orders AS (
SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
AND os.name LIKE '%6mg%'
AND os.name LIKE '%zyn%'
${timeWhere}
GROUP BY o.customer_email
)
SELECT
--
(SELECT COUNT(DISTINCT customer_email) FROM orders) AS users,
--
(SELECT COUNT(DISTINCT customer_email) FROM completed_orders WHERE order_count >= 1) AS users_one_purchase,
--
(SELECT COUNT(DISTINCT customer_email) FROM completed_orders WHERE order_count >= 2) AS users_two_purchases,
--
(SELECT COUNT(DISTINCT customer_email) FROM completed_orders WHERE order_count >= 3) AS users_three_purchases,
-- 'yoone'
(SELECT COUNT(DISTINCT customer_email) FROM yoone_orders WHERE order_count >= 1) AS users_with_yoone_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_orders WHERE order_count >= 2) AS users_with_yoone_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_orders WHERE order_count >= 3) AS users_with_yoone_three_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_3mg_orders WHERE order_count >= 1) AS users_with_yoone_3mg_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_3mg_orders WHERE order_count >= 2) AS users_with_yoone_3mg_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_3mg_orders WHERE order_count >= 3) AS users_with_yoone_3mg_three_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_6mg_orders WHERE order_count >= 1) AS users_with_yoone_6mg_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_6mg_orders WHERE order_count >= 2) AS users_with_yoone_6mg_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_6mg_orders WHERE order_count >= 3) AS users_with_yoone_6mg_three_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_9mg_orders WHERE order_count >= 1) AS users_with_yoone_9mg_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_9mg_orders WHERE order_count >= 2) AS users_with_yoone_9mg_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_9mg_orders WHERE order_count >= 3) AS users_with_yoone_9mg_three_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_12mg_orders WHERE order_count >= 1) AS users_with_yoone_12mg_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_12mg_orders WHERE order_count >= 2) AS users_with_yoone_12mg_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_12mg_orders WHERE order_count >= 3) AS users_with_yoone_12mg_three_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_15mg_orders WHERE order_count >= 1) AS users_with_yoone_15mg_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_15mg_orders WHERE order_count >= 2) AS users_with_yoone_15mg_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM yoone_15mg_orders WHERE order_count >= 3) AS users_with_yoone_15mg_three_purchases,
-- 'zyn'
(SELECT COUNT(DISTINCT customer_email) FROM zyn_orders WHERE order_count >= 1) AS users_with_zyn_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_orders WHERE order_count >= 2) AS users_with_zyn_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_orders WHERE order_count >= 3) AS users_with_zyn_three_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_3mg_orders WHERE order_count >= 1) AS users_with_zyn_3mg_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_3mg_orders WHERE order_count >= 2) AS users_with_zyn_3mg_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_3mg_orders WHERE order_count >= 3) AS users_with_zyn_3mg_three_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_6mg_orders WHERE order_count >= 1) AS users_with_zyn_6mg_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_6mg_orders WHERE order_count >= 2) AS users_with_zyn_6mg_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM zyn_6mg_orders WHERE order_count >= 3) AS users_with_zyn_6mg_three_purchases,
-- 'zex'
(SELECT COUNT(DISTINCT customer_email) FROM zex_orders WHERE order_count >= 1) AS users_with_zex_one_purchase,
(SELECT COUNT(DISTINCT customer_email) FROM zex_orders WHERE order_count >= 2) AS users_with_zex_two_purchases,
(SELECT COUNT(DISTINCT customer_email) FROM zex_orders WHERE order_count >= 3) AS users_with_zex_three_purchases,
(SELECT SUM(order_count) FROM zyn_3mg_orders) AS zyn_3mg_orders,
(SELECT SUM(order_count) FROM zyn_6mg_orders) AS zyn_6mg_orders,
(SELECT SUM(order_count) FROM zex_orders) AS zex_15mg_orders,
(SELECT SUM(order_count) FROM yoone_3mg_orders) AS order_with_3mg,
(SELECT SUM(order_count) FROM yoone_6mg_orders) AS order_with_6mg,
(SELECT SUM(order_count) FROM yoone_9mg_orders) AS order_with_9mg,
(SELECT SUM(order_count) FROM yoone_12mg_orders) AS order_with_12mg,
(SELECT SUM(order_count) FROM yoone_15mg_orders) AS order_with_15mg
`;
// 执行 SQL 查询
const result = await this.orderRepository.query(sql);
const sql1 = `
WITH product_purchase_counts AS (
SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
${timeWhere}
GROUP BY o.customer_email, os.productId, os.name
HAVING order_count > 0
)
SELECT
productId,
name,
COUNT(DISTINCT customer_email) AS user_count
FROM product_purchase_counts
GROUP BY productId, name
ORDER BY user_count DESC
LIMIT 30
`;
const sql2 = `
WITH product_purchase_counts AS (
SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
${timeWhere}
GROUP BY o.customer_email, os.productId, os.name
HAVING order_count > 1
)
SELECT
productId,
name,
COUNT(DISTINCT customer_email) AS user_count
FROM product_purchase_counts
GROUP BY productId, name
ORDER BY user_count DESC
LIMIT 30
`;
const sql3 = `
WITH product_purchase_counts AS (
SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
${timeWhere}
GROUP BY o.customer_email, os.productId, os.name
HAVING order_count > 2
)
SELECT
productId,
name,
COUNT(DISTINCT customer_email) AS user_count
FROM product_purchase_counts
GROUP BY productId, name
ORDER BY user_count DESC
LIMIT 30
`;
const first_hot_purchase = await this.orderRepository.query(sql1);
const second_hot_purchase = await this.orderRepository.query(sql2);
const third_hot_purchase = await this.orderRepository.query(sql3);
return {
...result[0],
first_hot_purchase,
second_hot_purchase,
third_hot_purchase,
};
}
async stockForecast(params) {
const { productName, pageSize, current } = params;
const countnameFilter = (
productName ? productName.split(' ').filter(Boolean) : []
)
.map(name => `AND p.name LIKE '%${name}%'`)
.join(' ');
const offset = (current - 1) * pageSize;
const countSql = `
WITH product_list AS (
SELECT DISTINCT s.productSku
FROM stock s
LEFT JOIN stock_point sp ON s.stockPointId = sp.id
LEFT JOIN product p ON s.productSku = p.sku
WHERE sp.ignore = FALSE
${countnameFilter}
)
SELECT COUNT(*) AS total FROM product_list;
`;
const nameFilter = (
productName ? productName.split(' ').filter(Boolean) : []
)
.map(name => `AND pns.productName LIKE '%${name}%'`)
.join(' ');
const sql = `
WITH stock_summary AS (
SELECT
s.productSku,
JSON_ARRAYAGG(JSON_OBJECT('id', sp.id, 'quantity', s.quantity)) AS stockDetails,
SUM(s.quantity) AS totalStock,
SUM(CASE WHEN sp.inCanada THEN s.quantity ELSE 0 END) AS caTotalStock
FROM stock s
JOIN stock_point sp ON s.stockPointId = sp.id
WHERE sp.ignore = FALSE
GROUP BY s.productSku
),
transfer_stock AS (
SELECT
ti.productSku,
SUM(ti.quantity) AS transitStock
FROM transfer_item ti
JOIN transfer t ON ti.transferId = t.id
WHERE t.isCancel = FALSE AND t.isArrived = FALSE
GROUP BY ti.productSku
),
30_sales_summary AS (
SELECT
os.sku AS productSku,
SUM(os.quantity) AS totalSales
FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id
WHERE o.status IN ('completed', 'refunded', 'processing')
AND o.date_paid >= NOW() - INTERVAL 30 DAY
GROUP BY os.sku
),
15_sales_summary AS (
SELECT
os.sku AS productSku,
2 * SUM(os.quantity) AS totalSales
FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id
WHERE o.status IN ('completed', 'refunded', 'processing')
AND o.date_paid >= NOW() - INTERVAL 15 DAY
GROUP BY os.sku
),
sales_max_summary AS (
SELECT
s30.productSku AS productSku,
COALESCE(s30.totalSales, 0) AS totalSales_30,
COALESCE(s15.totalSales, 0) AS totalSales_15,
GREATEST(COALESCE(s30.totalSales, 0), COALESCE(s15.totalSales, 0)) AS maxSales
FROM 30_sales_summary s30
LEFT JOIN 15_sales_summary s15
ON s30.productSku = s15.productSku
UNION ALL
SELECT
s15.productSku AS productSku,
0 AS totalSales_30,
COALESCE(s15.totalSales, 0) AS totalSales_15,
COALESCE(s15.totalSales, 0) AS maxSales
FROM 15_sales_summary s15
LEFT JOIN 30_sales_summary s30
ON s30.productSku = s15.productSku
WHERE s30.productSku IS NULL
),
product_name_summary AS (
SELECT
p.sku AS productSku,
COALESCE(MAX(os.name), MAX(p.name)) AS productName
FROM product p
LEFT JOIN order_sale os ON p.sku = os.sku
GROUP BY p.sku
)
SELECT
ss.productSku,
ss.stockDetails,
COALESCE(ts.transitStock, 0) AS transitStock,
(COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock,
sales.totalSales_30,
sales.totalSales_15,
CASE
WHEN sales.maxSales = 0 THEN NULL
ELSE FLOOR((COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) / sales.maxSales * 30)
END AS availableDays,
CASE
WHEN sales.maxSales = 0 THEN NULL
ELSE FLOOR(COALESCE(ss.caTotalStock, 0) / sales.maxSales * 30)
END AS caAvailableDays,
ss.caTotalStock,
sales.maxSales * 4 AS restockQuantity,
pns.productName
FROM stock_summary ss
LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku
LEFT JOIN sales_max_summary sales ON ss.productSku = sales.productSku
LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku
WHERE 1 = 1
${nameFilter}
ORDER BY caAvailableDays
LIMIT ${pageSize} OFFSET ${offset};
`;
const totalResult = await this.orderRepository.query(countSql);
const total = totalResult[0]?.total || 0;
const items = await this.orderRepository.query(sql);
return {
items,
total,
current,
pageSize,
};
}
async restocking(params) {
const { productName, pageSize, current } = params;
const countnameFilter = (
productName ? productName.split(' ').filter(Boolean) : []
)
.map(name => `AND p.name LIKE '%${name}%'`)
.join(' ');
const offset = (current - 1) * pageSize;
const countSql = `
WITH product_list AS (
SELECT DISTINCT s.productSku
FROM stock s
LEFT JOIN stock_point sp ON s.stockPointId = sp.id
LEFT JOIN product p ON s.productSku = p.sku
WHERE sp.ignore = FALSE
${countnameFilter}
)
SELECT COUNT(*) AS total FROM product_list;
`;
const nameFilter = (
productName ? productName.split(' ').filter(Boolean) : []
)
.map(name => `AND pns.productName LIKE '%${name}%'`)
.join(' ');
const sql = `
WITH stock_summary AS (
SELECT
s.productSku,
SUM(s.quantity) AS totalStock
FROM stock s
JOIN stock_point sp ON s.stockPointId = sp.id
WHERE sp.ignore = FALSE
GROUP BY s.productSku
),
transfer_stock AS (
SELECT
ti.productSku,
SUM(ti.quantity) AS transitStock
FROM transfer_item ti
JOIN transfer t ON ti.transferId = t.id
WHERE t.isCancel = FALSE AND t.isArrived = FALSE
GROUP BY ti.productSku
),
b_sales_data_raw As (
SELECT
sr.productSku,
DATE_FORMAT(sr.createdAt, '%Y-%m') AS month,
SUM(sr.quantityChange) AS sales
FROM stock_record sr
JOIN stock_point sp ON sr.stockPointId = sp.id
WHERE sp.isB
AND sr.createdAt >= DATE_FORMAT(NOW() - INTERVAL 2 MONTH, '%Y-%m-01')
GROUP BY sr.productSku, month
),
sales_data_raw AS (
SELECT
os.sku AS productSku,
DATE_FORMAT(o.date_paid, '%Y-%m') AS month,
SUM(CASE WHEN DAY(o.date_paid) <= 10 THEN os.quantity ELSE 0 END) AS early_sales,
SUM(CASE WHEN DAY(o.date_paid) > 10 AND DAY(o.date_paid) <= 20 THEN os.quantity ELSE 0 END) AS mid_sales,
SUM(CASE WHEN DAY(o.date_paid) > 20 THEN os.quantity ELSE 0 END) AS late_sales
FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id
WHERE o.status IN ('completed', 'refunded', 'processing')
AND o.date_paid >= DATE_FORMAT(NOW() - INTERVAL 2 MONTH, '%Y-%m-01')
GROUP BY os.sku, month
),
monthly_sales_summary AS (
SELECT
sdr.productSku,
JSON_ARRAYAGG(
JSON_OBJECT(
'month', sdr.month,
'early_sales', sdr.early_sales,
'mid_sales', sdr.mid_sales,
'late_sales', sdr.late_sales,
'b_sales', COALESCE(b.sales,0)
)
) AS sales_data
FROM sales_data_raw sdr
LEFT JOIN b_sales_data_raw b ON sdr.productSku = b.productSku AND sdr.month = b.month
GROUP BY sdr.productSku
),
sales_summary AS (
SELECT
os.sku AS productSku,
SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 30 DAY THEN os.quantity ELSE 0 END) AS last_30_days_sales,
SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 15 DAY THEN os.quantity ELSE 0 END) AS last_15_days_sales,
SUM(CASE WHEN DATE_FORMAT(o.date_paid, '%Y-%m') = DATE_FORMAT(CURDATE() - INTERVAL 1 MONTH, '%Y-%m') THEN os.quantity ELSE 0 END) AS last_month_sales
FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id
WHERE o.status IN ('completed', 'refunded', 'processing')
AND o.date_paid >= CURDATE() - INTERVAL 2 MONTH
GROUP BY os.sku
),
product_name_summary AS (
SELECT
p.sku AS productSku,
COALESCE(MAX(os.name), MAX(p.name)) AS productName
FROM product p
LEFT JOIN order_sale os ON p.sku = os.sku
GROUP BY p.sku
)
SELECT
ss.productSku,
(COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock,
ms.sales_data AS monthlySalesData,
pns.productName,
COALESCE(ssum.last_30_days_sales, 0) AS last30DaysSales,
COALESCE(ssum.last_15_days_sales, 0) AS last15DaysSales,
COALESCE(ssum.last_month_sales, 0) AS lastMonthSales,
CASE
WHEN COALESCE(ssum.last_month_sales, 0) > 0
THEN (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) / ssum.last_month_sales
ELSE NULL
END AS stock_ratio
FROM stock_summary ss
LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku
LEFT JOIN monthly_sales_summary ms ON ss.productSku = ms.productSku
LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku
LEFT JOIN sales_summary ssum ON ss.productSku = ssum.productSku
WHERE 1 = 1
${nameFilter}
ORDER BY
stock_ratio IS NULL ASC,
stock_ratio ASC
LIMIT ${pageSize} OFFSET ${offset};
`;
const totalResult = await this.orderRepository.query(countSql);
const total = totalResult[0]?.total || 0;
const items = await this.orderRepository.query(sql);
return {
items,
total,
current,
pageSize,
};
}
}

View File

@ -0,0 +1,557 @@
import { Provide } from '@midwayjs/core';
import { Between, Like, Repository } from 'typeorm';
import { Stock } from '../entity/stock.entity';
import { StockRecord } from '../entity/stock_record.entity';
import { paginate } from '../utils/paginate.util';
import { Product } from '../entity/product.entty';
import {
CreatePurchaseOrderDTO,
CreateStockPointDTO,
QueryPointDTO,
QueryPurchaseOrderDTO,
QueryStockDTO,
QueryStockRecordDTO,
UpdatePurchaseOrderDTO,
UpdateStockDTO,
UpdateStockPointDTO,
} from '../dto/stock.dto';
import { StockPoint } from '../entity/stock_point.entity';
import { PurchaseOrder } from '../entity/purchase_order.entity';
import { PurchaseOrderItem } from '../entity/purchase_order_item.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import {
PurchaseOrderStatus,
StockRecordOperationType,
} from '../enums/base.enum';
import { User } from '../entity/user.entity';
import dayjs = require('dayjs');
import { Transfer } from '../entity/transfer.entity';
import { TransferItem } from '../entity/transfer_item.entity';
@Provide()
export class StockService {
@InjectEntityModel(StockPoint)
stockPointModel: Repository<StockPoint>;
@InjectEntityModel(Stock)
stockModel: Repository<Stock>;
@InjectEntityModel(StockRecord)
stockRecordModel: Repository<StockRecord>;
@InjectEntityModel(PurchaseOrder)
purchaseOrderModel: Repository<PurchaseOrder>;
@InjectEntityModel(PurchaseOrderItem)
purchaseOrderItemModel: Repository<PurchaseOrderItem>;
@InjectEntityModel(Transfer)
transferModel: Repository<Transfer>;
@InjectEntityModel(TransferItem)
transferItemModel: Repository<TransferItem>;
async createStockPoint(data: CreateStockPointDTO) {
const { name, location, contactPerson, contactPhone } = data;
const stockPoint = new StockPoint();
stockPoint.name = name;
stockPoint.location = location;
stockPoint.contactPerson = contactPerson;
stockPoint.contactPhone = contactPhone;
await this.stockPointModel.save(stockPoint);
}
async updateStockPoint(id: number, data: UpdateStockPointDTO) {
// 确认产品是否存在
const point = await this.stockPointModel.findOneBy({ id });
if (!point) {
throw new Error(`产品 ID ${id} 不存在`);
}
// 更新产品
await this.stockPointModel.update(id, data);
}
async getStockPoints(query: QueryPointDTO) {
const { current = 1, pageSize = 10 } = query;
return await paginate(this.stockPointModel, {
pagination: { current, pageSize },
});
}
async getAllStockPoints(): Promise<StockPoint[]> {
return await this.stockPointModel.find();
}
async delStockPoints(id: number) {
const point = await this.stockPointModel.findOneBy({ id });
if (!point) {
throw new Error(`库存点 ID ${id} 不存在`);
}
await this.stockRecordModel.delete({ stockPointId: id });
await this.stockPointModel.softDelete(id);
}
async createPurchaseOrder(data: CreatePurchaseOrderDTO) {
const { stockPointId, expectedArrivalTime, status, items, note } = data;
const now = dayjs().format('YYYY-MM-DD');
const count = await this.purchaseOrderModel.count({
where: {
createdAt: Between(
dayjs(`${now} 00:00:00`).toDate(),
dayjs(`${now} 23:59:59`).toDate()
),
},
});
const orderNumber = `${now.replace(/-/g, '')}P0${count + 1}`;
const purchaseOrder = await this.purchaseOrderModel.save({
stockPointId,
orderNumber,
expectedArrivalTime,
status,
note,
});
items.forEach(item => (item.purchaseOrderId = purchaseOrder.id));
await this.purchaseOrderItemModel.save(items);
}
async updatePurchaseOrder(id: number, data: UpdatePurchaseOrderDTO) {
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
if (purchaseOrder.status === 'received')
throw new Error(`采购订单 ID ${id} 已到达,无法修改`);
const { stockPointId, expectedArrivalTime, status, items, note } = data;
purchaseOrder.stockPointId = stockPointId;
purchaseOrder.expectedArrivalTime = expectedArrivalTime;
purchaseOrder.status = status;
purchaseOrder.note = note;
this.purchaseOrderModel.save(purchaseOrder);
const dbItems = await this.purchaseOrderItemModel.find({
where: { purchaseOrderId: id },
});
const ids = new Set(items.map(v => String(v.id)));
const toDelete = dbItems.filter(
dbVariation => !ids.has(String(dbVariation.id))
);
if (toDelete.length > 0) {
const idsToDelete = toDelete.map(v => v.id);
await this.purchaseOrderItemModel.delete(idsToDelete);
}
for (const item of items) {
if (item.id) {
await this.purchaseOrderItemModel.update(item.id, item);
} else {
item.purchaseOrderId = id;
await this.purchaseOrderItemModel.save(item);
}
}
}
async getPurchaseOrders(query: QueryPurchaseOrderDTO) {
const { current = 1, pageSize = 10, orderNumber, stockPointId } = query;
const where: any = {};
if (orderNumber) where.orderNumber = Like(`%${orderNumber}%`);
if (stockPointId) where.stockPointId = Like(`%${stockPointId}%`);
return await paginate(
this.purchaseOrderModel
.createQueryBuilder('purchase_order')
.leftJoinAndSelect(
StockPoint,
'stock_point',
'purchase_order.stockPointId = stock_point.id'
)
.leftJoin(
qb =>
qb
.select([
'poi.purchaseOrderId AS purchaseOrderId',
"JSON_ARRAYAGG(JSON_OBJECT('id', poi.id, 'productName', poi.productName,'productSku', poi.productSku, 'quantity', poi.quantity, 'price', poi.price)) AS items",
])
.from(PurchaseOrderItem, 'poi')
.groupBy('poi.purchaseOrderId'),
'items',
'items.purchaseOrderId = purchase_order.id'
)
.select([
'purchase_order.*',
'stock_point.name as stockPointName',
'items.items',
])
.where(where)
.orderBy('createdAt', 'DESC'),
{
pagination: { current, pageSize },
}
);
}
async delPurchaseOrder(id: number) {
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
if (purchaseOrder.status === 'received')
throw new Error(`采购订单 ID ${id} 已到达,无法删除`);
await this.purchaseOrderItemModel.delete({ purchaseOrderId: id });
await this.purchaseOrderModel.delete({ id });
}
async receivePurchaseOrder(id: number, userId: number) {
const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id });
if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`);
if (purchaseOrder.status === 'received')
throw new Error(`采购订单 ID ${id} 已到达,不要重复操作`);
const items = await this.purchaseOrderItemModel.find({
where: { purchaseOrderId: id },
});
for (const item of items) {
const updateStock = new UpdateStockDTO();
updateStock.stockPointId = purchaseOrder.stockPointId;
updateStock.productSku = item.productSku;
updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId;
updateStock.note = '采购入库';
await this.updateStock(updateStock);
}
purchaseOrder.status = PurchaseOrderStatus.RECEIVED;
await this.purchaseOrderModel.save(purchaseOrder);
}
// 获取库存列表
async getStocks(query: QueryStockDTO) {
const { current = 1, pageSize = 10, productName } = query;
const nameKeywords = productName
? productName.split(' ').filter(Boolean)
: [];
let queryBuilder = this.stockModel
.createQueryBuilder('stock')
.select([
// 'stock.id as id',
'stock.productSku as productSku',
'product.name as productName',
'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint',
'MIN(stock.updatedAt) as updatedAt',
'MAX(stock.createdAt) as createdAt',
])
.leftJoin(Product, 'product', 'product.sku = stock.productSku')
.groupBy('stock.productSku')
.addGroupBy('product.name');
let totalQueryBuilder = this.stockModel
.createQueryBuilder('stock')
.select('COUNT(DISTINCT stock.productSku)', 'count')
.leftJoin(Product, 'product', 'product.sku = stock.productSku');
if (nameKeywords.length) {
nameKeywords.forEach((name, index) => {
queryBuilder.andWhere(
`EXISTS (
SELECT 1 FROM product p
WHERE p.sku = stock.productSku
AND p.name LIKE :name${index}
)`,
{ [`name${index}`]: `%${name}%` }
);
totalQueryBuilder.andWhere(
`EXISTS (
SELECT 1 FROM product p
WHERE p.sku = stock.productSku
AND p.name LIKE :name${index}
)`,
{ [`name${index}`]: `%${name}%` }
);
});
}
const items = await queryBuilder.getRawMany();
const total = await totalQueryBuilder.getRawOne();
const transfer = await this.transferModel
.createQueryBuilder('t')
.select(['ti.productSku as productSku', 'SUM(ti.quantity) as quantity'])
.leftJoin(TransferItem, 'ti', 'ti.transferId = t.id')
.where('!t.isArrived and !t.isCancel and !t.isLost')
.groupBy('ti.productSku')
.getRawMany();
for (const item of items) {
item.inTransitQuantity =
transfer.find(t => t.productSku === item.productSku)?.quantity || 0;
}
return {
items,
total,
current,
pageSize,
};
}
// 更新库存
async updateStock(data: UpdateStockDTO) {
const {
stockPointId,
productSku,
quantityChange,
operationType,
operatorId,
note,
} = data;
const stock = await this.stockModel.findOneBy({
stockPointId,
productSku,
});
if (!stock) {
// 如果库存不存在,则直接新增
const newStock = this.stockModel.create({
stockPointId,
productSku,
quantity: operationType === 'in' ? quantityChange : -quantityChange,
});
await this.stockModel.save(newStock);
} else {
// 更新库存
stock.quantity +=
operationType === 'in' ? quantityChange : -quantityChange;
if (stock.quantity < 0) {
throw new Error('库存不足,无法完成操作');
}
await this.stockModel.save(stock);
}
// 记录库存变更日志
const stockRecord = this.stockRecordModel.create({
stockPointId,
productSku,
operationType,
quantityChange,
operatorId,
note,
});
await this.stockRecordModel.save(stockRecord);
}
// 获取库存记录
async getStockRecords(query: QueryStockRecordDTO) {
const {
current = 1,
pageSize = 10,
stockPointId,
productSku,
productName,
} = query;
const where: any = {};
if (stockPointId) where.stockPointId = stockPointId;
if (productSku) where.productSku = productSku;
const queryBuilder = this.stockRecordModel
.createQueryBuilder('stock_record')
.leftJoin(Product, 'product', 'product.sku = stock_record.productSku')
.leftJoin(User, 'user', 'stock_record.operatorId = user.id')
.leftJoin(StockPoint, 'sp', 'sp.id = stock_record.stockPointId')
.select([
'stock_record.*',
'product.name as productName',
'user.username as operatorName',
'sp.name as stockPointName',
])
.where(where);
if (productName)
queryBuilder.andWhere('product.name LIKE :name', {
name: `%${productName}%`,
});
const items = await queryBuilder
.orderBy('stock_record.createdAt', 'DESC')
.skip((current - 1) * pageSize)
.take(pageSize)
.getRawMany();
const total = await queryBuilder.getCount();
return {
items,
total,
current,
pageSize,
};
}
async createTransfer(data: Record<string, any>, userId: number) {
const { sourceStockPointId, destStockPointId, sendAt, items, note } = data;
for (const item of items) {
const stock = await this.stockModel.findOneBy({
stockPointId: sourceStockPointId,
productSku: item.productSku,
});
if (!stock || stock.quantity < item.quantity)
throw new Error(`${item.productName} 库存不足`);
}
const now = dayjs().format('YYYY-MM-DD');
const count = await this.transferModel.count({
where: {
createdAt: Between(
dayjs(`${now} 00:00:00`).toDate(),
dayjs(`${now} 23:59:59`).toDate()
),
},
});
const orderNumber = `${now.replace(/-/g, '')}P0${count + 1}`;
const transfer = await this.transferModel.save({
sourceStockPointId,
destStockPointId,
orderNumber,
sendAt,
note,
});
for (const item of items) {
item.transferId = transfer.id;
const updateStock = new UpdateStockDTO();
updateStock.stockPointId = sourceStockPointId;
updateStock.productSku = item.productSku;
updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.OUT;
updateStock.operatorId = userId;
updateStock.note = `调拨${transfer.orderNumber} 出库`;
await this.updateStock(updateStock);
}
await this.transferItemModel.save(items);
}
async getTransfers(query: Record<string, any>) {
const {
current = 1,
pageSize = 10,
orderNumber,
sourceStockPointId,
destStockPointId,
} = query;
const where: any = {};
if (orderNumber) where.orderNumber = Like(`%${orderNumber}%`);
if (sourceStockPointId) where.sourceStockPointId = sourceStockPointId;
if (destStockPointId) where.destStockPointId = destStockPointId;
return await paginate(
this.transferModel
.createQueryBuilder('t')
.leftJoinAndSelect(StockPoint, 'sp', 't.sourceStockPointId = sp.id')
.leftJoinAndSelect(StockPoint, 'sp1', 't.destStockPointId = sp1.id')
.leftJoin(
qb =>
qb
.select([
'ti.transferId AS transferId',
"JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.productName,'productSku', ti.productSku, 'quantity', ti.quantity)) AS items",
])
.from(TransferItem, 'ti')
.groupBy('ti.transferId'),
'items',
'items.transferId = t.id'
)
.select([
't.*',
'sp.name as sourceStockPointName',
'sp1.name as destStockPointName',
'items.items',
])
.where(where)
.orderBy('createdAt', 'DESC'),
{
pagination: { current, pageSize },
}
);
}
async cancelTransfer(id: number, userId: number) {
const transfer = await this.transferModel.findOneBy({ id });
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
if (transfer.isArrived) throw new Error(`调拨 ID ${id} 已到达,无法取消`);
const items = await this.transferItemModel.find({
where: { transferId: id },
});
for (const item of items) {
const updateStock = new UpdateStockDTO();
updateStock.stockPointId = transfer.sourceStockPointId;
updateStock.productSku = item.productSku;
updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId;
updateStock.note = `取消调拨${transfer.orderNumber} 入库`;
await this.updateStock(updateStock);
}
transfer.isCancel = true;
await this.transferModel.save(transfer);
}
async receiveTransfer(id: number, userId: number) {
const transfer = await this.transferModel.findOneBy({ id });
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`);
if (transfer.isArrived)
throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
const items = await this.transferItemModel.find({
where: { transferId: id },
});
for (const item of items) {
const updateStock = new UpdateStockDTO();
updateStock.stockPointId = transfer.destStockPointId;
updateStock.productSku = item.productSku;
updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId;
updateStock.note = `调拨${transfer.orderNumber} 入库`;
await this.updateStock(updateStock);
}
transfer.isArrived = true;
transfer.arriveAt = new Date();
await this.transferModel.save(transfer);
}
async lostTransfer(id: number) {
const transfer = await this.transferModel.findOneBy({ id });
if (!transfer) throw new Error(`调拨 ID ${id} 不存在`);
if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`);
if (transfer.isArrived)
throw new Error(`调拨 ID ${id} 已到达,不要重复操作`);
transfer.isLost = true;
await this.transferModel.save(transfer);
}
async updateTransfer(id: number, data: Record<string, any>, userId: number) {
const { sourceStockPointId, destStockPointId, items, note } = data;
const transfer = await this.transferModel.findOneBy({ id });
if (!transfer) throw new Error(`调拨单 ID ${id} 不存在`);
if (transfer.isCancel) throw new Error(`调拨单 ID ${id} 已取消`);
if (transfer.isArrived) throw new Error(`调拨单 ID ${id} 已到达`);
const dbItems = await this.transferItemModel.find({
where: { transferId: id },
});
for (const item of dbItems) {
item.transferId = transfer.id;
const updateStock = new UpdateStockDTO();
updateStock.stockPointId = sourceStockPointId;
updateStock.productSku = item.productSku;
updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.IN;
updateStock.operatorId = userId;
updateStock.note = `调拨调整 ${transfer.orderNumber} 入库`;
await this.updateStock(updateStock);
}
await this.transferItemModel.delete(dbItems.map(v => v.id));
for (const item of items) {
item.transferId = transfer.id;
const updateStock = new UpdateStockDTO();
updateStock.stockPointId = sourceStockPointId;
updateStock.productSku = item.productSku;
updateStock.quantityChange = item.quantity;
updateStock.operationType = StockRecordOperationType.OUT;
updateStock.operatorId = userId;
updateStock.note = `调拨调整${transfer.orderNumber} 出库`;
await this.updateStock(updateStock);
await this.transferItemModel.save(item);
}
transfer.sourceStockPointId = sourceStockPointId;
transfer.destStockPointId = destStockPointId;
transfer.note = note;
await this.transferModel.save(transfer);
}
}

View File

@ -0,0 +1,79 @@
// src/service/user.service.ts
import { Inject, Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@midwayjs/jwt';
import { User } from '../entity/user.entity';
import { LoginResDTO } from '../dto/user.dto';
import { plainToInstance } from 'class-transformer';
@Provide()
export class UserService {
@InjectEntityModel(User)
userModel: Repository<User>;
@Inject()
jwtService: JwtService;
async login(username: string, password: string): Promise<LoginResDTO> {
const user = await this.userModel.findOne({
where: { username, isActive: true },
});
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new Error('用户名或者密码错误');
}
// 生成 JWT包含角色和权限信息
const token = await this.jwtService.sign({
id: user.id,
username: user.username,
});
return {
token, //role: user.role,
username: user.username,
userId: user.id,
permissions: user.permissions,
};
}
async addUser(username: string, password: string) {
const existingUser = await this.userModel.findOne({
where: { username },
});
if (existingUser) {
throw new Error('用户已存在');
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = this.userModel.create({
username,
password: hashedPassword,
});
return this.userModel.save(user);
}
async listUsers(current: number, pageSize: number) {
const [items, total] = await this.userModel.findAndCount({
skip: (current - 1) * pageSize,
take: pageSize,
});
return { items, total, current, pageSize };
}
async toggleUserActive(userId: number, isActive: boolean) {
const user = await this.userModel.findOne({ where: { id: userId } });
if (!user) {
throw new Error('User not found');
}
user.isActive = isActive;
return this.userModel.save(user);
}
async getUser(userId: number) {
return plainToInstance(
User,
await this.userModel.findOne({ where: { id: userId } })
);
}
}

279
src/service/wp.service.ts Normal file
View File

@ -0,0 +1,279 @@
import { Config, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios';
import { WpSite } from '../interface';
import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity';
import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto';
@Provide()
export class WPService {
@Config('wpSite')
sites: WpSite[];
/**
* WordPress
* @param wpApiUrl WordPress REST API
* @param endpoint API wc/v3/products
* @param consumerKey WooCommerce
* @param consumerSecret WooCommerce
*/
geSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
async fetchData<T>(
endpoint: string,
site: WpSite,
param: Record<string, any> = {}
): Promise<T> {
try {
const { wpApiUrl, consumerKey, consumerSecret } = site;
const url = `${wpApiUrl}/wp-json${endpoint}`;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
const response = await axios.request({
url,
headers: {
Authorization: `Basic ${auth}`,
},
method: 'GET',
...param,
});
return response.data;
} catch (error) {
throw error;
}
}
async fetchPagedData<T>(
endpoint: string,
site: WpSite,
page: number = 1,
perPage: number = 100
): Promise<T[]> {
const allData: T[] = [];
const { wpApiUrl, consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
let hasMore = true;
while (hasMore) {
const config: AxiosRequestConfig = {
method: 'GET',
url: `${wpApiUrl}/wp-json${endpoint}`,
headers: {
Authorization: `Basic ${auth}`,
},
params: {
page,
per_page: perPage,
},
};
try {
const response = await axios.request(config);
// Append the current page data
allData.push(...response.data);
// Check for more pages
const totalPages = parseInt(
response.headers['x-wp-totalpages'] || '1',
10
);
hasMore = page < totalPages;
page += 1;
} catch (error) {
throw error;
}
}
return allData;
}
async getProducts(site: WpSite): Promise<WpProduct[]> {
return await this.fetchPagedData<WpProduct>('/wc/v3/products', site);
}
async getVariations(site: WpSite, productId: number): Promise<Variation[]> {
return await this.fetchPagedData<Variation>(
`/wc/v3/products/${productId}/variations`,
site
);
}
async getVariation(
site: WpSite,
productId: number,
variationId: number
): Promise<Variation> {
return await this.fetchData<Variation>(
`/wc/v3/products/${productId}/variations/${variationId}`,
site
);
}
async getOrder(
siteId: string,
orderId: string
): Promise<Record<string, any>> {
const site = this.geSite(siteId);
return await this.fetchData<Record<string, any>>(
`/wc/v3/orders/${orderId}`,
site
);
}
async getOrders(siteId: string): Promise<Record<string, any>[]> {
const site = this.geSite(siteId);
return await this.fetchPagedData<Record<string, any>>(
'/wc/v3/orders',
site
);
}
async getOrderRefund(
siteId: string,
orderId: string,
refundId: number
): Promise<Record<string, any>> {
const site = this.geSite(siteId);
return await this.fetchData<Record<string, any>>(
`/wc/v3/orders/${orderId}/refunds/${refundId}`,
site
);
}
async getOrderRefunds(
siteId: string,
orderId: number
): Promise<Record<string, any>[]> {
const site = this.geSite(siteId);
return await this.fetchPagedData<Record<string, any>>(
`/wc/v3/orders/${orderId}/refunds`,
site
);
}
async getOrderNote(
siteId: string,
orderId: number,
noteId: number
): Promise<Record<string, any>> {
const site = this.geSite(siteId);
return await this.fetchData<Record<string, any>>(
`/wc/v3/orders/${orderId}/notes/${noteId}`,
site
);
}
async getOrderNotes(
siteId: string,
orderId: number
): Promise<Record<string, any>[]> {
const site = this.geSite(siteId);
return await this.fetchPagedData<Record<string, any>>(
`/wc/v3/orders/${orderId}/notes`,
site
);
}
async updateData<T>(
endpoint: string,
site: WpSite,
data: Record<string, any>
): Promise<Boolean> {
const { wpApiUrl, consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
const config: AxiosRequestConfig = {
method: 'PUT',
url: `${wpApiUrl}/wp-json${endpoint}`,
headers: {
Authorization: `Basic ${auth}`,
},
data,
};
try {
await axios.request(config);
return true;
} catch (error) {
return false;
}
}
/**
* WooCommerce
* @param productId ID
* @param data
*/
async updateProduct(
site: WpSite,
productId: string,
data: UpdateWpProductDTO
): Promise<Boolean> {
const { regular_price, sale_price, ...params } = data;
return await this.updateData(`/wc/v3/products/${productId}`, site, {
...params,
regular_price: regular_price ? regular_price.toString() : null,
sale_price: sale_price ? sale_price.toString() : null,
});
}
/**
* WooCommerce
* @param productId ID
* @param variationId ID
* @param data
*/
async updateVariation(
site: WpSite,
productId: string,
variationId: string,
data: UpdateVariationDTO
): Promise<Boolean> {
const { regular_price, sale_price, ...params } = data;
return await this.updateData(
`/wc/v3/products/${productId}/variations/${variationId}`,
site,
{
...params,
regular_price: regular_price ? regular_price.toString() : null,
sale_price: sale_price ? sale_price.toString() : null,
}
);
}
/**
* Order
*/
async updateOrder(
site: WpSite,
orderId: string,
data: Record<string, any>
): Promise<Boolean> {
return await this.updateData(`/wc/v3/orders/${orderId}`, site, data);
}
async createShipment(
site: WpSite,
orderId: string,
data: Record<string, any>
): Promise<Boolean> {
const { wpApiUrl, consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
const config: AxiosRequestConfig = {
method: 'POST',
url: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings`,
headers: {
Authorization: `Basic ${auth}`,
},
data,
};
return await axios.request(config);
}
}

View File

@ -0,0 +1,447 @@
import { Config, Inject, Provide } from '@midwayjs/core';
import { WPService } from './wp.service';
import { WpSite } from '../interface';
import { WpProduct } from '../entity/wp_product.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { And, Like, Not, Repository } from 'typeorm';
import { Variation } from '../entity/variation.entity';
import {
QueryWpProductDTO,
UpdateVariationDTO,
UpdateWpProductDTO,
} from '../dto/wp_product.dto';
import { Product } from '../entity/product.entty';
@Provide()
export class WpProductService {
@Config('wpSite')
sites: WpSite[];
@Inject()
private readonly wpApiService: WPService;
@InjectEntityModel(WpProduct)
wpProductModel: Repository<WpProduct>;
@InjectEntityModel(Variation)
variationModel: Repository<Variation>;
geSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
async syncAllSites() {
for (const site of this.sites) {
const products = await this.wpApiService.getProducts(site);
for (const product of products) {
const variations =
product.type === 'variable'
? await this.wpApiService.getVariations(site, product.id)
: [];
await this.syncProductAndVariations(site.id, product, variations);
}
}
}
async syncSite(siteId: string) {
const site = this.geSite(siteId);
const products = await this.wpApiService.getProducts(site);
for (const product of products) {
const variations =
product.type === 'variable'
? await this.wpApiService.getVariations(site, product.id)
: [];
await this.syncProductAndVariations(site.id, product, variations);
}
}
async findProduct(
siteId: string,
externalProductId: string
): Promise<WpProduct | null> {
return await this.wpProductModel.findOne({
where: { siteId, externalProductId },
});
}
async findVariation(
siteId: string,
externalProductId: string,
externalVariationId: string
): Promise<Variation | null> {
return await this.variationModel.findOne({
where: { siteId, externalProductId, externalVariationId },
});
}
async updateWpProduct(
siteId: string,
productId: string,
product: UpdateWpProductDTO
) {
let existingProduct = await this.findProduct(siteId, productId);
if (existingProduct) {
existingProduct.name = product.name;
existingProduct.sku = product.sku;
product.regular_price &&
(existingProduct.regular_price = product.regular_price);
product.sale_price && (existingProduct.sale_price = product.sale_price);
await this.wpProductModel.save(existingProduct);
}
}
async updateWpProductVaritation(
siteId: string,
productId: string,
variationId: string,
variation: UpdateVariationDTO
) {
const existingVariation = await this.findVariation(
siteId,
productId,
variationId
);
if (existingVariation) {
existingVariation.name = variation.name;
existingVariation.sku = variation.sku;
variation.regular_price &&
(existingVariation.regular_price = variation.regular_price);
variation.sale_price &&
(existingVariation.sale_price = variation.sale_price);
await this.variationModel.save(existingVariation);
}
}
async syncProductAndVariations(
siteId: string,
product: WpProduct,
variations: Variation[]
) {
// 1. 处理产品同步
let existingProduct = await this.findProduct(siteId, String(product.id));
if (existingProduct) {
existingProduct.name = product.name;
existingProduct.status = product.status;
existingProduct.type = product.type;
existingProduct.sku = product.sku;
product.regular_price &&
(existingProduct.regular_price = product.regular_price);
product.sale_price && (existingProduct.sale_price = product.sale_price);
existingProduct.on_sale = product.on_sale;
existingProduct.metadata = product.metadata;
await this.wpProductModel.save(existingProduct);
} else {
existingProduct = this.wpProductModel.create({
siteId,
externalProductId: String(product.id),
sku: product.sku,
status: product.status,
name: product.name,
type: product.type,
...(product.regular_price
? { regular_price: product.regular_price }
: {}),
...(product.sale_price ? { sale_price: product.sale_price } : {}),
on_sale: product.on_sale,
metadata: product.metadata,
});
await this.wpProductModel.save(existingProduct);
}
// 2. 处理变体同步
if (product.type === 'variable') {
const currentVariations = await this.variationModel.find({
where: { siteId, externalProductId: String(product.id) },
});
const syncedVariationIds = new Set(variations.map(v => String(v.id)));
const variationsToDelete = currentVariations.filter(
dbVariation =>
!syncedVariationIds.has(String(dbVariation.externalVariationId))
);
if (variationsToDelete.length > 0) {
const idsToDelete = variationsToDelete.map(v => v.id);
await this.variationModel.delete(idsToDelete);
}
for (const variation of variations) {
const existingVariation = await this.findVariation(
siteId,
String(product.id),
String(variation.id)
);
if (existingVariation) {
existingVariation.name = variation.name;
existingVariation.attributes = variation.attributes;
variation.regular_price &&
(existingVariation.regular_price = variation.regular_price);
variation.sale_price &&
(existingVariation.sale_price = variation.sale_price);
existingVariation.on_sale = variation.on_sale;
await this.variationModel.save(existingVariation);
} else {
const newVariation = this.variationModel.create({
siteId,
externalProductId: String(product.id),
externalVariationId: String(variation.id),
productId: existingProduct.id,
sku: variation.sku,
name: variation.name,
...(variation.regular_price
? { regular_price: variation.regular_price }
: {}),
...(variation.sale_price
? { sale_price: variation.sale_price }
: {}),
on_sale: variation.on_sale,
attributes: variation.attributes,
});
await this.variationModel.save(newVariation);
}
}
} else {
// 清理之前的变体
await this.variationModel.delete({
siteId,
externalProductId: String(product.id),
});
}
}
async syncVariation(siteId: string, productId: string, variation: Variation) {
let existingProduct = await this.findProduct(siteId, String(productId));
if (!existingProduct) return;
const existingVariation = await this.findVariation(
siteId,
String(productId),
String(variation.id)
);
if (existingVariation) {
existingVariation.name = variation.name;
existingVariation.attributes = variation.attributes;
variation.regular_price &&
(existingVariation.regular_price = variation.regular_price);
variation.sale_price &&
(existingVariation.sale_price = variation.sale_price);
existingVariation.on_sale = variation.on_sale;
await this.variationModel.save(existingVariation);
} else {
const newVariation = this.variationModel.create({
siteId,
externalProductId: String(productId),
externalVariationId: String(variation.id),
productId: existingProduct.id,
sku: variation.sku,
name: variation.name,
...(variation.regular_price
? { regular_price: variation.regular_price }
: {}),
...(variation.sale_price ? { sale_price: variation.sale_price } : {}),
on_sale: variation.on_sale,
attributes: variation.attributes,
});
await this.variationModel.save(newVariation);
}
}
async getProductList(param: QueryWpProductDTO) {
const { current = 1, pageSize = 10, name, siteId, status } = param;
// 第一步:先查询分页的产品
const where: any = {};
if (siteId) {
where.siteId = siteId;
}
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
if (nameFilter.length > 0) {
const nameConditions = nameFilter.map(word => Like(`%${word}%`));
where.name = And(...nameConditions);
}
if (status) {
where.status = status;
}
const products = await this.wpProductModel.find({
where,
skip: (current - 1) * pageSize,
take: pageSize,
});
const total = await this.wpProductModel.count({
where,
});
if (products.length === 0) {
return {
items: [],
total,
current,
pageSize,
};
}
const variationQuery = this.wpProductModel
.createQueryBuilder('wp_product')
.leftJoin(Variation, 'variation', 'variation.productId = wp_product.id')
.leftJoin(
Product,
'product',
'JSON_UNQUOTE(JSON_EXTRACT(wp_product.constitution, "$.sku")) = product.sku'
)
.leftJoin(
Product,
'variation_product',
'JSON_UNQUOTE(JSON_EXTRACT(variation.constitution, "$.sku")) = variation_product.sku'
)
.select([
'wp_product.*',
'variation.id as variation_id',
'variation.siteId as variation_siteId',
'variation.externalProductId as variation_externalProductId',
'variation.externalVariationId as variation_externalVariationId',
'variation.productId as variation_productId',
'variation.sku as variation_sku',
'variation.name as variation_name',
'variation.regular_price as variation_regular_price',
'variation.sale_price as variation_sale_price',
'variation.on_sale as variation_on_sale',
'variation.constitution as variation_constitution',
'product.name as product_name', // 关联查询返回 product.name
'variation_product.name as variation_product_name', // 关联查询返回 variation 的产品 name
])
.where('wp_product.id IN (:...ids)', {
ids: products.map(product => product.id),
});
const rawResult = await variationQuery.getRawMany();
// 数据转换
const items = rawResult.reduce((acc, row) => {
let product = acc.find(p => p.id === row.id);
if (!product) {
product = {
...Object.keys(row)
.filter(key => !key.startsWith('variation_'))
.reduce((obj, key) => {
obj[key] = row[key];
return obj;
}, {}),
variations: [],
};
acc.push(product);
}
if (row.variation_id) {
const variation: any = Object.keys(row)
.filter(key => key.startsWith('variation_'))
.reduce((obj, key) => {
obj[key.replace('variation_', '')] = row[key];
return obj;
}, {});
variation.constitution =
variation?.constitution?.map(item => {
const product = item.sku
? { ...item, name: row.variation_product_name }
: item;
return product;
}) || [];
product.variations.push(variation);
}
product.constitution =
product?.constitution?.map(item => {
const productWithName = item.sku
? { ...item, name: row.product_name }
: item;
return productWithName;
}) || [];
return acc;
}, []);
return {
items,
total,
current,
pageSize,
};
}
/**
* SKU
* @param sku SKU
* @param excludeSiteId ID
* @param excludeProductId ID
* @param excludeVariationId ID
* @returns
*/
async isSkuDuplicate(
sku: string,
excludeSiteId?: string,
excludeProductId?: string,
excludeVariationId?: string
): Promise<boolean> {
if (!sku) return false;
const where: any = { sku };
const varWhere: any = { sku };
if (excludeVariationId) {
varWhere.siteId = Not(excludeSiteId);
varWhere.externalProductId = Not(excludeProductId);
varWhere.externalVariationId = Not(excludeVariationId);
} else {
where.externalProductId = Not(excludeProductId);
where.externalProductId = Not(excludeProductId);
}
const productDuplicate = await this.wpProductModel.findOne({
where,
});
if (productDuplicate) {
return true;
}
const variationDuplicate = await this.variationModel.findOne({
where: varWhere,
});
return !!variationDuplicate;
}
/**
*
*/
async setConstitution(
id: number,
isProduct: boolean,
constitution: { sku: string; quantity: number }[]
): Promise<void> {
if (isProduct) {
// 更新产品的 constitution
const product = await this.wpProductModel.findOne({ where: { id } });
if (!product) {
throw new Error(`未找到 ID 为 ${id} 的产品`);
}
product.constitution = constitution;
await this.wpProductModel.save(product);
} else {
// 更新变体的 constitution
const variation = await this.variationModel.findOne({ where: { id } });
if (!variation) {
throw new Error(`未找到 ID 为 ${id} 的变体`);
}
variation.constitution = constitution;
await this.variationModel.save(variation);
}
}
async delWpProduct(siteId: string, productId: string) {
const product = await this.wpProductModel.findOne({
where: { siteId, externalProductId: productId },
});
if (!product) throw new Error('未找到该商品');
await this.variationModel.delete({ siteId, externalProductId: productId });
await this.wpProductModel.delete({ siteId, externalProductId: productId });
}
}

9
src/utils/helper.util.ts Normal file
View File

@ -0,0 +1,9 @@
import { randomBytes } from 'crypto';
export function generateUniqueId(): string {
return randomBytes(16).toString('hex');
}
export function sleep(timestamp: number) {
return new Promise(resolve => setTimeout(resolve, timestamp));
}

View File

@ -0,0 +1,77 @@
import {
FindManyOptions,
FindOptionsWhere,
Repository,
SelectQueryBuilder,
} from 'typeorm';
import { PaginationParams } from '../interface';
import { plainToInstance } from 'class-transformer';
export class PaginationResult<T> {
current: number;
pageSize: number;
total: number;
items: T[];
}
export async function paginate<T>(
repository: Repository<T> | SelectQueryBuilder<T>,
options: {
pagination: PaginationParams;
where?: FindOptionsWhere<T> | FindOptionsWhere<T>[];
relations?: string[];
order?: Record<string, 'ASC' | 'DESC'>;
transformerClass?: new () => T; // 可选:用于指定需要转换的类
}
): Promise<PaginationResult<T>> {
const {
pagination: { current, pageSize },
relations = [],
where = {},
order = {},
transformerClass,
} = options;
if (repository instanceof SelectQueryBuilder) {
let queryBuilder = repository;
const total = await queryBuilder.getCount();
queryBuilder = queryBuilder.skip((current - 1) * pageSize).take(pageSize);
const items = await queryBuilder.getRawMany();
const transformedItems = transformerClass
? plainToInstance(transformerClass, items)
: items;
return {
items: transformedItems,
total,
current,
pageSize,
};
}
const items = await repository.find({
where,
skip: (current - 1) * pageSize,
take: pageSize,
relations,
order,
} as FindManyOptions<T>);
const total = await repository.count({
where,
take: pageSize,
relations,
order,
} as FindManyOptions<T>);
const transformedItems = transformerClass
? plainToInstance(transformerClass, items)
: items;
return {
items: transformedItems,
total,
current,
pageSize,
};
}

View File

@ -0,0 +1,22 @@
import { ApiProperty, Type } from '@midwayjs/swagger';
/**
*
*/
export function PaginatedWrapper<T>(ItemCls: Type<T>): Type<any> {
class PaginatedResponse {
@ApiProperty({ description: '当前页码', example: 1 })
page: number;
@ApiProperty({ description: '每页大小', example: 10 })
pageSize: number;
@ApiProperty({ description: '总记录数', example: 100 })
total: number;
@ApiProperty({ description: '数据列表', type: [ItemCls] })
items: T[];
}
return PaginatedResponse;
}

View File

@ -0,0 +1,40 @@
import { ApiProperty, Type } from '@midwayjs/swagger';
/**
*
*/
export function SuccessWrapper<T>(ResourceCls: Type<T>): Type<any> {
class SuccessResponse {
@ApiProperty({ description: '状态码', example: 200 })
code: number;
@ApiProperty({ description: '是否成功', example: true })
success: boolean;
@ApiProperty({ description: '消息内容', example: '操作成功' })
message: string;
@ApiProperty({ description: '响应数据', type: ResourceCls })
data: T;
}
return SuccessResponse;
}
export function SuccessArrayWrapper<T>(ResourceCls: Type<T>): Type<any> {
class SuccessArrayResponse {
@ApiProperty({ description: '状态码', example: 200 })
code: number;
@ApiProperty({ description: '是否成功', example: true })
success: boolean;
@ApiProperty({ description: '消息内容', example: '操作成功' })
message: string;
@ApiProperty({ description: '响应数据', type: ResourceCls, isArray: true })
data: T[];
}
return SuccessArrayResponse;
}

View File

@ -0,0 +1,50 @@
// 通用响应结构
export class ApiResponse<T> {
code: number;
success: boolean;
message: string;
data: T;
}
/**
*
* @param data
* @param message
* @param code
* @returns ApiResponse
*/
export function successResponse<T>(
data: T = {} as T,
message = '操作成功',
code = 200
): ApiResponse<T> {
return {
success: true,
message,
data,
code,
};
}
/**
*
* @param message
* @param code
* @param data
* @returns ApiResponse
*/
export function errorResponse<T>(
message = '操作失败',
code = 500,
data: T = {} as T
): ApiResponse<T> {
return {
success: false,
message: message,
data,
code,
};
}

View File

@ -0,0 +1,20 @@
import { createApp, close, createHttpRequest } from '@midwayjs/mock';
import { Framework } from '@midwayjs/koa';
describe('test/controller/home.test.ts', () => {
it('should POST /api/get_user', async () => {
// create app
const app = await createApp<Framework>();
// make request
const result = await createHttpRequest(app).get('/api/get_user').query({ uid: 123 });
// use expect by jest
expect(result.status).toBe(200);
expect(result.body.message).toBe('OK');
// close app
await close(app);
});
});

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"stripInternal": true,
"skipLibCheck": true,
"pretty": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["./typings", "./node_modules/@types"],
"outDir": "dist",
"rootDir": "src"
},
"exclude": ["*.js", "*.ts", "dist", "node_modules", "test"]
}