forked from yoone/API
1
0
Fork 0

Compare commits

..

No commits in common. "1a1f7c8515cbdb040847a9fd95ab9760f3632fa2" and "1c828f49c975d1b3e1c6c4b34054ee3417a0bfe5" have entirely different histories.

32 changed files with 546 additions and 1270 deletions

View File

@ -45,8 +45,5 @@
"url": "" "url": ""
}, },
"author": "anonymous", "author": "anonymous",
"license": "MIT", "license": "MIT"
"devDependencies": {
"@midwayjs/mock": "^3.20.11"
}
} }

View File

@ -16,7 +16,6 @@ import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity'; import { OrderRefund } from '../entity/order_refund.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity'; import { OrderRefundItem } from '../entity/order_retund_item.entity';
import { OrderSale } from '../entity/order_sale.entity'; import { OrderSale } from '../entity/order_sale.entity';
import { OrderSaleOriginal } from '../entity/order_item_original.entity';
import { OrderShipping } from '../entity/order_shipping.entity'; import { OrderShipping } from '../entity/order_shipping.entity';
import { Service } from '../entity/service.entity'; import { Service } from '../entity/service.entity';
import { ShippingAddress } from '../entity/shipping_address.entity'; import { ShippingAddress } from '../entity/shipping_address.entity';
@ -58,7 +57,6 @@ export default {
OrderRefund, OrderRefund,
OrderRefundItem, OrderRefundItem,
OrderSale, OrderSale,
OrderSaleOriginal,
OrderShipment, OrderShipment,
ShipmentItem, ShipmentItem,
Shipment, Shipment,

View File

@ -3,24 +3,24 @@ export default {
koa: { koa: {
port: 7001, port: 7001,
}, },
// typeorm: {
// dataSource: {
// default: {
// host: '13.212.62.127',
// username: 'root',
// password: 'Yoone!@.2025',
// },
// },
// },
typeorm: { typeorm: {
dataSource: { dataSource: {
default: { default: {
host: 'localhost', host: '13.212.62.127',
username: 'root', username: 'root',
password: '12345678', password: 'Yoone!@.2025',
}, },
}, },
}, },
// typeorm: {
// dataSource: {
// default: {
// host: '127.0.0.1',
// username: 'root',
// password: '123456',
// },
// },
// },
cors: { cors: {
origin: '*', // 允许所有来源跨域请求 origin: '*', // 允许所有来源跨域请求
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
@ -35,35 +35,17 @@ export default {
{ {
id: '-1', id: '-1',
siteName: 'Admin', siteName: 'Admin',
email: '444693295@qq.com', email: 'tom@yoonevape.com',
}, },
{ {
id: '2', id: '2',
wpApiUrl: 'http://t2-shop.local/', wpApiUrl: 'http://localhost:10004',
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
siteName: 'Local', siteName: 'Local',
email: '444693295@qq.com', email: 'tom@yoonevape.com',
emailPswd: 'lulin91.', emailPswd: 'lulin91.',
}, },
{
id: '3',
wpApiUrl: 'http://t1-shop.local/',
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
siteName: 'Local-test-2',
email: '444693295@qq.com',
emailPswd: 'lulin91.',
},
// {
// id: '2',
// wpApiUrl: 'http://localhost:10004',
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
// siteName: 'Local',
// email: 'tom@yoonevape.com',
// emailPswd: 'lulin91.',
// },
], ],
freightcom: { freightcom: {
url: 'https://customer-external-api.ssd-test.freightcom.com', url: 'https://customer-external-api.ssd-test.freightcom.com',
@ -76,11 +58,4 @@ export default {
customerNumber: '0006122480', customerNumber: '0006122480',
contractId: '0044168528', contractId: '0044168528',
}, },
uniExpress: {
url: 'https://sjqa.uniexpress.org', // 测试环境url
// url: 'https://sj.uniexpress.ca', //正式环境url
clientId: '101018',
clientSecret: 'cbcb51bea204f3f69c47b5280064408e',
customerNo: 2067,
}
} as MidwayConfig; } as MidwayConfig;

View File

@ -22,7 +22,7 @@ import { errorResponse, successResponse } from '../utils/response.util';
import { LogisticsService } from '../service/logistics.service'; import { LogisticsService } from '../service/logistics.service';
import { ShippingDetailsDTO } from '../dto/freightcom.dto'; import { ShippingDetailsDTO } from '../dto/freightcom.dto';
import { ShippingAddress } from '../entity/shipping_address.entity'; import { ShippingAddress } from '../entity/shipping_address.entity';
import { QueryServiceDTO, ShipmentBookDTO, ShipmentFeeBookDTO } from '../dto/logistics.dto'; import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto';
import { User } from '../decorator/user.decorator'; import { User } from '../decorator/user.decorator';
@Controller('/logistics') @Controller('/logistics')
@ -145,19 +145,6 @@ export class LogisticsController {
} }
} }
@ApiOkResponse({
type: BooleanRes,
})
@Post('/deleteShipment/:id')
async deleteShipment(@Param('id') id: number) {
try {
const res = await this.logisticsService.removeShipment(id);
return successResponse(res);
} catch (error) {
return errorResponse(error?.message || '删除运单失败' );
}
}
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })
@ -168,43 +155,8 @@ export class LogisticsController {
@User() user @User() user
) { ) {
try { try {
const res: any = await this.logisticsService.createShipment(orderId, data, user.id); await this.logisticsService.createShipment(orderId, data, user.id);
return successResponse(res.data); return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse(
{type: BooleanRes}
)
@Post('/getShipmentFee')
async getShipmentFee(
@Body() data: ShipmentFeeBookDTO
) {
try {
const fee = await this.logisticsService.getShipmentFee(data);
return successResponse(fee);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@ApiOkResponse(
{type: BooleanRes}
)
@Post('/getShipmentLabel/:shipmentId')
async getShipmentLabel(
@Param('shipmentId') shipmentId: number
) {
try {
const res = await this.logisticsService.getShipmentLabel(shipmentId);
if (res.data.data[0].status === 'Success') {
return successResponse({ content: res.data.data[0].labelContent });
} else {
return errorResponse(res.data.data[0].errors);
}
} catch (error) { } catch (error) {
return errorResponse(error?.message || '创建失败'); return errorResponse(error?.message || '创建失败');
} }
@ -221,22 +173,9 @@ export class LogisticsController {
} }
} }
@ApiOkResponse()
@Post('/updateState/:shipmentId')
async updateShipmentState(
@Param('shipmentId') shipmentId: number
) {
try {
const data = await this.logisticsService.updateShipmentStateById(shipmentId);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '更新运单状态失败')
}
}
@ApiOkResponse() @ApiOkResponse()
@Del('/shipment/:id') @Del('/shipment/:id')
async delShipment(@Param('id') id: number, @User() user) { async delShipment(@Param('id') id: string, @User() user) {
try { try {
const data = await this.logisticsService.delShipment(id, user.id); const data = await this.logisticsService.delShipment(id, user.id);
return successResponse(data); return successResponse(data);
@ -247,11 +186,11 @@ export class LogisticsController {
} }
@ApiOkResponse() @ApiOkResponse()
@Post('/getOrderList') @Post('/getTrackingNumber')
async getOrderList(@Query('number') number: string) { async getTrackingNumber(@Query('number') number: string) {
try { try {
return successResponse( return successResponse(
await this.logisticsService.getOrderList(number) await this.logisticsService.getTrackingNumber(number)
); );
} catch (error) { } catch (error) {
return errorResponse(error?.message || '获取失败'); return errorResponse(error?.message || '获取失败');
@ -259,11 +198,11 @@ export class LogisticsController {
} }
@ApiOkResponse() @ApiOkResponse()
@Post('/getListByOrderId') @Post('/getListByTrackingId')
async getListByOrderId(@Query('id') orderId: number) { async getListByTrackingId(@Query('shipment_id') shipment_id: string) {
try { try {
return successResponse( return successResponse(
await this.logisticsService.getListByOrderId(orderId) await this.logisticsService.getListByTrackingId(shipment_id)
); );
} catch (error) { } catch (error) {
return errorResponse(error?.message || '获取失败'); return errorResponse(error?.message || '获取失败');

View File

@ -68,12 +68,11 @@ export class OrderController {
@Get('/getOrders') @Get('/getOrders')
async getOrders( async getOrders(
@Query() @Query()
param: QueryOrderDTO, param: QueryOrderDTO
@User() user
) { ) {
try { try {
const count = await this.orderService.getOrderStatus(param); const count = await this.orderService.getOrderStatus(param);
const data = await this.orderService.getOrders(param, user.id); const data = await this.orderService.getOrders(param);
return successResponse({ return successResponse({
...data, ...data,
count, count,
@ -109,22 +108,6 @@ export class OrderController {
} }
} }
@ApiOkResponse({
type: BooleanRes,
})
@Post('/updateOrderItems/:orderId')
async updateOrderItems(
@Param('orderId') orderId: number,
@Body() data: any
) {
try {
const res = await this.orderService.updateOrderSales(orderId, data);
return successResponse(res);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })

View File

@ -12,15 +12,11 @@ export class UserController {
@Inject() @Inject()
userService: UserService; userService: UserService;
@Inject()
ctx;
@ApiOkResponse({ @ApiOkResponse({
type: LoginRes, type: LoginRes,
}) })
@Post('/login') @Post('/login')
async login(@Body() body) { async login(@Body() body) {
this.ctx.logger.info('ip:', this.ctx.ip, '; path:', this.ctx.path, '; user:', body?.username);
try { try {
const result = await this.userService.login(body); const result = await this.userService.login(body);
return successResponse(result, '登录成功'); return successResponse(result, '登录成功');

View File

@ -75,7 +75,7 @@ export class WebhookController {
switch (topic) { switch (topic) {
case 'product.created': case 'product.created':
case 'product.updated': case 'product.updated':
const site = await this.wpProductService.getSite(siteId); const site = await this.wpProductService.geSite(siteId);
// 变体更新 // 变体更新
if (body.type === 'variation') { if (body.type === 'variation') {
const variation = await this.wpApiService.getVariation( const variation = await this.wpApiService.getVariation(

View File

@ -7,7 +7,6 @@ import {
Query, Query,
Put, Put,
Body, Body,
Config,
} from '@midwayjs/core'; } from '@midwayjs/core';
import { WpProductService } from '../service/wp_product.service'; import { WpProductService } from '../service/wp_product.service';
import { errorResponse, successResponse } from '../utils/response.util'; import { errorResponse, successResponse } from '../utils/response.util';
@ -20,21 +19,9 @@ import {
UpdateWpProductDTO, UpdateWpProductDTO,
} from '../dto/wp_product.dto'; } from '../dto/wp_product.dto';
import { WPService } from '../service/wp.service'; import { WPService } from '../service/wp.service';
import { WpSite } from '../interface';
@Controller('/wp_product') @Controller('/wp_product')
export class WpProductController { export class WpProductController {
@Inject()
wpService: WPService;
@Config('wpSite')
sites: WpSite[];
getSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
@Inject() @Inject()
private readonly wpProductService: WpProductService; private readonly wpProductService: WpProductService;
@ -86,22 +73,6 @@ export class WpProductController {
} }
} }
@ApiOkResponse({
type: BooleanRes
})
@Post('/updateState/:id')
async updateWPProductState(
@Param('id') id: number,
@Body() body: any, // todo
) {
try {
const res = await this.wpProductService.updateProductStatus(id, body?.status, body?.stock_status);
return successResponse(res);
} catch (error) {
return errorResponse(error.message);
}
}
/** /**
* *
* @param productId ID * @param productId ID
@ -125,7 +96,7 @@ export class WpProductController {
if (isDuplicate) { if (isDuplicate) {
return errorResponse('SKU已存在'); return errorResponse('SKU已存在');
} }
const site = await this.wpProductService.getSite(siteId); const site = await this.wpProductService.geSite(siteId);
const result = await this.wpApiService.updateProduct( const result = await this.wpApiService.updateProduct(
site, site,
productId, productId,
@ -165,7 +136,7 @@ export class WpProductController {
if (isDuplicate) { if (isDuplicate) {
return errorResponse('SKU已存在'); return errorResponse('SKU已存在');
} }
const site = await this.wpProductService.getSite(siteId); const site = await this.wpProductService.geSite(siteId);
const result = await this.wpApiService.updateVariation( const result = await this.wpApiService.updateVariation(
site, site,
productId, productId,

View File

@ -219,9 +219,6 @@ export class PackagingEnvelope {
// } // }
export class ShippingDetailsDTO { export class ShippingDetailsDTO {
@ApiProperty()
shipmentFee: number;
@ApiProperty({ type: Location }) @ApiProperty({ type: Location })
@Rule(RuleType.object<Location>()) @Rule(RuleType.object<Location>())
origin: Location; origin: Location;

View File

@ -2,12 +2,25 @@ import { ApiProperty } from '@midwayjs/swagger';
import { ShippingDetailsDTO } from './freightcom.dto'; import { ShippingDetailsDTO } from './freightcom.dto';
import { Rule, RuleType } from '@midwayjs/validate'; import { Rule, RuleType } from '@midwayjs/validate';
import { OrderSale } from '../entity/order_sale.entity'; import { OrderSale } from '../entity/order_sale.entity';
import { ShipmentType } from '../enums/base.enum';
export class ShipmentBookDTO { export class ShipmentBookDTO {
@ApiProperty({ type: OrderSale, isArray: true }) @ApiProperty({ type: OrderSale, isArray: true })
@Rule(RuleType.array<OrderSale>()) @Rule(RuleType.array<OrderSale>())
sales: 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 }) @ApiProperty({ type: ShippingDetailsDTO })
@Rule(RuleType.object<ShippingDetailsDTO>()) @Rule(RuleType.object<ShippingDetailsDTO>())
details: ShippingDetailsDTO; details: ShippingDetailsDTO;
@ -21,50 +34,6 @@ export class ShipmentBookDTO {
orderIds?: number[]; orderIds?: number[];
} }
export class ShipmentFeeBookDTO {
@ApiProperty()
stockPointId: number;
@ApiProperty()
sender: string;
@ApiProperty()
startPhone: string;
@ApiProperty()
startPostalCode: string;
@ApiProperty()
pickupAddress: string;
// pickupWarehouse: number; // 此处用 stockPointId 到后端解析
@ApiProperty()
shipperCountryCode: string;
@ApiProperty()
receiver: string;
@ApiProperty()
city: string;
@ApiProperty()
province: string;
@ApiProperty()
country: string;
@ApiProperty()
postalCode: string;
@ApiProperty()
deliveryAddress: string;
@ApiProperty()
receiverPhone: string;
@ApiProperty()
receiverEmail: string;
@ApiProperty()
length: number;
@ApiProperty()
width: number;
@ApiProperty()
height: number;
@ApiProperty()
dimensionUom: string;
@ApiProperty()
weight: number;
@ApiProperty()
weightUom: string;
}
export class PaymentMethodDTO { export class PaymentMethodDTO {
@ApiProperty() @ApiProperty()
id: string; id: string;

View File

@ -1,8 +1,8 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { ErpOrderStatus } from '../enums/base.enum'; import { ErpOrderStatus } from '../enums/base.enum';
import { Rule, RuleType } from '@midwayjs/validate'; import { Rule, RuleType } from '@midwayjs/validate';
// import { Shipment } from '../entity/shipment.entity'; import { Shipment } from '../entity/shipment.entity';
// import { ShipmentItem } from '../entity/shipment_item.entity'; import { ShipmentItem } from '../entity/shipment_item.entity';
export class OrderAddress { export class OrderAddress {
@ApiProperty() @ApiProperty()
@ -90,10 +90,6 @@ export class QueryOrderSalesDTO {
@Rule(RuleType.bool().default(false)) @Rule(RuleType.bool().default(false))
isSource: boolean; isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' }) @ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number()) @Rule(RuleType.number())
current: number; current: number;
@ -119,10 +115,10 @@ export class QueryOrderSalesDTO {
endDate: Date; endDate: Date;
} }
// export class Tracking extends Shipment { export class Tracking extends Shipment {
// @ApiProperty({ type: ShipmentItem, isArray: true }) @ApiProperty({ type: ShipmentItem, isArray: true })
// products?: ShipmentItem[]; products?: ShipmentItem[];
// } }
export class CreateOrderNoteDTO { export class CreateOrderNoteDTO {
@ApiProperty() @ApiProperty()

View File

@ -8,7 +8,7 @@ import {
SuccessArrayWrapper, SuccessArrayWrapper,
SuccessWrapper, SuccessWrapper,
} from '../utils/response-wrapper.util'; } from '../utils/response-wrapper.util';
import { OrderStatusCountDTO } from './order.dto'; import { OrderStatusCountDTO, Tracking } from './order.dto';
import { SiteConfig } from './site.dto'; import { SiteConfig } from './site.dto';
import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto'; import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto';
import { LoginResDTO } from './user.dto'; import { LoginResDTO } from './user.dto';
@ -107,8 +107,8 @@ export class OrderDetail extends Order {
@ApiProperty({ type: OrderRefundItem, isArray: true }) @ApiProperty({ type: OrderRefundItem, isArray: true })
refundItems: OrderRefundItem[]; refundItems: OrderRefundItem[];
// @ApiProperty({ type: Tracking, isArray: true }) @ApiProperty({ type: Tracking, isArray: true })
// trackings: Tracking[]; trackings: Tracking[];
@ApiProperty({ type: OrderNote, isArray: true }) @ApiProperty({ type: OrderNote, isArray: true })
notes: OrderNote[]; notes: OrderNote[];

View File

@ -7,13 +7,10 @@ import {
Entity, Entity,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
OneToOne,
JoinColumn
} from 'typeorm'; } from 'typeorm';
import { ErpOrderStatus, OrderStatus } from '../enums/base.enum'; import { ErpOrderStatus, OrderStatus } from '../enums/base.enum';
import { OrderAddress } from '../dto/order.dto'; import { OrderAddress } from '../dto/order.dto';
import { Exclude, Expose } from 'class-transformer'; import { Exclude, Expose } from 'class-transformer';
import { Shipment } from './shipment.entity';
@Entity('order') @Entity('order')
@Exclude() @Exclude()
@ -47,15 +44,6 @@ export class Order {
@Expose() @Expose()
orderStatus: ErpOrderStatus; // 订单状态 orderStatus: ErpOrderStatus; // 订单状态
@ApiProperty()
@Column({ name: 'shipment_id', nullable: true })
@Expose()
shipmentId: number;
@OneToOne(() => Shipment)
@JoinColumn({ name: 'shipment_id' })
shipment: Shipment;
@ApiProperty() @ApiProperty()
@Column() @Column()
@Expose() @Expose()

View File

@ -37,7 +37,7 @@ export class OrderItem {
externalOrderId: string; // WooCommerce 订单 ID externalOrderId: string; // WooCommerce 订单 ID
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column()
@Expose() @Expose()
externalOrderItemId: string; // WooCommerce 订单item ID externalOrderItemId: string; // WooCommerce 订单item ID

View File

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

View File

@ -1,8 +1,6 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer'; import { Exclude, Expose } from 'class-transformer';
import { import {
BeforeInsert,
BeforeUpdate,
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
@ -29,7 +27,7 @@ export class OrderSale {
siteId: string; // 来源站点唯一标识 siteId: string; // 来源站点唯一标识
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column()
@Expose() @Expose()
externalOrderItemId: string; // WooCommerce 订单item ID externalOrderItemId: string; // WooCommerce 订单item ID
@ -58,26 +56,6 @@ export class OrderSale {
@Expose() @Expose()
isPackage: boolean; isPackage: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
isYoone: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
isZex: boolean;
@ApiProperty({ nullable: true })
@Column({ type: 'int', nullable: true })
@Expose()
size: number | null;
@ApiProperty()
@Column({ default: false })
@Expose()
isYooneNew: boolean;
@ApiProperty({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
description: '创建时间', description: '创建时间',
@ -93,25 +71,4 @@ export class OrderSale {
@UpdateDateColumn() @UpdateDateColumn()
@Expose() @Expose()
updatedAt?: Date; updatedAt?: Date;
// === 自动计算逻辑 ===
@BeforeInsert()
@BeforeUpdate()
setFlags() {
if (!this.name) return;
const lower = this.name.toLowerCase();
this.isYoone = lower.includes('yoone');
this.isZex = lower.includes('zex');
this.isYooneNew = this.isYoone && lower.includes('new');
let size: number | null = null;
const sizes = [3, 6, 9, 12, 15, 18];
for (const s of sizes) {
if (lower.includes(s.toString())) {
size = s;
break;
}
}
this.size = size;
}
} }

View File

@ -13,7 +13,7 @@ export class OrderShipment {
@ApiProperty() @ApiProperty()
@Column() @Column()
shipment_id: number; shipment_id: string;
@ApiProperty() @ApiProperty()
@Column() @Column()

View File

@ -4,49 +4,17 @@ import {
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
PrimaryGeneratedColumn, PrimaryColumn,
UpdateDateColumn, UpdateDateColumn,
ManyToOne,
OneToOne,
JoinColumn
} from 'typeorm'; } from 'typeorm';
import { StockPoint } from './stock_point.entity';
import { Order } from './order.entity';
@Entity('shipment') @Entity('shipment')
@Exclude() @Exclude()
export class Shipment { export class Shipment {
@ApiProperty() @ApiProperty()
@PrimaryGeneratedColumn() @PrimaryColumn()
@Expose() @Expose()
id: number; id: string;
@ApiProperty()
@Column({ nullable: true })
order_id: number;
@ApiProperty()
@OneToOne(() => Order)
@JoinColumn({ name: 'order_id' })
order: Order;
@ApiProperty()
@Column({ name: 'stock_point_id' })
@Expose()
stockPointId: string;
@ManyToOne(() => StockPoint)
@JoinColumn({ name: 'stock_point_id' })
stockPoint: StockPoint;
@Column({ nullable: false, default: 0})
@Expose()
fee: number;
@ApiProperty()
@Column({ nullable: true })
@Expose()
tracking_id: string;
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@ -62,10 +30,6 @@ export class Shipment {
@Expose() @Expose()
state?: string; state?: string;
@Column({ nullable: false, default: false })
@Expose()
finished: boolean;
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
type?: string; type?: string;

View File

@ -4,13 +4,9 @@ import {
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
} from 'typeorm'; } from 'typeorm';
import { Shipment } from './shipment.entity';
import { Order } from './order.entity';
@Entity('shipment_item') @Entity('shipment_item')
@Exclude() @Exclude()
@ -21,19 +17,17 @@ export class ShipmentItem {
id: number; id: number;
@ApiProperty() @ApiProperty()
@Column()
@Expose() @Expose()
@ManyToOne(() => Shipment) shipment_id: string;
@JoinColumn({ name: 'shipment_id' })
shipment_id: number;
@ApiProperty() @ApiProperty()
@Column()
@Expose() @Expose()
@ManyToOne(() => Order) productId: number;
@JoinColumn({ name: 'order_id' })
order_id: number;
@ApiProperty() @ApiProperty()
@Column({ nullable: true }) @Column()
@Expose() @Expose()
name: string; name: string;

View File

@ -7,9 +7,7 @@ import {
Entity, Entity,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
OneToMany,
} from 'typeorm'; } from 'typeorm';
import { Shipment } from './shipment.entity';
@Entity('stock_point') @Entity('stock_point')
export class StockPoint extends BaseEntity { export class StockPoint extends BaseEntity {
@ -17,9 +15,6 @@ export class StockPoint extends BaseEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@OneToMany(() => Shipment, shipment => shipment.stockPoint)
shipments: Shipment[];
@ApiProperty({ type: 'string' }) @ApiProperty({ type: 'string' })
@Column() @Column()
name: string; name: string;
@ -48,12 +43,6 @@ export class StockPoint extends BaseEntity {
@Column({ default: false }) @Column({ default: false })
isB: boolean; isB: boolean;
@Column({ default: 'uniuni' })
upStreamName: string;
@Column()
upStreamStockPointId: number;
@ApiProperty({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
description: '创建时间', description: '创建时间',

View File

@ -7,7 +7,7 @@ import {
Entity, Entity,
} from 'typeorm'; } from 'typeorm';
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { ProductStatus, ProductStockStatus, ProductType } from '../enums/base.enum'; import { ProductStatus, ProductType } from '../enums/base.enum';
@Entity('wp_product') @Entity('wp_product')
@Unique(['siteId', 'externalProductId']) // 确保产品的唯一性 @Unique(['siteId', 'externalProductId']) // 确保产品的唯一性
@ -56,15 +56,6 @@ export class WpProduct {
@Column({ type: 'enum', enum: ProductStatus }) @Column({ type: 'enum', enum: ProductStatus })
status: ProductStatus; status: ProductStatus;
@ApiProperty({ description: '上下架状态', enum: ProductStockStatus })
@Column({
name: 'stock_status',
type: 'enum',
enum: ProductStockStatus,
default: ProductStockStatus.INSTOCK
})
stockStatus: ProductStockStatus;
@ApiProperty({ description: '常规价格', type: Number }) @ApiProperty({ description: '常规价格', type: Number })
@Column('decimal', { precision: 10, scale: 2, nullable: true }) @Column('decimal', { precision: 10, scale: 2, nullable: true })
regular_price: number; // 常规价格 regular_price: number; // 常规价格

View File

@ -9,16 +9,9 @@ export enum ProductStatus {
INHERIT = 'inherit', // 继承状态 INHERIT = 'inherit', // 继承状态
} }
export enum ProductStockStatus {
INSTOCK = 'instock',
OUT_OF_STOCK = 'outofstock',
ON_BACK_ORDER = 'onbackorder',
}
export enum ProductType { export enum ProductType {
SIMPLE = 'simple', SIMPLE = 'simple',
VARIABLE = 'variable', VARIABLE = 'variable',
WOOSB = 'woosb',
} }
export enum PurchaseOrderStatus { export enum PurchaseOrderStatus {

View File

@ -1,9 +1,6 @@
import { FORMAT, ILogger, Inject, Logger } from '@midwayjs/core'; import { FORMAT, ILogger, Inject, Logger } from '@midwayjs/core';
import { IJob, Job } from '@midwayjs/cron'; import { IJob, Job } from '@midwayjs/cron';
import { LogisticsService } from '../service/logistics.service'; import { LogisticsService } from '../service/logistics.service';
import { Repository } from 'typeorm';
import { Shipment } from '../entity/shipment.entity';
import { InjectEntityModel } from '@midwayjs/typeorm';
@Job({ @Job({
cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE, cronTime: FORMAT.CRONTAB.EVERY_PER_30_MINUTE,
@ -23,65 +20,3 @@ export class SyncShipmentJob implements IJob {
} }
onComplete?(result: any) {} onComplete?(result: any) {}
} }
@Job({
cronTime: '0 0 12 * * *', // 每天12点执行
start: true
})
export class SyncUniuniShipmentJob implements IJob{
@Logger()
logger: ILogger;
@Inject()
logisticsService: LogisticsService;
@InjectEntityModel(Shipment)
shipmentModel: Repository<Shipment>
uniuniStateCodes = {
'190': 'ORDER_RECEIVED',
'192': 'CUSTOM_HOLD',
'195': 'GATEWAY_TRANSIT_OUT',
'198': 'CUSTOM_RELEASE_DIRECT',
'199': 'GATEWAY_TRANSIT',
'200': 'PARCEL_SCANNED',
'202': 'IN_TRANSIT',
'203': 'DELIVERED',
'204': 'TRANSSHIPMENT',
'206': 'WRONG_ADDRESS_FROM_TRANSIT',
'207': 'PARCEL_LOST',
'209': 'OTHER_EXCEPTION',
'211': 'RETURN_OFFICE_FROM_TRANSIT',
'212': 'WRONG_ADDRESS_FROM_RECEIVE',
'213': 'STORAGE_30_DAYS_FROM_OFFICE',
'214': 'STORAGE_30_DAYS_AFTER_SCAN',
'215': 'PARCEL_ABANDONED',
'216': 'SELF_PICK_UP',
'217': 'TRANSSHIPMENT_COMPLETE',
'218': 'SCANNED_PARCEL_MISSING',
'219': 'WRONG_ROUTE_PARCEL',
'220': 'SECOND_DELIVERY',
'221': 'RETURNED_PARCEL_SCANNED',
'222': 'REJECTED_PARCEL_FROM_TRANSIT',
'223': 'CHANGED_ORDER_RESENT',
'224': 'RESENT_ORDER_VOIDED',
'225': 'FORWARDED_3RDPARTY',
'226': 'STORAGE_3RDPARTY_SERVICE_POINT',
'228': 'SECOND_DELIVERED',
'229': 'DROP_OFF_SERVICE_POINTS',
'230': 'RETURN TO SENDER WAREHOUSE',
'231': 'FAILED_DELIVERY_RETRY1',
'232': 'FAILED_DELIVERY_RETRY2',
'255': 'Gateway_To_Gateway_Transit'
};
async onTick() {
const shipments:Shipment[] = await this.shipmentModel.findBy({ finished: false });
shipments.forEach(shipment => {
this.logisticsService.updateShipmentState(shipment);
});
}
onComplete(result: any) {
}
}

View File

@ -157,7 +157,7 @@ export class CanadaPostService {
} }
/** 取消运单 */ /** 取消运单 */
async cancelShipment(shipmentId: number) { async cancelShipment(shipmentId: string) {
const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment/${shipmentId}`; const url = `${this.url}/rs/${this.customerNumber}/${this.customerNumber}/shipment/${shipmentId}`;
const res = await axios.delete(url, { const res = await axios.delete(url, {

View File

@ -98,7 +98,7 @@ export class FreightcomService {
} }
// 查询运单详细信息 // 查询运单详细信息
async getShipment(shipment_id: number) { async getShipment(shipment_id: string) {
let { status, data } = await axios.request({ let { status, data } = await axios.request({
url: `${this.apiUrl}/shipment/${shipment_id}`, url: `${this.apiUrl}/shipment/${shipment_id}`,
method: 'GET', method: 'GET',
@ -117,7 +117,7 @@ export class FreightcomService {
} }
// 取消发货 // 取消发货
async cancelShipment(shipment_id: number) { async cancelShipment(shipment_id: string) {
const response = await axios.request({ const response = await axios.request({
url: `${this.apiUrl}/shipment/${shipment_id}`, url: `${this.apiUrl}/shipment/${shipment_id}`,
method: 'DELETE', method: 'DELETE',

View File

@ -3,34 +3,30 @@ import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
import { Service } from '../entity/service.entity'; import { Service } from '../entity/service.entity';
import { In, IsNull, Like, Repository } from 'typeorm'; import { In, IsNull, Like, Repository } from 'typeorm';
import { ShippingAddress } from '../entity/shipping_address.entity'; import { ShippingAddress } from '../entity/shipping_address.entity';
// import { ShipmentBookDTO } from '../dto/logistics.dto';
import { Order } from '../entity/order.entity'; import { Order } from '../entity/order.entity';
import { Shipment } from '../entity/shipment.entity'; import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity'; import { ShipmentItem } from '../entity/shipment_item.entity';
import { OrderShipment } from '../entity/order_shipment.entity'; import { OrderShipment } from '../entity/order_shipment.entity';
import { QueryServiceDTO, ShipmentBookDTO, ShipmentFeeBookDTO } from '../dto/logistics.dto'; import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto';
import { import {
ErpOrderStatus, ErpOrderStatus,
OrderStatus, OrderStatus,
ShipmentType, ShipmentType,
StockRecordOperationType, StockRecordOperationType,
} from '../enums/base.enum'; } from '../enums/base.enum';
// import { generateUniqueId } from '../utils/helper.util'; import { generateUniqueId } from '../utils/helper.util';
import { FreightcomService } from './freightcom.service'; import { FreightcomService } from './freightcom.service';
import { StockRecord } from '../entity/stock_record.entity'; import { StockRecord } from '../entity/stock_record.entity';
import { Stock } from '../entity/stock.entity'; import { Stock } from '../entity/stock.entity';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { WPService } from './wp.service'; import { WPService } from './wp.service';
import { WpSite } from '../interface'; import { WpSite } from '../interface';
// import { Product } from '../entity/product.entty'; import { Product } from '../entity/product.entty';
import { ShippingDetailsDTO } from '../dto/freightcom.dto'; import { ShippingDetailsDTO } from '../dto/freightcom.dto';
import { CanadaPostService } from './canadaPost.service'; import { CanadaPostService } from './canadaPost.service';
import { OrderItem } from '../entity/order_item.entity'; import { OrderItem } from '../entity/order_item.entity';
import { OrderSale } from '../entity/order_sale.entity'; import { OrderSale } from '../entity/order_sale.entity';
import { UniExpressService } from './uni_express.service';
import { StockPoint } from '../entity/stock_point.entity';
import { OrderService } from './order.service';
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
@Provide() @Provide()
export class LogisticsService { export class LogisticsService {
@ -46,9 +42,6 @@ export class LogisticsService {
@InjectEntityModel(Order) @InjectEntityModel(Order)
orderModel: Repository<Order>; orderModel: Repository<Order>;
@InjectEntityModel(StockPoint)
stockPointModel: Repository<StockPoint>
@InjectEntityModel(OrderSale) @InjectEntityModel(OrderSale)
orderSaleModel: Repository<OrderSale>; orderSaleModel: Repository<OrderSale>;
@ -70,22 +63,16 @@ export class LogisticsService {
@Inject() @Inject()
canadaPostService: CanadaPostService; canadaPostService: CanadaPostService;
@Inject()
uniExpressService: UniExpressService;
@Inject() @Inject()
wpService: WPService; wpService: WPService;
@Inject()
orderService: OrderService;
@Inject() @Inject()
dataSourceManager: TypeORMDataSourceManager; dataSourceManager: TypeORMDataSourceManager;
@Config('wpSite') @Config('wpSite')
sites: WpSite[]; sites: WpSite[];
getSite(id: string): WpSite { geSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id); let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx]; return this.sites[idx];
} }
@ -126,27 +113,6 @@ export class LogisticsService {
return await this.shippingAddressModel.save(shippingAddress); return await this.shippingAddressModel.save(shippingAddress);
} }
async updateShipmentState(shipment: Shipment) {
try {
const data = await this.uniExpressService.getOrderStatus(shipment.return_tracking_number);
console.log('updateShipmentState data:', data);
shipment.state = data.data[0].state;
if (shipment.state in [203, 215, 216, 230]) { // todo,写常数
shipment.finished = true;
}
this.shipmentModel.save(shipment);
return shipment.state;
} catch (error) {
throw error;
// throw new Error(`更新运单状态失败 ${error.message}`);
}
}
async updateShipmentStateById(id: number) {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
return this.updateShipmentState(shipment);
}
async updateShippingAddress(id: number, shippingAddress: ShippingAddress) { async updateShippingAddress(id: number, shippingAddress: ShippingAddress) {
const address = await this.shippingAddressModel.findOneBy({ id }); const address = await this.shippingAddressModel.findOneBy({ id });
if (!address) { if (!address) {
@ -218,96 +184,8 @@ export class LogisticsService {
return [...rates, ...canadaPostRates]; return [...rates, ...canadaPostRates];
} }
async getShipmentLabel(shipmentId) {
try {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
if (!shipment) {
throw new Error('运单不存在');
}
return await this.uniExpressService.getLabel(shipment.return_tracking_number);
} catch (e) {
throw new Error('获取运单失败');
}
}
async removeShipment(shipmentId: number) {
try {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
if (shipment.state !== '190') { // todo写常数
throw new Error('订单当前状态无法删除');
}
const order: Order = await this.orderModel.findOneBy({ id: shipment.order_id });
const dataSource = this.dataSourceManager.getDataSource('default');
let transactionError = undefined;
await dataSource.transaction(async manager => {
const orderRepo = manager.getRepository(Order);
const shipmentRepo = manager.getRepository(Shipment);
order.shipmentId = null;
orderRepo.save(order);
shipmentRepo.remove(shipment);
const res = await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
console.log('res', res.data); // todo
await orderRepo.save(order);
}).catch(error => {
transactionError = error;
});
if (transactionError !== undefined) {
throw new Error(`数据库同步错误: ${transactionError.message}`);
}
try {
// 同步订单状态到woocommerce
const site = await this.getSite(order.siteId);
if (order.status === OrderStatus.COMPLETED) {
await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.PROCESSING,
});
order.status = OrderStatus.PROCESSING;
}
order.orderStatus = ErpOrderStatus.PROCESSING;
this.orderModel.save(order);
// todo 同步到wooccommerce删除运单信息
await this.wpService.deleteShipment(site, order.externalOrderId, shipment.tracking_id);
} catch (error) {
console.log('同步到woocommerce失败', error);
return true;
}
return true;
} catch {
throw new Error('删除运单失败');
}
}
async getShipmentFee(data: ShipmentFeeBookDTO) {
try {
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
const reqBody = {
...convertKeysFromCamelToSnake(data),
pickup_warehouse: stock_point.upStreamStockPointId,
currency: 'CAD',
// item_description: data.sales, // todo: 货品信息
}
const resShipmentFee = await this.uniExpressService.getRates(reqBody);
if (resShipmentFee.status !== 'SUCCESS') {
throw new Error(resShipmentFee.ret_msg);
}
return resShipmentFee.data.totalAfterTax * 100;
} catch (e) {
throw e;
}
}
async createShipment(orderId: number, data: ShipmentBookDTO, userId: number) { async createShipment(orderId: number, data: ShipmentBookDTO, userId: number) {
const order = await this.orderModel.findOneBy({ id: orderId }); const order = await this.orderModel.findOneBy({ id: orderId });
const { sales } = data;
if (!order) { if (!order) {
throw new Error('订单不存在'); throw new Error('订单不存在');
} }
@ -317,110 +195,147 @@ export class LogisticsService {
) { ) {
throw new Error('订单状态不正确 '); 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;
let resShipmentOrder; if (data.service_type === ShipmentType.FREIGHTCOM) {
try { const uuid = generateUniqueId();
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId }); data.details.reference_codes = [String(orderId)];
const reqBody = { const { id } = await this.freightcomService.createShipment({
sender: data.details.origin.contact_name, unique_id: uuid,
start_phone: data.details.origin.phone_number, payment_method_id: data.payment_method_id,
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''), service_id: data.service_id,
pickup_address: data.details.origin.address.address_line_1, details: data.details,
pickup_warehouse: stock_point.upStreamStockPointId,
shipper_country_code: data.details.origin.address.country,
receiver: data.details.destination.contact_name,
city: data.details.destination.address.city,
province: data.details.destination.address.region,
country: data.details.destination.address.country,
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
delivery_address: data.details.destination.address.address_line_1,
receiver_phone: data.details.destination.phone_number.number,
receiver_email: data.details.destination.email_addresses,
// item_description: data.sales, // todo: 货品信息
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
currency: 'CAD',
custom_field: {
'order_id': order.externalOrderId
}
}
// 添加运单
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
// 记录物流信息,并将订单状态转到完成
if (resShipmentOrder.status === 'SUCCESS') {
order.orderStatus = ErpOrderStatus.COMPLETED;
} else {
throw new Error('运单生成失败');
}
const dataSource = this.dataSourceManager.getDataSource('default');
let transactionError = undefined;
let shipmentId = undefined;
await dataSource.transaction(async manager => {
const orderRepo = manager.getRepository(Order);
const shipmentRepo = manager.getRepository(Shipment);
const tracking_provider = 'UniUni'; // todo: id未确定后写进常数
// 同步物流信息到woocommerce
const site = await this.getSite(order.siteId);
const res = await this.wpService.createShipment(site, order.externalOrderId, {
tracking_number: resShipmentOrder.data.tno,
tracking_provider: tracking_provider,
}); });
if (order.orderStatus === ErpOrderStatus.COMPLETED) { const service = await this.serviceModel.findOneBy({
const shipment = await shipmentRepo.save({ id: data.service_id,
tracking_provider: tracking_provider,
tracking_id: res.data.tracking_id,
unique_id: resShipmentOrder.data.uni_order_sn,
stockPointId: String(data.stockPointId), // todo
state: resShipmentOrder.data.uni_status_code,
return_tracking_number: resShipmentOrder.data.tno,
fee: data.details.shipmentFee,
order: order
}); });
order.shipmentId = shipment.id; shipment = {
shipmentId = shipment.id; id,
} unique_id: uuid,
tracking_provider: service?.carrier_name || '',
// 同步订单状态到woocommerce
if (order.status !== OrderStatus.COMPLETED) {
await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.COMPLETED,
});
order.status = OrderStatus.COMPLETED;
}
order.orderStatus = ErpOrderStatus.COMPLETED;
await orderRepo.save(order);
}).catch(error => {
transactionError = error
});
if (transactionError !== undefined) {
console.log('err', transactionError);
throw transactionError;
}
// 更新产品发货信息
this.orderService.updateOrderSales(order.id, sales);
return {
data: {
shipmentId
}
}; };
} catch (error) { } else if (data.service_type === ShipmentType.CANADAPOST) {
if (resShipmentOrder.status === 'SUCCESS') { const shipmentRequest = {
await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno); '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);
} }
throw new Error(`上游请求错误:${error}`); 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() { async syncShipment() {
@ -471,8 +386,8 @@ export class LogisticsService {
} }
} }
async getShipment(id: number) { async getShipment(id: string) {
const orderShipments: OrderShipment[] = await this.orderShipmentModel.find({ const orderShipments = await this.orderShipmentModel.find({
where: { shipment_id: id }, where: { shipment_id: id },
}); });
if (!orderShipments || orderShipments.length === 0) return; if (!orderShipments || orderShipments.length === 0) return;
@ -493,7 +408,7 @@ export class LogisticsService {
const order = await this.orderModel.findOneBy({ const order = await this.orderModel.findOneBy({
id: orderShipment.order_id, id: orderShipment.order_id,
}); });
const site = this.getSite(order.siteId); const site = this.geSite(order.siteId);
await this.wpService.updateOrder(site, order.externalOrderId, { await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.COMPLETED, status: OrderStatus.COMPLETED,
}); });
@ -504,8 +419,7 @@ export class LogisticsService {
} }
} }
async delShipment(id: number, userId: number) { async delShipment(id: string, userId: number) {
const shipment = await this.shipmentModel.findOneBy({ id }); const shipment = await this.shipmentModel.findOneBy({ id });
if (!shipment) throw new Error('物流不存在'); if (!shipment) throw new Error('物流不存在');
if (shipment.type === ShipmentType.FREIGHTCOM) { if (shipment.type === ShipmentType.FREIGHTCOM) {
@ -556,7 +470,7 @@ export class LogisticsService {
}); });
} }
async getOrderList(number: string) { async getTrackingNumber(number: string) {
const orders = await this.orderModel.find({ const orders = await this.orderModel.find({
where: { where: {
externalOrderId: Like(`%${number}%`), externalOrderId: Like(`%${number}%`),
@ -571,51 +485,88 @@ export class LogisticsService {
})); }));
} }
async getListByOrderId(id: number) { async getListByTrackingId(id: string) {
const item = await this.orderItem.find({ where: { orderId: id } }); const qb = `
const saleItem = await this.orderSaleModel.find({ where: { orderId: id } }); SELECT
oi.name,
oi.quantity,
CASE
WHEN oi.externalVariationId != 0 THEN v.constitution
ELSE p.constitution
END AS constitution
FROM order_item oi
LEFT JOIN wp_product p ON oi.siteId=p.siteId AND oi.externalProductId=p.externalProductId
LEFT JOIN variation v ON oi.siteId=v.siteId AND oi.externalVariationId=v.externalVariationId
WHERE oi.orderId=?
`;
const saleItem = await this.orderSaleModel.query(qb, [id]);
const allSkus = new Set<string>();
for (const item of saleItem) {
if (!item.constitution) continue;
try {
item.constitution.forEach(c => allSkus.add(c.sku));
} catch (e) {
console.warn('Invalid constitution JSON:', item.constitution);
}
}
console.log(allSkus);
const skuList = Array.from(allSkus);
let skuNameMap = new Map<string, string>();
return { if (skuList.length > 0) {
item, const placeholders = skuList.map(() => '?').join(', ');
saleItem const productRows = await this.orderSaleModel.query(
}; `SELECT sku, name FROM product WHERE sku IN (${placeholders})`,
skuList
);
skuNameMap = new Map(productRows.map(p => [p.sku, p.name]));
}
for (const item of saleItem) {
if (!item.constitution) continue;
try {
item.constitution = item.constitution.map(c => ({
...c,
name: skuNameMap.get(c.sku) || null,
}));
} catch (e) {
item.constitution = [];
}
}
return saleItem;
} }
async getList(param: Record<string, any>) { async getList(param: Record<string, any>) {
const { const {
pageSize = 10, pageSize = 10,
current = 1, current = 1,
return_tracking_number, primary_tracking_number,
stockPointId, stockPointId,
externalOrderId,
} = param; } = param;
console.log(pageSize, current, primary_tracking_number, stockPointId);
const offset = pageSize * (current - 1); const offset = pageSize * (current - 1);
const values: any[] = []; const values: any[] = [];
let whereClause = 'WHERE 1=1'; let whereClause = 'WHERE 1=1';
if (return_tracking_number) { if (primary_tracking_number) {
whereClause += ' AND s.return_tracking_number LIKE ?'; whereClause += ' AND s.primary_tracking_number LIKE ?';
values.push(`%${return_tracking_number}%`); values.push(`%${primary_tracking_number}%`);
} }
if (stockPointId) { if (stockPointId) {
whereClause += ' AND sp.id = ?'; whereClause += ' AND os.stockPointId = ?';
values.push(stockPointId); values.push(stockPointId);
} }
// todo增加订单号搜索
if (externalOrderId) {
whereClause += ' AND o.externalOrderId = ?';
values.push(externalOrderId);
}
const sql = ` const sql = `
SELECT s.*, sp.name, o.externalOrderId, o.siteId SELECT s.*, sp.name
FROM shipment s FROM shipment s
LEFT JOIN \`order\` o ON s.order_id = o.id LEFT JOIN order_shipment os ON s.id = os.shipment_id
LEFT JOIN stock_point sp ON s.stock_point_id = sp.id LEFT JOIN stock_point sp ON os.stockPointId = sp.id
${whereClause} ${whereClause}
GROUP BY s.id
ORDER BY s.createdAt DESC ORDER BY s.createdAt DESC
LIMIT ?, ? LIMIT ?, ?
`; `;
@ -623,12 +574,12 @@ export class LogisticsService {
values.push(offset, Number(pageSize)); values.push(offset, Number(pageSize));
const items = await this.serviceModel.query(sql, values); const items = await this.serviceModel.query(sql, values);
// 单独计算总数 // 单独计算总数
const countSql = ` const countSql = `
SELECT COUNT(DISTINCT s.id) as total SELECT COUNT(DISTINCT s.id) as total
FROM shipment s FROM shipment s
LEFT JOIN \`order\` o ON s.order_id = o.id LEFT JOIN order_shipment os ON s.id = os.shipment_id
LEFT JOIN stock_point sp ON s.stock_point_id = sp.id
${whereClause} ${whereClause}
`; `;

View File

@ -29,7 +29,6 @@ import { WpSite } from '../interface';
import { ShipmentItem } from '../entity/shipment_item.entity'; import { ShipmentItem } from '../entity/shipment_item.entity';
import { UpdateStockDTO } from '../dto/stock.dto'; import { UpdateStockDTO } from '../dto/stock.dto';
import { StockService } from './stock.service'; import { StockService } from './stock.service';
import { OrderSaleOriginal } from '../entity/order_item_original.entity';
@Provide() @Provide()
export class OrderService { export class OrderService {
@ -45,18 +44,12 @@ export class OrderService {
@InjectEntityModel(Order) @InjectEntityModel(Order)
orderModel: Repository<Order>; orderModel: Repository<Order>;
@InjectEntityModel(User)
userModel: Repository<User>;
@InjectEntityModel(OrderItem) @InjectEntityModel(OrderItem)
orderItemModel: Repository<OrderItem>; orderItemModel: Repository<OrderItem>;
@InjectEntityModel(OrderSale) @InjectEntityModel(OrderSale)
orderSaleModel: Repository<OrderSale>; orderSaleModel: Repository<OrderSale>;
@InjectEntityModel(OrderSaleOriginal)
orderSaleOriginalModel: Repository<OrderSaleOriginal>;
@InjectEntityModel(WpProduct) @InjectEntityModel(WpProduct)
wpProductModel: Repository<WpProduct>; wpProductModel: Repository<WpProduct>;
@ -209,6 +202,7 @@ export class OrderService {
el => el.key === '_wc_order_attribution_utm_source' el => el.key === '_wc_order_attribution_utm_source'
)?.value || ''; )?.value || '';
order.customer_email = order?.billing?.email || order?.shipping?.email; order.customer_email = order?.billing?.email || order?.shipping?.email;
const entity = plainToClass(Order, order); const entity = plainToClass(Order, order);
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: order.externalOrderId, siteId: siteId }, where: { externalOrderId: order.externalOrderId, siteId: siteId },
@ -549,7 +543,7 @@ export class OrderService {
current, current,
pageSize, pageSize,
customer_email, customer_email,
}, userId = undefined) { }) {
const parameters: any[] = []; const parameters: any[] = [];
// 基础查询 // 基础查询
@ -632,14 +626,6 @@ export class OrderService {
totalQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_created <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
const user = await this.userModel.findOneBy({id: userId});
if (user?.permissions?.includes('order-10-days')) {
sqlQuery += ` AND o.date_created >= ?`;
totalQuery += ` AND o.date_created >= ?`;
const tenDaysAgo = new Date();
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);
parameters.push(tenDaysAgo.toISOString());
}
// 处理 status 参数 // 处理 status 参数
if (status) { if (status) {
@ -749,202 +735,298 @@ export class OrderService {
return await query.getRawMany(); return await query.getRawMany();
} }
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { async getOrderSales({ siteId, startDate, endDate, current, pageSize, name }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const offset = (current - 1) * pageSize;
// ------------------------- const parameters: any[] = [startDate, endDate];
// 1. 查询总条数
// ------------------------- // 主查询:带分页
let sqlQuery = `
WITH product_purchase_counts AS (
SELECT
o.customer_email,
os.productId,
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')
GROUP BY o.customer_email, os.productId
)
SELECT
os.productId AS productId,
os.name AS name,
SUM(os.quantity) AS totalQuantity,
COUNT(DISTINCT os.orderId) AS totalOrders,
c.name AS categoryName,
COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
INNER JOIN product p ON os.productId = p.id
INNER JOIN category c ON p.categoryId = c.id
INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing')
`;
if (siteId) {
sqlQuery += ' AND os.siteId = ?';
parameters.push(siteId);
}
if (nameKeywords.length > 0) {
sqlQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
parameters.push(...nameKeywords.map(word => `%${word}%`));
}
sqlQuery += `
GROUP BY os.productId, os.name, c.name
ORDER BY totalQuantity DESC
LIMIT ? OFFSET ?
`;
parameters.push(pageSize, (current - 1) * pageSize);
const items = await this.orderSaleModel.query(sqlQuery, parameters);
// 总条数
const countParams: any[] = [startDate, endDate]; const countParams: any[] = [startDate, endDate];
let countSql = ` let totalCountQuery = `
SELECT COUNT(DISTINCT os.productId) AS totalCount SELECT COUNT(DISTINCT os.productId) AS totalCount
FROM order_sale os FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed','processing') AND o.status IN ('completed', 'processing')
`; `;
if (siteId) { if (siteId) {
countSql += ' AND os.siteId = ?'; totalCountQuery += ' AND os.siteId = ?';
countParams.push(siteId); countParams.push(siteId);
} }
if (nameKeywords.length > 0) { if (nameKeywords.length > 0) {
countSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
countParams.push(...nameKeywords.map(w => `%${w}%`)); countParams.push(...nameKeywords.map(word => `%${word}%`));
}
const [countResult] = await this.orderSaleModel.query(countSql, countParams);
const totalCount = Number(countResult?.totalCount || 0);
// -------------------------
// 2. 分页查询 product 基础信息
// -------------------------
const itemParams: any[] = [startDate, endDate];
let nameCondition = '';
if (nameKeywords.length > 0) {
nameCondition = ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')';
itemParams.push(...nameKeywords.map(w => `%${w}%`));
} }
let itemSql = ` const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams);
SELECT os.productId, os.name, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders
// 一次查询获取所有 yoone box 数量
const totalQuantityParams: any[] = [startDate, endDate];
let totalQuantityQuery = `
SELECT
SUM(os.quantity) AS totalQuantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%3%' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%6%' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%9%' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' AND os.name LIKE '%NEW%' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%15%' THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%18%' THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.name LIKE '%zex%' THEN os.quantity ELSE 0 END) AS zexQuantity
FROM order_sale os FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed','processing') AND o.status IN ('completed', 'processing')
`; `;
if (siteId) { if (siteId) {
itemSql += ' AND os.siteId = ?'; totalQuantityQuery += ' AND os.siteId = ?';
itemParams.push(siteId); totalQuantityParams.push(siteId);
} }
if (exceptPackage) {
itemSql += `
AND os.orderId IN (
SELECT orderId
FROM order_sale
GROUP BY orderId
HAVING COUNT(*) = 1
)
`;
}
itemSql += nameCondition;
itemSql += `
GROUP BY os.productId, os.name
ORDER BY totalQuantity DESC
LIMIT ? OFFSET ?
`;
itemParams.push(pageSize, offset);
const items = await this.orderSaleModel.query(itemSql, itemParams);
// -------------------------
// 3. 批量统计当前页 product 历史复购
// -------------------------
if (items.length > 0) {
const productIds = items.map(i => i.productId);
const pcParams: any[] = [...productIds, startDate, endDate];
if (siteId) pcParams.push(siteId);
let pcSql = `
SELECT
os.productId,
SUM(CASE WHEN t.purchaseIndex = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN t.purchaseIndex = 1 THEN os.orderId END) AS firstOrderCount,
SUM(CASE WHEN t.purchaseIndex = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN t.purchaseIndex = 2 THEN os.orderId END) AS secondOrderCount,
SUM(CASE WHEN t.purchaseIndex = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN t.purchaseIndex = 3 THEN os.orderId END) AS thirdOrderCount,
SUM(CASE WHEN t.purchaseIndex > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN t.purchaseIndex > 3 THEN os.orderId END) AS moreThirdOrderCount
FROM order_sale os
INNER JOIN (
SELECT o2.id AS orderId,
@idx := IF(@prev_email = o2.customer_email, @idx + 1, 1) AS purchaseIndex,
@prev_email := o2.customer_email
FROM \`order\` o2
CROSS JOIN (SELECT @idx := 0, @prev_email := '') vars
WHERE o2.status IN ('completed','processing')
ORDER BY o2.customer_email, o2.date_paid
) t ON t.orderId = os.orderId
WHERE os.productId IN (${productIds.map(() => '?').join(',')})
AND os.orderId IN (
SELECT id FROM \`order\`
WHERE date_paid BETWEEN ? AND ?
${siteId ? 'AND siteId = ?' : ''}
)
`;
if (exceptPackage) {
pcSql += `
AND os.orderId IN (
SELECT orderId
FROM order_sale
GROUP BY orderId
HAVING COUNT(*) = 1
)
`;
}
pcSql += `
GROUP BY os.productId
`;
console.log('------3.5-----', pcSql, pcParams, exceptPackage);
const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
const pcMap = new Map<number, any>();
pcResults.forEach(r => pcMap.set(r.productId, r));
items.forEach(i => {
const r = pcMap.get(i.productId) || {};
i.firstOrderYOONEBoxCount = Number(r.firstOrderYOONEBoxCount || 0);
i.firstOrderCount = Number(r.firstOrderCount || 0);
i.secondOrderYOONEBoxCount = Number(r.secondOrderYOONEBoxCount || 0);
i.secondOrderCount = Number(r.secondOrderCount || 0);
i.thirdOrderYOONEBoxCount = Number(r.thirdOrderYOONEBoxCount || 0);
i.thirdOrderCount = Number(r.thirdOrderCount || 0);
i.moreThirdOrderYOONEBoxCount = Number(r.moreThirdOrderYOONEBoxCount || 0);
i.moreThirdOrderCount = Number(r.moreThirdOrderCount || 0);
});
}
// -------------------------
// 4. 总量统计(时间段 + siteId
// -------------------------
const totalParams: any[] = [startDate, endDate];
const yooneParams: any[] = [startDate, endDate];
let totalSql = `
SELECT
SUM(os.quantity) AS totalQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed','processing')
`;
let yooneSql = `
SELECT
SUM(CASE WHEN os.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed','processing')
`;
if (siteId) {
totalSql += ' AND os.siteId = ?';
totalParams.push(siteId);
yooneSql += ' AND os.siteId = ?';
yooneParams.push(siteId);
}
if (nameKeywords.length > 0) { if (nameKeywords.length > 0) {
totalSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')'; totalQuantityQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
totalParams.push(...nameKeywords.map(w => `%${w}%`)); totalQuantityParams.push(...nameKeywords.map(word => `%${word}%`));
} }
const [totalResult] = await this.orderSaleModel.query(totalSql, totalParams); const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams);
const [yooneResult] = await this.orderSaleModel.query(yooneSql, yooneParams);
return { return {
items, items,
total: totalCount, // ✅ 总条数 total: totalCountResult[0]?.totalCount || 0,
totalQuantity: Number(totalResult.totalQuantity || 0), totalQuantity: Number(totalQuantityResult.totalQuantity || 0),
yoone3Quantity: Number(yooneResult.yoone3Quantity || 0), yoone3Quantity: Number(totalQuantityResult.yoone3Quantity || 0),
yoone6Quantity: Number(yooneResult.yoone6Quantity || 0), yoone6Quantity: Number(totalQuantityResult.yoone6Quantity || 0),
yoone9Quantity: Number(yooneResult.yoone9Quantity || 0), yoone9Quantity: Number(totalQuantityResult.yoone9Quantity || 0),
yoone12Quantity: Number(yooneResult.yoone12Quantity || 0), yoone12Quantity: Number(totalQuantityResult.yoone12Quantity || 0),
yoone12QuantityNew: Number(yooneResult.yoone12QuantityNew || 0), yoone12QuantityNew: Number(totalQuantityResult.yoone12QuantityNew || 0),
yoone15Quantity: Number(yooneResult.yoone15Quantity || 0), yoone15Quantity: Number(totalQuantityResult.yoone15Quantity || 0),
yoone18Quantity: Number(yooneResult.yoone18Quantity || 0), yoone18Quantity: Number(totalQuantityResult.yoone18Quantity || 0),
zexQuantity: Number(yooneResult.zexQuantity || 0), zexQuantity: Number(totalQuantityResult.zexQuantity || 0),
current, current,
pageSize, pageSize,
}; };
} }
// async getOrderSales({
// siteId,
// startDate,
// endDate,
// current,
// pageSize,
// name,
// }: QueryOrderSalesDTO) {
// const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
// // 分页查询
// let sqlQuery = `
// WITH product_purchase_counts AS (
// SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id,os.productId) AS order_count
// FROM \`order\` o
// JOIN order_sale os ON o.id = os.orderId
// WHERE o.status IN ('completed', 'processing')
// GROUP BY o.customer_email, os.productId, os.name
// )
// SELECT
// os.productId AS productId,
// os.name AS name,
// SUM(os.quantity) AS totalQuantity,
// COUNT(distinct os.orderId) AS totalOrders,
// c.name AS categoryName,
// COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
// SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
// COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
// SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
// COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
// SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
// COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
// SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
// FROM order_sale os
// INNER JOIN \`order\` o ON o.id = os.orderId
// INNER JOIN product p ON os.productId = p.id
// INNER JOIN category c ON p.categoryId = c.id
// INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
// WHERE o.date_paid BETWEEN ? AND ?
// AND o.status IN ('processing', 'completed')
// `;
// const parameters: any[] = [startDate, endDate];
// if (siteId) {
// sqlQuery += ' AND os.siteId = ?';
// parameters.push(siteId);
// }
// if (nameKeywords.length > 0) {
// sqlQuery +=
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
// parameters.push(...nameKeywords.map(word => `%${word}%`));
// }
// sqlQuery += `
// GROUP BY os.productId, os.name, c.name
// ORDER BY totalQuantity DESC
// `;
// sqlQuery += ' LIMIT ? OFFSET ?';
// parameters.push(pageSize, (current - 1) * pageSize);
// // 执行查询并传递参数
// const items = await this.orderSaleModel.query(sqlQuery, parameters);
// let totalCountQuery = `
// SELECT COUNT(DISTINCT os.productId) AS totalCount
// FROM order_sale os
// INNER JOIN \`order\` o ON o.id = os.orderId
// INNER JOIN product p ON os.productId = p.id
// INNER JOIN category c ON p.categoryId = c.id
// WHERE o.date_created BETWEEN ? AND ?
// AND o.status IN ('processing', 'completed')
// `;
// const totalCountParameters: any[] = [startDate, endDate];
// if (siteId) {
// totalCountQuery += ' AND os.siteId = ?';
// totalCountParameters.push(siteId);
// }
// if (nameKeywords.length > 0) {
// totalCountQuery +=
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
// totalCountParameters.push(...nameKeywords.map(word => `%${word}%`));
// }
// const totalCountResult = await this.orderSaleModel.query(
// totalCountQuery,
// totalCountParameters
// );
// let totalQuantityQuery = `
// SELECT SUM(os.quantity) AS totalQuantity
// FROM order_sale os
// INNER JOIN \`order\` o ON o.id = os.orderId
// INNER JOIN product p ON os.productId = p.id
// INNER JOIN category c ON p.categoryId = c.id
// WHERE o.date_created BETWEEN ? AND ?
// AND o.status IN ('processing', 'completed')
// `;
// const totalQuantityParameters: any[] = [startDate, endDate];
// if (siteId) {
// totalQuantityQuery += ' AND os.siteId = ?';
// totalQuantityParameters.push(siteId);
// }
// const yoone3QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%3%"';
// const yoone6QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%6%"';
// const yoone9QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%9%"';
// const yoone12QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%12%"';
// const yoone15QuantityQuery =
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%15%"';
// const yooneParameters = [...totalQuantityParameters];
// if (nameKeywords.length > 0) {
// totalQuantityQuery +=
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
// totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`));
// }
// const totalQuantityResult = await this.orderSaleModel.query(
// totalQuantityQuery,
// totalQuantityParameters
// );
// const yoone3QuantityResult = await this.orderSaleModel.query(
// yoone3QuantityQuery,
// yooneParameters
// );
// const yoone6QuantityResult = await this.orderSaleModel.query(
// yoone6QuantityQuery,
// yooneParameters
// );
// const yoone9QuantityResult = await this.orderSaleModel.query(
// yoone9QuantityQuery,
// yooneParameters
// );
// const yoone12QuantityResult = await this.orderSaleModel.query(
// yoone12QuantityQuery,
// yooneParameters
// );
// const yoone15QuantityResult = await this.orderSaleModel.query(
// yoone15QuantityQuery,
// yooneParameters
// );
// return {
// items,
// total: totalCountResult[0]?.totalCount,
// totalQuantity: Number(
// totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone3Quantity: Number(
// yoone3QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone6Quantity: Number(
// yoone6QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone9Quantity: Number(
// yoone9QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone12Quantity: Number(
// yoone12QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// yoone15Quantity: Number(
// yoone15QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
// ),
// current,
// pageSize,
// };
// }
async getOrderItems({ async getOrderItems({
siteId, siteId,
startDate, startDate,
@ -1099,21 +1181,6 @@ export class OrderService {
} }
} }
// update order_sale_origin if not exist
try {
const order_sale_origin_count = await this.orderSaleOriginalModel.countBy({ orderId: id });
if (order_sale_origin_count === 0) {
sales.forEach(async sale => {
const { id: saleId, ...saleData } = sale;
await this.orderSaleOriginalModel.save(saleData);
});
}
} catch (error) {
console.log('create order sale origin error: ', error.message);
}
return { return {
...order, ...order,
siteName: site.siteName, siteName: site.siteName,
@ -1229,7 +1296,6 @@ export class OrderService {
return dataSource.transaction(async manager => { return dataSource.transaction(async manager => {
const orderRepo = manager.getRepository(Order); const orderRepo = manager.getRepository(Order);
const orderSaleRepo = manager.getRepository(OrderSale); const orderSaleRepo = manager.getRepository(OrderSale);
const OrderSaleOriginalRepo = manager.getRepository(OrderSaleOriginal);
const productRepo = manager.getRepository(Product); const productRepo = manager.getRepository(Product);
const order = await orderRepo.save({ const order = await orderRepo.save({
siteId: '-1', siteId: '-1',
@ -1247,7 +1313,7 @@ export class OrderService {
}); });
for (const sale of sales) { for (const sale of sales) {
const product = await productRepo.findOne({ where: { sku: sale.sku } }); const product = await productRepo.findOne({ where: { sku: sale.sku } });
const saleItem = { await orderSaleRepo.save({
orderId: order.id, orderId: order.id,
siteId: '-1', siteId: '-1',
externalOrderItemId: '-1', externalOrderItemId: '-1',
@ -1255,9 +1321,7 @@ export class OrderService {
name: product.name, name: product.name,
sku: sale.sku, sku: sale.sku,
quantity: sale.quantity, quantity: sale.quantity,
}; });
await orderSaleRepo.save(saleItem);
await OrderSaleOriginalRepo.save(saleItem);
} }
}); });
} }
@ -1298,43 +1362,4 @@ export class OrderService {
pageSize, pageSize,
}; };
} }
async updateOrderSales(orderId: number, sales: OrderSale[]) {
try {
const dataSource = this.dataSourceManager.getDataSource('default');
let transactionError = undefined;
await dataSource.transaction(async manager => {
const orderRepo = manager.getRepository(Order);
const orderSaleRepo = manager.getRepository(OrderSale);
const productRepo = manager.getRepository(Product);
const order = await orderRepo.findOneBy({ id: orderId });
let product:Product;
await orderSaleRepo.delete({ orderId });
for (const sale of sales) {
product = await productRepo.findOneBy({ sku: sale.sku });
await orderSaleRepo.save({
orderId,
siteId: order.siteId,
productId: product.id,
name: product.name,
sku: sale.sku,
quantity: sale.quantity,
// externalOrderItemId:
});
};
}).catch(error => {
transactionError = error;
});
if (transactionError !== undefined) {
throw new Error(`更新物流信息错误:${transactionError.message}`);
}
return true;
} catch (error) {
throw new Error(`更新发货产品失败:${error.message}`);
}
}
} }

View File

@ -1,156 +0,0 @@
import { Provide } from "@midwayjs/core";
import { Config } from '@midwayjs/decorator';
import axios, { AxiosRequestConfig } from 'axios';
@Provide()
export class UniExpressService {
@Config('uniExpress.url')
url;
@Config('uniExpress.clientId')
clientId;
@Config('uniExpress.clientSecret')
cliientSecret;
@Config('uniExpress.customerNo')
customerNo;
async getToken() {
const config: AxiosRequestConfig = {
method: 'POST',
url: `${this.url}/storeauth/customertoken`,
data: {
'grant_type': 'client_credentials',
'client_id': this.clientId,
'client_secret': this.cliientSecret
}
};
const tokenBody = await axios.request(config);
return tokenBody.data.data.access_token;
}
async getRates(data: any) {
try {
const requiredKeys = {
customer_no: this.customerNo,
pickup_warehouse: 1
};
const body = {
...requiredKeys,
...data
};
const token = await this.getToken();
const config: AxiosRequestConfig= {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
url: `${this.url}/orders/estimateshipping`,
data: body
};
return (await axios.request(config)).data;
} catch (error) {
console.log(error);
}
}
async createShipment(
data: any
) {
try {
const requiredKeys = {
customer_no: this.customerNo,
pickup_warehouse: 1
};
const body = {
...requiredKeys,
...data
};
const token = await this.getToken();
const config: AxiosRequestConfig= {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
url: `${this.url}/orders/createbusinessorder`,
data: body
};
const req = await axios.request(config);
const res = req.data;
return res;
} catch (error) {
console.log(error);
}
}
async deleteShipment(tno: string) {
const body = {
tno
};
const token = await this.getToken();
const config: AxiosRequestConfig= {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
url: `${this.url}/orders/cancelorder`,
data: body
};
return await axios.request(config);
}
async getLabel(tracking_number: string) {
const body = {
packageId: tracking_number
};
const token = await this.getToken();
const config: AxiosRequestConfig= {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
url: `${this.url}/orders/printlabel`,
data: body
};
return await axios.request(config);
}
async getOrdersByDate(from: string, to: string, page: number = 1, perPage: number = 100) {
try {
const token = await this.getToken();
const config: AxiosRequestConfig= {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
},
url: `${this.url}/orders`,
params: {
from, to, page, perPage
}
};
const res = (await axios.request(config)).data;
return res;
} catch (error) {
console.log(error);
}
}
async getOrderStatus(tracking_number: string) {
try {
const key = 'SMq45nJhQuNR3WHsJA6N'; // todo写进常数
const config: AxiosRequestConfig= {
method: 'GET',
url: `${this.url}/orders/trackinguniuni`,
params: {
key,
id: tracking_number
}
};
const res = (await axios.request(config)).data;
return res;
} catch (error) {
console.log(error);
}
}
}

View File

@ -70,7 +70,6 @@ export class UserService {
id: user.id, id: user.id,
deviceId, deviceId,
username: user.username, username: user.username,
isSuper: user.isSuper,
}); });
return { return {

View File

@ -4,7 +4,6 @@ import { WpSite } from '../interface';
import { WpProduct } from '../entity/wp_product.entity'; import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity'; import { Variation } from '../entity/variation.entity';
import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto'; import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto';
import { ProductStatus, ProductStockStatus } from '../enums/base.enum';
@Provide() @Provide()
export class WPService { export class WPService {
@ -223,27 +222,6 @@ export class WPService {
}); });
} }
/**
* WooCommerce
* @param productId ID
* @param status
* @param stock_status
*/
async updateProductStatus(
site: WpSite,
productId: string,
status: ProductStatus,
stock_status: ProductStockStatus
): Promise<Boolean> {
const res = await this.updateData(`/wc/v3/products/${productId}`, site, {
status,
manage_stock: false, // 为true的时候用quantity控制库存为false时直接用stock_status控制
stock_status,
});
console.log('res', res);
return res;
}
/** /**
* WooCommerce * WooCommerce
* @param productId ID * @param productId ID
@ -283,7 +261,7 @@ export class WPService {
site: WpSite, site: WpSite,
orderId: string, orderId: string,
data: Record<string, any> data: Record<string, any>
) { ): Promise<Boolean> {
const { wpApiUrl, consumerKey, consumerSecret } = site; const { wpApiUrl, consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
@ -298,26 +276,4 @@ export class WPService {
}; };
return await axios.request(config); return await axios.request(config);
} }
async deleteShipment(
site: WpSite,
orderId: string,
trackingId: string,
): Promise<Boolean> {
const { wpApiUrl, consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64'
);
console.log('del', orderId, trackingId);
// 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
const config: AxiosRequestConfig = {
method: 'DELETE',
url: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings/${trackingId}`,
headers: {
Authorization: `Basic ${auth}`,
},
};
return await axios.request(config);
}
} }

View File

@ -11,7 +11,6 @@ import {
UpdateWpProductDTO, UpdateWpProductDTO,
} from '../dto/wp_product.dto'; } from '../dto/wp_product.dto';
import { Product } from '../entity/product.entty'; import { Product } from '../entity/product.entty';
import { ProductStatus, ProductStockStatus } from '../enums/base.enum';
@Provide() @Provide()
export class WpProductService { export class WpProductService {
@ -27,7 +26,7 @@ export class WpProductService {
@InjectEntityModel(Variation) @InjectEntityModel(Variation)
variationModel: Repository<Variation>; variationModel: Repository<Variation>;
getSite(id: string): WpSite { geSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id); let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx]; return this.sites[idx];
} }
@ -46,7 +45,7 @@ export class WpProductService {
} }
async syncSite(siteId: string) { async syncSite(siteId: string) {
const site = this.getSite(siteId); const site = this.geSite(siteId);
const products = await this.wpApiService.getProducts(site); const products = await this.wpApiService.getProducts(site);
for (const product of products) { for (const product of products) {
const variations = const variations =
@ -57,21 +56,6 @@ export class WpProductService {
} }
} }
// 控制产品上下架
async updateProductStatus(id: number, status: ProductStatus, stock_status: ProductStockStatus) {
const wpProduct = await this.wpProductModel.findOneBy({ id });
const site = await this.getSite(wpProduct.siteId);
wpProduct.status = status;
wpProduct.stockStatus = stock_status;
const res = await this.wpApiService.updateProductStatus(site, wpProduct.externalProductId, status, stock_status);
if (res === true) {
this.wpProductModel.save(wpProduct);
return true;
} else {
return res;
}
}
async findProduct( async findProduct(
siteId: string, siteId: string,
externalProductId: string externalProductId: string

View File

@ -1,36 +0,0 @@
function camelToSnake(str: string): string {
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}
function snakeToCamel(str: string): string {
return str.replace(/(_[\w])/g, (match) =>
match[1].toUpperCase()
);
}
export function convertKeysFromSnakeToCamel<T>(obj: T): T {
if (Array.isArray(obj)) {
return obj.map(convertKeysFromSnakeToCamel) as unknown as T;
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = snakeToCamel(key);
acc[newKey] = convertKeysFromSnakeToCamel((obj as any)[key]);
return acc;
}, {} as any);
}
return obj;
}
export function convertKeysFromCamelToSnake<T>(obj: T): T {
if (Array.isArray(obj)) {
return obj.map(convertKeysFromCamelToSnake) as unknown as T;
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = camelToSnake(key);
acc[newKey] = convertKeysFromCamelToSnake((obj as any)[key]);
return acc;
}, {} as any);
}
return obj;
}