diff --git a/package.json b/package.json index d397f78..d8b9c90 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@midwayjs/swagger": "^3.20.2", "@midwayjs/typeorm": "^3.20.0", "@midwayjs/validate": "^3.20.2", + "@woocommerce/woocommerce-rest-api": "^1.0.2", "axios": "^1.7.9", "bcryptjs": "^2.4.3", "class-transformer": "^0.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d142c9c..c7fd255 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@midwayjs/validate': specifier: ^3.20.2 version: 3.20.17 + '@woocommerce/woocommerce-rest-api': + specifier: ^1.0.2 + version: 1.0.2 axios: specifier: ^1.7.9 version: 1.13.2 @@ -303,6 +306,10 @@ packages: '@types/supertest@2.0.16': resolution: {integrity: sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==} + '@woocommerce/woocommerce-rest-api@1.0.2': + resolution: {integrity: sha512-G+0VwM0MINF83KnT7Rg/htm9EEYADWvDPT/UWEJdZ0de1vXvsPrr4M1ksKaxgKHO8qIJViRrIHCtrui2JoVA+Q==} + engines: {node: '>=8.0.0'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -416,6 +423,10 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + cipher-base@1.0.7: + resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} + engines: {node: '>= 0.10'} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -474,6 +485,15 @@ packages: copy-to@2.0.1: resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + cron@3.5.0: resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} @@ -692,6 +712,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash-base@3.1.2: + resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} + engines: {node: '>= 0.8'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -769,6 +793,9 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -858,6 +885,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -931,6 +961,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + oauth-1.0a@2.2.6: + resolution: {integrity: sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -979,6 +1012,9 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -986,6 +1022,9 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-lit@1.5.2: resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} engines: {node: '>=12'} @@ -997,6 +1036,9 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -1008,6 +1050,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -1015,9 +1060,16 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + ripemd160@2.0.3: + resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} + engines: {node: '>= 0.8'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -1124,6 +1176,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1249,6 +1304,12 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -1570,6 +1631,15 @@ snapshots: dependencies: '@types/superagent': 4.1.14 + '@woocommerce/woocommerce-rest-api@1.0.2': + dependencies: + axios: 1.13.2 + create-hmac: 1.1.7 + oauth-1.0a: 2.2.6 + url-parse: 1.5.10 + transitivePeerDependencies: + - debug + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -1679,6 +1749,12 @@ snapshots: chownr@3.0.0: {} + cipher-base@1.0.7: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + class-transformer@0.5.1: {} cli-table3@0.6.5: @@ -1734,6 +1810,25 @@ snapshots: copy-to@2.0.1: {} + core-util-is@1.0.3: {} + + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.7 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.3 + sha.js: 2.4.12 + + create-hmac@1.1.7: + dependencies: + cipher-base: 1.0.7 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + cron@3.5.0: dependencies: '@types/luxon': 3.4.2 @@ -1943,6 +2038,13 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash-base@3.1.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -2021,6 +2123,8 @@ snapshots: dependencies: which-typed-array: 1.1.19 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -2138,6 +2242,12 @@ snapshots: math-intrinsics@1.1.0: {} + md5.js@1.3.5: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + safe-buffer: 5.2.1 + media-typer@0.3.0: {} merge2@1.4.1: {} @@ -2203,6 +2313,8 @@ snapshots: normalize-path@3.0.0: {} + oauth-1.0a@2.2.6: {} + object-inspect@1.13.4: {} on-finished@2.4.1: @@ -2238,12 +2350,16 @@ snapshots: possible-typed-array-names@1.1.0: {} + process-nextick-args@2.0.1: {} + proxy-from-env@1.1.0: {} qs@6.14.0: dependencies: side-channel: 1.1.0 + querystringify@2.2.0: {} + queue-lit@1.5.2: {} queue-microtask@1.2.3: {} @@ -2255,6 +2371,16 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -2263,14 +2389,23 @@ snapshots: require-directory@2.1.1: {} + requires-port@1.0.0: {} + resolve-pkg-maps@1.0.0: {} reusify@1.1.0: {} + ripemd160@2.0.3: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-regex-test@1.1.0: @@ -2375,6 +2510,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -2483,6 +2622,13 @@ snapshots: unpipe@1.0.0: {} + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + uuid@11.1.0: {} vary@1.1.2: {} diff --git a/src/config/config.default.ts b/src/config/config.default.ts index ba91972..fb7e085 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -1,5 +1,5 @@ import { MidwayConfig } from '@midwayjs/core'; -import { Product } from '../entity/product.entty'; +import { Product } from '../entity/product.entity'; import { Category } from '../entity/category.entity'; import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts index aa71e46..cdeb3ad 100644 --- a/src/controller/order.controller.ts +++ b/src/controller/order.controller.ts @@ -22,6 +22,7 @@ import { CreateOrderNoteDTO, QueryOrderDTO, QueryOrderSalesDTO, + QueryOrderItemDTO, } from '../dto/order.dto'; import { User } from '../decorator/user.decorator'; import { ErpOrderStatus } from '../enums/base.enum'; @@ -97,6 +98,26 @@ export class OrderController { } } + @ApiOkResponse() + @Get('/getOrderItems') + async getOrderItems(@Query() param: QueryOrderSalesDTO) { + try { + return successResponse(await this.orderService.getOrderItems(param)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + + @ApiOkResponse() + @Get('/getOrderItemList') + async getOrderItemList(@Query() param: QueryOrderItemDTO) { + try { + return successResponse(await this.orderService.getOrderItemList(param)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + @ApiOkResponse({ type: OrderDetailRes, }) @@ -109,6 +130,16 @@ export class OrderController { } } + @ApiOkResponse() + @Get('/:orderId/related') + async getRelatedByOrder(@Param('orderId') orderId: number) { + try { + return successResponse(await this.orderService.getRelatedByOrder(orderId)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } + @ApiOkResponse({ type: BooleanRes, }) diff --git a/src/controller/subscription.controller.ts b/src/controller/subscription.controller.ts index 75decc7..5506063 100644 --- a/src/controller/subscription.controller.ts +++ b/src/controller/subscription.controller.ts @@ -10,6 +10,7 @@ export class SubscriptionController { @Inject() subscriptionService: SubscriptionService; + // 同步订阅:根据站点 ID 拉取并更新本地订阅数据 @ApiOkResponse({ type: BooleanRes }) @Post('/sync/:siteId') async sync(@Param('siteId') siteId: string) { @@ -21,6 +22,7 @@ export class SubscriptionController { } } + // 订阅列表:分页 + 筛选 @ApiOkResponse({ type: SubscriptionListRes }) @Get('/list') async list(@Query() query: QuerySubscriptionDTO) { diff --git a/src/dto/order.dto.ts b/src/dto/order.dto.ts index 38ca086..0f0b133 100644 --- a/src/dto/order.dto.ts +++ b/src/dto/order.dto.ts @@ -91,6 +91,10 @@ export class QueryOrderDTO { @ApiProperty() @Rule(RuleType.string()) payment_method: string; + + @ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' }) + @Rule(RuleType.bool().default(false)) + isSubscriptionOnly?: boolean; } export class QueryOrderSalesDTO { @@ -119,11 +123,11 @@ export class QueryOrderSalesDTO { name: string; @ApiProperty() - @Rule(RuleType.date().required()) + @Rule(RuleType.date()) startDate: Date; @ApiProperty() - @Rule(RuleType.date().required()) + @Rule(RuleType.date()) endDate: Date; } @@ -141,3 +145,37 @@ export class CreateOrderNoteDTO { @Rule(RuleType.string()) content: string; } + +export class QueryOrderItemDTO { + @ApiProperty({ example: '1', description: '页码' }) + @Rule(RuleType.number()) + current: number; + + @ApiProperty({ example: '10', description: '每页大小' }) + @Rule(RuleType.number()) + pageSize: number; + + @ApiProperty() + @Rule(RuleType.string().allow('')) + siteId: string; + + @ApiProperty() + @Rule(RuleType.string().allow('')) + name: string; // 商品名称关键字 + + @ApiProperty() + @Rule(RuleType.string().allow('')) + externalProductId: string; + + @ApiProperty() + @Rule(RuleType.string().allow('')) + externalVariationId: string; + + @ApiProperty() + @Rule(RuleType.date()) + startDate: Date; + + @ApiProperty() + @Rule(RuleType.date()) + endDate: Date; +} diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts index f55fa96..f4aae3c 100644 --- a/src/dto/reponse.dto.ts +++ b/src/dto/reponse.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@midwayjs/swagger'; import { Category } from '../entity/category.entity'; import { Order } from '../entity/order.entity'; -import { Product } from '../entity/product.entty'; +import { Product } from '../entity/product.entity'; import { StockPoint } from '../entity/stock_point.entity'; import { PaginatedWrapper } from '../utils/paginated-response.util'; import { @@ -119,7 +119,7 @@ export class PaymentMethodListRes extends SuccessArrayWrapper( PaymentMethodDTO ) {} -// 订阅分页数据 +// 订阅分页数据(列表 + 总数等分页信息) export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {} -// 订阅分页返回数据 +// 订阅分页返回数据(统一成功包装) export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {} diff --git a/src/dto/subscription.dto.ts b/src/dto/subscription.dto.ts index e231948..79794db 100644 --- a/src/dto/subscription.dto.ts +++ b/src/dto/subscription.dto.ts @@ -2,27 +2,34 @@ import { ApiProperty } from '@midwayjs/swagger'; import { Rule, RuleType } from '@midwayjs/validate'; import { SubscriptionStatus } from '../enums/base.enum'; +// 订阅列表查询参数(分页与筛选) export class QuerySubscriptionDTO { + // 当前页码(从 1 开始) @ApiProperty({ example: 1, description: '页码' }) @Rule(RuleType.number().default(1)) current: number; + // 每页数量 @ApiProperty({ example: 10, description: '每页大小' }) @Rule(RuleType.number().default(10)) pageSize: number; + // 站点 ID(可选) @ApiProperty({ description: '站点ID' }) @Rule(RuleType.string().allow('')) siteId: string; + // 订阅状态筛选(可选),支持枚举值 @ApiProperty({ description: '订阅状态', enum: SubscriptionStatus }) @Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow('')) status: SubscriptionStatus | ''; + // 客户邮箱(模糊匹配,可选) @ApiProperty({ description: '客户邮箱' }) @Rule(RuleType.string().allow('')) customer_email: string; + // 关键字(订阅ID、邮箱等,模糊匹配,可选) @ApiProperty({ description: '关键字(订阅ID、邮箱等)' }) @Rule(RuleType.string().allow('')) keyword: string; diff --git a/src/entity/order_item.entity.ts b/src/entity/order_item.entity.ts index d41eef2..0a3fd5b 100644 --- a/src/entity/order_item.entity.ts +++ b/src/entity/order_item.entity.ts @@ -76,16 +76,61 @@ export class OrderItem { @Expose() total_tax: number; + @ApiProperty() + @Column({ nullable: true }) + @Expose() + tax_class?: string; // 税类(来自 line_items.tax_class) + + @ApiProperty() + @Column({ type: 'json', nullable: true }) + @Expose() + taxes?: any[]; // 税明细(来自 line_items.taxes,数组) + + @ApiProperty() + @Column({ type: 'json', nullable: true }) + @Expose() + meta_data?: any[]; // 行项目元数据(包含订阅相关键值) + @ApiProperty() @Column({ nullable: true }) @Expose() sku?: string; + @ApiProperty() + @Column({ nullable: true }) + @Expose() + global_unique_id?: string; // 全局唯一ID(部分主题/插件会提供) + @ApiProperty() @Column('decimal', { precision: 10, scale: 2 }) @Expose() price: number; + @ApiProperty() + @Column({ type: 'json', nullable: true }) + @Expose() + image?: { id?: string | number; src?: string }; // 商品图片(对象,包含 id/src) + + @ApiProperty() + @Column({ nullable: true }) + @Expose() + parent_name?: string; // 父商品名称(组合/捆绑时可能使用) + + @ApiProperty() + @Column({ nullable: true }) + @Expose() + bundled_by?: string; // 捆绑来源标识(bundled_by) + + @ApiProperty() + @Column({ nullable: true }) + @Expose() + bundled_item_title?: string; // 捆绑项标题 + + @ApiProperty() + @Column({ type: 'json', nullable: true }) + @Expose() + bundled_items?: any[]; // 捆绑项列表(数组) + @ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', diff --git a/src/entity/product.entty.ts b/src/entity/product.entity.ts similarity index 100% rename from src/entity/product.entty.ts rename to src/entity/product.entity.ts diff --git a/src/entity/subscription.entity.ts b/src/entity/subscription.entity.ts index 76830e6..f4c76bb 100644 --- a/src/entity/subscription.entity.ts +++ b/src/entity/subscription.entity.ts @@ -12,96 +12,115 @@ import { SubscriptionStatus } from '../enums/base.enum'; @Entity('subscription') @Exclude() export class Subscription { + // 本地主键,自增 ID @ApiProperty() @PrimaryGeneratedColumn() @Expose() id: number; + // 站点唯一标识,用于区分不同来源站点 @ApiProperty({ description: '来源站点唯一标识' }) @Column() @Expose() siteId: string; + // WooCommerce 订阅的原始 ID(字符串化),用于幂等更新 @ApiProperty({ description: 'WooCommerce 订阅 ID' }) @Column() @Expose() externalSubscriptionId: string; + // 订阅状态(active/cancelled/on-hold 等) @ApiProperty({ type: SubscriptionStatus }) @Column({ type: 'enum', enum: SubscriptionStatus }) @Expose() status: SubscriptionStatus; + // 货币代码,例如 USD/CAD @ApiProperty() @Column({ default: '' }) @Expose() currency: string; + // 总金额,保留两位小数 @ApiProperty() @Column('decimal', { precision: 10, scale: 2, default: 0 }) @Expose() total: number; + // 计费周期(day/week/month/year) @ApiProperty({ description: '计费周期 e.g. day/week/month/year' }) @Column({ default: '' }) @Expose() billing_period: string; + // 计费周期间隔(例如 1/3/12) @ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' }) @Column({ type: 'int', default: 0 }) @Expose() billing_interval: number; + // 客户 ID(WooCommerce 用户 ID) @ApiProperty() @Column({ type: 'int', default: 0 }) @Expose() customer_id: number; + // 客户邮箱(从 billing.email 或 customer_email 提取) @ApiProperty() @Column({ default: '' }) @Expose() customer_email: string; + // 父订单/订阅 ID(如有) @ApiProperty({ description: '父订单/父订阅ID(如有)' }) @Column({ type: 'int', default: 0 }) @Expose() parent_id: number; + // 订阅开始时间 @ApiProperty() @Column({ type: 'timestamp', nullable: true }) @Expose() start_date: Date; + // 试用结束时间 @ApiProperty() @Column({ type: 'timestamp', nullable: true }) @Expose() trial_end: Date; + // 下次支付时间 @ApiProperty() @Column({ type: 'timestamp', nullable: true }) @Expose() next_payment_date: Date; + // 订阅结束时间 @ApiProperty() @Column({ type: 'timestamp', nullable: true }) @Expose() end_date: Date; + // 商品项(订阅行项目) @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() line_items: any[]; + // 额外元数据(键值对) @ApiProperty() @Column({ type: 'json', nullable: true }) @Expose() meta_data: any[]; + // 创建时间(数据库自动生成) @ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true }) @CreateDateColumn() @Expose() createdAt: Date; + // 更新时间(数据库自动生成) @ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true }) @UpdateDateColumn() @Expose() diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 3b1399e..7f7855c 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -8,7 +8,7 @@ import { OrderItem } from '../entity/order_item.entity'; import { OrderItemOriginal } from '../entity/order_items_original.entity'; import { OrderSale } from '../entity/order_sale.entity'; import { WpProduct } from '../entity/wp_product.entity'; -import { Product } from '../entity/product.entty'; +import { Product } from '../entity/product.entity'; import { OrderFee } from '../entity/order_fee.entity'; import { OrderRefund } from '../entity/order_refund.entity'; import { OrderRefundItem } from '../entity/order_retund_item.entity'; @@ -23,6 +23,7 @@ import { } from '../enums/base.enum'; import { Variation } from '../entity/variation.entity'; import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; +import dayjs = require('dayjs'); import { OrderDetailRes } from '../dto/reponse.dto'; import { OrderNote } from '../entity/order_note.entity'; import { User } from '../entity/user.entity'; @@ -38,7 +39,7 @@ export class OrderService { sites: WpSite[]; @Inject() - wPService: WPService; + wpService: WPService; @Inject() stockService: StockService; @@ -101,14 +102,14 @@ export class OrderService { customerModel: Repository; async syncOrders(siteId: string) { - const orders = await this.wPService.getOrders(siteId); // 调用 WooCommerce API 获取订单 + const orders = await this.wpService.getOrders(siteId); // 调用 WooCommerce API 获取订单 for (const order of orders) { await this.syncSingleOrder(siteId, order); } } async syncOrderById(siteId: string, orderId: string) { - const order = await this.wPService.getOrder(siteId, orderId); + const order = await this.wpService.getOrder(siteId, orderId); await this.syncSingleOrder(siteId, order, true); } // 订单状态切换表 @@ -131,7 +132,7 @@ export class OrderService { throw new Error(`更新订单信息,但失败,原因为 ${siteId} 的站点信息不存在`) } // 同步更新回 wordpress 的 order 状态 - await this.wPService.updateOrder(site, order.id, { status: order.status }); + await this.wpService.updateOrder(site, order.id, { status: order.status }); order.status = this.orderAutoNextStatusMap[originStatus]; } catch (error) { console.error('更新订单状态失败,原因为:', error) @@ -153,7 +154,6 @@ export class OrderService { }); // 更新状态 await this.autoUpdateOrderStatus(siteId, order); - const orderId = (await this.saveOrder(siteId, orderData)).id; const externalOrderId = order.id; if ( existingOrder && @@ -166,6 +166,8 @@ export class OrderService { if (existingOrder && !existingOrder.is_editable && !forceUpdate) { return; } + const orderRecord = await this.saveOrder(siteId, orderData); + const orderId = orderRecord.id; await this.saveOrderItems({ siteId, orderId, @@ -309,6 +311,7 @@ export class OrderService { externalOrderId: string; orderItems: Record[]; }) { + console.log('saveOrderItems params',params) const { siteId, orderId, externalOrderId, orderItems } = params; const currentOrderItems = await this.orderItemModel.find({ where: { siteId, externalOrderId: externalOrderId }, @@ -442,7 +445,7 @@ export class OrderService { refunds: Record[]; }) { for (const item of refunds) { - const refund = await this.wPService.getOrderRefund( + const refund = await this.wpService.getOrderRefund( siteId, externalOrderId, item.id @@ -613,6 +616,7 @@ export class OrderService { customer_email, payment_method, billing_phone, + isSubscriptionOnly = false, }, userId = undefined) { const parameters: any[] = []; @@ -639,6 +643,37 @@ export class OrderService { o.payment_method as payment_method, cs.order_count as order_count, cs.total_spent as total_spent, + CASE WHEN EXISTS ( + SELECT 1 FROM subscription s + WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId + ) THEN 1 ELSE 0 END AS isSubscription, + ( + SELECT COALESCE( + JSON_ARRAYAGG( + JSON_OBJECT( + 'id', s.id, + 'externalSubscriptionId', s.externalSubscriptionId, + 'status', s.status, + 'currency', s.currency, + 'total', s.total, + 'billing_period', s.billing_period, + 'billing_interval', s.billing_interval, + 'customer_id', s.customer_id, + 'customer_email', s.customer_email, + 'parent_id', s.parent_id, + 'start_date', s.start_date, + 'trial_end', s.trial_end, + 'next_payment_date', s.next_payment_date, + 'end_date', s.end_date, + 'line_items', s.line_items, + 'meta_data', s.meta_data + ) + ), + JSON_ARRAY() + ) + FROM subscription s + WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId + ) AS related, COALESCE( JSON_ARRAYAGG( CASE WHEN s.id IS NOT NULL THEN JSON_OBJECT( @@ -698,12 +733,14 @@ export class OrderService { totalQuery += ` AND o.date_created <= ?`; parameters.push(endDate); } + // 支付方式筛选(使用参数化,避免SQL注入) if (payment_method) { - sqlQuery += ` AND o.payment_method like "%${payment_method}%" `; - totalQuery += ` AND o.payment_method like "%${payment_method}%" `; + sqlQuery += ` AND o.payment_method LIKE ?`; + totalQuery += ` AND o.payment_method LIKE ?`; + parameters.push(`%${payment_method}%`); } const user = await this.userModel.findOneBy({ id: userId }); - if (user?.permissions?.includes('order-10-days')) { + if (user?.permissions?.includes('order-10-days') && !startDate && !endDate) { sqlQuery += ` AND o.date_created >= ?`; totalQuery += ` AND o.date_created >= ?`; const tenDaysAgo = new Date(); @@ -728,6 +765,21 @@ export class OrderService { } } + // 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储) + if (isSubscriptionOnly) { + const subCond = ` + AND ( + EXISTS ( + SELECT 1 FROM subscription s + WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId + ) + + ) + `; + sqlQuery += subCond; + totalQuery += subCond; + } + if (customer_email) { sqlQuery += ` AND o.customer_email LIKE ?`; totalQuery += ` AND o.customer_email LIKE ?`; @@ -773,7 +825,6 @@ export class OrderService { // 执行查询 const orders = await this.orderModel.query(sqlQuery, parameters); - return { items: orders, total, current, pageSize }; } @@ -785,7 +836,8 @@ export class OrderService { keyword, customer_email, billing_phone, - }) { + isSubscriptionOnly = false, + }: any) { const query = this.orderModel .createQueryBuilder('order') .select('order.orderStatus', 'status') @@ -823,11 +875,24 @@ export class OrderService { ); } + if (isSubscriptionOnly) { + query.andWhere(`( + EXISTS ( + SELECT 1 FROM subscription s + WHERE s.siteId = order.siteId AND s.parent_id = order.externalOrderId + ) + )`); + } + return await query.getRawMany(); } async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; + const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'); + startDate = (startDate as any) || defaultStart as any; + endDate = (endDate as any) || defaultEnd as any; const offset = (current - 1) * pageSize; // ------------------------- @@ -1031,6 +1096,10 @@ export class OrderService { name, }: QueryOrderSalesDTO) { const nameKeywords = name ? name.split(' ').filter(Boolean) : []; + const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); + const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'); + startDate = (startDate as any) || defaultStart as any; + endDate = (endDate as any) || defaultEnd as any; // 分页查询 let sqlQuery = ` WITH product_purchase_counts AS ( @@ -1133,6 +1202,64 @@ export class OrderService { pageSize, }; } + + async getOrderItemList({ + siteId, + startDate, + endDate, + current, + pageSize, + name, + externalProductId, + externalVariationId, + }: any) { + const params: any[] = []; + let sql = ` + SELECT + oi.*, + o.id AS orderId, + o.externalOrderId AS orderExternalOrderId, + o.date_created AS orderDateCreated, + o.customer_email AS orderCustomerEmail, + o.orderStatus AS orderStatus, + o.siteId AS orderSiteId, + CASE WHEN + JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"is_subscription"') + OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcs_bought_as_subscription"') + OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcsatt_scheme"') + OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_subscription"') + THEN 1 ELSE 0 END AS isSubscriptionItem + FROM order_item oi + INNER JOIN \`order\` o ON o.id = oi.orderId + WHERE 1=1 + `; + let countSql = ` + SELECT COUNT(*) AS total + FROM order_item oi + INNER JOIN \`order\` o ON o.id = oi.orderId + WHERE 1=1 + `; + const pushFilter = (cond: string, value: any) => { + sql += cond; countSql += cond; params.push(value); + }; + if (startDate) pushFilter(' AND o.date_created >= ?', startDate); + if (endDate) pushFilter(' AND o.date_created <= ?', endDate); + if (siteId) pushFilter(' AND oi.siteId = ?', siteId); + if (name) { + pushFilter(' AND oi.name LIKE ?', `%${name}%`); + } + if (externalProductId) pushFilter(' AND oi.externalProductId = ?', externalProductId); + if (externalVariationId) pushFilter(' AND oi.externalVariationId = ?', externalVariationId); + + sql += ' ORDER BY o.date_created DESC LIMIT ? OFFSET ?'; + const listParams = [...params, pageSize, (current - 1) * pageSize]; + + const items = await this.orderItemModel.query(sql, listParams); + const [countRow] = await this.orderItemModel.query(countSql, params); + const total = Number(countRow?.total || 0); + + return { items, total, current, pageSize }; + } async getOrderDetail(id: number): Promise { const order = await this.orderModel.findOne({ where: { id } }); const site = this.sites.find(site => site.id === order.siteId); @@ -1199,15 +1326,55 @@ export class OrderService { console.log('create order sale origin error: ', error.message); } + // 关联数据:订阅与相关订单(用于前端关联展示) + let relatedList: any[] = []; + try { + const related = await this.getRelatedByOrder(id); + const subs = Array.isArray(related?.subscriptions) ? related.subscriptions : []; + const ords = Array.isArray(related?.orders) ? related.orders : []; + const seen = new Set(); + const merge = [...subs, ...ords]; + for (const it of merge) { + const key = it?.externalSubscriptionId + ? `sub:${it.externalSubscriptionId}` + : it?.externalOrderId + ? `ord:${it.externalOrderId}` + : `id:${it?.id}`; + if (!seen.has(key)) { + seen.add(key); + relatedList.push(it); + } + } + } catch (error) { + // 关联查询失败不影响详情返回 + } + return { ...order, - siteName: site.siteName, - email: site.email, + siteName: site?.siteName, + email: site?.email, items, sales, refundItems, notes, shipment, + related: relatedList, + }; + } + + async getRelatedByOrder(orderId: number) { + const order = await this.orderModel.findOne({ where: { id: orderId } }); + if (!order) throw new Error('订单不存在'); + const siteId = order.siteId; + const subSql = ` + SELECT * FROM subscription s + WHERE s.siteId = ? AND s.parent_id = ? + `; + const subscriptions = await this.orderModel.query(subSql, [siteId, order.externalOrderId]); + return { + order, + subscriptions, + orders: [], }; } @@ -1261,9 +1428,9 @@ export class OrderService { async cancelOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); - const site = this.wPService.geSite(order.siteId); + const site = this.wpService.geSite(order.siteId); if (order.status !== OrderStatus.CANCEL) { - await this.wPService.updateOrder(site, order.externalOrderId, { + await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.CANCEL, }); order.status = OrderStatus.CANCEL; @@ -1275,9 +1442,9 @@ export class OrderService { async refundOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); - const site = this.wPService.geSite(order.siteId); + const site = this.wpService.geSite(order.siteId); if (order.status !== OrderStatus.REFUNDED) { - await this.wPService.updateOrder(site, order.externalOrderId, { + await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.REFUNDED, }); order.status = OrderStatus.REFUNDED; @@ -1289,9 +1456,9 @@ export class OrderService { async completedOrder(id: number) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error(`订单 ${id}不存在`); - const site = this.wPService.geSite(order.siteId); + const site = this.wpService.geSite(order.siteId); if (order.status !== OrderStatus.COMPLETED) { - await this.wPService.updateOrder(site, order.externalOrderId, { + await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.COMPLETED, }); order.status = OrderStatus.COMPLETED; diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 509af9a..22973d5 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1,6 +1,6 @@ import { Provide } from '@midwayjs/core'; import { In, Like, Not, Repository } from 'typeorm'; -import { Product } from '../entity/product.entty'; +import { Product } from '../entity/product.entity'; import { Category } from '../entity/category.entity'; import { paginate } from '../utils/paginate.util'; import { PaginationParams } from '../interface'; diff --git a/src/service/stock.service.ts b/src/service/stock.service.ts index 0333605..c730269 100644 --- a/src/service/stock.service.ts +++ b/src/service/stock.service.ts @@ -3,7 +3,7 @@ import { Between, Like, Repository, LessThan, MoreThan } from 'typeorm'; import { Stock } from '../entity/stock.entity'; import { StockRecord } from '../entity/stock_record.entity'; import { paginate } from '../utils/paginate.util'; -import { Product } from '../entity/product.entty'; +import { Product } from '../entity/product.entity'; import { CreatePurchaseOrderDTO, CreateStockPointDTO, diff --git a/src/service/subscription.service.ts b/src/service/subscription.service.ts index d8b2b92..5ae83b0 100644 --- a/src/service/subscription.service.ts +++ b/src/service/subscription.service.ts @@ -10,18 +10,27 @@ import { QuerySubscriptionDTO } from '../dto/subscription.dto'; @Provide() export class SubscriptionService { @Inject() - wPService: WPService; + wpService: WPService; @InjectEntityModel(Subscription) subscriptionModel: Repository; + /** + * 同步指定站点的订阅列表 + * - 从 WooCommerce 拉取订阅并逐条入库/更新 + */ async syncSubscriptions(siteId: string) { - const subs = await this.wPService.getSubscriptions(siteId); + const subs = await this.wpService.getSubscriptions(siteId); for (const sub of subs) { await this.syncSingleSubscription(siteId, sub); } } + /** + * 同步单条订阅 + * - 规范化字段、设置幂等键 externalSubscriptionId + * - 已存在则更新,不存在则新增 + */ async syncSingleSubscription(siteId: string, sub: any) { const { line_items, ...raw } = sub; const entity: Partial = { @@ -44,6 +53,9 @@ export class SubscriptionService { } } + /** + * 获取订阅分页列表(支持站点、状态、邮箱与关键字筛选) + */ async getSubscriptionList({ current = 1, pageSize = 10, diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 7021fef..ae91d7b 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -1,5 +1,6 @@ import { Config, Provide } from '@midwayjs/core'; import axios, { AxiosRequestConfig } from 'axios'; +import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api'; import { WpSite } from '../interface'; import { WpProduct } from '../entity/wp_product.entity'; import { Variation } from '../entity/variation.entity'; @@ -11,6 +12,64 @@ export class WPService { @Config('wpSite') sites: WpSite[]; + /** + * 构建 URL,自动规范各段的斜杠,避免出现多 / 或少 / 导致请求失败 + * 使用示例:this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId) + */ + private buildURL(base: string, ...parts: Array): string { + // 去掉 base 末尾多余斜杠,但不影响协议中的 // + const baseSanitized = String(base).replace(/\/+$/g, ''); + // 规范各段前后斜杠 + const segments = parts + .filter((p) => p !== undefined && p !== null) + .map((p) => String(p)) + .map((s) => s.replace(/^\/+|\/+$/g, '')) + .filter(Boolean); + const joined = [baseSanitized, ...segments].join('/'); + // 折叠除协议外的多余斜杠,例如 https://example.com//a///b -> https://example.com/a/b + return joined.replace(/([^:])\/{2,}/g, '$1/'); + } + + /** + * 创建 WooCommerce SDK 实例 + * @param site 站点配置 + * @param namespace API 命名空间,默认 wc/v3;订阅推荐 wcs/v1 + */ + private createApi(site: WpSite, namespace: WooCommerceRestApiVersion = 'wc/v3') { + return new WooCommerceRestApi({ + url: site.wpApiUrl, + consumerKey: site.consumerKey, + consumerSecret: site.consumerSecret, + // SDK 的版本字段有联合类型限制,这里兼容插件命名空间(例如 wcs/v1) + version: namespace, + }); + } + + /** + * 通过 SDK 获取单页数据,并返回数据与 totalPages + */ + private async sdkGetPage(api: any, resource: string, params: Record = {}) { + const page = params.page ?? 1; + const per_page = params.per_page ?? 100; + const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page }); + const data = res.data as T[]; + const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1); + return { items: data, totalPages, page, per_page }; + } + + /** + * 通过 SDK 聚合分页数据,返回全部数据 + */ + private async sdkGetAll(api: any, resource: string, params: Record = {}, maxPages: number = 50): Promise { + const result: T[] = []; + for (let page = 1; page <= maxPages; page++) { + const { items, totalPages } = await this.sdkGetPage(api, resource, { ...params, page }); + result.push(...items); + if (page >= totalPages) break; + } + return result; + } + /** * 获取 WordPress 数据 * @param wpApiUrl WordPress REST API 的基础地址 @@ -31,7 +90,8 @@ export class WPService { ): Promise { try { const { wpApiUrl, consumerKey, consumerSecret } = site; - const url = `${wpApiUrl}/wp-json${endpoint}`; + // 构建 URL,规避多/或少/问题 + const url = this.buildURL(wpApiUrl, '/wp-json', endpoint); const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); @@ -60,11 +120,13 @@ export class WPService { const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( 'base64' ); + console.log(`!!!wpApiUrl, consumerKey, consumerSecret, auth`,wpApiUrl, consumerKey, consumerSecret, auth) let hasMore = true; while (hasMore) { const config: AxiosRequestConfig = { method: 'GET', - url: `${wpApiUrl}/wp-json${endpoint}`, + // 构建 URL,规避多/或少/问题 + url: this.buildURL(wpApiUrl, '/wp-json', endpoint), headers: { Authorization: `Basic ${auth}`, }, @@ -95,14 +157,13 @@ export class WPService { } async getProducts(site: WpSite): Promise { - return await this.fetchPagedData('/wc/v3/products', site); + const api = this.createApi(site, 'wc/v3'); + return await this.sdkGetAll(api, 'products'); } async getVariations(site: WpSite, productId: number): Promise { - return await this.fetchPagedData( - `/wc/v3/products/${productId}/variations`, - site - ); + const api = this.createApi(site, 'wc/v3'); + return await this.sdkGetAll(api, `products/${productId}/variations`); } async getVariation( @@ -110,10 +171,9 @@ export class WPService { productId: number, variationId: number ): Promise { - return await this.fetchData( - `/wc/v3/products/${productId}/variations/${variationId}`, - site - ); + const api = this.createApi(site, 'wc/v3'); + const res = await api.get(`products/${productId}/variations/${variationId}`); + return res.data as Variation; } async getOrder( @@ -121,37 +181,27 @@ export class WPService { orderId: string ): Promise> { const site = this.geSite(siteId); - return await this.fetchData>( - `/wc/v3/orders/${orderId}`, - site - ); + const api = this.createApi(site, 'wc/v3'); + const res = await api.get(`orders/${orderId}`); + return res.data as Record; } async getOrders(siteId: string): Promise[]> { const site = this.geSite(siteId); - return await this.fetchPagedData>( - '/wc/v3/orders', - site - ); + const api = this.createApi(site, 'wc/v3'); + return await this.sdkGetAll>(api, 'orders'); } /** * 获取 WooCommerce Subscriptions - * 优先尝试 wc/v1/subscriptions (Subscriptions 插件提供), 如失败则回退 wc/v3/subscriptions(部分版本提供)。 + * 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions。 + * 返回所有分页合并后的订阅数组。 */ async getSubscriptions(siteId: string): Promise[]> { const site = this.geSite(siteId); - try { - return await this.fetchPagedData>( - '/wc/v1/subscriptions', - site - ); - } catch (e) { - // fallback - return await this.fetchPagedData>( - '/wc/v3/subscriptions', - site - ); - } + // 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3 + const api = this.createApi(site, 'wc/v3'); + return await this.sdkGetAll>(api, 'subscriptions'); + } async getOrderRefund( @@ -160,10 +210,9 @@ export class WPService { refundId: number ): Promise> { const site = this.geSite(siteId); - return await this.fetchData>( - `/wc/v3/orders/${orderId}/refunds/${refundId}`, - site - ); + const api = this.createApi(site, 'wc/v3'); + const res = await api.get(`orders/${orderId}/refunds/${refundId}`); + return res.data as Record; } async getOrderRefunds( @@ -171,10 +220,8 @@ export class WPService { orderId: number ): Promise[]> { const site = this.geSite(siteId); - return await this.fetchPagedData>( - `/wc/v3/orders/${orderId}/refunds`, - site - ); + const api = this.createApi(site, 'wc/v3'); + return await this.sdkGetAll>(api, `orders/${orderId}/refunds`); } async getOrderNote( @@ -183,10 +230,9 @@ export class WPService { noteId: number ): Promise> { const site = this.geSite(siteId); - return await this.fetchData>( - `/wc/v3/orders/${orderId}/notes/${noteId}`, - site - ); + const api = this.createApi(site, 'wc/v3'); + const res = await api.get(`orders/${orderId}/notes/${noteId}`); + return res.data as Record; } async getOrderNotes( @@ -194,10 +240,8 @@ export class WPService { orderId: number ): Promise[]> { const site = this.geSite(siteId); - return await this.fetchPagedData>( - `/wc/v3/orders/${orderId}/notes`, - site - ); + const api = this.createApi(site, 'wc/v3'); + return await this.sdkGetAll>(api, `orders/${orderId}/notes`); } async updateData( @@ -211,7 +255,8 @@ export class WPService { ); const config: AxiosRequestConfig = { method: 'PUT', - url: `${wpApiUrl}/wp-json${endpoint}`, + // 构建 URL,规避多/或少/问题 + url: this.buildURL(wpApiUrl, '/wp-json', endpoint), headers: { Authorization: `Basic ${auth}`, }, @@ -309,7 +354,14 @@ export class WPService { ); const config: AxiosRequestConfig = { method: 'POST', - url: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings`, + // 构建 URL,规避多/或少/问题 + url: this.buildURL( + wpApiUrl, + '/wp-json', + 'wc-ast/v3/orders', + orderId, + 'shipment-trackings' + ), headers: { Authorization: `Basic ${auth}`, }, @@ -332,7 +384,15 @@ export class WPService { // 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders//shipment-trackings/ const config: AxiosRequestConfig = { method: 'DELETE', - url: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings/${trackingId}`, + // 构建 URL,规避多/或少/问题 + url: this.buildURL( + wpApiUrl, + '/wp-json', + 'wc-ast/v3/orders', + orderId, + 'shipment-trackings', + trackingId + ), headers: { Authorization: `Basic ${auth}`, }, diff --git a/src/service/wp_product.service.ts b/src/service/wp_product.service.ts index 400a411..3f570e6 100644 --- a/src/service/wp_product.service.ts +++ b/src/service/wp_product.service.ts @@ -1,4 +1,4 @@ -import { Product } from './../entity/product.entty'; +import { Product } from '../entity/product.entity'; import { Config, Inject, Provide } from '@midwayjs/core'; import { WPService } from './wp.service'; import { WpSite } from '../interface';