forked from yoone/API
1
0
Fork 0

Compare commits

..

No commits in common. "927857a795cc1c2e541b55ab8babac1d5ddf255b" and "d04841c7afad44fc99d630d03c406dd10fc6bf9c" have entirely different histories.

29 changed files with 236 additions and 7837 deletions

1
.gitignore vendored
View File

@ -14,4 +14,3 @@ run/
yarn.lock yarn.lock
**/config.prod.ts **/config.prod.ts
**/config.local.ts **/config.local.ts
container

3975
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,6 @@
"@midwayjs/swagger": "^3.20.2", "@midwayjs/swagger": "^3.20.2",
"@midwayjs/typeorm": "^3.20.0", "@midwayjs/typeorm": "^3.20.0",
"@midwayjs/validate": "^3.20.2", "@midwayjs/validate": "^3.20.2",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"axios": "^1.7.9", "axios": "^1.7.9",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
@ -48,9 +47,6 @@
"author": "anonymous", "author": "anonymous",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@midwayjs/mock": "^3.20.11", "@midwayjs/mock": "^3.20.11"
"cross-env": "^10.1.0",
"mwtsc": "^1.15.2",
"typescript": "^5.9.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { MidwayConfig } from '@midwayjs/core'; import { MidwayConfig } from '@midwayjs/core';
import { Product } from '../entity/product.entity'; import { Product } from '../entity/product.entty';
import { Category } from '../entity/category.entity'; import { Category } from '../entity/category.entity';
import { WpProduct } from '../entity/wp_product.entity'; import { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity'; import { Variation } from '../entity/variation.entity';
@ -32,8 +32,6 @@ import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity'; import { Customer } from '../entity/customer.entity';
import { DeviceWhitelist } from '../entity/device_whitelist'; import { DeviceWhitelist } from '../entity/device_whitelist';
import { AuthCode } from '../entity/auth_code'; import { AuthCode } from '../entity/auth_code';
import { Subscription } from '../entity/subscription.entity';
import { Site } from '../entity/site.entity';
export default { export default {
// use for cookie sign key, should change to your own and keep security // use for cookie sign key, should change to your own and keep security
@ -74,8 +72,6 @@ export default {
Customer, Customer,
DeviceWhitelist, DeviceWhitelist,
AuthCode, AuthCode,
Subscription,
Site,
], ],
synchronize: true, synchronize: true,
logging: false, logging: false,

View File

@ -17,7 +17,6 @@ import * as crossDomain from '@midwayjs/cross-domain';
import * as cron from '@midwayjs/cron'; import * as cron from '@midwayjs/cron';
import * as jwt from '@midwayjs/jwt'; import * as jwt from '@midwayjs/jwt';
import { USER_KEY } from './decorator/user.decorator'; import { USER_KEY } from './decorator/user.decorator';
import { SiteService } from './service/site.service';
import { AuthMiddleware } from './middleware/auth.middleware'; import { AuthMiddleware } from './middleware/auth.middleware';
@Configuration({ @Configuration({
@ -46,9 +45,6 @@ export class MainConfiguration {
@Inject() @Inject()
jwtService: jwt.JwtService; // 注入 JwtService 实例 jwtService: jwt.JwtService; // 注入 JwtService 实例
@Inject()
siteService: SiteService;
async onReady() { async onReady() {
// add middleware // add middleware
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]); this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
@ -78,8 +74,5 @@ export class MainConfiguration {
} }
} }
); );
const sites = this.app.getConfig('wpSite') || [];
await this.siteService.syncFromConfig(sites);
} }
} }

View File

@ -22,7 +22,6 @@ import {
CreateOrderNoteDTO, CreateOrderNoteDTO,
QueryOrderDTO, QueryOrderDTO,
QueryOrderSalesDTO, QueryOrderSalesDTO,
QueryOrderItemDTO,
} from '../dto/order.dto'; } from '../dto/order.dto';
import { User } from '../decorator/user.decorator'; import { User } from '../decorator/user.decorator';
import { ErpOrderStatus } from '../enums/base.enum'; import { ErpOrderStatus } from '../enums/base.enum';
@ -98,26 +97,6 @@ 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({ @ApiOkResponse({
type: OrderDetailRes, type: OrderDetailRes,
}) })
@ -130,16 +109,6 @@ 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({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })

View File

@ -1,75 +1,25 @@
import { Body, Controller, Get, Inject, Param, Put, Post, Query } from '@midwayjs/core'; import { Config, Controller, Get } from '@midwayjs/core';
import { ApiOkResponse } from '@midwayjs/swagger'; import { ApiOkResponse } from '@midwayjs/swagger';
import { WpSitesResponse } from '../dto/reponse.dto'; import { WpSitesResponse } from '../dto/reponse.dto';
import { errorResponse, successResponse } from '../utils/response.util'; import { successResponse } from '../utils/response.util';
import { SiteService } from '../service/site.service'; import { WpSite } from '../interface';
import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto';
@Controller('/site') @Controller('/site')
export class SiteController { export class SiteController {
@Inject() @Config('wpSite')
siteService: SiteService; sites: WpSite[];
@ApiOkResponse({ description: '关联网站', type: WpSitesResponse }) @ApiOkResponse({
description: '关联网站',
type: WpSitesResponse,
})
@Get('/all') @Get('/all')
async all() { async all() {
try { return successResponse(
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false }); this.sites.map(v => ({
return successResponse(items.map((v: any) => ({ id: v.id, siteName: v.siteName }))); id: v.id,
} catch (error) { siteName: v.siteName,
return errorResponse(error?.message || '获取失败'); }))
} );
}
@Post('/create')
async create(@Body() body: CreateSiteDTO) {
try {
await this.siteService.create(body);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '创建失败');
}
}
@Put('/update/:id')
async update(@Param('id') id: string, @Body() body: UpdateSiteDTO) {
try {
await this.siteService.update(Number(id), body);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
}
@Get('/get/:id')
async get(@Param('id') id: string) {
try {
const data = await this.siteService.get(Number(id), false);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@Get('/list')
async list(@Query() query: QuerySiteDTO) {
try {
const data = await this.siteService.list(query, false);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
// 批量查询改为使用 /site/list?ids=1,2,3
@Put('/disable/:id')
async disable(@Param('id') id: string, @Body() body: DisableSiteDTO) {
try {
await this.siteService.disable(Number(id), body.disabled);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '更新失败');
}
} }
} }

View File

@ -1,36 +0,0 @@
import { Controller, Inject, Param, Post, Get, Query } from '@midwayjs/core';
import { ApiOkResponse } from '@midwayjs/swagger';
import { SubscriptionService } from '../service/subscription.service';
import { errorResponse, successResponse } from '../utils/response.util';
import { BooleanRes, SubscriptionListRes } from '../dto/reponse.dto';
import { QuerySubscriptionDTO } from '../dto/subscription.dto';
@Controller('/subscription')
export class SubscriptionController {
@Inject()
subscriptionService: SubscriptionService;
// 同步订阅:根据站点 ID 拉取并更新本地订阅数据
@ApiOkResponse({ type: BooleanRes })
@Post('/sync/:siteId')
async sync(@Param('siteId') siteId: string) {
try {
await this.subscriptionService.syncSubscriptions(siteId);
return successResponse(true);
} catch (error) {
return errorResponse(error?.message || '同步失败');
}
}
// 订阅列表:分页 + 筛选
@ApiOkResponse({ type: SubscriptionListRes })
@Get('/list')
async list(@Query() query: QuerySubscriptionDTO) {
try {
const data = await this.subscriptionService.getSubscriptionList(query);
return successResponse(data);
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
}

View File

@ -1,4 +1,4 @@
import { HttpStatus, Inject } from '@midwayjs/core'; import { Config, HttpStatus, Inject } from '@midwayjs/core';
import { import {
Controller, Controller,
Post, Post,
@ -11,8 +11,8 @@ import { Context } from '@midwayjs/koa';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { WpProductService } from '../service/wp_product.service'; import { WpProductService } from '../service/wp_product.service';
import { WPService } from '../service/wp.service'; import { WPService } from '../service/wp.service';
import { SiteService } from '../service/site.service';
import { OrderService } from '../service/order.service'; import { OrderService } from '../service/order.service';
import { WpSite } from '../interface';
@Controller('/webhook') @Controller('/webhook')
export class WebhookController { export class WebhookController {
@ -30,10 +30,8 @@ export class WebhookController {
@Inject() @Inject()
ctx: Context; ctx: Context;
@Inject() @Config('wpSite')
private readonly siteService: SiteService; sites: WpSite[];
// 移除配置中的站点数组,来源统一改为数据库
@Get('/') @Get('/')
async test() { async test() {
@ -49,10 +47,9 @@ export class WebhookController {
const signature = header['x-wc-webhook-signature']; const signature = header['x-wc-webhook-signature'];
const topic = header['x-wc-webhook-topic']; const topic = header['x-wc-webhook-topic'];
const source = header['x-wc-webhook-source']; const source = header['x-wc-webhook-source'];
// 从数据库获取站点配置 let site = this.sites.find(item => item.id === siteId);
const site = await this.siteService.get(Number(siteId), true);
if (!site || !source.includes(site.apiUrl)) { if (!site || !source.includes(site.wpApiUrl)) {
console.log('domain not match'); console.log('domain not match');
return { return {
code: HttpStatus.BAD_REQUEST, code: HttpStatus.BAD_REQUEST,
@ -97,13 +94,13 @@ export class WebhookController {
? await this.wpApiService.getVariations(site, body.id) ? await this.wpApiService.getVariations(site, body.id)
: []; : [];
await this.wpProductService.syncProductAndVariations( await this.wpProductService.syncProductAndVariations(
String(site.id), site.id,
body, body,
variations variations
); );
break; break;
case 'product.deleted': case 'product.deleted':
await this.wpProductService.delWpProduct(String(site.id), body.id); await this.wpProductService.delWpProduct(site.id, body.id);
break; break;
case 'order.created': case 'order.created':
case 'order.updated': case 'order.updated':

View File

@ -7,6 +7,7 @@ import {
Query, Query,
Put, Put,
Body, Body,
Config,
} from '@midwayjs/core'; } from '@midwayjs/core';
import { WpProductService } from '../service/wp_product.service'; import { WpProductService } from '../service/wp_product.service';
import { errorResponse, successResponse } from '../utils/response.util'; import { errorResponse, successResponse } from '../utils/response.util';
@ -19,13 +20,22 @@ import {
UpdateWpProductDTO, UpdateWpProductDTO,
} from '../dto/wp_product.dto'; } from '../dto/wp_product.dto';
import { WPService } from '../service/wp.service'; import { WPService } from '../service/wp.service';
import { SiteService } from '../service/site.service'; import { WpSite } from '../interface';
import { import {
ProductsRes, ProductsRes,
} from '../dto/reponse.dto'; } from '../dto/reponse.dto';
@Controller('/wp_product') @Controller('/wp_product')
export class WpProductController { export class WpProductController {
// 移除控制器内的配置站点引用,统一由服务层处理站点数据 @Inject()
wpService: WPService;
@Config('wpSite')
sites: WpSite[];
getSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
@Inject() @Inject()
private readonly wpProductService: WpProductService; private readonly wpProductService: WpProductService;
@ -33,9 +43,6 @@ export class WpProductController {
@Inject() @Inject()
private readonly wpApiService: WPService; private readonly wpApiService: WPService;
@Inject()
private readonly siteService: SiteService;
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })
@ -120,7 +127,7 @@ export class WpProductController {
if (isDuplicate) { if (isDuplicate) {
return errorResponse('SKU已存在'); return errorResponse('SKU已存在');
} }
const site = await this.siteService.get(Number(siteId), true); const site = await this.wpProductService.getSite(siteId);
const result = await this.wpApiService.updateProduct( const result = await this.wpApiService.updateProduct(
site, site,
productId, productId,
@ -160,7 +167,7 @@ export class WpProductController {
if (isDuplicate) { if (isDuplicate) {
return errorResponse('SKU已存在'); return errorResponse('SKU已存在');
} }
const site = await this.siteService.get(Number(siteId), true); const site = await this.wpProductService.getSite(siteId);
const result = await this.wpApiService.updateVariation( const result = await this.wpApiService.updateVariation(
site, site,
productId, productId,

View File

View File

@ -91,10 +91,6 @@ export class QueryOrderDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
payment_method: string; payment_method: string;
@ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
@Rule(RuleType.bool().default(false))
isSubscriptionOnly?: boolean;
} }
export class QueryOrderSalesDTO { export class QueryOrderSalesDTO {
@ -123,11 +119,11 @@ export class QueryOrderSalesDTO {
name: string; name: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.date()) @Rule(RuleType.date().required())
startDate: Date; startDate: Date;
@ApiProperty() @ApiProperty()
@Rule(RuleType.date()) @Rule(RuleType.date().required())
endDate: Date; endDate: Date;
} }
@ -145,37 +141,3 @@ export class CreateOrderNoteDTO {
@Rule(RuleType.string()) @Rule(RuleType.string())
content: 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;
}

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Category } from '../entity/category.entity'; import { Category } from '../entity/category.entity';
import { Order } from '../entity/order.entity'; import { Order } from '../entity/order.entity';
import { Product } from '../entity/product.entity'; import { Product } from '../entity/product.entty';
import { StockPoint } from '../entity/stock_point.entity'; import { StockPoint } from '../entity/stock_point.entity';
import { PaginatedWrapper } from '../utils/paginated-response.util'; import { PaginatedWrapper } from '../utils/paginated-response.util';
import { import {
@ -23,7 +23,6 @@ import { OrderNote } from '../entity/order_note.entity';
import { PaymentMethodDTO } from './logistics.dto'; import { PaymentMethodDTO } from './logistics.dto';
import { Flavors } from '../entity/flavors.entity'; import { Flavors } from '../entity/flavors.entity';
import { Strength } from '../entity/strength.entity'; import { Strength } from '../entity/strength.entity';
import { Subscription } from '../entity/subscription.entity';
export class BooleanRes extends SuccessWrapper(Boolean) {} export class BooleanRes extends SuccessWrapper(Boolean) {}
//网站配置返回数据 //网站配置返回数据
@ -118,8 +117,3 @@ export class OrderDetailRes extends SuccessWrapper(OrderDetail) {}
export class PaymentMethodListRes extends SuccessArrayWrapper( export class PaymentMethodListRes extends SuccessArrayWrapper(
PaymentMethodDTO PaymentMethodDTO
) {} ) {}
// 订阅分页数据(列表 + 总数等分页信息)
export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {}
// 订阅分页返回数据(统一成功包装)
export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {}

View File

@ -8,7 +8,7 @@ export class SiteConfig {
@ApiProperty({ description: '站点 URL' }) @ApiProperty({ description: '站点 URL' })
@Rule(RuleType.string()) @Rule(RuleType.string())
apiUrl: string; wpApiUrl: string;
@ApiProperty({ description: '站点 rest key' }) @ApiProperty({ description: '站点 rest key' })
@Rule(RuleType.string()) @Rule(RuleType.string())
@ -22,61 +22,11 @@ export class SiteConfig {
@Rule(RuleType.string()) @Rule(RuleType.string())
siteName: string; siteName: string;
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] }) @ApiProperty({ description: '站点邮箱' })
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
type: string;
@ApiProperty({ description: 'SKU 前缀' })
@Rule(RuleType.string()) @Rule(RuleType.string())
skuPrefix: string; email?: string;
}
export class CreateSiteDTO { @ApiProperty({ description: '站点邮箱密码' })
@Rule(RuleType.string().optional())
apiUrl?: string;
@Rule(RuleType.string().optional())
consumerKey?: string;
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string()) @Rule(RuleType.string())
siteName: string; emailPswd?: string;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@Rule(RuleType.string().optional())
skuPrefix?: string;
}
export class UpdateSiteDTO {
@Rule(RuleType.string().optional())
apiUrl?: string;
@Rule(RuleType.string().optional())
consumerKey?: string;
@Rule(RuleType.string().optional())
consumerSecret?: string;
@Rule(RuleType.string().optional())
siteName?: string;
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
type?: string;
@Rule(RuleType.string().optional())
skuPrefix?: string;
}
export class QuerySiteDTO {
@Rule(RuleType.number().optional())
current?: number;
@Rule(RuleType.number().optional())
pageSize?: number;
@Rule(RuleType.string().optional())
keyword?: string;
@Rule(RuleType.boolean().optional())
isDisabled?: boolean;
@Rule(RuleType.string().optional())
ids?: string;
}
export class DisableSiteDTO {
@Rule(RuleType.boolean())
disabled: boolean;
} }

View File

@ -1,36 +0,0 @@
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;
}

View File

@ -76,61 +76,16 @@ export class OrderItem {
@Expose() @Expose()
total_tax: number; 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() @ApiProperty()
@Column({ nullable: true }) @Column({ nullable: true })
@Expose() @Expose()
sku?: string; sku?: string;
@ApiProperty()
@Column({ nullable: true })
@Expose()
global_unique_id?: string; // 全局唯一ID部分主题/插件会提供)
@ApiProperty() @ApiProperty()
@Column('decimal', { precision: 10, scale: 2 }) @Column('decimal', { precision: 10, scale: 2 })
@Expose() @Expose()
price: number; 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({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
description: '创建时间', description: '创建时间',

View File

@ -1,28 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('site')
export class Site {
@PrimaryGeneratedColumn({ type: 'int' })
id: number;
@Column({ type: 'varchar', length: 255, nullable: true })
apiUrl: string;
@Column({ type: 'varchar', length: 255, nullable: true })
consumerKey: string;
@Column({ type: 'varchar', length: 255, nullable: true })
consumerSecret: string;
@Column({ type: 'varchar', length: 255, unique: true })
siteName: string;
@Column({ type: 'varchar', length: 32, default: 'woocommerce' })
type: string; // 平台类型woocommerce | shopyy
@Column({ type: 'varchar', length: 64, nullable: true })
skuPrefix: string;
@Column({ type: 'tinyint', default: 0 })
isDisabled: number;
}

View File

@ -1,128 +0,0 @@
import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer';
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
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;
// 客户 IDWooCommerce 用户 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()
updatedAt: Date;
}

View File

@ -31,22 +31,19 @@ export enum StockRecordOperationType {
IN = 'in', IN = 'in',
OUT = 'out', OUT = 'out',
} }
// Order status. Options: pending, processing, on-hold, completed, cancelled, refunded, failed and trash. Default is pending.
// 原始订单状态
export enum OrderStatus { export enum OrderStatus {
PENDING = 'pending', // default // 待付款 PENDING = 'pending',
PROCESSING = 'processing', // 正在处理 PROCESSING = 'processing',
ON_HOLD = 'on-hold', // 保留 COMPLETED = 'completed',
COMPLETED = 'completed', // 已完成 ON_HOLD = 'on-hold',
CANCEL = 'cancelled', // 已取消 CANCEL = 'cancelled',
REFUNDED = 'refunded', // 已退款 REFUNDED = 'refunded',
FAILED = 'failed', // 失败订单 FAILED = 'failed',
DRAFT = 'draft', // 草稿 DRAFT = 'draft',
// TRASH = 'trash', REFUND_REQUESTED = 'refund_requested', // 已申请退款
// refund 也就是退款相关的状态 REFUND_APPROVED = 'refund_approved', // 退款申请已通过
RETURN_REQUESTED = 'return-requested', // 已申请退款 REFUND_CANCELLED = 'refund_cancelled', // 已取消退款
RETURN_APPROVED = 'return-approved', // 退款申请已通过
RETURN_CANCELLED = 'return-cancelled', // 已取消退款
} }
export enum ErpOrderStatus { export enum ErpOrderStatus {
@ -59,9 +56,9 @@ export enum ErpOrderStatus {
AFTER_SALE_PROCESSING = 'after_sale_pending', // 售后处理中 AFTER_SALE_PROCESSING = 'after_sale_pending', // 售后处理中
PENDING_RESHIPMENT = 'pending_reshipment', // 待补发 PENDING_RESHIPMENT = 'pending_reshipment', // 待补发
PENDING_REFUND = 'pending_refund', // 待退款 PENDING_REFUND = 'pending_refund', // 待退款
RETURN_REQUESTED = 'return-requested', // 已申请退款 REFUND_REQUESTED = 'refund_requested', // 已申请退款
RETURN_APPROVED = 'return-approved', // 退款申请已通过 REFUND_APPROVED = 'refund_approved', // 退款申请已通过
RETURN_CANCELLED = 'return-cancelled', // 已取消退款 REFUND_CANCELLED = 'refund_cancelled', // 已取消退款
} }
export enum ShipmentType { export enum ShipmentType {
@ -70,17 +67,5 @@ export enum ShipmentType {
} }
export enum staticValue { export enum staticValue {
// 万能验证码
STATIC_CAPTCHA = 'yoone2025!@YOONE0923' STATIC_CAPTCHA = 'yoone2025!@YOONE0923'
} }
// WooCommerce Subscription status
// Reference: https://woocommerce.com/document/subscriptions/statuses/
export enum SubscriptionStatus {
ACTIVE = 'active', // 活跃
PENDING = 'pending', // 待处理/待激活
ON_HOLD = 'on-hold', // 暂停
CANCELLED = 'cancelled', // 已取消
EXPIRED = 'expired', // 已过期
PENDING_CANCELLATION = 'pending-cancel', // 待取消
}

View File

@ -1,4 +1,4 @@
import { Inject, Provide, sleep } from '@midwayjs/core'; import { Config, Inject, Provide, sleep } from '@midwayjs/core';
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
import { Service } from '../entity/service.entity'; import { Service } from '../entity/service.entity';
import { In, IsNull, Like, Repository } from 'typeorm'; import { In, IsNull, Like, Repository } from 'typeorm';
@ -21,6 +21,7 @@ import { StockRecord } from '../entity/stock_record.entity';
import { Stock } from '../entity/stock.entity'; import { Stock } from '../entity/stock.entity';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { WPService } from './wp.service'; 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 { ShippingDetailsDTO } from '../dto/freightcom.dto';
import { CanadaPostService } from './canadaPost.service'; import { CanadaPostService } from './canadaPost.service';
@ -30,7 +31,6 @@ import { UniExpressService } from './uni_express.service';
import { StockPoint } from '../entity/stock_point.entity'; import { StockPoint } from '../entity/stock_point.entity';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util'; import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
import { SiteService } from './site.service';
@Provide() @Provide()
export class LogisticsService { export class LogisticsService {
@ -82,8 +82,13 @@ export class LogisticsService {
@Inject() @Inject()
dataSourceManager: TypeORMDataSourceManager; dataSourceManager: TypeORMDataSourceManager;
@Inject() @Config('wpSite')
private readonly siteService: SiteService; sites: WpSite[];
getSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
async getServiceList(param: QueryServiceDTO) { async getServiceList(param: QueryServiceDTO) {
const { pageSize, current, carrier_name, isActive } = param; const { pageSize, current, carrier_name, isActive } = param;
@ -258,7 +263,7 @@ export class LogisticsService {
try { try {
// 同步订单状态到woocommerce // 同步订单状态到woocommerce
const site = await this.siteService.get(Number(order.siteId), true); const site = await this.getSite(order.siteId);
if (order.status === OrderStatus.COMPLETED) { if (order.status === OrderStatus.COMPLETED) {
await this.wpService.updateOrder(site, order.externalOrderId, { await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.PROCESSING, status: OrderStatus.PROCESSING,
@ -362,7 +367,7 @@ export class LogisticsService {
const tracking_provider = 'UniUni'; // todo: id未确定后写进常数 const tracking_provider = 'UniUni'; // todo: id未确定后写进常数
// 同步物流信息到woocommerce // 同步物流信息到woocommerce
const site = await this.siteService.get(Number(order.siteId), true); const site = await this.getSite(order.siteId);
const res = await this.wpService.createShipment(site, order.externalOrderId, { const res = await this.wpService.createShipment(site, order.externalOrderId, {
tracking_number: resShipmentOrder.data.tno, tracking_number: resShipmentOrder.data.tno,
tracking_provider: tracking_provider, tracking_provider: tracking_provider,
@ -488,7 +493,7 @@ export class LogisticsService {
const order = await this.orderModel.findOneBy({ const order = await this.orderModel.findOneBy({
id: orderShipment.order_id, id: orderShipment.order_id,
}); });
const site = await this.siteService.get(Number(order.siteId), true); const site = this.getSite(order.siteId);
await this.wpService.updateOrder(site, order.externalOrderId, { await this.wpService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.COMPLETED, status: OrderStatus.COMPLETED,
}); });
@ -558,10 +563,7 @@ export class LogisticsService {
}, },
}); });
// 从数据库批量获取站点信息,构建映射以避免 N+1 查询 const siteMap = new Map(this.sites.map(site => [site.id, site.siteName]));
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean)));
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName]));
return orders.map(order => ({ return orders.map(order => ({
...order, ...order,

View File

@ -1,6 +1,5 @@
import { Inject, Provide } from '@midwayjs/core'; import { Config, Inject, Provide } from '@midwayjs/core';
import { WPService } from './wp.service'; import { WPService } from './wp.service';
import { WpSite } from '../interface';
import { Order } from '../entity/order.entity'; import { Order } from '../entity/order.entity';
import { In, Like, Repository } from 'typeorm'; import { In, Like, Repository } from 'typeorm';
import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm'; import { InjectEntityModel, TypeORMDataSourceManager } from '@midwayjs/typeorm';
@ -9,7 +8,7 @@ import { OrderItem } from '../entity/order_item.entity';
import { OrderItemOriginal } from '../entity/order_items_original.entity'; import { OrderItemOriginal } from '../entity/order_items_original.entity';
import { OrderSale } from '../entity/order_sale.entity'; import { OrderSale } from '../entity/order_sale.entity';
import { WpProduct } from '../entity/wp_product.entity'; import { WpProduct } from '../entity/wp_product.entity';
import { Product } from '../entity/product.entity'; import { Product } from '../entity/product.entty';
import { OrderFee } from '../entity/order_fee.entity'; import { OrderFee } from '../entity/order_fee.entity';
import { OrderRefund } from '../entity/order_refund.entity'; import { OrderRefund } from '../entity/order_refund.entity';
import { OrderRefundItem } from '../entity/order_retund_item.entity'; import { OrderRefundItem } from '../entity/order_retund_item.entity';
@ -24,11 +23,10 @@ import {
} from '../enums/base.enum'; } from '../enums/base.enum';
import { Variation } from '../entity/variation.entity'; import { Variation } from '../entity/variation.entity';
import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto';
import dayjs = require('dayjs');
import { OrderDetailRes } from '../dto/reponse.dto'; import { OrderDetailRes } from '../dto/reponse.dto';
import { OrderNote } from '../entity/order_note.entity'; import { OrderNote } from '../entity/order_note.entity';
import { User } from '../entity/user.entity'; import { User } from '../entity/user.entity';
import { SiteService } from './site.service'; import { WpSite } from '../interface';
import { ShipmentItem } from '../entity/shipment_item.entity'; import { ShipmentItem } from '../entity/shipment_item.entity';
import { UpdateStockDTO } from '../dto/stock.dto'; import { UpdateStockDTO } from '../dto/stock.dto';
import { StockService } from './stock.service'; import { StockService } from './stock.service';
@ -36,9 +34,11 @@ import { OrderSaleOriginal } from '../entity/order_item_original.entity';
@Provide() @Provide()
export class OrderService { export class OrderService {
@Config('wpSite')
sites: WpSite[];
@Inject() @Inject()
wpService: WPService; wPService: WPService;
@Inject() @Inject()
stockService: StockService; stockService: StockService;
@ -100,44 +100,18 @@ export class OrderService {
@InjectEntityModel(Customer) @InjectEntityModel(Customer)
customerModel: Repository<Customer>; customerModel: Repository<Customer>;
@Inject()
siteService: SiteService;
async syncOrders(siteId: string) { 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) { for (const order of orders) {
await this.syncSingleOrder(siteId, order); await this.syncSingleOrder(siteId, order);
} }
} }
async syncOrderById(siteId: string, orderId: string) { 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); await this.syncSingleOrder(siteId, order, true);
} }
// 订单状态切换表
orderAutoNextStatusMap = {
[OrderStatus.RETURN_APPROVED]: OrderStatus.ON_HOLD, // 退款申请已通过转为 on-hold
[OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded
}
// 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
async autoUpdateOrderStatus(siteId: string, order: any) {
console.log('更新订单状态', order)
// 其他状态保持不变
const originStatus = order.status;
// 如果有值就赋值
if (!this.orderAutoNextStatusMap[originStatus]) {
return
}
try {
const site = await this.siteService.get(siteId);
// 将订单状态同步到 WooCommerce然后切换至下一状态
await this.wpService.updateOrder(site, String(order.id), { status: order.status });
order.status = this.orderAutoNextStatusMap[originStatus];
} catch (error) {
console.error('更新订单状态失败,原因为:', error)
}
}
// wordpress 发来,
async syncSingleOrder(siteId: string, order: any, forceUpdate = false) { async syncSingleOrder(siteId: string, order: any, forceUpdate = false) {
let { let {
line_items, line_items,
@ -147,12 +121,10 @@ export class OrderService {
refunds, refunds,
...orderData ...orderData
} = order; } = order;
console.log('同步进单个订单', order)
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: order.id, siteId: siteId }, where: { externalOrderId: order.id, siteId: siteId },
}); });
// 更新状态 const orderId = (await this.saveOrder(siteId, orderData)).id;
await this.autoUpdateOrderStatus(siteId, order);
const externalOrderId = order.id; const externalOrderId = order.id;
if ( if (
existingOrder && existingOrder &&
@ -165,8 +137,6 @@ export class OrderService {
if (existingOrder && !existingOrder.is_editable && !forceUpdate) { if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
return; return;
} }
const orderRecord = await this.saveOrder(siteId, orderData);
const orderId = orderRecord.id;
await this.saveOrderItems({ await this.saveOrderItems({
siteId, siteId,
orderId, orderId,
@ -293,12 +263,12 @@ export class OrderService {
return ErpOrderStatus.REFUNDED; return ErpOrderStatus.REFUNDED;
case OrderStatus.FAILED: case OrderStatus.FAILED:
return ErpOrderStatus.FAILED; return ErpOrderStatus.FAILED;
case OrderStatus.RETURN_REQUESTED: case OrderStatus.REFUND_REQUESTED:
return ErpOrderStatus.RETURN_REQUESTED; return ErpOrderStatus.REFUND_REQUESTED;
case OrderStatus.RETURN_APPROVED: case OrderStatus.REFUND_APPROVED:
return ErpOrderStatus.RETURN_APPROVED; return ErpOrderStatus.REFUND_APPROVED;
case OrderStatus.RETURN_CANCELLED: case OrderStatus.REFUND_CANCELLED:
return ErpOrderStatus.RETURN_CANCELLED; return ErpOrderStatus.REFUND_CANCELLED;
default: default:
return ErpOrderStatus.PENDING; return ErpOrderStatus.PENDING;
} }
@ -310,7 +280,6 @@ export class OrderService {
externalOrderId: string; externalOrderId: string;
orderItems: Record<string, any>[]; orderItems: Record<string, any>[];
}) { }) {
console.log('saveOrderItems params',params)
const { siteId, orderId, externalOrderId, orderItems } = params; const { siteId, orderId, externalOrderId, orderItems } = params;
const currentOrderItems = await this.orderItemModel.find({ const currentOrderItems = await this.orderItemModel.find({
where: { siteId, externalOrderId: externalOrderId }, where: { siteId, externalOrderId: externalOrderId },
@ -444,7 +413,7 @@ export class OrderService {
refunds: Record<string, any>[]; refunds: Record<string, any>[];
}) { }) {
for (const item of refunds) { for (const item of refunds) {
const refund = await this.wpService.getOrderRefund( const refund = await this.wPService.getOrderRefund(
siteId, siteId,
externalOrderId, externalOrderId,
item.id item.id
@ -615,7 +584,6 @@ export class OrderService {
customer_email, customer_email,
payment_method, payment_method,
billing_phone, billing_phone,
isSubscriptionOnly = false,
}, userId = undefined) { }, userId = undefined) {
const parameters: any[] = []; const parameters: any[] = [];
@ -642,37 +610,6 @@ export class OrderService {
o.payment_method as payment_method, o.payment_method as payment_method,
cs.order_count as order_count, cs.order_count as order_count,
cs.total_spent as total_spent, 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( COALESCE(
JSON_ARRAYAGG( JSON_ARRAYAGG(
CASE WHEN s.id IS NOT NULL THEN JSON_OBJECT( CASE WHEN s.id IS NOT NULL THEN JSON_OBJECT(
@ -732,14 +669,12 @@ export class OrderService {
totalQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_created <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
// 支付方式筛选使用参数化避免SQL注入
if (payment_method) { if (payment_method) {
sqlQuery += ` AND o.payment_method LIKE ?`; sqlQuery += ` AND o.payment_method like "%${payment_method}%" `;
totalQuery += ` AND o.payment_method LIKE ?`; totalQuery += ` AND o.payment_method like "%${payment_method}%" `;
parameters.push(`%${payment_method}%`);
} }
const user = await this.userModel.findOneBy({ id: userId }); const user = await this.userModel.findOneBy({ id: userId });
if (user?.permissions?.includes('order-10-days') && !startDate && !endDate) { if (user?.permissions?.includes('order-10-days')) {
sqlQuery += ` AND o.date_created >= ?`; sqlQuery += ` AND o.date_created >= ?`;
totalQuery += ` AND o.date_created >= ?`; totalQuery += ` AND o.date_created >= ?`;
const tenDaysAgo = new Date(); const tenDaysAgo = new Date();
@ -764,21 +699,6 @@ 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) { if (customer_email) {
sqlQuery += ` AND o.customer_email LIKE ?`; sqlQuery += ` AND o.customer_email LIKE ?`;
totalQuery += ` AND o.customer_email LIKE ?`; totalQuery += ` AND o.customer_email LIKE ?`;
@ -824,6 +744,7 @@ export class OrderService {
// 执行查询 // 执行查询
const orders = await this.orderModel.query(sqlQuery, parameters); const orders = await this.orderModel.query(sqlQuery, parameters);
return { items: orders, total, current, pageSize }; return { items: orders, total, current, pageSize };
} }
@ -835,8 +756,7 @@ export class OrderService {
keyword, keyword,
customer_email, customer_email,
billing_phone, billing_phone,
isSubscriptionOnly = false, }) {
}: any) {
const query = this.orderModel const query = this.orderModel
.createQueryBuilder('order') .createQueryBuilder('order')
.select('order.orderStatus', 'status') .select('order.orderStatus', 'status')
@ -874,24 +794,11 @@ 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(); return await query.getRawMany();
} }
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; 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; const offset = (current - 1) * pageSize;
// ------------------------- // -------------------------
@ -1095,10 +1002,6 @@ export class OrderService {
name, name,
}: QueryOrderSalesDTO) { }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; 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 = ` let sqlQuery = `
WITH product_purchase_counts AS ( WITH product_purchase_counts AS (
@ -1201,67 +1104,9 @@ export class OrderService {
pageSize, 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<OrderDetailRes> { async getOrderDetail(id: number): Promise<OrderDetailRes> {
const order = await this.orderModel.findOne({ where: { id } }); const order = await this.orderModel.findOne({ where: { id } });
const site = await this.siteService.get(Number(order.siteId), true); const site = this.sites.find(site => site.id === order.siteId);
const items = await this.orderItemModel.find({ where: { orderId: id } }); const items = await this.orderItemModel.find({ where: { orderId: id } });
const sales = await this.orderSaleModel.find({ where: { orderId: id } }); const sales = await this.orderSaleModel.find({ where: { orderId: id } });
const refunds = await this.orderRefundModel.find({ const refunds = await this.orderRefundModel.find({
@ -1325,56 +1170,15 @@ export class OrderService {
console.log('create order sale origin error: ', error.message); 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<string>();
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 { return {
...order, ...order,
siteName: site?.siteName, siteName: site.siteName,
// Site 实体无邮箱字段,这里返回空字符串保持兼容 email: site.email,
email: '',
items, items,
sales, sales,
refundItems, refundItems,
notes, notes,
shipment, 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: [],
}; };
} }
@ -1415,24 +1219,22 @@ export class OrderService {
]), ]),
}, },
}); });
// 批量获取订单涉及的站点名称,避免使用配置文件 return orders.map(order => {
const siteIds = Array.from(new Set(orders.map(o => o.siteId).filter(Boolean))); return {
const { items: sites } = await this.siteService.list({ current: 1, pageSize: 1000, ids: siteIds.join(',') }, false);
const siteMap = new Map(sites.map((s: any) => [String(s.id), s.siteName]));
return orders.map(order => ({
externalOrderId: order.externalOrderId, externalOrderId: order.externalOrderId,
id: order.id, id: order.id,
siteName: siteMap.get(order.siteId) || '', siteName:
})); this.sites.find(site => site.id === order.siteId)?.siteName || '',
};
});
} }
async cancelOrder(id: number) { async cancelOrder(id: number) {
const order = await this.orderModel.findOne({ where: { id } }); const order = await this.orderModel.findOne({ where: { id } });
if (!order) throw new Error(`订单 ${id}不存在`); if (!order) throw new Error(`订单 ${id}不存在`);
const s: any = await this.siteService.get(Number(order.siteId), true); const site = this.wPService.geSite(order.siteId);
const site = { id: String(s.id), wpApiUrl: s.apiUrl, consumerKey: s.consumerKey, consumerSecret: s.consumerSecret, siteName: s.siteName, email: '', emailPswd: '' } as WpSite;
if (order.status !== OrderStatus.CANCEL) { if (order.status !== OrderStatus.CANCEL) {
await this.wpService.updateOrder(site, order.externalOrderId, { await this.wPService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.CANCEL, status: OrderStatus.CANCEL,
}); });
order.status = OrderStatus.CANCEL; order.status = OrderStatus.CANCEL;
@ -1444,9 +1246,9 @@ export class OrderService {
async refundOrder(id: number) { async refundOrder(id: number) {
const order = await this.orderModel.findOne({ where: { id } }); const order = await this.orderModel.findOne({ where: { id } });
if (!order) throw new Error(`订单 ${id}不存在`); if (!order) throw new Error(`订单 ${id}不存在`);
const site = await this.siteService.get(Number(order.siteId), true); const site = this.wPService.geSite(order.siteId);
if (order.status !== OrderStatus.REFUNDED) { if (order.status !== OrderStatus.REFUNDED) {
await this.wpService.updateOrder(site, order.externalOrderId, { await this.wPService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.REFUNDED, status: OrderStatus.REFUNDED,
}); });
order.status = OrderStatus.REFUNDED; order.status = OrderStatus.REFUNDED;
@ -1458,9 +1260,9 @@ export class OrderService {
async completedOrder(id: number) { async completedOrder(id: number) {
const order = await this.orderModel.findOne({ where: { id } }); const order = await this.orderModel.findOne({ where: { id } });
if (!order) throw new Error(`订单 ${id}不存在`); if (!order) throw new Error(`订单 ${id}不存在`);
const site = await this.siteService.get(order.siteId); const site = this.wPService.geSite(order.siteId);
if (order.status !== OrderStatus.COMPLETED) { if (order.status !== OrderStatus.COMPLETED) {
await this.wpService.updateOrder(site, order.externalOrderId, { await this.wPService.updateOrder(site, order.externalOrderId, {
status: OrderStatus.COMPLETED, status: OrderStatus.COMPLETED,
}); });
order.status = OrderStatus.COMPLETED; order.status = OrderStatus.COMPLETED;

View File

@ -1,6 +1,6 @@
import { Provide } from '@midwayjs/core'; import { Provide } from '@midwayjs/core';
import { In, Like, Not, Repository } from 'typeorm'; import { In, Like, Not, Repository } from 'typeorm';
import { Product } from '../entity/product.entity'; import { Product } from '../entity/product.entty';
import { Category } from '../entity/category.entity'; import { Category } from '../entity/category.entity';
import { paginate } from '../utils/paginate.util'; import { paginate } from '../utils/paginate.util';
import { PaginationParams } from '../interface'; import { PaginationParams } from '../interface';

View File

@ -1,96 +0,0 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository, Like, In } from 'typeorm';
import { Site } from '../entity/site.entity';
import { WpSite } from '../interface';
import { UpdateSiteDTO } from '../dto/site.dto';
@Provide()
@Scope(ScopeEnum.Singleton)
export class SiteService {
@InjectEntityModel(Site)
siteModel: Repository<Site>;
async syncFromConfig(sites: WpSite[] = []) {
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
for (const siteConfig of sites) {
// 按站点名称查询是否已存在记录
const exist = await this.siteModel.findOne({ where: { siteName: siteConfig.siteName } });
// 将 WpSite 字段映射为 Site 实体字段
const payload: Partial<Site> = {
siteName: siteConfig.siteName,
apiUrl: (siteConfig as any).wpApiUrl,
consumerKey: (siteConfig as any).consumerKey,
consumerSecret: (siteConfig as any).consumerSecret,
type: 'woocommerce',
};
// 存在则更新,不存在则插入新记录
if (exist) await this.siteModel.update({ id: exist.id }, payload);
else await this.siteModel.insert(payload as Site);
}
}
async create(data: Partial<Site>) {
// 创建新的站点记录
await this.siteModel.insert(data as Site);
return true;
}
async update(id: string | number, data: UpdateSiteDTO) {
// 更新指定站点记录,将布尔 isDisabled 转换为数值 0/1
const payload: Partial<Site> = {
...data,
isDisabled:
data.isDisabled === undefined // 未传入则不更新该字段
? undefined
: data.isDisabled // true -> 1, false -> 0
? 1
: 0,
} as any;
await this.siteModel.update({ id: Number(id) }, payload);
return true;
}
async get(id: string | number, includeSecret = false) {
// 根据主键获取站点includeSecret 为 true 时返回密钥字段
const site = await this.siteModel.findOne({ where: { id: Number(id) } });
if (!site) return null;
if (includeSecret) return site;
// 默认不返回密钥,进行字段脱敏
const { consumerKey, consumerSecret, ...rest } = site;
return rest;
}
async list(param: { current?: number; pageSize?: number; keyword?: string; isDisabled?: boolean; ids?: string }, includeSecret = false) {
// 分页查询站点列表,支持关键字、禁用状态与 ID 列表过滤
const { current = 1, pageSize = 10, keyword, isDisabled, ids } = (param || {}) as any;
const where: any = {};
// 按名称模糊查询
if (keyword) where.siteName = Like(`%${keyword}%`);
// 按禁用状态过滤(布尔转数值)
if (typeof isDisabled === 'boolean') where.isDisabled = isDisabled ? 1 : 0;
if (ids) {
// 解析逗号分隔的 ID 字符串为数字数组,并过滤非法值
const numIds = String(ids)
.split(',')
.filter(Boolean)
.map((i) => Number(i))
.filter((v) => !Number.isNaN(v));
if (numIds.length > 0) where.id = In(numIds);
}
// 进行分页查询skip/take并返回总条数
const [items, total] = await this.siteModel.findAndCount({ where, skip: (current - 1) * pageSize, take: pageSize });
// 根据 includeSecret 决定是否脱敏返回密钥字段
const data = includeSecret ? items : items.map((item: any) => {
const { consumerKey, consumerSecret, ...rest } = item;
return rest;
});
return { items: data, total, current, pageSize };
}
async disable(id: string | number, disabled: boolean) {
// 设置站点禁用状态true -> 1, false -> 0
await this.siteModel.update({ id: Number(id) }, { isDisabled: disabled ? 1 : 0 });
return true;
}
}

View File

@ -3,7 +3,7 @@ import { Between, Like, Repository, LessThan, MoreThan } from 'typeorm';
import { Stock } from '../entity/stock.entity'; import { Stock } from '../entity/stock.entity';
import { StockRecord } from '../entity/stock_record.entity'; import { StockRecord } from '../entity/stock_record.entity';
import { paginate } from '../utils/paginate.util'; import { paginate } from '../utils/paginate.util';
import { Product } from '../entity/product.entity'; import { Product } from '../entity/product.entty';
import { import {
CreatePurchaseOrderDTO, CreatePurchaseOrderDTO,
CreateStockPointDTO, CreateStockPointDTO,

View File

@ -1,87 +0,0 @@
import { Inject, Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository, Like } from 'typeorm';
import { WPService } from './wp.service';
import { Subscription } from '../entity/subscription.entity';
import { plainToClass } from 'class-transformer';
import { SubscriptionStatus } from '../enums/base.enum';
import { QuerySubscriptionDTO } from '../dto/subscription.dto';
@Provide()
export class SubscriptionService {
@Inject()
wpService: WPService;
@InjectEntityModel(Subscription)
subscriptionModel: Repository<Subscription>;
/**
*
* - WooCommerce /
*/
async syncSubscriptions(siteId: string) {
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<Subscription> = {
...raw,
externalSubscriptionId: String(raw.id),
siteId,
status: raw.status as SubscriptionStatus,
customer_email: raw?.billing?.email || raw?.customer_email || '',
line_items,
};
delete (entity as any).id;
const existing = await this.subscriptionModel.findOne({
where: { externalSubscriptionId: String(sub.id), siteId },
});
const saveEntity = plainToClass(Subscription, entity);
if (existing) {
await this.subscriptionModel.update({ id: existing.id }, saveEntity);
} else {
await this.subscriptionModel.save(saveEntity);
}
}
/**
*
*/
async getSubscriptionList({
current = 1,
pageSize = 10,
siteId,
status,
customer_email,
keyword,
}: QuerySubscriptionDTO) {
const where: any = {};
if (siteId) where.siteId = siteId;
if (status) where.status = status;
if (customer_email) where.customer_email = Like(`%${customer_email}%`);
if (keyword) {
where.externalSubscriptionId = Like(`%${keyword}%`);
}
const [list, total] = await this.subscriptionModel.findAndCount({
where,
order: { id: 'DESC' },
skip: (current - 1) * pageSize,
take: pageSize,
});
return {
list,
total,
current,
pageSize,
};
}
}

View File

@ -1,77 +1,15 @@
import { Inject, Provide } from '@midwayjs/core'; import { Config, Provide } from '@midwayjs/core';
import axios, { AxiosRequestConfig } from 'axios'; 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 { WpProduct } from '../entity/wp_product.entity';
import { Variation } from '../entity/variation.entity'; import { Variation } from '../entity/variation.entity';
import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto'; import { UpdateVariationDTO, UpdateWpProductDTO } from '../dto/wp_product.dto';
import { ProductStatus, ProductStockStatus } from '../enums/base.enum'; import { ProductStatus, ProductStockStatus } from '../enums/base.enum';
import { SiteService } from './site.service';
@Provide() @Provide()
export class WPService { export class WPService {
@Inject() @Config('wpSite')
private readonly siteService: SiteService; sites: WpSite[];
/**
* URL / /
* 使this.buildURL(wpApiUrl, '/wp-json', 'wc/v3/products', productId)
*/
private buildURL(base: string, ...parts: Array<string | number>): 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: any, namespace: WooCommerceRestApiVersion = 'wc/v3') {
return new WooCommerceRestApi({
url: site.apiUrl,
consumerKey: site.consumerKey,
consumerSecret: site.consumerSecret,
version: namespace,
});
}
/**
* SDK totalPages
*/
private async sdkGetPage<T>(api: any, resource: string, params: Record<string, any> = {}) {
const page = params.page ?? 1;
const per_page = params.per_page ?? 100;
const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page });
if (res?.headers?.['content-type']?.includes('text/html')) {
throw new Error('接口返回了 text/html可能为 WordPress 登录页或错误页,请检查站点配置或权限');
}
const data = res.data as T[];
const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1);
const total = Number(res.headers?.['x-wp-total']?? 1)
return { items: data, total, totalPages, page, per_page };
}
/**
* SDK
*/
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
const result: T[] = [];
for (let page = 1; page <= maxPages; page++) {
const { items, totalPages } = await this.sdkGetPage<T>(api, resource, { ...params, page });
result.push(...items);
if (page >= totalPages) break;
}
return result;
}
/** /**
* WordPress * WordPress
@ -81,18 +19,19 @@ export class WPService {
* @param consumerSecret WooCommerce * @param consumerSecret WooCommerce
*/ */
geSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
async fetchData<T>( async fetchData<T>(
endpoint: string, endpoint: string,
site: any, site: WpSite,
param: Record<string, any> = {} param: Record<string, any> = {}
): Promise<T> { ): Promise<T> {
try { try {
const apiUrl = site.apiUrl; const { wpApiUrl, consumerKey, consumerSecret } = site;
const { consumerKey, consumerSecret } = site; const url = `${wpApiUrl}/wp-json${endpoint}`;
// 构建 URL规避多/或少/问题
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
); );
@ -112,22 +51,20 @@ export class WPService {
async fetchPagedData<T>( async fetchPagedData<T>(
endpoint: string, endpoint: string,
site: any, site: WpSite,
page: number = 1, page: number = 1,
perPage: number = 100 perPage: number = 100
): Promise<T[]> { ): Promise<T[]> {
const allData: T[] = []; const allData: T[] = [];
const { apiUrl, consumerKey, consumerSecret } = site; const { wpApiUrl, consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
); );
console.log(`!!!wpApiUrl, consumerKey, consumerSecret, auth`,site.apiUrl, consumerKey, consumerSecret, auth)
let hasMore = true; let hasMore = true;
while (hasMore) { while (hasMore) {
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'GET', method: 'GET',
// 构建 URL规避多/或少/问题 url: `${wpApiUrl}/wp-json${endpoint}`,
url: this.buildURL(apiUrl, '/wp-json', endpoint),
headers: { headers: {
Authorization: `Basic ${auth}`, Authorization: `Basic ${auth}`,
}, },
@ -157,52 +94,44 @@ export class WPService {
return allData; return allData;
} }
async getProducts(site: any): Promise<WpProduct[]> { async getProducts(site: WpSite): Promise<WpProduct[]> {
const api = this.createApi(site, 'wc/v3'); return await this.fetchPagedData<WpProduct>('/wc/v3/products', site);
return await this.sdkGetAll<WpProduct>(api, 'products');
} }
async getVariations(site: any, productId: number): Promise<Variation[]> { async getVariations(site: WpSite, productId: number): Promise<Variation[]> {
const api = this.createApi(site, 'wc/v3'); return await this.fetchPagedData<Variation>(
return await this.sdkGetAll<Variation>(api, `products/${productId}/variations`); `/wc/v3/products/${productId}/variations`,
site
);
} }
async getVariation( async getVariation(
site: any, site: WpSite,
productId: number, productId: number,
variationId: number variationId: number
): Promise<Variation> { ): Promise<Variation> {
const api = this.createApi(site, 'wc/v3'); return await this.fetchData<Variation>(
const res = await api.get(`products/${productId}/variations/${variationId}`); `/wc/v3/products/${productId}/variations/${variationId}`,
return res.data as Variation; site
);
} }
async getOrder( async getOrder(
siteId: string, siteId: string,
orderId: string orderId: string
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
const site = await this.siteService.get(siteId); const site = this.geSite(siteId);
const api = this.createApi(site, 'wc/v3'); return await this.fetchData<Record<string, any>>(
const res = await api.get(`orders/${orderId}`); `/wc/v3/orders/${orderId}`,
return res.data as Record<string, any>; site
);
} }
async getOrders(siteId: string): Promise<Record<string, any>[]> { async getOrders(siteId: string): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId); const site = this.geSite(siteId);
const api = this.createApi(site, 'wc/v3'); return await this.fetchPagedData<Record<string, any>>(
return await this.sdkGetAll<Record<string, any>>(api, 'orders'); '/wc/v3/orders',
} site
);
/**
* WooCommerce Subscriptions
* wc/v1/subscriptionsSubscriptions 退 wc/v3/subscriptions
*
*/
async getSubscriptions(siteId: string): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId);
// 优先使用 Subscriptions 命名空间 wcs/v1失败回退 wc/v3
const api = this.createApi(site, 'wc/v3');
return await this.sdkGetAll<Record<string, any>>(api, 'subscriptions');
} }
async getOrderRefund( async getOrderRefund(
@ -210,19 +139,22 @@ export class WPService {
orderId: string, orderId: string,
refundId: number refundId: number
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
const site = await this.siteService.get(siteId); const site = this.geSite(siteId);
const api = this.createApi(site, 'wc/v3'); return await this.fetchData<Record<string, any>>(
const res = await api.get(`orders/${orderId}/refunds/${refundId}`); `/wc/v3/orders/${orderId}/refunds/${refundId}`,
return res.data as Record<string, any>; site
);
} }
async getOrderRefunds( async getOrderRefunds(
siteId: string, siteId: string,
orderId: number orderId: number
): Promise<Record<string, any>[]> { ): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId); const site = this.geSite(siteId);
const api = this.createApi(site, 'wc/v3'); return await this.fetchPagedData<Record<string, any>>(
return await this.sdkGetAll<Record<string, any>>(api, `orders/${orderId}/refunds`); `/wc/v3/orders/${orderId}/refunds`,
site
);
} }
async getOrderNote( async getOrderNote(
@ -230,35 +162,36 @@ export class WPService {
orderId: number, orderId: number,
noteId: number noteId: number
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
const site = await this.siteService.get(siteId); const site = this.geSite(siteId);
const api = this.createApi(site, 'wc/v3'); return await this.fetchData<Record<string, any>>(
const res = await api.get(`orders/${orderId}/notes/${noteId}`); `/wc/v3/orders/${orderId}/notes/${noteId}`,
return res.data as Record<string, any>; site
);
} }
async getOrderNotes( async getOrderNotes(
siteId: string, siteId: string,
orderId: number orderId: number
): Promise<Record<string, any>[]> { ): Promise<Record<string, any>[]> {
const site = await this.siteService.get(siteId); const site = this.geSite(siteId);
const api = this.createApi(site, 'wc/v3'); return await this.fetchPagedData<Record<string, any>>(
return await this.sdkGetAll<Record<string, any>>(api, `orders/${orderId}/notes`); `/wc/v3/orders/${orderId}/notes`,
site
);
} }
async updateData<T>( async updateData<T>(
endpoint: string, endpoint: string,
site: any, site: WpSite,
data: Record<string, any> data: Record<string, any>
): Promise<Boolean> { ): Promise<Boolean> {
const apiUrl = site.apiUrl; const { wpApiUrl, consumerKey, consumerSecret } = site;
const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
); );
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'PUT', method: 'PUT',
// 构建 URL规避多/或少/问题 url: `${wpApiUrl}/wp-json${endpoint}`,
url: this.buildURL(apiUrl, '/wp-json', endpoint),
headers: { headers: {
Authorization: `Basic ${auth}`, Authorization: `Basic ${auth}`,
}, },
@ -278,7 +211,7 @@ export class WPService {
* @param data * @param data
*/ */
async updateProduct( async updateProduct(
site: any, site: WpSite,
productId: string, productId: string,
data: UpdateWpProductDTO data: UpdateWpProductDTO
): Promise<Boolean> { ): Promise<Boolean> {
@ -297,7 +230,7 @@ export class WPService {
* @param stock_status * @param stock_status
*/ */
async updateProductStatus( async updateProductStatus(
site: any, site: WpSite,
productId: string, productId: string,
status: ProductStatus, status: ProductStatus,
stock_status: ProductStockStatus stock_status: ProductStockStatus
@ -317,7 +250,7 @@ export class WPService {
* @param data * @param data
*/ */
async updateVariation( async updateVariation(
site: any, site: WpSite,
productId: string, productId: string,
variationId: string, variationId: string,
data: UpdateVariationDTO data: UpdateVariationDTO
@ -338,7 +271,7 @@ export class WPService {
* Order * Order
*/ */
async updateOrder( async updateOrder(
site: any, site: WpSite,
orderId: string, orderId: string,
data: Record<string, any> data: Record<string, any>
): Promise<Boolean> { ): Promise<Boolean> {
@ -346,25 +279,17 @@ export class WPService {
} }
async createShipment( async createShipment(
site: any, site: WpSite,
orderId: string, orderId: string,
data: Record<string, any> data: Record<string, any>
) { ) {
const apiUrl = site.apiUrl; const { wpApiUrl, consumerKey, consumerSecret } = site;
const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
); );
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'POST', method: 'POST',
// 构建 URL规避多/或少/问题 url: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings`,
url: this.buildURL(
apiUrl,
'/wp-json',
'wc-ast/v3/orders',
orderId,
'shipment-trackings'
),
headers: { headers: {
Authorization: `Basic ${auth}`, Authorization: `Basic ${auth}`,
}, },
@ -374,12 +299,11 @@ export class WPService {
} }
async deleteShipment( async deleteShipment(
site: any, site: WpSite,
orderId: string, orderId: string,
trackingId: string, trackingId: string,
): Promise<Boolean> { ): Promise<Boolean> {
const apiUrl = site.apiUrl; const { wpApiUrl, consumerKey, consumerSecret } = site;
const { consumerKey, consumerSecret } = site;
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString( const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
'base64' 'base64'
); );
@ -388,15 +312,7 @@ export class WPService {
// 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id> // 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
const config: AxiosRequestConfig = { const config: AxiosRequestConfig = {
method: 'DELETE', method: 'DELETE',
// 构建 URL规避多/或少/问题 url: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings/${trackingId}`,
url: this.buildURL(
apiUrl,
'/wp-json',
'wc-ast/v3/orders',
orderId,
'shipment-trackings',
trackingId
),
headers: { headers: {
Authorization: `Basic ${auth}`, Authorization: `Basic ${auth}`,
}, },

View File

@ -1,6 +1,7 @@
import { Product } from '../entity/product.entity'; import { Product } from './../entity/product.entty';
import { Inject, Provide } from '@midwayjs/core'; import { Config, Inject, Provide } from '@midwayjs/core';
import { WPService } from './wp.service'; import { WPService } from './wp.service';
import { WpSite } from '../interface';
import { WpProduct } from '../entity/wp_product.entity'; import { WpProduct } from '../entity/wp_product.entity';
import { InjectEntityModel } from '@midwayjs/typeorm'; import { InjectEntityModel } from '@midwayjs/typeorm';
import { And, Like, Not, Repository } from 'typeorm'; import { And, Like, Not, Repository } from 'typeorm';
@ -11,43 +12,41 @@ import {
UpdateWpProductDTO, UpdateWpProductDTO,
} from '../dto/wp_product.dto'; } from '../dto/wp_product.dto';
import { ProductStatus, ProductStockStatus } from '../enums/base.enum'; import { ProductStatus, ProductStockStatus } from '../enums/base.enum';
import { SiteService } from './site.service';
@Provide() @Provide()
export class WpProductService { export class WpProductService {
// 移除配置中的站点数组,统一从数据库获取站点信息 @Config('wpSite')
sites: WpSite[];
@Inject() @Inject()
private readonly wpApiService: WPService; private readonly wpApiService: WPService;
@Inject()
private readonly siteService: SiteService;
@InjectEntityModel(WpProduct) @InjectEntityModel(WpProduct)
wpProductModel: Repository<WpProduct>; wpProductModel: Repository<WpProduct>;
@InjectEntityModel(Variation) @InjectEntityModel(Variation)
variationModel: Repository<Variation>; variationModel: Repository<Variation>;
getSite(id: string): WpSite {
let idx = this.sites.findIndex(item => item.id === id);
return this.sites[idx];
}
async syncAllSites() { async syncAllSites() {
// 从数据库获取所有启用的站点,并逐站点同步产品与变体 for (const site of this.sites) {
const { items: sites } = await this.siteService.list({ current: 1, pageSize: Infinity, isDisabled: false }, true);
for (const site of sites) {
const products = await this.wpApiService.getProducts(site); const products = await this.wpApiService.getProducts(site);
for (const product of products) { for (const product of products) {
const variations = const variations =
product.type === 'variable' product.type === 'variable'
? await this.wpApiService.getVariations(site, product.id) ? await this.wpApiService.getVariations(site, product.id)
: []; : [];
await this.syncProductAndVariations(String(site.id), product, variations); await this.syncProductAndVariations(site.id, product, variations);
} }
} }
} }
// 同步一个网站
async syncSite(siteId: string) { async syncSite(siteId: string) {
// 通过数据库获取站点并转换为 WpSite用于后续 WooCommerce 同步 const site = this.getSite(siteId);
const site = await this.siteService.get(Number(siteId), true);
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product') const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
.select([ .select([
'wp_product.id ', 'wp_product.id ',
@ -70,7 +69,7 @@ export class WpProductService {
? await this.wpApiService.getVariations(site, product.id) ? await this.wpApiService.getVariations(site, product.id)
: []; : [];
await this.syncProductAndVariations(String(site.id), product, variations); await this.syncProductAndVariations(site.id, product, variations);
} }
const filteredIds = externalIds.filter(id => !excludeValues.includes(id)); const filteredIds = externalIds.filter(id => !excludeValues.includes(id));
@ -92,7 +91,7 @@ export class WpProductService {
// 控制产品上下架 // 控制产品上下架
async updateProductStatus(id: number, status: ProductStatus, stock_status: ProductStockStatus) { async updateProductStatus(id: number, status: ProductStatus, stock_status: ProductStockStatus) {
const wpProduct = await this.wpProductModel.findOneBy({ id }); const wpProduct = await this.wpProductModel.findOneBy({ id });
const site = await this.siteService.get(Number(wpProduct.siteId), true); const site = await this.getSite(wpProduct.siteId);
wpProduct.status = status; wpProduct.status = status;
wpProduct.stockStatus = stock_status; wpProduct.stockStatus = stock_status;
const res = await this.wpApiService.updateProductStatus(site, wpProduct.externalProductId, status, stock_status); const res = await this.wpApiService.updateProductStatus(site, wpProduct.externalProductId, status, stock_status);