forked from yoone/API
init
This commit is contained in:
parent
a5e9fd7a63
commit
037df80080
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "./node_modules/mwts/",
|
||||||
|
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
...require('mwts/.prettierrc.json')
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
const { Bootstrap } = require('@midwayjs/bootstrap')
|
||||||
|
Bootstrap.run()
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
|
||||||
|
coveragePathIgnorePatterns: ['<rootDir>/test/'],
|
||||||
|
};
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { MidwayConfig } from '@midwayjs/core';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
koa: {
|
||||||
|
port: null,
|
||||||
|
},
|
||||||
|
} as MidwayConfig;
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Inject, Controller } from '@midwayjs/core';
|
||||||
|
import { Context } from '@midwayjs/koa';
|
||||||
|
|
||||||
|
@Controller('/')
|
||||||
|
export class APIController {
|
||||||
|
@Inject()
|
||||||
|
ctx: Context;
|
||||||
|
}
|
||||||
|
|
@ -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 || '获取失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 || '创建失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 || '获取失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 || '更新失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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('获取失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 || '产品变体更新失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createCustomParamDecorator } from '@midwayjs/core';
|
||||||
|
|
||||||
|
export const USER_KEY = 'USER_KEY';
|
||||||
|
|
||||||
|
// 定义装饰器
|
||||||
|
export function User(): ParameterDecorator {
|
||||||
|
return createCustomParamDecorator(USER_KEY, {});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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[];
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
) {}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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; // 软删除时间
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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; // 用户是否启用
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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) {}
|
||||||
|
}
|
||||||
|
|
@ -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) {}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 } })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue