feat(订单): 添加获取订单总数功能

实现订单总数统计接口,包括:
1. 在ISiteAdapter接口添加countOrders方法
2. 在WooCommerce和Shopyy适配器中实现该方法
3. 添加控制器端点暴露该功能
4. 优化订单查询参数映射逻辑

refactor(Shopyy): 重构搜索参数映射逻辑

将通用的搜索参数映射逻辑提取为独立方法,提高代码复用性
This commit is contained in:
tikkhun 2026-01-07 15:22:18 +08:00
parent 983ba47dbf
commit 1814d9734b
8 changed files with 165 additions and 137 deletions

View File

@ -23,7 +23,9 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import {
ShopyyCustomer,
ShopyyOrder,
ShopyyOrderQuery,
ShopyyProduct,
ShopyyProductQuery,
ShopyyVariant,
ShopyyWebhook,
} from '../dto/shopyy.dto';
@ -63,6 +65,31 @@ export class ShopyyAdapter implements ISiteAdapter {
};
}
private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any {
return this.mapSearchParams(params)
}
/**
* where orderBy
* ShopYY API
*/
private mapSearchParams(params: UnifiedSearchParamsDTO): any {
// 处理分页参数
const page = Number(params.page || 1);
const limit = Number(params.per_page ?? 20);
// 处理 where 条件
const query: any = {
...(params.where || {}),
page,
limit,
}
if(params.orderBy){
const [field, dir] = Object.entries(params.orderBy)[0];
query.order_by = dir === 'desc' ? 'desc' : 'asc';
query.order_field = field
}
return query;
mapMediaSearchParams(params: UnifiedSearchParamsDTO): any {
const { search, page, per_page } = params;
const shopyyParams: any = {
@ -393,14 +420,19 @@ export class ShopyyAdapter implements ISiteAdapter {
raw: item,
};
}
mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery {
return this.mapSearchParams(query)
}
async getProducts(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedProductDTO>> {
// 转换搜索参数
const requestParams = this.mapProductQuery(params);
const response = await this.shopyyService.fetchResourcePaged<ShopyyProduct>(
this.site,
'products/list',
params
requestParams
);
const { items = [], total, totalPages, page, per_page } = response;
const finalItems = items.map((item) => ({
@ -431,7 +463,6 @@ export class ShopyyAdapter implements ISiteAdapter {
const res = await this.shopyyService.createProduct(this.site, data);
return this.mapProduct(res);
}
async updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean> {
// Shopyy update returns boolean?
// shopyyService.updateProduct returns boolean.
@ -466,34 +497,45 @@ export class ShopyyAdapter implements ISiteAdapter {
): Promise<any> {
return await this.shopyyService.batchProcessProducts(this.site, data);
}
mapUnifiedOrderQueryToShopyyQuery(params: UnifiedSearchParamsDTO) {
const { where = {} as any, ...restParams } = params || {}
/**
* ShopYY
*
*/
private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<ShopyyOrderQuery> {
// 首先使用通用参数转换
const baseParams = this.mapSearchParams(params);
// 订单状态映射
const statusMap = {
'pending': '100', // 100 未完成
'processing': '110', // 110 待处理
'completed': "180", // 180 已完成(确认收货)
'cancelled': '190', // 190 取消
}
const normalizedParams: any = {
...restParams,
}
if (where) {
normalizedParams.where = {
...where,
}
if (where.status) {
normalizedParams.where.status = statusMap[where.status];
};
// 如果有状态参数,进行特殊映射
if (baseParams.status) {
const unifiedStatus = baseParams.status
if (statusMap[unifiedStatus]) {
baseParams.status = statusMap[unifiedStatus];
}
}
return normalizedParams
// 处理ID参数
if (baseParams.id) {
baseParams.ids = baseParams.id;
delete baseParams.id;
}
return baseParams;
}
async getOrders(
params: UnifiedSearchParamsDTO
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
const normalizedParams = this.mapUnifiedOrderQueryToShopyyQuery(params);
const { items, total, totalPages, page, per_page } =
await this.shopyyService.fetchResourcePaged<any>(
// 转换订单查询参数
const normalizedParams = this.mapOrderSearchParams(params);
const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged<any>(
this.site,
'orders',
normalizedParams
@ -507,6 +549,17 @@ export class ShopyyAdapter implements ISiteAdapter {
};
}
async countOrders(where: Record<string,any>): Promise<number> {
// 使用最小分页只获取总数
const searchParams = {
where,
page: 1,
per_page: 1,
}
const data = await this.getOrders(searchParams);
return data.total || 0;
}
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
const data = await this.shopyyService.getAllOrders(this.site.id, params);
return data.map(this.mapOrder.bind(this));

View File

@ -718,6 +718,18 @@ export class WooCommerceAdapter implements ISiteAdapter {
};
}
async countOrders(where: Record<string,any>): Promise<number> {
// 使用最小分页只获取总数
const searchParams: UnifiedSearchParamsDTO = {
where,
page: 1,
per_page: 1,
};
const requestParams = this.mapOrderSearchParams(searchParams);
const { total } = await this.wpService.fetchResourcePaged<any>(this.site, 'orders', requestParams);
return total || 0;
}
async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
// 获取单个订单详情
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
@ -1294,4 +1306,3 @@ export class WooCommerceAdapter implements ISiteAdapter {
}
}
}

View File

@ -1,79 +0,0 @@
import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core';
import { WPService } from '../service/wp.service';
import { successResponse, errorResponse } from '../utils/response.util';
@Controller('/media')
export class MediaController {
@Inject()
wpService: WPService;
@Get('/list')
async list(
@Query('siteId') siteId: number,
@Query('page') page: number = 1,
@Query('pageSize') pageSize: number = 20
) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.getMedia(siteId, page, pageSize);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/upload')
async upload(@Fields() fields, @Files() files) {
try {
const siteId = fields.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
if (!files || files.length === 0) {
return errorResponse('file is required');
}
const file = files[0];
const result = await this.wpService.createMedia(siteId, file);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Post('/update/:id')
async update(@Param('id') id: number, @Body() body) {
try {
const siteId = body.siteId;
if (!siteId) {
return errorResponse('siteId is required');
}
// 过滤出需要更新的字段
const { title, caption, description, alt_text } = body;
const data: any = {};
if (title !== undefined) data.title = title;
if (caption !== undefined) data.caption = caption;
if (description !== undefined) data.description = description;
if (alt_text !== undefined) data.alt_text = alt_text;
const result = await this.wpService.updateMedia(siteId, id, data);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
@Del('/:id')
async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) {
try {
if (!siteId) {
return errorResponse('siteId is required');
}
const result = await this.wpService.deleteMedia(siteId, id, force);
return successResponse(result);
} catch (error) {
return errorResponse(error.message);
}
}
}

View File

@ -672,6 +672,26 @@ export class SiteApiController {
}
}
@Get('/:siteId/orders/count')
@ApiOkResponse({ type: Object })
async countOrders(
@Param('siteId') siteId: number,
@Query() query: any
) {
this.logger.info(`[Site API] 获取订单总数开始, siteId: ${siteId}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const total = await adapter.countOrders(query);
this.logger.info(`[Site API] 获取订单总数成功, siteId: ${siteId}, total: ${total}`);
return successResponse({ total });
} catch (error) {
this.logger.error(`[Site API] 获取订单总数失败, siteId: ${siteId}, 错误信息: ${error.message}`);
return errorResponse(error.message);
}
}
@Get('/:siteId/customers/:customerId/orders')
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
async getCustomerOrders(
@ -1050,13 +1070,13 @@ export class SiteApiController {
}
}
@Get('/:siteId/orders/:orderId/trackings')
@Get('/:siteId/orders/:orderId/fulfillments')
@ApiOkResponse({ type: Object })
async getOrderTrackings(
async getOrderFulfillments(
@Param('siteId') siteId: number,
@Param('orderId') orderId: string
) {
this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`);
this.logger.info(`[Site API] 获取订单履约信息开始, siteId: ${siteId}, orderId: ${orderId}`);
try {
const adapter = await this.siteApiService.getAdapter(siteId);
const data = await adapter.getOrderFulfillments(orderId);

View File

@ -4,6 +4,10 @@ export interface ShopyyTag {
id?: number;
name?: string;
}
export interface ShopyyProductQuery{
page: string;
limit: string;
}
// 产品类型
export interface ShopyyProduct {
// 产品主键
@ -83,6 +87,42 @@ export interface ShopyyVariant {
position?: number | string;
sku_code?: string;
}
//
// 订单查询参数类型
export interface ShopyyOrderQuery {
// 订单ID集合 多个ID用','联接 例1,2,3
ids?: string;
// 订单状态 100 未完成110 待处理180 已完成(确认收货); 190 取消;
status?: string;
// 物流状态 300 未发货310 部分发货320 已发货330(确认收货)
fulfillment_status?: string;
// 支付状态 200 待支付210 支付中220 部分支付230 已支付240 支付失败250 部分退款260 已退款 290 已取消;
financial_status?: string;
// 支付时间 下限值
pay_at_min?: string;
// 支付时间 上限值
pay_at_max?: string;
// 创建开始时间
created_at_min?: number;
// 创建结束时间
created_at_max?: number;
// 更新时间开始
updated_at_min?: string;
// 更新时间结束
updated_at_max?: string;
// 起始ID
since_id?: string;
// 页码
page?: string;
// 每页条数
limit?: string;
// 排序字段默认id id=订单ID updated_at=最后更新时间 pay_at=支付时间
order_field?: string;
// 排序方式默认desc desc=降序 asc=升序
order_by?: string;
// 订单列表类型
group?: string;
}
// 订单类型
export interface ShopyyOrder {

View File

@ -65,9 +65,6 @@ export class Product {
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
promotionPrice: number;
// 分类关联
@ManyToOne(() => Category, category => category.products)
@JoinColumn({ name: 'categoryId' })

View File

@ -46,6 +46,11 @@ export interface ISiteAdapter {
*/
getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>;
/**
*
*/
countOrders(params: Record<string,any>): Promise<number>;
/**
*
*/

View File

@ -1,3 +1,6 @@
/**
* https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
*/
import { ILogger, Inject, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios';
import * as fs from 'fs';
@ -180,41 +183,19 @@ export class ShopyyService {
*
*/
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
const page = Number(params.page || 1);
const limit = Number(params.per_page ?? 20);
const where = params.where && typeof params.where === 'object' ? params.where : {};
let orderby: string | undefined = params.orderby;
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
if (!orderby && params.order && typeof params.order === 'object') {
const entries = Object.entries(params.order as Record<string, any>);
if (entries.length > 0) {
const [field, dir] = entries[0];
orderby = field;
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
const response = await this.request(site, endpoint, 'GET', null, params);
return this.mapPageResponse<T>(response,params);
}
}
// 映射统一入参到平台入参
const requestParams = {
...where,
...(params.search ? { search: params.search } : {}),
...(params.status ? { status: params.status } : {}),
...(orderby ? { orderby } : {}),
...(order ? { order } : {}),
page,
limit
};
this.logger.debug('ShopYY API请求分页参数:'+ JSON.stringify(requestParams));
const response = await this.request(site, endpoint, 'GET', null, requestParams);
mapPageResponse<T>(response:any,query: Record<string, any>){
if (response?.code !== 0) {
throw new Error(response?.msg)
}
return {
items: (response.data.list || []) as T[],
total: response.data?.paginate?.total || 0,
totalPages: response.data?.paginate?.pageTotal || 0,
page: response.data?.paginate?.current || requestParams.page,
per_page: response.data?.paginate?.pagesize || requestParams.limit,
page: response.data?.paginate?.current || query.page,
per_page: response.data?.paginate?.pagesize || query.limit,
};
}