forked from yoone/API
Compare commits
26 Commits
d04841c7af
...
927857a795
| Author | SHA1 | Date |
|---|---|---|
|
|
927857a795 | |
|
|
a59d5a7b75 | |
|
|
a64e611294 | |
|
|
c7480ccc8a | |
|
|
1d62730ca0 | |
|
|
c75d620516 | |
|
|
ec6a8c3154 | |
|
|
1bdae88c11 | |
|
|
27935f113d | |
|
|
12dc5ac876 | |
|
|
8778b8138d | |
|
|
eff9efc2c3 | |
|
|
11814a7c39 | |
|
|
3b5e3ec906 | |
|
|
79b7e96175 | |
|
|
4ce5cb8bb0 | |
|
|
e94ea5ed58 | |
|
|
dc070fadde | |
|
|
2d36370acf | |
|
|
533b2cd726 | |
|
|
795b13ce31 | |
|
|
b8290d0cda | |
|
|
d0b3b54ad8 | |
|
|
e3b102259c | |
|
|
c9342396df | |
|
|
ca4502d4a3 |
|
|
@ -14,3 +14,4 @@ run/
|
||||||
yarn.lock
|
yarn.lock
|
||||||
**/config.prod.ts
|
**/config.prod.ts
|
||||||
**/config.local.ts
|
**/config.local.ts
|
||||||
|
container
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,6 +16,7 @@
|
||||||
"@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",
|
||||||
|
|
@ -47,6 +48,9 @@
|
||||||
"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
|
|
@ -1,5 +1,5 @@
|
||||||
import { MidwayConfig } from '@midwayjs/core';
|
import { MidwayConfig } from '@midwayjs/core';
|
||||||
import { Product } from '../entity/product.entty';
|
import { Product } from '../entity/product.entity';
|
||||||
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,6 +32,8 @@ 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
|
||||||
|
|
@ -72,6 +74,8 @@ export default {
|
||||||
Customer,
|
Customer,
|
||||||
DeviceWhitelist,
|
DeviceWhitelist,
|
||||||
AuthCode,
|
AuthCode,
|
||||||
|
Subscription,
|
||||||
|
Site,
|
||||||
],
|
],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ 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({
|
||||||
|
|
@ -45,6 +46,9 @@ 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]);
|
||||||
|
|
@ -74,5 +78,8 @@ export class MainConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sites = this.app.getConfig('wpSite') || [];
|
||||||
|
await this.siteService.syncFromConfig(sites);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ 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';
|
||||||
|
|
@ -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({
|
@ApiOkResponse({
|
||||||
type: OrderDetailRes,
|
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({
|
@ApiOkResponse({
|
||||||
type: BooleanRes,
|
type: BooleanRes,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,75 @@
|
||||||
import { Config, Controller, Get } from '@midwayjs/core';
|
import { Body, Controller, Get, Inject, Param, Put, Post, Query } 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 { successResponse } from '../utils/response.util';
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import { WpSite } from '../interface';
|
import { SiteService } from '../service/site.service';
|
||||||
|
import { CreateSiteDTO, DisableSiteDTO, QuerySiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||||
|
|
||||||
@Controller('/site')
|
@Controller('/site')
|
||||||
export class SiteController {
|
export class SiteController {
|
||||||
@Config('wpSite')
|
@Inject()
|
||||||
sites: WpSite[];
|
siteService: SiteService;
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({ description: '关联网站', type: WpSitesResponse })
|
||||||
description: '关联网站',
|
|
||||||
type: WpSitesResponse,
|
|
||||||
})
|
|
||||||
@Get('/all')
|
@Get('/all')
|
||||||
async all() {
|
async all() {
|
||||||
return successResponse(
|
try {
|
||||||
this.sites.map(v => ({
|
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
|
||||||
id: v.id,
|
return successResponse(items.map((v: any) => ({ id: v.id, siteName: v.siteName })));
|
||||||
siteName: v.siteName,
|
} catch (error) {
|
||||||
}))
|
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 || '更新失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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 || '获取失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Config, HttpStatus, Inject } from '@midwayjs/core';
|
import { 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,8 +30,10 @@ export class WebhookController {
|
||||||
@Inject()
|
@Inject()
|
||||||
ctx: Context;
|
ctx: Context;
|
||||||
|
|
||||||
@Config('wpSite')
|
@Inject()
|
||||||
sites: WpSite[];
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
|
// 移除配置中的站点数组,来源统一改为数据库
|
||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async test() {
|
async test() {
|
||||||
|
|
@ -47,9 +49,10 @@ 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.wpApiUrl)) {
|
if (!site || !source.includes(site.apiUrl)) {
|
||||||
console.log('domain not match');
|
console.log('domain not match');
|
||||||
return {
|
return {
|
||||||
code: HttpStatus.BAD_REQUEST,
|
code: HttpStatus.BAD_REQUEST,
|
||||||
|
|
@ -94,13 +97,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(
|
||||||
site.id,
|
String(site.id),
|
||||||
body,
|
body,
|
||||||
variations
|
variations
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'product.deleted':
|
case 'product.deleted':
|
||||||
await this.wpProductService.delWpProduct(site.id, body.id);
|
await this.wpProductService.delWpProduct(String(site.id), body.id);
|
||||||
break;
|
break;
|
||||||
case 'order.created':
|
case 'order.created':
|
||||||
case 'order.updated':
|
case 'order.updated':
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ 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';
|
||||||
|
|
@ -20,22 +19,13 @@ 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 { WpSite } from '../interface';
|
import { SiteService } from '../service/site.service';
|
||||||
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;
|
||||||
|
|
@ -43,6 +33,9 @@ export class WpProductController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly wpApiService: WPService;
|
private readonly wpApiService: WPService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: BooleanRes,
|
type: BooleanRes,
|
||||||
})
|
})
|
||||||
|
|
@ -127,7 +120,7 @@ export class WpProductController {
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
return errorResponse('SKU已存在');
|
return errorResponse('SKU已存在');
|
||||||
}
|
}
|
||||||
const site = await this.wpProductService.getSite(siteId);
|
const site = await this.siteService.get(Number(siteId), true);
|
||||||
const result = await this.wpApiService.updateProduct(
|
const result = await this.wpApiService.updateProduct(
|
||||||
site,
|
site,
|
||||||
productId,
|
productId,
|
||||||
|
|
@ -167,7 +160,7 @@ export class WpProductController {
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
return errorResponse('SKU已存在');
|
return errorResponse('SKU已存在');
|
||||||
}
|
}
|
||||||
const site = await this.wpProductService.getSite(siteId);
|
const site = await this.siteService.get(Number(siteId), true);
|
||||||
const result = await this.wpApiService.updateVariation(
|
const result = await this.wpApiService.updateVariation(
|
||||||
site,
|
site,
|
||||||
productId,
|
productId,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@ 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 {
|
||||||
|
|
@ -119,11 +123,11 @@ export class QueryOrderSalesDTO {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Rule(RuleType.date().required())
|
@Rule(RuleType.date())
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Rule(RuleType.date().required())
|
@Rule(RuleType.date())
|
||||||
endDate: Date;
|
endDate: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,3 +145,37 @@ 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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.entty';
|
import { Product } from '../entity/product.entity';
|
||||||
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,6 +23,7 @@ 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) {}
|
||||||
//网站配置返回数据
|
//网站配置返回数据
|
||||||
|
|
@ -117,3 +118,8 @@ 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) {}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export class SiteConfig {
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 URL' })
|
@ApiProperty({ description: '站点 URL' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
wpApiUrl: string;
|
apiUrl: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 rest key' })
|
@ApiProperty({ description: '站点 rest key' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
|
|
@ -22,11 +22,61 @@ export class SiteConfig {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
siteName: string;
|
siteName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点邮箱' })
|
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
|
||||||
email?: string;
|
type: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '站点邮箱密码' })
|
@ApiProperty({ description: 'SKU 前缀' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
emailPswd?: string;
|
skuPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateSiteDTO {
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
apiUrl?: string;
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
consumerKey?: string;
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
consumerSecret?: string;
|
||||||
|
@Rule(RuleType.string())
|
||||||
|
siteName: 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -76,16 +76,61 @@ 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: '创建时间',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 客户 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()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
@ -31,19 +31,22 @@ 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',
|
PENDING = 'pending', // default // 待付款
|
||||||
PROCESSING = 'processing',
|
PROCESSING = 'processing', // 正在处理
|
||||||
COMPLETED = 'completed',
|
ON_HOLD = 'on-hold', // 保留
|
||||||
ON_HOLD = 'on-hold',
|
COMPLETED = 'completed', // 已完成
|
||||||
CANCEL = 'cancelled',
|
CANCEL = 'cancelled', // 已取消
|
||||||
REFUNDED = 'refunded',
|
REFUNDED = 'refunded', // 已退款
|
||||||
FAILED = 'failed',
|
FAILED = 'failed', // 失败订单
|
||||||
DRAFT = 'draft',
|
DRAFT = 'draft', // 草稿
|
||||||
REFUND_REQUESTED = 'refund_requested', // 已申请退款
|
// TRASH = 'trash',
|
||||||
REFUND_APPROVED = 'refund_approved', // 退款申请已通过
|
// refund 也就是退款相关的状态
|
||||||
REFUND_CANCELLED = 'refund_cancelled', // 已取消退款
|
RETURN_REQUESTED = 'return-requested', // 已申请退款
|
||||||
|
RETURN_APPROVED = 'return-approved', // 退款申请已通过
|
||||||
|
RETURN_CANCELLED = 'return-cancelled', // 已取消退款
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ErpOrderStatus {
|
export enum ErpOrderStatus {
|
||||||
|
|
@ -56,9 +59,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', // 待退款
|
||||||
REFUND_REQUESTED = 'refund_requested', // 已申请退款
|
RETURN_REQUESTED = 'return-requested', // 已申请退款
|
||||||
REFUND_APPROVED = 'refund_approved', // 退款申请已通过
|
RETURN_APPROVED = 'return-approved', // 退款申请已通过
|
||||||
REFUND_CANCELLED = 'refund_cancelled', // 已取消退款
|
RETURN_CANCELLED = 'return-cancelled', // 已取消退款
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ShipmentType {
|
export enum ShipmentType {
|
||||||
|
|
@ -67,5 +70,17 @@ 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', // 待取消
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Config, Inject, Provide, sleep } from '@midwayjs/core';
|
import { 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,7 +21,6 @@ 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';
|
||||||
|
|
@ -31,6 +30,7 @@ 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,13 +82,8 @@ export class LogisticsService {
|
||||||
@Inject()
|
@Inject()
|
||||||
dataSourceManager: TypeORMDataSourceManager;
|
dataSourceManager: TypeORMDataSourceManager;
|
||||||
|
|
||||||
@Config('wpSite')
|
@Inject()
|
||||||
sites: WpSite[];
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -263,7 +258,7 @@ export class LogisticsService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 同步订单状态到woocommerce
|
// 同步订单状态到woocommerce
|
||||||
const site = await this.getSite(order.siteId);
|
const site = await this.siteService.get(Number(order.siteId), true);
|
||||||
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,
|
||||||
|
|
@ -367,7 +362,7 @@ export class LogisticsService {
|
||||||
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
|
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
|
||||||
|
|
||||||
// 同步物流信息到woocommerce
|
// 同步物流信息到woocommerce
|
||||||
const site = await this.getSite(order.siteId);
|
const site = await this.siteService.get(Number(order.siteId), true);
|
||||||
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,
|
||||||
|
|
@ -493,7 +488,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 = this.getSite(order.siteId);
|
const site = await this.siteService.get(Number(order.siteId), true);
|
||||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||||
status: OrderStatus.COMPLETED,
|
status: OrderStatus.COMPLETED,
|
||||||
});
|
});
|
||||||
|
|
@ -563,7 +558,10 @@ export class LogisticsService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const siteMap = new Map(this.sites.map(site => [site.id, site.siteName]));
|
// 从数据库批量获取站点信息,构建映射以避免 N+1 查询
|
||||||
|
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,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Config, Inject, Provide } from '@midwayjs/core';
|
import { 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';
|
||||||
|
|
@ -8,7 +9,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.entty';
|
import { Product } from '../entity/product.entity';
|
||||||
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';
|
||||||
|
|
@ -23,10 +24,11 @@ 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 { WpSite } from '../interface';
|
import { SiteService } from './site.service';
|
||||||
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';
|
||||||
|
|
@ -34,11 +36,9 @@ 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,18 +100,44 @@ 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,
|
||||||
|
|
@ -121,10 +147,12 @@ 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 &&
|
||||||
|
|
@ -137,6 +165,8 @@ 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,
|
||||||
|
|
@ -263,12 +293,12 @@ export class OrderService {
|
||||||
return ErpOrderStatus.REFUNDED;
|
return ErpOrderStatus.REFUNDED;
|
||||||
case OrderStatus.FAILED:
|
case OrderStatus.FAILED:
|
||||||
return ErpOrderStatus.FAILED;
|
return ErpOrderStatus.FAILED;
|
||||||
case OrderStatus.REFUND_REQUESTED:
|
case OrderStatus.RETURN_REQUESTED:
|
||||||
return ErpOrderStatus.REFUND_REQUESTED;
|
return ErpOrderStatus.RETURN_REQUESTED;
|
||||||
case OrderStatus.REFUND_APPROVED:
|
case OrderStatus.RETURN_APPROVED:
|
||||||
return ErpOrderStatus.REFUND_APPROVED;
|
return ErpOrderStatus.RETURN_APPROVED;
|
||||||
case OrderStatus.REFUND_CANCELLED:
|
case OrderStatus.RETURN_CANCELLED:
|
||||||
return ErpOrderStatus.REFUND_CANCELLED;
|
return ErpOrderStatus.RETURN_CANCELLED;
|
||||||
default:
|
default:
|
||||||
return ErpOrderStatus.PENDING;
|
return ErpOrderStatus.PENDING;
|
||||||
}
|
}
|
||||||
|
|
@ -280,6 +310,7 @@ 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 },
|
||||||
|
|
@ -413,7 +444,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
|
||||||
|
|
@ -584,6 +615,7 @@ 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[] = [];
|
||||||
|
|
||||||
|
|
@ -610,6 +642,37 @@ 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(
|
||||||
|
|
@ -669,12 +732,14 @@ 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 "%${payment_method}%" `;
|
sqlQuery += ` AND o.payment_method LIKE ?`;
|
||||||
totalQuery += ` AND o.payment_method like "%${payment_method}%" `;
|
totalQuery += ` AND o.payment_method LIKE ?`;
|
||||||
|
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')) {
|
if (user?.permissions?.includes('order-10-days') && !startDate && !endDate) {
|
||||||
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();
|
||||||
|
|
@ -699,6 +764,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) {
|
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 ?`;
|
||||||
|
|
@ -744,7 +824,6 @@ 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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -756,7 +835,8 @@ 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')
|
||||||
|
|
@ -794,11 +874,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();
|
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;
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
@ -1002,6 +1095,10 @@ 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 (
|
||||||
|
|
@ -1104,9 +1201,67 @@ 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 = this.sites.find(site => site.id === order.siteId);
|
const site = await this.siteService.get(Number(order.siteId), true);
|
||||||
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({
|
||||||
|
|
@ -1170,15 +1325,56 @@ 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,
|
||||||
email: site.email,
|
// Site 实体无邮箱字段,这里返回空字符串保持兼容
|
||||||
|
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: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1219,22 +1415,24 @@ export class OrderService {
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return orders.map(order => {
|
// 批量获取订单涉及的站点名称,避免使用配置文件
|
||||||
return {
|
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 => ({
|
||||||
externalOrderId: order.externalOrderId,
|
externalOrderId: order.externalOrderId,
|
||||||
id: order.id,
|
id: order.id,
|
||||||
siteName:
|
siteName: siteMap.get(order.siteId) || '',
|
||||||
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 site = this.wPService.geSite(order.siteId);
|
const s: any = await this.siteService.get(Number(order.siteId), true);
|
||||||
|
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;
|
||||||
|
|
@ -1246,9 +1444,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 = this.wPService.geSite(order.siteId);
|
const site = await this.siteService.get(Number(order.siteId), true);
|
||||||
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;
|
||||||
|
|
@ -1260,9 +1458,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 = this.wPService.geSite(order.siteId);
|
const site = await this.siteService.get(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;
|
||||||
|
|
|
||||||
|
|
@ -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.entty';
|
import { Product } from '../entity/product.entity';
|
||||||
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';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.entty';
|
import { Product } from '../entity/product.entity';
|
||||||
import {
|
import {
|
||||||
CreatePurchaseOrderDTO,
|
CreatePurchaseOrderDTO,
|
||||||
CreateStockPointDTO,
|
CreateStockPointDTO,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,77 @@
|
||||||
import { Config, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
import { WpSite } from '../interface';
|
import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/woocommerce-rest-api';
|
||||||
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 {
|
||||||
@Config('wpSite')
|
@Inject()
|
||||||
sites: WpSite[];
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 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 数据
|
||||||
|
|
@ -19,19 +81,18 @@ 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: WpSite,
|
site: any,
|
||||||
param: Record<string, any> = {}
|
param: Record<string, any> = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
try {
|
try {
|
||||||
const { wpApiUrl, consumerKey, consumerSecret } = site;
|
const apiUrl = site.apiUrl;
|
||||||
const url = `${wpApiUrl}/wp-json${endpoint}`;
|
const { consumerKey, consumerSecret } = site;
|
||||||
|
// 构建 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'
|
||||||
);
|
);
|
||||||
|
|
@ -51,20 +112,22 @@ export class WPService {
|
||||||
|
|
||||||
async fetchPagedData<T>(
|
async fetchPagedData<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
site: WpSite,
|
site: any,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
perPage: number = 100
|
perPage: number = 100
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const allData: T[] = [];
|
const allData: T[] = [];
|
||||||
const { wpApiUrl, consumerKey, consumerSecret } = site;
|
const { apiUrl, 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: `${wpApiUrl}/wp-json${endpoint}`,
|
// 构建 URL,规避多/或少/问题
|
||||||
|
url: this.buildURL(apiUrl, '/wp-json', endpoint),
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${auth}`,
|
Authorization: `Basic ${auth}`,
|
||||||
},
|
},
|
||||||
|
|
@ -94,44 +157,52 @@ export class WPService {
|
||||||
return allData;
|
return allData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProducts(site: WpSite): Promise<WpProduct[]> {
|
async getProducts(site: any): Promise<WpProduct[]> {
|
||||||
return await this.fetchPagedData<WpProduct>('/wc/v3/products', site);
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
return await this.sdkGetAll<WpProduct>(api, 'products');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVariations(site: WpSite, productId: number): Promise<Variation[]> {
|
async getVariations(site: any, productId: number): Promise<Variation[]> {
|
||||||
return await this.fetchPagedData<Variation>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/products/${productId}/variations`,
|
return await this.sdkGetAll<Variation>(api, `products/${productId}/variations`);
|
||||||
site
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVariation(
|
async getVariation(
|
||||||
site: WpSite,
|
site: any,
|
||||||
productId: number,
|
productId: number,
|
||||||
variationId: number
|
variationId: number
|
||||||
): Promise<Variation> {
|
): Promise<Variation> {
|
||||||
return await this.fetchData<Variation>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/products/${productId}/variations/${variationId}`,
|
const res = await api.get(`products/${productId}/variations/${variationId}`);
|
||||||
site
|
return res.data as Variation;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrder(
|
async getOrder(
|
||||||
siteId: string,
|
siteId: string,
|
||||||
orderId: string
|
orderId: string
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
const site = this.geSite(siteId);
|
const site = await this.siteService.get(siteId);
|
||||||
return await this.fetchData<Record<string, any>>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/orders/${orderId}`,
|
const res = await api.get(`orders/${orderId}`);
|
||||||
site
|
return res.data as Record<string, any>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
async getOrders(siteId: string): Promise<Record<string, any>[]> {
|
async getOrders(siteId: string): Promise<Record<string, any>[]> {
|
||||||
const site = this.geSite(siteId);
|
const site = await this.siteService.get(siteId);
|
||||||
return await this.fetchPagedData<Record<string, any>>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
'/wc/v3/orders',
|
return await this.sdkGetAll<Record<string, any>>(api, 'orders');
|
||||||
site
|
}
|
||||||
);
|
|
||||||
|
/**
|
||||||
|
* 获取 WooCommerce Subscriptions
|
||||||
|
* 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 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(
|
||||||
|
|
@ -139,22 +210,19 @@ export class WPService {
|
||||||
orderId: string,
|
orderId: string,
|
||||||
refundId: number
|
refundId: number
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
const site = this.geSite(siteId);
|
const site = await this.siteService.get(siteId);
|
||||||
return await this.fetchData<Record<string, any>>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/orders/${orderId}/refunds/${refundId}`,
|
const res = await api.get(`orders/${orderId}/refunds/${refundId}`);
|
||||||
site
|
return res.data as Record<string, any>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderRefunds(
|
async getOrderRefunds(
|
||||||
siteId: string,
|
siteId: string,
|
||||||
orderId: number
|
orderId: number
|
||||||
): Promise<Record<string, any>[]> {
|
): Promise<Record<string, any>[]> {
|
||||||
const site = this.geSite(siteId);
|
const site = await this.siteService.get(siteId);
|
||||||
return await this.fetchPagedData<Record<string, any>>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/orders/${orderId}/refunds`,
|
return await this.sdkGetAll<Record<string, any>>(api, `orders/${orderId}/refunds`);
|
||||||
site
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderNote(
|
async getOrderNote(
|
||||||
|
|
@ -162,36 +230,35 @@ export class WPService {
|
||||||
orderId: number,
|
orderId: number,
|
||||||
noteId: number
|
noteId: number
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
const site = this.geSite(siteId);
|
const site = await this.siteService.get(siteId);
|
||||||
return await this.fetchData<Record<string, any>>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/orders/${orderId}/notes/${noteId}`,
|
const res = await api.get(`orders/${orderId}/notes/${noteId}`);
|
||||||
site
|
return res.data as Record<string, any>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderNotes(
|
async getOrderNotes(
|
||||||
siteId: string,
|
siteId: string,
|
||||||
orderId: number
|
orderId: number
|
||||||
): Promise<Record<string, any>[]> {
|
): Promise<Record<string, any>[]> {
|
||||||
const site = this.geSite(siteId);
|
const site = await this.siteService.get(siteId);
|
||||||
return await this.fetchPagedData<Record<string, any>>(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/orders/${orderId}/notes`,
|
return await this.sdkGetAll<Record<string, any>>(api, `orders/${orderId}/notes`);
|
||||||
site
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateData<T>(
|
async updateData<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
site: WpSite,
|
site: any,
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
): Promise<Boolean> {
|
): Promise<Boolean> {
|
||||||
const { wpApiUrl, consumerKey, consumerSecret } = site;
|
const apiUrl = site.apiUrl;
|
||||||
|
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: `${wpApiUrl}/wp-json${endpoint}`,
|
// 构建 URL,规避多/或少/问题
|
||||||
|
url: this.buildURL(apiUrl, '/wp-json', endpoint),
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${auth}`,
|
Authorization: `Basic ${auth}`,
|
||||||
},
|
},
|
||||||
|
|
@ -211,7 +278,7 @@ export class WPService {
|
||||||
* @param data 更新的数据
|
* @param data 更新的数据
|
||||||
*/
|
*/
|
||||||
async updateProduct(
|
async updateProduct(
|
||||||
site: WpSite,
|
site: any,
|
||||||
productId: string,
|
productId: string,
|
||||||
data: UpdateWpProductDTO
|
data: UpdateWpProductDTO
|
||||||
): Promise<Boolean> {
|
): Promise<Boolean> {
|
||||||
|
|
@ -230,7 +297,7 @@ export class WPService {
|
||||||
* @param stock_status 上下架状态
|
* @param stock_status 上下架状态
|
||||||
*/
|
*/
|
||||||
async updateProductStatus(
|
async updateProductStatus(
|
||||||
site: WpSite,
|
site: any,
|
||||||
productId: string,
|
productId: string,
|
||||||
status: ProductStatus,
|
status: ProductStatus,
|
||||||
stock_status: ProductStockStatus
|
stock_status: ProductStockStatus
|
||||||
|
|
@ -250,7 +317,7 @@ export class WPService {
|
||||||
* @param data 更新的数据
|
* @param data 更新的数据
|
||||||
*/
|
*/
|
||||||
async updateVariation(
|
async updateVariation(
|
||||||
site: WpSite,
|
site: any,
|
||||||
productId: string,
|
productId: string,
|
||||||
variationId: string,
|
variationId: string,
|
||||||
data: UpdateVariationDTO
|
data: UpdateVariationDTO
|
||||||
|
|
@ -271,7 +338,7 @@ export class WPService {
|
||||||
* 更新 Order
|
* 更新 Order
|
||||||
*/
|
*/
|
||||||
async updateOrder(
|
async updateOrder(
|
||||||
site: WpSite,
|
site: any,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
): Promise<Boolean> {
|
): Promise<Boolean> {
|
||||||
|
|
@ -279,17 +346,25 @@ export class WPService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createShipment(
|
async createShipment(
|
||||||
site: WpSite,
|
site: any,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
) {
|
) {
|
||||||
const { wpApiUrl, consumerKey, consumerSecret } = site;
|
const apiUrl = site.apiUrl;
|
||||||
|
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: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings`,
|
// 构建 URL,规避多/或少/问题
|
||||||
|
url: this.buildURL(
|
||||||
|
apiUrl,
|
||||||
|
'/wp-json',
|
||||||
|
'wc-ast/v3/orders',
|
||||||
|
orderId,
|
||||||
|
'shipment-trackings'
|
||||||
|
),
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${auth}`,
|
Authorization: `Basic ${auth}`,
|
||||||
},
|
},
|
||||||
|
|
@ -299,11 +374,12 @@ export class WPService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteShipment(
|
async deleteShipment(
|
||||||
site: WpSite,
|
site: any,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
trackingId: string,
|
trackingId: string,
|
||||||
): Promise<Boolean> {
|
): Promise<Boolean> {
|
||||||
const { wpApiUrl, consumerKey, consumerSecret } = site;
|
const apiUrl = site.apiUrl;
|
||||||
|
const { consumerKey, consumerSecret } = site;
|
||||||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
||||||
'base64'
|
'base64'
|
||||||
);
|
);
|
||||||
|
|
@ -312,7 +388,15 @@ 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: `${wpApiUrl}/wp-json/wc-ast/v3/orders/${orderId}/shipment-trackings/${trackingId}`,
|
// 构建 URL,规避多/或少/问题
|
||||||
|
url: this.buildURL(
|
||||||
|
apiUrl,
|
||||||
|
'/wp-json',
|
||||||
|
'wc-ast/v3/orders',
|
||||||
|
orderId,
|
||||||
|
'shipment-trackings',
|
||||||
|
trackingId
|
||||||
|
),
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${auth}`,
|
Authorization: `Basic ${auth}`,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Product } from './../entity/product.entty';
|
import { Product } from '../entity/product.entity';
|
||||||
import { Config, Inject, Provide } from '@midwayjs/core';
|
import { 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';
|
||||||
|
|
@ -12,41 +11,43 @@ 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(site.id, product, variations);
|
await this.syncProductAndVariations(String(site.id), product, variations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 同步一个网站
|
||||||
async syncSite(siteId: string) {
|
async syncSite(siteId: string) {
|
||||||
const site = this.getSite(siteId);
|
// 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
|
||||||
|
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 ',
|
||||||
|
|
@ -69,7 +70,7 @@ const excludeValues = [];
|
||||||
? await this.wpApiService.getVariations(site, product.id)
|
? await this.wpApiService.getVariations(site, product.id)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
await this.syncProductAndVariations(site.id, product, variations);
|
await this.syncProductAndVariations(String(site.id), product, variations);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredIds = externalIds.filter(id => !excludeValues.includes(id));
|
const filteredIds = externalIds.filter(id => !excludeValues.includes(id));
|
||||||
|
|
@ -91,7 +92,7 @@ const excludeValues = [];
|
||||||
// 控制产品上下架
|
// 控制产品上下架
|
||||||
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.getSite(wpProduct.siteId);
|
const site = await this.siteService.get(Number(wpProduct.siteId), true);
|
||||||
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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue