forked from yoone/API
1
0
Fork 0

Compare commits

..

No commits in common. "ca1a15c75daaa7bdac97178e4d4d332324fe6d6b" and "e4e4ceb7c1075d0cb4cc7bf05a402ad46ea302c6" have entirely different histories.

11 changed files with 115 additions and 207 deletions

View File

@ -22,7 +22,7 @@ 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, ShipmentFeeBookDTO } from '../dto/logistics.dto';
import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto';
import { User } from '../decorator/user.decorator';
@Controller('/logistics')
@ -180,7 +180,7 @@ export class LogisticsController {
)
@Post('/getShipmentFee')
async getShipmentFee(
@Body() data: ShipmentFeeBookDTO
@Body() data: ShipmentBookDTO
) {
try {
const fee = await this.logisticsService.getShipmentFee(data);
@ -222,7 +222,7 @@ export class LogisticsController {
}
@ApiOkResponse()
@Post('/updateState/:shipmentId')
@Post('/updateState/:id')
async updateShipmentState(
@Param('shipmentId') shipmentId: number
) {
@ -247,11 +247,11 @@ export class LogisticsController {
}
@ApiOkResponse()
@Post('/getOrderList')
async getOrderList(@Query('number') number: string) {
@Post('/getTrackingNumber')
async getTrackingNumber(@Query('number') number: string) {
try {
return successResponse(
await this.logisticsService.getOrderList(number)
await this.logisticsService.getTrackingNumber(number)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');
@ -259,11 +259,11 @@ export class LogisticsController {
}
@ApiOkResponse()
@Post('/getListByOrderId')
async getListByOrderId(@Query('id') orderId: number) {
@Post('/getListByTrackingId')
async getListByTrackingId(@Query('shipment_id') shipment_id: number) {
try {
return successResponse(
await this.logisticsService.getListByOrderId(orderId)
await this.logisticsService.getListByTrackingId(shipment_id)
);
} catch (error) {
return errorResponse(error?.message || '获取失败');

View File

@ -68,12 +68,11 @@ export class OrderController {
@Get('/getOrders')
async getOrders(
@Query()
param: QueryOrderDTO,
@User() user
param: QueryOrderDTO
) {
try {
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({
...data,
count,

View File

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

View File

@ -21,50 +21,6 @@ export class ShipmentBookDTO {
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 {
@ApiProperty()
id: string;

View File

@ -90,10 +90,6 @@ export class QueryOrderSalesDTO {
@Rule(RuleType.bool().default(false))
isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;

View File

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

View File

@ -22,7 +22,6 @@ export class OrderSaleOriginal {
@ApiProperty()
@ManyToOne(() => Order)
@JoinColumn({ name: 'order_id' })
@Column({ name: 'order_id' })
@Expose()
orderId: number; // 订单 ID
@ -32,7 +31,7 @@ export class OrderSaleOriginal {
siteId: string; // 来源站点唯一标识
@ApiProperty()
@Column({ nullable: true })
@Column()
@Expose()
externalOrderItemId: string; // WooCommerce 订单item ID

View File

@ -8,7 +8,7 @@ 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, ShipmentFeeBookDTO } from '../dto/logistics.dto';
import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto';
import {
ErpOrderStatus,
OrderStatus,
@ -30,7 +30,6 @@ 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()
export class LogisticsService {
@ -129,7 +128,6 @@ export class LogisticsService {
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;
@ -137,13 +135,12 @@ export class LogisticsService {
this.shipmentModel.save(shipment);
return shipment.state;
} catch (error) {
throw error;
// throw new Error(`更新运单状态失败 ${error.message}`);
throw new Error(`更新运单状态失败 ${error.message}`);
}
}
async updateShipmentStateById(id: number) {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
const shipment:Shipment = await this.shipmentModel.findOneBy({ id : id });
return this.updateShipmentState(shipment);
}
@ -220,7 +217,7 @@ export class LogisticsService {
async getShipmentLabel(shipmentId) {
try {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
const shipment:Shipment = await this.shipmentModel.findOneBy({id: shipmentId});
if (!shipment) {
throw new Error('运单不存在');
}
@ -232,11 +229,11 @@ export class LogisticsService {
async removeShipment(shipmentId: number) {
try {
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
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 order:Order = await this.orderModel.findOneBy({id: shipment.order_id});
const dataSource = this.dataSourceManager.getDataSource('default');
let transactionError = undefined;
await dataSource.transaction(async manager => {
@ -286,19 +283,34 @@ export class LogisticsService {
}
}
async getShipmentFee(data: ShipmentFeeBookDTO) {
async getShipmentFee(data: ShipmentBookDTO) {
try {
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId});
const reqBody = {
...convertKeysFromCamelToSnake(data),
sender: data.details.origin.contact_name,
start_phone: data.details.origin.phone_number,
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
pickup_address: data.details.origin.address.address_line_1,
pickup_warehouse: stock_point.upStreamStockPointId,
currency: 'CAD',
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',
}
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;
@ -320,7 +332,7 @@ export class LogisticsService {
let resShipmentOrder;
try {
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId});
const reqBody = {
sender: data.details.origin.contact_name,
start_phone: data.details.origin.phone_number,
@ -410,12 +422,10 @@ export class LogisticsService {
// 更新产品发货信息
this.orderService.updateOrderSales(order.id, sales);
return {
data: {
shipmentId
}
};
} catch (error) {
return { data: {
shipmentId
} };
} catch(error) {
if (resShipmentOrder.status === 'SUCCESS') {
await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno);
}
@ -472,7 +482,7 @@ export class LogisticsService {
}
async getShipment(id: number) {
const orderShipments: OrderShipment[] = await this.orderShipmentModel.find({
const orderShipments:OrderShipment[] = await this.orderShipmentModel.find({
where: { shipment_id: id },
});
if (!orderShipments || orderShipments.length === 0) return;
@ -556,7 +566,7 @@ export class LogisticsService {
});
}
async getOrderList(number: string) {
async getTrackingNumber(number: string) {
const orders = await this.orderModel.find({
where: {
externalOrderId: Like(`%${number}%`),
@ -571,14 +581,56 @@ export class LogisticsService {
}));
}
async getListByOrderId(id: number) {
const item = await this.orderItem.find({ where: { orderId: id } });
const saleItem = await this.orderSaleModel.find({ where: { orderId: id } });
async getListByTrackingId(id: number) {
const qb = `
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 {
item,
saleItem
};
if (skuList.length > 0) {
const placeholders = skuList.map(() => '?').join(', ');
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>) {

View File

@ -45,9 +45,6 @@ export class OrderService {
@InjectEntityModel(Order)
orderModel: Repository<Order>;
@InjectEntityModel(User)
userModel: Repository<User>;
@InjectEntityModel(OrderItem)
orderItemModel: Repository<OrderItem>;
@ -549,7 +546,7 @@ export class OrderService {
current,
pageSize,
customer_email,
}, userId = undefined) {
}) {
const parameters: any[] = [];
// 基础查询
@ -632,14 +629,6 @@ export class OrderService {
totalQuery += ` AND o.date_created <= ?`;
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 参数
if (status) {
@ -749,7 +738,7 @@ export class OrderService {
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 offset = (current - 1) * pageSize;
@ -796,16 +785,6 @@ export class OrderService {
itemSql += ' AND os.siteId = ?';
itemParams.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
@ -823,17 +802,17 @@ export class OrderService {
const pcParams: any[] = [...productIds, startDate, endDate];
if (siteId) pcParams.push(siteId);
let pcSql = `
const 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,
COUNT(CASE WHEN t.purchaseIndex = 1 THEN 1 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,
COUNT(CASE WHEN t.purchaseIndex = 2 THEN 1 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,
COUNT(CASE WHEN t.purchaseIndex = 3 THEN 1 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
COUNT(CASE WHEN t.purchaseIndex > 3 THEN 1 END) AS moreThirdOrderCount
FROM order_sale os
INNER JOIN (
SELECT o2.id AS orderId,
@ -850,22 +829,9 @@ export class OrderService {
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>();
@ -887,17 +853,9 @@ export class OrderService {
// 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(os.quantity) AS totalQuantity,
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,
@ -911,34 +869,24 @@ export class OrderService {
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) {
totalSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')';
totalParams.push(...nameKeywords.map(w => `%${w}%`));
}
const [totalResult] = await this.orderSaleModel.query(totalSql, totalParams);
const [yooneResult] = await this.orderSaleModel.query(yooneSql, yooneParams);
return {
items,
total: totalCount, // ✅ 总条数
totalQuantity: Number(totalResult.totalQuantity || 0),
yoone3Quantity: Number(yooneResult.yoone3Quantity || 0),
yoone6Quantity: Number(yooneResult.yoone6Quantity || 0),
yoone9Quantity: Number(yooneResult.yoone9Quantity || 0),
yoone12Quantity: Number(yooneResult.yoone12Quantity || 0),
yoone12QuantityNew: Number(yooneResult.yoone12QuantityNew || 0),
yoone15Quantity: Number(yooneResult.yoone15Quantity || 0),
yoone18Quantity: Number(yooneResult.yoone18Quantity || 0),
zexQuantity: Number(yooneResult.zexQuantity || 0),
yoone3Quantity: Number(totalResult.yoone3Quantity || 0),
yoone6Quantity: Number(totalResult.yoone6Quantity || 0),
yoone9Quantity: Number(totalResult.yoone9Quantity || 0),
yoone12Quantity: Number(totalResult.yoone12Quantity || 0),
yoone12QuantityNew: Number(totalResult.yoone12QuantityNew || 0),
yoone15Quantity: Number(totalResult.yoone15Quantity || 0),
yoone18Quantity: Number(totalResult.yoone18Quantity || 0),
zexQuantity: Number(totalResult.zexQuantity || 0),
current,
pageSize,
};
@ -1320,8 +1268,7 @@ export class OrderService {
productId: product.id,
name: product.name,
sku: sale.sku,
quantity: sale.quantity,
// externalOrderItemId:
quantity: sale.quantity
});
};
}).catch(error => {

View File

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

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