diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 33d1e61..0cca7ff 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -23,7 +23,9 @@ import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { ShopyyCustomer, ShopyyOrder, + ShopyyOrderQuery, ShopyyProduct, + ShopyyProductQuery, ShopyyVariant, ShopyyWebhook, } from '../dto/shopyy.dto'; @@ -64,17 +66,30 @@ export class ShopyyAdapter implements ISiteAdapter { } private mapMediaSearchParams(params: UnifiedSearchParamsDTO): any { - const { search, page, per_page } = params; - const shopyyParams: any = { - page: page || 1, - limit: per_page || 10, - }; + return this.mapSearchParams(params) + } - if (search) { - shopyyParams.query = search; + /** + * 通用搜索参数转换方法,处理 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, } - - return shopyyParams; + if(params.orderBy){ + const [field, dir] = Object.entries(params.orderBy)[0]; + query.order_by = dir === 'desc' ? 'desc' : 'asc'; + query.order_field = field + } + return query; } private mapProduct(item: ShopyyProduct & { permalink?: string }): UnifiedProductDTO { @@ -393,14 +408,19 @@ export class ShopyyAdapter implements ISiteAdapter { raw: item, }; } + mapProductQuery(query: UnifiedSearchParamsDTO): ShopyyProductQuery { + return this.mapSearchParams(query) + } async getProducts( params: UnifiedSearchParamsDTO ): Promise> { + // 转换搜索参数 + const requestParams = this.mapProductQuery(params); const response = await this.shopyyService.fetchResourcePaged( this.site, 'products/list', - params + requestParams ); const { items = [], total, totalPages, page, per_page } = response; const finalItems = items.map((item) => ({ @@ -431,7 +451,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): Promise { // Shopyy update returns boolean? // shopyyService.updateProduct returns boolean. @@ -466,38 +485,49 @@ export class ShopyyAdapter implements ISiteAdapter { ): Promise { return await this.shopyyService.batchProcessProducts(this.site, data); } - mapUnifiedOrderQueryToShopyyQuery(params: UnifiedSearchParamsDTO) { - const { where = {} as any, ...restParams } = params || {} + /** + * 将统一的订单查询参数转换为 ShopYY 订单查询参数 + * 包含状态映射等特殊处理 + */ + private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { + // 首先使用通用参数转换 + 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> { - const normalizedParams = this.mapUnifiedOrderQueryToShopyyQuery(params); - const { items, total, totalPages, page, per_page } = - await this.shopyyService.fetchResourcePaged( - this.site, - 'orders', - normalizedParams - ); + // 转换订单查询参数 + const normalizedParams = this.mapOrderSearchParams(params); + const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( + this.site, + 'orders', + normalizedParams + ); return { items: items.map(this.mapOrder.bind(this)), total, @@ -507,6 +537,17 @@ export class ShopyyAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams = { + where, + page: 1, + per_page: 1, + } + const data = await this.getOrders(searchParams); + return data.total || 0; + } + async getAllOrders(params?: UnifiedSearchParamsDTO): Promise { const data = await this.shopyyService.getAllOrders(this.site.id, params); return data.map(this.mapOrder.bind(this)); @@ -669,7 +710,7 @@ export class ShopyyAdapter implements ISiteAdapter { const { items, total, totalPages, page, per_page } = await this.shopyyService.fetchResourcePaged( this.site, 'media', // Shopyy的媒体API端点可能需要调整 - requestParams + requestParams ); return { items: items.map(this.mapMedia.bind(this)), diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index badf467..9828a07 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -718,6 +718,18 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } + async countOrders(where: Record): Promise { + // 使用最小分页只获取总数 + const searchParams: UnifiedSearchParamsDTO = { + where, + page: 1, + per_page: 1, + }; + const requestParams = this.mapOrderSearchParams(searchParams); + const { total } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + return total || 0; + } + async getOrder(id: string | number): Promise { // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); @@ -1294,4 +1306,3 @@ export class WooCommerceAdapter implements ISiteAdapter { } } } - diff --git a/src/controller/media.controller.ts b/src/controller/media.controller.ts deleted file mode 100644 index cafd85f..0000000 --- a/src/controller/media.controller.ts +++ /dev/null @@ -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); - } - } -} diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 9f69b87..5013390 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -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); diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index 2bcbca7..fc534d6 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -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 { diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index ea763ec..1d7b296 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -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' }) diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index 2e02cc3..ea6db0b 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -40,6 +40,11 @@ export interface ISiteAdapter { */ getOrders(params: UnifiedSearchParamsDTO): Promise>; + /** + * 获取订单总数 + */ + countOrders(params: Record): Promise; + /** * 获取所有订单 */ diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index d083e2f..c111a03 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -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(site: any, endpoint: string, params: Record = {}) { - 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); - if (entries.length > 0) { - const [field, dir] = entries[0]; - orderby = field; - order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; - } - } - // 映射统一入参到平台入参 - 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); + const response = await this.request(site, endpoint, 'GET', null, params); + return this.mapPageResponse(response,params); + } + mapPageResponse(response:any,query: Record){ 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, }; }