diff --git a/src/config/config.local.ts b/src/config/config.local.ts index ffec600..dd83048 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -3,24 +3,24 @@ export default { koa: { port: 7001, }, - typeorm: { - dataSource: { - default: { - host: '13.212.62.127', - username: 'root', - password: 'Yoone!@.2025', - }, - }, - }, // typeorm: { // dataSource: { // default: { - // host: '127.0.0.1', + // host: '13.212.62.127', // username: 'root', - // password: '123456', + // password: 'Yoone!@.2025', // }, // }, // }, + typeorm: { + dataSource: { + default: { + host: 'localhost', + username: 'root', + password: '12345678', + }, + }, + }, cors: { origin: '*', // 允许所有来源跨域请求 allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 @@ -35,17 +35,26 @@ export default { { id: '-1', siteName: 'Admin', - email: 'tom@yoonevape.com', + email: '444693295@qq.com', }, - { + { id: '2', - wpApiUrl: 'http://localhost:10004', - consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652', - consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38', + wpApiUrl: 'http://t1-shop.local/', + consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742', + consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c', siteName: 'Local', - email: 'tom@yoonevape.com', + 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: { url: 'https://customer-external-api.ssd-test.freightcom.com', @@ -58,4 +67,11 @@ export default { customerNumber: '0006122480', contractId: '0044168528', }, + uniExpress: { + url: 'https://sjqa.uniexpress.org', // 测试环境url + // url: 'https://sj.uniexpress.ca', //正式环境url + clientId: '101018', + clientSecret: 'cbcb51bea204f3f69c47b5280064408e', + customerNo: 2067, + } } as MidwayConfig; diff --git a/src/controller/logistics.controller.ts b/src/controller/logistics.controller.ts index 4e2435b..540dec7 100644 --- a/src/controller/logistics.controller.ts +++ b/src/controller/logistics.controller.ts @@ -155,8 +155,8 @@ export class LogisticsController { @User() user ) { try { - await this.logisticsService.createShipment(orderId, data, user.id); - return successResponse(true); + const res: any = await this.logisticsService.createShipment(orderId, data, user.id); + return successResponse(res.data); } catch (error) { return errorResponse(error?.message || '创建失败'); } diff --git a/src/dto/logistics.dto.ts b/src/dto/logistics.dto.ts index d7c8249..4533ad7 100644 --- a/src/dto/logistics.dto.ts +++ b/src/dto/logistics.dto.ts @@ -2,25 +2,12 @@ import { ApiProperty } from '@midwayjs/swagger'; import { ShippingDetailsDTO } from './freightcom.dto'; import { Rule, RuleType } from '@midwayjs/validate'; import { OrderSale } from '../entity/order_sale.entity'; -import { ShipmentType } from '../enums/base.enum'; export class ShipmentBookDTO { @ApiProperty({ type: OrderSale, isArray: true }) @Rule(RuleType.array()) sales: OrderSale[]; - @ApiProperty() - @Rule(RuleType.string()) - payment_method_id: string; - - @ApiProperty() - @Rule(RuleType.string()) - service_id: string; - - @ApiProperty() - @Rule(RuleType.string()) - service_type: ShipmentType; - @ApiProperty({ type: ShippingDetailsDTO }) @Rule(RuleType.object()) details: ShippingDetailsDTO; diff --git a/src/dto/order.dto.ts b/src/dto/order.dto.ts index 13898c7..0ae7cdf 100644 --- a/src/dto/order.dto.ts +++ b/src/dto/order.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@midwayjs/swagger'; import { ErpOrderStatus } from '../enums/base.enum'; import { Rule, RuleType } from '@midwayjs/validate'; -import { Shipment } from '../entity/shipment.entity'; -import { ShipmentItem } from '../entity/shipment_item.entity'; +// import { Shipment } from '../entity/shipment.entity'; +// import { ShipmentItem } from '../entity/shipment_item.entity'; export class OrderAddress { @ApiProperty() @@ -115,10 +115,10 @@ export class QueryOrderSalesDTO { endDate: Date; } -export class Tracking extends Shipment { - @ApiProperty({ type: ShipmentItem, isArray: true }) - products?: ShipmentItem[]; -} +// export class Tracking extends Shipment { +// @ApiProperty({ type: ShipmentItem, isArray: true }) +// products?: ShipmentItem[]; +// } export class CreateOrderNoteDTO { @ApiProperty() diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts index 10049bf..4e3c056 100644 --- a/src/dto/reponse.dto.ts +++ b/src/dto/reponse.dto.ts @@ -8,7 +8,7 @@ import { SuccessArrayWrapper, SuccessWrapper, } from '../utils/response-wrapper.util'; -import { OrderStatusCountDTO, Tracking } from './order.dto'; +import { OrderStatusCountDTO } from './order.dto'; import { SiteConfig } from './site.dto'; import { PurchaseOrderDTO, StockDTO, StockRecordDTO } from './stock.dto'; import { LoginResDTO } from './user.dto'; @@ -107,8 +107,8 @@ export class OrderDetail extends Order { @ApiProperty({ type: OrderRefundItem, isArray: true }) refundItems: OrderRefundItem[]; - @ApiProperty({ type: Tracking, isArray: true }) - trackings: Tracking[]; + // @ApiProperty({ type: Tracking, isArray: true }) + // trackings: Tracking[]; @ApiProperty({ type: OrderNote, isArray: true }) notes: OrderNote[]; diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts index bc9e18d..62d15b0 100644 --- a/src/entity/order.entity.ts +++ b/src/entity/order.entity.ts @@ -7,10 +7,13 @@ import { Entity, PrimaryGeneratedColumn, UpdateDateColumn, + OneToOne, + JoinColumn } from 'typeorm'; import { ErpOrderStatus, OrderStatus } from '../enums/base.enum'; import { OrderAddress } from '../dto/order.dto'; import { Exclude, Expose } from 'class-transformer'; +import { Shipment } from './shipment.entity'; @Entity('order') @Exclude() @@ -44,6 +47,15 @@ export class Order { @Expose() orderStatus: ErpOrderStatus; // 订单状态 + @ApiProperty() + @Column({ name: 'shipment_id', nullable: true }) + @Expose() + shipmentId: string; + + @OneToOne(() => Shipment) + @JoinColumn({ name: 'shipment_id' }) + shipment: Shipment; + @ApiProperty() @Column() @Expose() diff --git a/src/entity/shipment.entity.ts b/src/entity/shipment.entity.ts index 96b7a93..dcdbf5d 100644 --- a/src/entity/shipment.entity.ts +++ b/src/entity/shipment.entity.ts @@ -4,18 +4,41 @@ import { Column, CreateDateColumn, Entity, - PrimaryColumn, + PrimaryGeneratedColumn, UpdateDateColumn, + ManyToOne, + OneToOne, + JoinColumn } from 'typeorm'; +import { StockPoint } from './stock_point.entity'; +import { Order } from './order.entity'; @Entity('shipment') @Exclude() export class Shipment { @ApiProperty() - @PrimaryColumn() + @PrimaryGeneratedColumn() @Expose() id: string; + @ApiProperty() + @Column({ name: 'order_id', nullable: true }) + @Expose() + orderId: string; + + @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; + @ApiProperty() @Column({ nullable: true }) @Expose() diff --git a/src/entity/stock_point.entity.ts b/src/entity/stock_point.entity.ts index b15d573..86f2de7 100644 --- a/src/entity/stock_point.entity.ts +++ b/src/entity/stock_point.entity.ts @@ -7,7 +7,9 @@ import { Entity, PrimaryGeneratedColumn, UpdateDateColumn, + OneToMany, } from 'typeorm'; +import { Shipment } from './shipment.entity'; @Entity('stock_point') export class StockPoint extends BaseEntity { @@ -15,6 +17,9 @@ export class StockPoint extends BaseEntity { @PrimaryGeneratedColumn() id: number; + @OneToMany(() => Shipment, shipment => shipment.stockPoint) + shipments: Shipment[]; + @ApiProperty({ type: 'string' }) @Column() name: string; diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index 1e24517..cf314c1 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -3,7 +3,7 @@ import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { Service } from '../entity/service.entity'; import { In, IsNull, Like, Repository } from 'typeorm'; import { ShippingAddress } from '../entity/shipping_address.entity'; -// import { ShipmentBookDTO } from '../dto/logistics.dto'; + import { Order } from '../entity/order.entity'; import { Shipment } from '../entity/shipment.entity'; import { ShipmentItem } from '../entity/shipment_item.entity'; @@ -15,18 +15,19 @@ import { ShipmentType, StockRecordOperationType, } from '../enums/base.enum'; -import { generateUniqueId } from '../utils/helper.util'; +// import { generateUniqueId } from '../utils/helper.util'; import { FreightcomService } from './freightcom.service'; import { StockRecord } from '../entity/stock_record.entity'; import { Stock } from '../entity/stock.entity'; import { plainToClass } from 'class-transformer'; import { WPService } from './wp.service'; import { WpSite } from '../interface'; -import { Product } from '../entity/product.entty'; +// import { Product } from '../entity/product.entty'; import { ShippingDetailsDTO } from '../dto/freightcom.dto'; import { CanadaPostService } from './canadaPost.service'; import { OrderItem } from '../entity/order_item.entity'; import { OrderSale } from '../entity/order_sale.entity'; +import { UniExpressService } from './uni_express.service'; @Provide() export class LogisticsService { @@ -63,6 +64,9 @@ export class LogisticsService { @Inject() canadaPostService: CanadaPostService; + @Inject() + uniExpressService: UniExpressService; + @Inject() wpService: WPService; @@ -195,147 +199,130 @@ export class LogisticsService { ) { throw new Error('订单状态不正确 '); } - // for (const item of data?.sales) { - // const stock = await this.stockModel.findOne({ - // where: { + try { + const reqBody = { + 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: 1, // todo, 可能需要添加 + 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, // todo,待确认 + 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, // todo, (只能一个包) + 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}S`, // todo,换成KGS和LBS + currency: 'CAD', + } + + // todo: 两个请求做异步 参考promise.all()方法 + // 获取预估费率 + const resShipmentFee = await this.uniExpressService.getRates(reqBody); + // 添加运单 + const resShipmentOrder = await this.uniExpressService.createShipment(reqBody); + + // 记录物流信息,并将订单状态转到完成 + if (resShipmentOrder.status === 'SUCCESS') { + order.orderStatus = ErpOrderStatus.COMPLETED; + } + const dataSource = this.dataSourceManager.getDataSource('default'); + dataSource.transaction(async manager => { + const orderRepo = manager.getRepository(Order); + const shipmentRepo = manager.getRepository(Shipment); + + if (order.orderStatus === ErpOrderStatus.COMPLETED) { + const shipment = await shipmentRepo.save({ + tracking_provider: 'uniuni-express', // todo: id未确定,后写进常数 + unique_id: resShipmentOrder.data.uni_order_sn, + stockPointId: '1', // todo + state: resShipmentOrder.data.uni_status_code, + order_id: order.id + }); + order.shipmentId = shipment.id; + } + + await orderRepo.save(order); + + }); + + return { data: { + resShipmentFee, + resShipmentOrder + } }; + } catch(error) { + throw new Error(`上游请求错误:${error}`); + } + + // 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, // }); - // if (!stock || stock.quantity < item.quantity) - // throw new Error(item.name + '库存不足'); - // } - let shipment: Shipment; - - if (data.service_type === ShipmentType.FREIGHTCOM) { - const uuid = generateUniqueId(); - data.details.reference_codes = [String(orderId)]; - const { id } = await this.freightcomService.createShipment({ - unique_id: uuid, - payment_method_id: data.payment_method_id, - service_id: data.service_id, - details: data.details, - }); - - const service = await this.serviceModel.findOneBy({ - id: data.service_id, - }); - shipment = { - id, - unique_id: uuid, - tracking_provider: service?.carrier_name || '', - }; - } else if (data.service_type === ShipmentType.CANADAPOST) { - const shipmentRequest = { - 'transmit-shipment': true, - 'requested-shipping-point': - data.details.origin.address.postal_code.replace(/\s/g, ''), - 'delivery-spec': { - 'service-code': data.service_id, - sender: { - name: data.details.origin.name, - company: data.details.origin.name, - 'contact-phone': data.details.origin.phone_number.number, - 'address-details': { - 'address-line-1': data.details.origin.address.address_line_1, - city: data.details.origin.address.city, - 'prov-state': data.details.origin.address.region, - 'postal-zip-code': - data.details.origin.address.postal_code.replace(/\s/g, ''), - 'country-code': data.details.origin.address.country, - }, - }, - destination: { - name: data.details.destination.contact_name, - company: data.details.destination.name, - 'address-details': { - 'address-line-1': data.details.destination.address.address_line_1, - city: data.details.destination.address.city, - 'prov-state': data.details.destination.address.region, - 'postal-zip-code': - data.details.destination.address.postal_code.replace(/\s/g, ''), - 'country-code': data.details.destination.address.country, - }, - }, - 'parcel-characteristics': { - weight: data.details.packaging_properties.packages?.reduce( - (cur, next) => cur + (next?.measurements?.weight?.value || 0), - 0 - ), - }, - preferences: { - 'show-packing-instructions': true, - }, - 'settlement-info': { - 'contract-id': this.canadaPostService.contractId, - 'intended-method-of-payment': 'CreditCard', - }, - }, - }; - shipment = await this.canadaPostService.createShipment(shipmentRequest); - } - shipment.type = data.service_type; - - const dataSource = this.dataSourceManager.getDataSource('default'); - return dataSource.transaction(async manager => { - const productRepo = manager.getRepository(Product); - const shipmentRepo = manager.getRepository(Shipment); - const shipmentItemRepo = manager.getRepository(ShipmentItem); - const orderShipmentRepo = manager.getRepository(OrderShipment); - const stockRecordRepo = manager.getRepository(StockRecord); - const stockRepo = manager.getRepository(Stock); - const orderRepo = manager.getRepository(Order); - - await shipmentRepo.save(shipment); - await this.getShipment(shipment.id); - const shipmentItems = []; - for (const item of data?.sales) { - const product = await productRepo.findOne({ where: { sku: item.sku } }); - shipmentItems.push({ - shipment_id: shipment.id, - productId: product.id, - name: product.name, - sku: item.sku, - quantity: item.quantity, - }); - const stock = await stockRepo.findOne({ - where: { - stockPointId: data.stockPointId, - productSku: item.sku, - }, - }); - stock.quantity -= item.quantity; - await stockRepo.save(stock); - await stockRecordRepo.save({ - stockPointId: data.stockPointId, - productSku: item.sku, - operationType: StockRecordOperationType.OUT, - quantityChange: item.quantity, - operatorId: userId, - note: `订单${[orderId, ...data.orderIds].join(',')} 发货`, - }); - } - await shipmentItemRepo.save(shipmentItems); - await orderShipmentRepo.save({ - order_id: orderId, - shipment_id: shipment.id, - stockPointId: data.stockPointId, - }); - for (const orderId of data?.orderIds) { - await orderShipmentRepo.save({ - order_id: orderId, - shipment_id: shipment.id, - stockPointId: data.stockPointId, - }); - const order = await orderRepo.findOneBy({ id: orderId }); - order.orderStatus = ErpOrderStatus.COMPLETED; - order.status = OrderStatus.COMPLETED; - await orderRepo.save(order); - } - order.orderStatus = ErpOrderStatus.COMPLETED; - order.status = OrderStatus.COMPLETED; - await orderRepo.save(order); - }); + // 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() { @@ -544,7 +531,6 @@ export class LogisticsService { primary_tracking_number, stockPointId, } = param; - console.log(pageSize, current, primary_tracking_number, stockPointId); const offset = pageSize * (current - 1); const values: any[] = []; @@ -566,7 +552,6 @@ export class LogisticsService { LEFT JOIN order_shipment os ON s.id = os.shipment_id LEFT JOIN stock_point sp ON os.stockPointId = sp.id ${whereClause} - GROUP BY s.id ORDER BY s.createdAt DESC LIMIT ?, ? `; diff --git a/src/service/order.service.ts b/src/service/order.service.ts index d5c7878..2793b60 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -113,7 +113,9 @@ export class OrderService { const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: order.id, siteId: siteId }, }); + console.log('before save', order); const orderId = (await this.saveOrder(siteId, orderData)).id; + console.log('order_id', orderId); const externalOrderId = order.id; if ( existingOrder && @@ -202,7 +204,6 @@ export class OrderService { el => el.key === '_wc_order_attribution_utm_source' )?.value || ''; order.customer_email = order?.billing?.email || order?.shipping?.email; - const entity = plainToClass(Order, order); const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: order.externalOrderId, siteId: siteId }, @@ -215,16 +216,18 @@ export class OrderService { entity.id = existingOrder.id; return entity; } + console.log('/////////////'); entity.orderStatus = this.mapOrderStatus(entity.status); const customer = await this.customerModel.findOne({ where: { email: order.customer_email }, }); + console.log('error? ', this.customerModel); if(!customer) { await this.customerModel.save({ email: order.customer_email, rate: 0, }); - } + } return await this.orderModel.save(entity); } diff --git a/src/service/uni_express.service.ts b/src/service/uni_express.service.ts new file mode 100644 index 0000000..b5c5b7a --- /dev/null +++ b/src/service/uni_express.service.ts @@ -0,0 +1,106 @@ +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 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}` + }, + params: { + from, to, page, perPage + } + }; + const res = (await axios.request(config)).data; + return res; + } catch (error) { + console.log(error); + } + } + +}