Compare commits
10 Commits
998e1e31c7
...
2508164395
| Author | SHA1 | Date |
|---|---|---|
|
|
2508164395 | |
|
|
3f3569995d | |
|
|
87b4039a67 | |
|
|
50317abff3 | |
|
|
40a445830b | |
|
|
d7cccad895 | |
|
|
62f9ca947a | |
|
|
d91ec7bc60 | |
|
|
4bb0988034 | |
|
|
4bbfa0cc2d |
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": "./node_modules/mwts/",
|
"extends": "./node_modules/mwts/",
|
||||||
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
|
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings", "scripts"],
|
||||||
"env": {
|
"env": {
|
||||||
"jest": true
|
"jest": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,8 @@ yarn.lock
|
||||||
**/config.prod.ts
|
**/config.prod.ts
|
||||||
**/config.local.ts
|
**/config.local.ts
|
||||||
container
|
container
|
||||||
ai/products-20251202 (1).csv
|
scripts
|
||||||
|
ai
|
||||||
|
tmp_uploads/
|
||||||
|
*.json
|
||||||
|
*config*
|
||||||
|
|
@ -28,10 +28,12 @@
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"csv-parse": "^6.1.0",
|
"csv-parse": "^6.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"eta": "^4.4.1",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"mysql2": "^3.15.3",
|
"mysql2": "^3.15.3",
|
||||||
"nodemailer": "^7.0.5",
|
"nodemailer": "^7.0.5",
|
||||||
"npm-check-updates": "^19.1.2",
|
"npm-check-updates": "^19.1.2",
|
||||||
|
"qs": "^6.14.0",
|
||||||
"swagger-ui-dist": "^5.18.2",
|
"swagger-ui-dist": "^5.18.2",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"typeorm-extension": "^3.7.2",
|
"typeorm-extension": "^3.7.2",
|
||||||
|
|
@ -2278,6 +2280,18 @@
|
||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/eta": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eta/-/eta-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-4o6fYxhRmFmO9SJcU9PxBLYPGapvJ/Qha0ZE+Y6UE9QIUd0Wk1qaLISQ6J1bM7nOcWHhs1YmY3mfrfwkJRBTWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/bgub/eta?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/event-target-shim": {
|
"node_modules/event-target-shim": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
|
@ -3744,7 +3758,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,12 @@
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"csv-parse": "^6.1.0",
|
"csv-parse": "^6.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"eta": "^4.4.1",
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"mysql2": "^3.15.3",
|
"mysql2": "^3.15.3",
|
||||||
"nodemailer": "^7.0.5",
|
"nodemailer": "^7.0.5",
|
||||||
"npm-check-updates": "^19.1.2",
|
"npm-check-updates": "^19.1.2",
|
||||||
|
"qs": "^6.14.0",
|
||||||
"swagger-ui-dist": "^5.18.2",
|
"swagger-ui-dist": "^5.18.2",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"typeorm-extension": "^3.7.2",
|
"typeorm-extension": "^3.7.2",
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const map = {
|
|
||||||
',': ',',
|
|
||||||
'.': '.',
|
|
||||||
':': ':',
|
|
||||||
'?': '?',
|
|
||||||
'!': '!',
|
|
||||||
'"': '"',
|
|
||||||
'"': '"',
|
|
||||||
''': "'",
|
|
||||||
''': "'",
|
|
||||||
'(': '(',
|
|
||||||
')': ')',
|
|
||||||
'[': '[',
|
|
||||||
']': ']',
|
|
||||||
',': ',',
|
|
||||||
';': ';'
|
|
||||||
};
|
|
||||||
|
|
||||||
function getAllFiles(dirPath, arrayOfFiles) {
|
|
||||||
const files = fs.readdirSync(dirPath);
|
|
||||||
|
|
||||||
arrayOfFiles = arrayOfFiles || [];
|
|
||||||
|
|
||||||
files.forEach(function(file) {
|
|
||||||
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
|
|
||||||
arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles);
|
|
||||||
} else {
|
|
||||||
if (extensions.some(ext => file.endsWith(ext))) {
|
|
||||||
arrayOfFiles.push(path.join(dirPath, "/", file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return arrayOfFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
const srcDirAPI = path.join(__dirname);
|
|
||||||
const srcDirWEB = path.join(__dirname, '../WEB');
|
|
||||||
|
|
||||||
const targetDirs = [srcDirAPI, srcDirWEB];
|
|
||||||
|
|
||||||
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.vue', '.html', '.css', '.scss', '.less', '.json', '.md'];
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
targetDirs.forEach(dir => {
|
|
||||||
if (fs.existsSync(dir)) {
|
|
||||||
const files = getAllFiles(dir);
|
|
||||||
files.forEach(file => {
|
|
||||||
// Skip node_modules, .git, dist, build, .idea, .vscode
|
|
||||||
if (file.includes('/node_modules/') ||
|
|
||||||
file.includes('/.git/') ||
|
|
||||||
file.includes('/dist/') ||
|
|
||||||
file.includes('/build/') ||
|
|
||||||
file.includes('/.idea/') ||
|
|
||||||
file.includes('/.vscode/') ||
|
|
||||||
file.includes('/coverage/')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs.readFileSync(file, 'utf8');
|
|
||||||
let originalContent = content;
|
|
||||||
|
|
||||||
for (const [cn, en] of Object.entries(map)) {
|
|
||||||
const regex = new RegExp(cn, 'g');
|
|
||||||
content = content.replace(regex, en);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content !== originalContent) {
|
|
||||||
fs.writeFileSync(file, content, 'utf8');
|
|
||||||
console.log(`Updated: ${file}`);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn(`Directory not found: ${dir}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Total files updated: ${count}`);
|
|
||||||
|
|
@ -0,0 +1,335 @@
|
||||||
|
import { ISiteAdapter } from '../interface/site-adapter.interface';
|
||||||
|
import { ShopyyService } from '../service/shopyy.service';
|
||||||
|
import {
|
||||||
|
UnifiedMediaDTO,
|
||||||
|
UnifiedOrderDTO,
|
||||||
|
UnifiedPaginationDTO,
|
||||||
|
UnifiedProductDTO,
|
||||||
|
UnifiedSearchParamsDTO,
|
||||||
|
UnifiedSubscriptionDTO,
|
||||||
|
UnifiedCustomerDTO,
|
||||||
|
} from '../dto/site-api.dto';
|
||||||
|
|
||||||
|
export class ShopyyAdapter implements ISiteAdapter {
|
||||||
|
constructor(private site: any, private shopyyService: ShopyyService) { }
|
||||||
|
// private mapProductStatus(status: number){
|
||||||
|
// return status === 1 ? 'publish' : 'draft';
|
||||||
|
// }
|
||||||
|
|
||||||
|
private mapProduct(item: any): UnifiedProductDTO {
|
||||||
|
function mapProductStatus(status: number) {
|
||||||
|
return status === 1 ? 'publish' : 'draft';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name || item.title,
|
||||||
|
type: item.product_type,
|
||||||
|
status: mapProductStatus(item.status),
|
||||||
|
sku: item.variant?.sku || '',
|
||||||
|
regular_price: item.variant?.price,
|
||||||
|
sale_price: item.special_price,
|
||||||
|
price: item.price,
|
||||||
|
stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock',
|
||||||
|
stock_quantity: item.inventory_quantity,
|
||||||
|
images: (item.images || []).map((img: any) => ({
|
||||||
|
id: img.id || 0,
|
||||||
|
src: img.src,
|
||||||
|
name: '',
|
||||||
|
alt: img.alt || '',
|
||||||
|
// 排序
|
||||||
|
position: img.position || ''
|
||||||
|
})),
|
||||||
|
attributes: [],
|
||||||
|
tags: item.tags || [],
|
||||||
|
variations: item.variants?.map(this.mapVariation.bind(this)) || [],
|
||||||
|
date_created: item.created_at,
|
||||||
|
date_modified: item.updated_at,
|
||||||
|
raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
mapVariation(mapVariation: any) {
|
||||||
|
return {
|
||||||
|
id: mapVariation.id,
|
||||||
|
sku: mapVariation.sku || '',
|
||||||
|
regular_price: mapVariation.price,
|
||||||
|
sale_price: mapVariation.special_price,
|
||||||
|
price: mapVariation.price,
|
||||||
|
stock_status: mapVariation.inventory_tracking === 1 ? 'instock' : 'outofstock',
|
||||||
|
stock_quantity: mapVariation.inventory_quantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapOrder(item: any): UnifiedOrderDTO {
|
||||||
|
const billing = item.billing_address || {};
|
||||||
|
const shipping = item.shipping_address || {};
|
||||||
|
|
||||||
|
const billingObj = {
|
||||||
|
first_name: billing.first_name || item.firstname || '',
|
||||||
|
last_name: billing.last_name || item.lastname || '',
|
||||||
|
fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(),
|
||||||
|
company: billing.company || '',
|
||||||
|
email: item.customer_email || item.email || '',
|
||||||
|
phone: billing.phone || item.telephone || '',
|
||||||
|
address_1: billing.address1 || item.payment_address || '',
|
||||||
|
address_2: billing.address2 || '',
|
||||||
|
city: billing.city || item.payment_city || '',
|
||||||
|
state: billing.province || item.payment_zone || '',
|
||||||
|
postcode: billing.zip || item.payment_postcode || '',
|
||||||
|
country: billing.country_name || billing.country_code || item.payment_country || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const shippingObj = {
|
||||||
|
first_name: shipping.first_name || item.firstname || '',
|
||||||
|
last_name: shipping.last_name || item.lastname || '',
|
||||||
|
fullname: shipping.name || '',
|
||||||
|
company: shipping.company || '',
|
||||||
|
address_1: shipping.address1 || item.shipping_address || '',
|
||||||
|
address_2: shipping.address2 || '',
|
||||||
|
city: shipping.city || item.shipping_city || '',
|
||||||
|
state: shipping.province || item.shipping_zone || '',
|
||||||
|
postcode: shipping.zip || item.shipping_postcode || '',
|
||||||
|
country: shipping.country_name || shipping.country_code || item.shipping_country || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAddress = (addr: any) => {
|
||||||
|
return [
|
||||||
|
addr.fullname,
|
||||||
|
addr.company,
|
||||||
|
addr.address_1,
|
||||||
|
addr.address_2,
|
||||||
|
addr.city,
|
||||||
|
addr.state,
|
||||||
|
addr.postcode,
|
||||||
|
addr.country,
|
||||||
|
addr.phone
|
||||||
|
].filter(Boolean).join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id || item.order_id,
|
||||||
|
number: item.order_number || item.order_sn,
|
||||||
|
status: String(item.status || item.order_status),
|
||||||
|
currency: item.currency_code || item.currency,
|
||||||
|
total: String(item.total_price || item.total_amount),
|
||||||
|
customer_id: item.customer_id || item.user_id,
|
||||||
|
customer_name: item.customer_name || `${item.firstname} ${item.lastname}`.trim(),
|
||||||
|
email: item.customer_email || item.email,
|
||||||
|
line_items: (item.products || []).map((p: any) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.product_title || p.name,
|
||||||
|
product_id: p.product_id,
|
||||||
|
quantity: p.quantity,
|
||||||
|
total: String(p.price),
|
||||||
|
sku: p.sku || p.sku_code || ''
|
||||||
|
})),
|
||||||
|
sales: (item.products || []).map((p: any) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.product_title || p.name,
|
||||||
|
product_id: p.product_id,
|
||||||
|
productId: p.product_id,
|
||||||
|
quantity: p.quantity,
|
||||||
|
total: String(p.price),
|
||||||
|
sku: p.sku || p.sku_code || ''
|
||||||
|
})),
|
||||||
|
billing: billingObj,
|
||||||
|
shipping: shippingObj,
|
||||||
|
billing_full_address: formatAddress(billingObj),
|
||||||
|
shipping_full_address: formatAddress(shippingObj),
|
||||||
|
payment_method: item.payment_method,
|
||||||
|
date_created: item.created_at ? new Date(item.created_at * 1000).toISOString() : item.date_added,
|
||||||
|
raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapCustomer(item: any): UnifiedCustomerDTO {
|
||||||
|
// 处理多地址结构
|
||||||
|
const addresses = item.addresses || [];
|
||||||
|
const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {});
|
||||||
|
|
||||||
|
// 尝试从地址列表中获取billing和shipping
|
||||||
|
// 如果没有明确区分,默认使用默认地址或第一个地址
|
||||||
|
const billing = defaultAddress;
|
||||||
|
const shipping = defaultAddress;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id || item.customer_id,
|
||||||
|
first_name: item.first_name || item.firstname || '',
|
||||||
|
last_name: item.last_name || item.lastname || '',
|
||||||
|
fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(),
|
||||||
|
email: item.email || item.customer_email || '',
|
||||||
|
phone: item.contact || billing.phone || item.phone || '',
|
||||||
|
billing: {
|
||||||
|
first_name: billing.first_name || item.first_name || '',
|
||||||
|
last_name: billing.last_name || item.last_name || '',
|
||||||
|
fullname: billing.name || `${billing.first_name || ''} ${billing.last_name || ''}`.trim(),
|
||||||
|
company: billing.company || '',
|
||||||
|
email: item.email || '',
|
||||||
|
phone: billing.phone || item.contact || '',
|
||||||
|
address_1: billing.address1 || '',
|
||||||
|
address_2: billing.address2 || '',
|
||||||
|
city: billing.city || '',
|
||||||
|
state: billing.province || '',
|
||||||
|
postcode: billing.zip || '',
|
||||||
|
country: billing.country_name || billing.country_code || item.country?.country_name || ''
|
||||||
|
},
|
||||||
|
shipping: {
|
||||||
|
first_name: shipping.first_name || item.first_name || '',
|
||||||
|
last_name: shipping.last_name || item.last_name || '',
|
||||||
|
fullname: shipping.name || `${shipping.first_name || ''} ${shipping.last_name || ''}`.trim(),
|
||||||
|
company: shipping.company || '',
|
||||||
|
address_1: shipping.address1 || '',
|
||||||
|
address_2: shipping.address2 || '',
|
||||||
|
city: shipping.city || '',
|
||||||
|
state: shipping.province || '',
|
||||||
|
postcode: shipping.zip || '',
|
||||||
|
country: shipping.country_name || shipping.country_code || item.country?.country_name || ''
|
||||||
|
},
|
||||||
|
raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProducts(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedProductDTO>> {
|
||||||
|
const response = await this.shopyyService.fetchResourcePaged<any>(
|
||||||
|
this.site,
|
||||||
|
'products/list',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
const { items, total, totalPages, page, per_page } = response;
|
||||||
|
return {
|
||||||
|
items: items.map(this.mapProduct.bind(this)),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
|
||||||
|
// 使用ShopyyService获取单个产品
|
||||||
|
const product = await this.shopyyService.getProduct(this.site, id);
|
||||||
|
return this.mapProduct(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
|
||||||
|
const res = await this.shopyyService.createProduct(this.site, data);
|
||||||
|
return this.mapProduct(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
|
||||||
|
// Shopyy update returns boolean?
|
||||||
|
// shopyyService.updateProduct returns boolean.
|
||||||
|
// So I can't return the updated product.
|
||||||
|
// I have to fetch it again or return empty/input.
|
||||||
|
// Since getProduct is missing, I'll return input data as UnifiedProductDTO (mock).
|
||||||
|
const success = await this.shopyyService.updateProduct(this.site, String(id), data);
|
||||||
|
if (!success) throw new Error('Update failed');
|
||||||
|
return { ...data, id } as UnifiedProductDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateVariation(productId: string | number, variationId: string | number, data: any): Promise<any> {
|
||||||
|
const success = await this.shopyyService.updateVariation(this.site, String(productId), String(variationId), data);
|
||||||
|
if (!success) throw new Error('Update variation failed');
|
||||||
|
return { ...data, id: variationId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrderNotes(orderId: string | number): Promise<any[]> {
|
||||||
|
return await this.shopyyService.getOrderNotes(this.site, orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrderNote(orderId: string | number, data: any): Promise<any> {
|
||||||
|
return await this.shopyyService.createOrderNote(this.site, orderId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProduct(id: string | number): Promise<boolean> {
|
||||||
|
// Use batch delete
|
||||||
|
await this.shopyyService.batchProcessProducts(this.site, { delete: [id] });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchProcessProducts(
|
||||||
|
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.shopyyService.batchProcessProducts(this.site, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrders(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
|
||||||
|
const { items, total, totalPages, page, per_page } =
|
||||||
|
await this.shopyyService.fetchResourcePaged<any>(
|
||||||
|
this.site,
|
||||||
|
'orders',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
items: items.map(this.mapOrder.bind(this)),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
|
||||||
|
const data = await this.shopyyService.getOrder(String(this.site.id), String(id));
|
||||||
|
return this.mapOrder(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
|
||||||
|
const createdOrder = await this.shopyyService.createOrder(this.site, data);
|
||||||
|
return this.mapOrder(createdOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
|
||||||
|
return await this.shopyyService.updateOrder(this.site, String(id), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrder(id: string | number): Promise<boolean> {
|
||||||
|
return await this.shopyyService.deleteOrder(this.site, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSubscriptions(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
|
||||||
|
throw new Error('Shopyy does not support subscriptions.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMedia(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
||||||
|
throw new Error('Shopyy does not support media API.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>> {
|
||||||
|
const { items, total, totalPages, page, per_page } =
|
||||||
|
await this.shopyyService.fetchCustomersPaged(this.site, params);
|
||||||
|
return {
|
||||||
|
items: items.map(this.mapCustomer.bind(this)),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomer(id: string | number): Promise<UnifiedCustomerDTO> {
|
||||||
|
const customer = await this.shopyyService.getCustomer(this.site, id);
|
||||||
|
return this.mapCustomer(customer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
|
||||||
|
const createdCustomer = await this.shopyyService.createCustomer(this.site, data);
|
||||||
|
return this.mapCustomer(createdCustomer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
|
||||||
|
const updatedCustomer = await this.shopyyService.updateCustomer(this.site, id, data);
|
||||||
|
return this.mapCustomer(updatedCustomer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomer(id: string | number): Promise<boolean> {
|
||||||
|
return await this.shopyyService.deleteCustomer(this.site, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
import { ISiteAdapter } from '../interface/site-adapter.interface';
|
||||||
|
import { WPService } from '../service/wp.service';
|
||||||
|
import {
|
||||||
|
UnifiedMediaDTO,
|
||||||
|
UnifiedOrderDTO,
|
||||||
|
UnifiedPaginationDTO,
|
||||||
|
UnifiedProductDTO,
|
||||||
|
UnifiedSearchParamsDTO,
|
||||||
|
UnifiedSubscriptionDTO,
|
||||||
|
UnifiedCustomerDTO,
|
||||||
|
} from '../dto/site-api.dto';
|
||||||
|
|
||||||
|
export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
|
constructor(private site: any, private wpService: WPService) {}
|
||||||
|
|
||||||
|
private mapProduct(item: any): UnifiedProductDTO {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
type: item.type,
|
||||||
|
status: item.status,
|
||||||
|
sku: item.sku,
|
||||||
|
regular_price: item.regular_price,
|
||||||
|
sale_price: item.sale_price,
|
||||||
|
price: item.price,
|
||||||
|
stock_status: item.stock_status,
|
||||||
|
stock_quantity: item.stock_quantity,
|
||||||
|
images: (item.images || []).map((img: any) => ({
|
||||||
|
id: img.id,
|
||||||
|
src: img.src,
|
||||||
|
name: img.name,
|
||||||
|
alt: img.alt,
|
||||||
|
})),
|
||||||
|
attributes: item.attributes,
|
||||||
|
variations: item.variations,
|
||||||
|
date_created: item.date_created,
|
||||||
|
date_modified: item.date_modified,
|
||||||
|
raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapOrder(item: any): UnifiedOrderDTO {
|
||||||
|
const formatAddress = (addr: any) => {
|
||||||
|
if (!addr) return '';
|
||||||
|
const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim();
|
||||||
|
return [
|
||||||
|
name,
|
||||||
|
addr.company,
|
||||||
|
addr.address_1,
|
||||||
|
addr.address_2,
|
||||||
|
addr.city,
|
||||||
|
addr.state,
|
||||||
|
addr.postcode,
|
||||||
|
addr.country,
|
||||||
|
addr.phone
|
||||||
|
].filter(Boolean).join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
number: item.number,
|
||||||
|
status: item.status,
|
||||||
|
currency: item.currency,
|
||||||
|
total: item.total,
|
||||||
|
customer_id: item.customer_id,
|
||||||
|
customer_name: `${item.billing?.first_name || ''} ${
|
||||||
|
item.billing?.last_name || ''
|
||||||
|
}`.trim(),
|
||||||
|
email: item.billing?.email || '',
|
||||||
|
line_items: item.line_items,
|
||||||
|
sales: (item.line_items || []).map((li: any) => ({
|
||||||
|
...li,
|
||||||
|
productId: li.product_id,
|
||||||
|
// Ensure other fields match frontend expectation if needed
|
||||||
|
})),
|
||||||
|
billing: item.billing,
|
||||||
|
shipping: item.shipping,
|
||||||
|
billing_full_address: formatAddress(item.billing),
|
||||||
|
shipping_full_address: formatAddress(item.shipping),
|
||||||
|
payment_method: item.payment_method_title,
|
||||||
|
date_created: item.date_created,
|
||||||
|
raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapSubscription(item: any): UnifiedSubscriptionDTO {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
status: item.status,
|
||||||
|
customer_id: item.customer_id,
|
||||||
|
billing_period: item.billing_period,
|
||||||
|
billing_interval: item.billing_interval,
|
||||||
|
start_date: item.start_date,
|
||||||
|
next_payment_date: item.next_payment_date,
|
||||||
|
line_items: item.line_items,
|
||||||
|
raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapMedia(item: any): UnifiedMediaDTO {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
title: item.title?.rendered || '',
|
||||||
|
media_type: item.media_type,
|
||||||
|
mime_type: item.mime_type,
|
||||||
|
source_url: item.source_url,
|
||||||
|
date_created: item.date_created,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProducts(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedProductDTO>> {
|
||||||
|
const { items, total, totalPages, page, per_page } =
|
||||||
|
await this.wpService.fetchResourcePaged<any>(
|
||||||
|
this.site,
|
||||||
|
'products',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
items: items.map(this.mapProduct),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.get(`products/${id}`);
|
||||||
|
return this.mapProduct(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
|
||||||
|
const res = await this.wpService.createProduct(this.site, data);
|
||||||
|
return this.mapProduct(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
|
||||||
|
const res = await this.wpService.updateProduct(this.site, String(id), data as any);
|
||||||
|
return this.mapProduct(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateVariation(productId: string | number, variationId: string | number, data: any): Promise<any> {
|
||||||
|
const res = await this.wpService.updateVariation(this.site, String(productId), String(variationId), data);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrderNotes(orderId: string | number): Promise<any[]> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.get(`orders/${orderId}/notes`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrderNote(orderId: string | number, data: any): Promise<any> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.post(`orders/${orderId}/notes`, data);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProduct(id: string | number): Promise<boolean> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
await api.delete(`products/${id}`, { force: true });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchProcessProducts(
|
||||||
|
data: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
): Promise<any> {
|
||||||
|
return await this.wpService.batchProcessProducts(this.site, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrders(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>> {
|
||||||
|
const { items, total, totalPages, page, per_page } =
|
||||||
|
await this.wpService.fetchResourcePaged<any>(this.site, 'orders', params);
|
||||||
|
return {
|
||||||
|
items: items.map(this.mapOrder),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrder(id: string | number): Promise<UnifiedOrderDTO> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.get(`orders/${id}`);
|
||||||
|
return this.mapOrder(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.post('orders', data);
|
||||||
|
return this.mapOrder(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean> {
|
||||||
|
return await this.wpService.updateOrder(this.site, String(id), data as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrder(id: string | number): Promise<boolean> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
await api.delete(`orders/${id}`, { force: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSubscriptions(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
|
||||||
|
const { items, total, totalPages, page, per_page } =
|
||||||
|
await this.wpService.fetchResourcePaged<any>(
|
||||||
|
this.site,
|
||||||
|
'subscriptions',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
items: items.map(this.mapSubscription),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMedia(
|
||||||
|
params: UnifiedSearchParamsDTO
|
||||||
|
): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
||||||
|
const { items, total, totalPages } = await this.wpService.getMedia(
|
||||||
|
this.site.id,
|
||||||
|
params.page || 1,
|
||||||
|
params.per_page || 20
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
items: items.map(this.mapMedia),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page: params.page || 1,
|
||||||
|
per_page: params.per_page || 20,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMedia(id: string | number): Promise<boolean> {
|
||||||
|
await this.wpService.deleteMedia(Number(this.site.id), Number(id), true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateMedia(id: string | number, data: any): Promise<any> {
|
||||||
|
return await this.wpService.updateMedia(Number(this.site.id), Number(id), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapCustomer(item: any): UnifiedCustomerDTO {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
email: item.email,
|
||||||
|
first_name: item.first_name,
|
||||||
|
last_name: item.last_name,
|
||||||
|
username: item.username,
|
||||||
|
phone: item.billing?.phone || item.shipping?.phone,
|
||||||
|
billing: item.billing,
|
||||||
|
shipping: item.shipping,
|
||||||
|
raw: item,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>> {
|
||||||
|
const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged<any>(
|
||||||
|
this.site,
|
||||||
|
'customers',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
items: items.map((i: any) => this.mapCustomer(i)),
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomer(id: string | number): Promise<UnifiedCustomerDTO> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.get(`customers/${id}`);
|
||||||
|
return this.mapCustomer(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.post('customers', data);
|
||||||
|
return this.mapCustomer(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const res = await api.put(`customers/${id}`, data);
|
||||||
|
return this.mapCustomer(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCustomer(id: string | number): Promise<boolean> {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
await api.delete(`customers/${id}`, { force: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { MidwayConfig } from '@midwayjs/core';
|
import { MidwayConfig } from '@midwayjs/core';
|
||||||
|
import { join } from 'path';
|
||||||
import { Product } from '../entity/product.entity';
|
import { Product } from '../entity/product.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';
|
||||||
|
|
@ -36,6 +37,7 @@ import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { Template } from '../entity/template.entity';
|
import { Template } from '../entity/template.entity';
|
||||||
import { Area } from '../entity/area.entity';
|
import { Area } from '../entity/area.entity';
|
||||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
|
import { ProductSiteSku } from '../entity/product_site_sku.entity';
|
||||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||||
import { Category } from '../entity/category.entity';
|
import { Category } from '../entity/category.entity';
|
||||||
import DictSeeder from '../db/seeds/dict.seeder';
|
import DictSeeder from '../db/seeds/dict.seeder';
|
||||||
|
|
@ -50,6 +52,7 @@ export default {
|
||||||
entities: [
|
entities: [
|
||||||
Product,
|
Product,
|
||||||
ProductStockComponent,
|
ProductStockComponent,
|
||||||
|
ProductSiteSku,
|
||||||
WpProduct,
|
WpProduct,
|
||||||
Variation,
|
Variation,
|
||||||
User,
|
User,
|
||||||
|
|
@ -146,5 +149,11 @@ export default {
|
||||||
mode: 'file',
|
mode: 'file',
|
||||||
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
|
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
|
||||||
whitelist: ['.csv'], // 支持的文件后缀
|
whitelist: ['.csv'], // 支持的文件后缀
|
||||||
|
tmpdir: join(__dirname, '../../tmp_uploads'),
|
||||||
|
cleanTimeout: 5 * 60 * 1000,
|
||||||
|
},
|
||||||
|
koa: {
|
||||||
|
queryParseMode: 'extended', // 使用 qs 模块
|
||||||
|
// 其他选项:'strict', 'first'
|
||||||
},
|
},
|
||||||
} as MidwayConfig;
|
} as MidwayConfig;
|
||||||
|
|
|
||||||
|
|
@ -3,64 +3,70 @@ export default {
|
||||||
koa: {
|
koa: {
|
||||||
port: 7001,
|
port: 7001,
|
||||||
},
|
},
|
||||||
// typeorm: {
|
|
||||||
// dataSource: {
|
|
||||||
// default: {
|
|
||||||
// host: '13.212.62.127',
|
|
||||||
// username: 'root',
|
|
||||||
// password: 'Yoone!@.2025',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
typeorm: {
|
typeorm: {
|
||||||
dataSource: {
|
dataSource: {
|
||||||
default: {
|
default: {
|
||||||
host: 'localhost',
|
host: '13.212.62.127',
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: '12345678',
|
password: 'Yoone!@.2025',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// typeorm: {
|
||||||
|
// dataSource: {
|
||||||
|
// default: {
|
||||||
|
// host: 'localhost',
|
||||||
|
// port: "33306",
|
||||||
|
// username: 'root',
|
||||||
|
// password: 'root',
|
||||||
|
// database: 'inventory',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*', // 允许所有来源跨域请求
|
origin: '*', // 允许所有来源跨域请求
|
||||||
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
|
||||||
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
allowHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
|
||||||
credentials: true, // 允许携带凭据(cookies等)
|
credentials: true, // 允许携带凭据(cookies等)
|
||||||
},
|
},
|
||||||
jwt: {
|
jwt: {
|
||||||
secret: 'YOONE2024!@abc',
|
secret: 'YOONE2024!@abc',
|
||||||
expiresIn: '7d',
|
expiresIn: '7d',
|
||||||
},
|
},
|
||||||
wpSite: [
|
wpSite: [
|
||||||
{
|
// {
|
||||||
id: '-1',
|
// id: '200',
|
||||||
siteName: 'Admin',
|
// wpApiUrl: "http://simple.local",
|
||||||
email: '2469687281@qq.com',
|
// consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886',
|
||||||
},
|
// consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8',
|
||||||
{
|
// name: 'LocalSimple',
|
||||||
id: '2',
|
// email: '2469687281@qq.com',
|
||||||
wpApiUrl: 'http://t2-shop.local/',
|
// emailPswd: 'lulin91.',
|
||||||
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
// },
|
||||||
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
// {
|
||||||
siteName: 'Local',
|
// id: '2',
|
||||||
email: '2469687281@qq.com',
|
// wpApiUrl: 'http://t2-shop.local/',
|
||||||
emailPswd: 'lulin91.',
|
// consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||||
},
|
// consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||||
{
|
// name: 'Local',
|
||||||
id: '3',
|
// email: '2469687281@qq.com',
|
||||||
wpApiUrl: 'http://t1-shop.local/',
|
// emailPswd: 'lulin91.',
|
||||||
consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
// },
|
||||||
consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
// {
|
||||||
siteName: 'Local-test-2',
|
// id: '3',
|
||||||
email: '2469687281@qq.com',
|
// wpApiUrl: 'http://t1-shop.local/',
|
||||||
emailPswd: 'lulin91.',
|
// consumerKey: 'ck_a369473a6451dbaec63d19cbfd74a074b2c5f742',
|
||||||
},
|
// consumerSecret: 'cs_0946bbbeea1bfefff08a69e817ac62a48412df8c',
|
||||||
|
// name: 'Local-test-2',
|
||||||
|
// email: '2469687281@qq.com',
|
||||||
|
// emailPswd: 'lulin91.',
|
||||||
|
// },
|
||||||
// {
|
// {
|
||||||
// id: '2',
|
// id: '2',
|
||||||
// wpApiUrl: 'http://localhost:10004',
|
// wpApiUrl: 'http://localhost:10004',
|
||||||
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
// consumerKey: 'ck_dc9e151e9048c8ed3e27f35ac79d2bf7d6840652',
|
||||||
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
// consumerSecret: 'cs_d05d625d7b0ac05c6d765671d8417f41d9477e38',
|
||||||
// siteName: 'Local',
|
// name: 'Local',
|
||||||
// email: 'tom@yoonevape.com',
|
// email: 'tom@yoonevape.com',
|
||||||
// emailPswd: 'lulin91.',
|
// emailPswd: 'lulin91.',
|
||||||
// },
|
// },
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import * as validate from '@midwayjs/validate';
|
||||||
import * as info from '@midwayjs/info';
|
import * as info from '@midwayjs/info';
|
||||||
import * as orm from '@midwayjs/typeorm';
|
import * as orm from '@midwayjs/typeorm';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
// import { DefaultErrorFilter } from './filter/default.filter';
|
import { DefaultErrorFilter } from './filter/default.filter';
|
||||||
// import { NotFoundFilter } from './filter/notfound.filter';
|
import { NotFoundFilter } from './filter/notfound.filter';
|
||||||
import { ReportMiddleware } from './middleware/report.middleware';
|
import { ReportMiddleware } from './middleware/report.middleware';
|
||||||
import * as swagger from '@midwayjs/swagger';
|
import * as swagger from '@midwayjs/swagger';
|
||||||
import * as crossDomain from '@midwayjs/cross-domain';
|
import * as crossDomain from '@midwayjs/cross-domain';
|
||||||
|
|
@ -55,7 +55,7 @@ export class MainConfiguration {
|
||||||
// add middleware
|
// add middleware
|
||||||
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
|
this.app.useMiddleware([ReportMiddleware, AuthMiddleware]);
|
||||||
// add filter
|
// add filter
|
||||||
// this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
|
this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
|
||||||
|
|
||||||
this.decoratorService.registerParameterHandler(
|
this.decoratorService.registerParameterHandler(
|
||||||
USER_KEY,
|
USER_KEY,
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,66 @@
|
||||||
import {
|
import { Controller, Get, Post, Inject, Query, Body } from '@midwayjs/core';
|
||||||
Body,
|
import { successResponse, errorResponse } from '../utils/response.util';
|
||||||
Context,
|
|
||||||
Controller,
|
|
||||||
Del,
|
|
||||||
Get,
|
|
||||||
Inject,
|
|
||||||
Post,
|
|
||||||
Put,
|
|
||||||
Query,
|
|
||||||
} from '@midwayjs/core';
|
|
||||||
import { CustomerService } from '../service/customer.service';
|
import { CustomerService } from '../service/customer.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
import { QueryCustomerListDTO, CustomerTagDTO } from '../dto/customer.dto';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import { BooleanRes } from '../dto/reponse.dto';
|
|
||||||
import { CustomerTagDTO, QueryCustomerListDTO } from '../dto/customer.dto';
|
|
||||||
|
|
||||||
@Controller('/customer')
|
@Controller('/customer')
|
||||||
export class CustomerController {
|
export class CustomerController {
|
||||||
@Inject()
|
|
||||||
ctx: Context;
|
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
customerService: CustomerService;
|
customerService: CustomerService;
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse({ type: Object })
|
||||||
@Get('/list')
|
@Get('/getcustomerlist')
|
||||||
async getCustomerList(@Query() param: QueryCustomerListDTO) {
|
async getCustomerList(@Query() query: QueryCustomerListDTO) {
|
||||||
try {
|
try {
|
||||||
const data = await this.customerService.getCustomerList(param);
|
const result = await this.customerService.getCustomerList(query as any);
|
||||||
return successResponse(data);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
return errorResponse(error.message);
|
||||||
return errorResponse(error?.message || error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({ type: BooleanRes })
|
@ApiOkResponse({ type: Object })
|
||||||
@Post('/tag/add')
|
@Post('/addtag')
|
||||||
async addTag(@Body() dto: CustomerTagDTO) {
|
async addTag(@Body() body: CustomerTagDTO) {
|
||||||
try {
|
try {
|
||||||
await this.customerService.addTag(dto.email, dto.tag);
|
const result = await this.customerService.addTag(body.email, body.tag);
|
||||||
return successResponse(true);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({ type: BooleanRes })
|
@ApiOkResponse({ type: Object })
|
||||||
@Del('/tag/del')
|
@Post('/deltag')
|
||||||
async delTag(@Body() dto: CustomerTagDTO) {
|
async delTag(@Body() body: CustomerTagDTO) {
|
||||||
try {
|
try {
|
||||||
await this.customerService.delTag(dto.email, dto.tag);
|
const result = await this.customerService.delTag(body.email, body.tag);
|
||||||
return successResponse(true);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse({ type: Object })
|
||||||
@Get('/tags')
|
@Get('/gettags')
|
||||||
async getTags() {
|
async getTags() {
|
||||||
try {
|
try {
|
||||||
const data = await this.customerService.getTags();
|
const result = await this.customerService.getTags();
|
||||||
return successResponse(data);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
@ApiOkResponse({ type: BooleanRes })
|
@Post('/setrate')
|
||||||
@Put('/rate')
|
async setRate(@Body() body: { id: number; rate: number }) {
|
||||||
async setRate(@Body() params: { id: number; rate: number }) {
|
|
||||||
try {
|
try {
|
||||||
await this.customerService.setRate(params);
|
const result = await this.customerService.setRate({ id: body.id, rate: body.rate });
|
||||||
return successResponse(true);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Controller, Get, Inject, Query, Post, Del, Param, Files, Fields, Body } from '@midwayjs/core';
|
||||||
|
import { WPService } from '../service/wp.service';
|
||||||
|
import { successResponse, errorResponse } from '../utils/response.util';
|
||||||
|
|
||||||
|
@Controller('/media')
|
||||||
|
export class MediaController {
|
||||||
|
@Inject()
|
||||||
|
wpService: WPService;
|
||||||
|
|
||||||
|
@Get('/list')
|
||||||
|
async list(
|
||||||
|
@Query('siteId') siteId: number,
|
||||||
|
@Query('page') page: number = 1,
|
||||||
|
@Query('pageSize') pageSize: number = 20
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (!siteId) {
|
||||||
|
return errorResponse('siteId is required');
|
||||||
|
}
|
||||||
|
const result = await this.wpService.getMedia(siteId, page, pageSize);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/upload')
|
||||||
|
async upload(@Fields() fields, @Files() files) {
|
||||||
|
try {
|
||||||
|
const siteId = fields.siteId;
|
||||||
|
if (!siteId) {
|
||||||
|
return errorResponse('siteId is required');
|
||||||
|
}
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
return errorResponse('file is required');
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
const result = await this.wpService.createMedia(siteId, file);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/update/:id')
|
||||||
|
async update(@Param('id') id: number, @Body() body) {
|
||||||
|
try {
|
||||||
|
const siteId = body.siteId;
|
||||||
|
if (!siteId) {
|
||||||
|
return errorResponse('siteId is required');
|
||||||
|
}
|
||||||
|
// 过滤出需要更新的字段
|
||||||
|
const { title, caption, description, alt_text } = body;
|
||||||
|
const data: any = {};
|
||||||
|
if (title !== undefined) data.title = title;
|
||||||
|
if (caption !== undefined) data.caption = caption;
|
||||||
|
if (description !== undefined) data.description = description;
|
||||||
|
if (alt_text !== undefined) data.alt_text = alt_text;
|
||||||
|
|
||||||
|
const result = await this.wpService.updateMedia(siteId, id, data);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Del('/:id')
|
||||||
|
async delete(@Param('id') id: number, @Query('siteId') siteId: number, @Query('force') force: boolean = true) {
|
||||||
|
try {
|
||||||
|
if (!siteId) {
|
||||||
|
return errorResponse('siteId is required');
|
||||||
|
}
|
||||||
|
const result = await this.wpService.deleteMedia(siteId, id, force);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,8 +38,8 @@ export class OrderController {
|
||||||
@Post('/syncOrder/:siteId')
|
@Post('/syncOrder/:siteId')
|
||||||
async syncOrder(@Param('siteId') siteId: number) {
|
async syncOrder(@Param('siteId') siteId: number) {
|
||||||
try {
|
try {
|
||||||
await this.orderService.syncOrders(siteId);
|
const result = await this.orderService.syncOrders(siteId);
|
||||||
return successResponse(true);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return errorResponse('同步失败');
|
return errorResponse('同步失败');
|
||||||
|
|
@ -252,4 +252,21 @@ export class OrderController {
|
||||||
return errorResponse(error?.message || '获取失败');
|
return errorResponse(error?.message || '获取失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/order/export')
|
||||||
|
async exportOrder(
|
||||||
|
@Query() queryParams: any
|
||||||
|
) {
|
||||||
|
// 处理 ids 参数,支持多种格式:ids=1,2,3、ids[]=1&ids[]=2、ids=1
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csv = await this.orderService.exportOrder(queryParams?.ids);
|
||||||
|
// 返回CSV内容给前端,由前端决定是否下载
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || '导出失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,9 @@ import {
|
||||||
Query,
|
Query,
|
||||||
Controller,
|
Controller,
|
||||||
} from '@midwayjs/core';
|
} from '@midwayjs/core';
|
||||||
import * as fs from 'fs';
|
|
||||||
import { ProductService } from '../service/product.service';
|
import { ProductService } from '../service/product.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, SetProductComponentsDTO } from '../dto/product.dto';
|
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, BatchUpdateProductDTO, BatchDeleteProductDTO } from '../dto/product.dto';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
||||||
import { ContentType, Files } from '@midwayjs/core';
|
import { ContentType, Files } from '@midwayjs/core';
|
||||||
|
|
@ -83,7 +82,7 @@ export class ProductController {
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async createProduct(@Body() productData: CreateProductDTO) {
|
async createProduct(@Body() productData: CreateProductDTO) {
|
||||||
try {
|
try {
|
||||||
const data = this.productService.createProduct(productData);
|
const data = await this.productService.createProduct(productData);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -115,19 +114,9 @@ export class ProductController {
|
||||||
try {
|
try {
|
||||||
// 条件判断:确保存在文件
|
// 条件判断:确保存在文件
|
||||||
const file = files?.[0];
|
const file = files?.[0];
|
||||||
if (!file?.data) return errorResponse('未接收到上传文件');
|
if (!file) return errorResponse('未接收到上传文件');
|
||||||
|
|
||||||
// midway/upload file 模式下,data 是临时文件路径
|
const result = await this.productService.importProductsCSV(file);
|
||||||
let buffer = file.data;
|
|
||||||
if (typeof file.data === 'string') {
|
|
||||||
try {
|
|
||||||
buffer = fs.readFileSync(file.data);
|
|
||||||
} catch (err) {
|
|
||||||
return errorResponse('读取上传文件失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.productService.importProductsCSV(buffer);
|
|
||||||
return successResponse(result);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -138,18 +127,67 @@ export class ProductController {
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
|
async updateProduct(@Param('id') id: number, @Body() productData: UpdateProductDTO) {
|
||||||
try {
|
try {
|
||||||
const data = this.productService.updateProduct(id, productData);
|
const data = await this.productService.updateProduct(id, productData);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
|
@Put('/batch-update')
|
||||||
|
async batchUpdateProduct(@Body() batchUpdateProductDTO: BatchUpdateProductDTO) {
|
||||||
|
try {
|
||||||
|
await this.productService.batchUpdateProduct(batchUpdateProductDTO);
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({ type: BooleanRes })
|
||||||
|
@Post('/batch-delete')
|
||||||
|
async batchDeleteProduct(@Body() body: BatchDeleteProductDTO) {
|
||||||
|
try {
|
||||||
|
const result = await this.productService.batchDeleteProduct(body.ids);
|
||||||
|
if (result.failed > 0) {
|
||||||
|
return errorResponse(`成功删除 ${result.success} 个,失败 ${result.failed} 个。首个错误: ${result.errors[0]}`);
|
||||||
|
}
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOkResponse({ type: ProductRes })
|
@ApiOkResponse({ type: ProductRes })
|
||||||
@Put('updateNameCn/:id/:nameCn')
|
@Put('updateNameCn/:id/:nameCn')
|
||||||
async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
|
async updatenameCn(@Param('id') id: number, @Param('nameCn') nameCn: string) {
|
||||||
try {
|
try {
|
||||||
const data = this.productService.updatenameCn(id, nameCn);
|
const data = await this.productService.updatenameCn(id, nameCn);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取产品的站点SKU绑定
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Get('/:id/site-skus')
|
||||||
|
async getProductSiteSkus(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.productSiteSkuModel.find({ where: { productId: id } });
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖式绑定产品的站点SKU列表
|
||||||
|
@ApiOkResponse()
|
||||||
|
@Post('/:id/site-skus')
|
||||||
|
async bindProductSiteSkus(@Param('id') id: number, @Body() body: { codes: string[] }) {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.bindSiteSkus(id, body?.codes || []);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
|
|
@ -179,17 +217,6 @@ export class ProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置产品的库存组成(覆盖式)
|
|
||||||
@ApiOkResponse()
|
|
||||||
@Post('/:id/components')
|
|
||||||
async setProductComponents(@Param('id') id: number, @Body() body: SetProductComponentsDTO) {
|
|
||||||
try {
|
|
||||||
const data = await this.productService.setProductComponents(id, body?.components || []);
|
|
||||||
return successResponse(data);
|
|
||||||
} catch (error) {
|
|
||||||
return errorResponse(error?.message || error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
|
// 根据 SKU 自动绑定组成(匹配所有相同 SKU 的库存)
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
|
|
@ -328,7 +355,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/brand')
|
@Post('/brand')
|
||||||
async compatCreateBrand(@Body() body: { title: string; name: string }) {
|
async compatCreateBrand(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
|
const has = await this.productService.hasAttribute('brand', body.name); // 唯一性校验
|
||||||
if (has) return errorResponse('品牌已存在');
|
if (has) return errorResponse('品牌已存在');
|
||||||
|
|
@ -341,7 +368,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/brand/:id')
|
@Put('/brand/:id')
|
||||||
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
async compatUpdateBrand(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身)
|
const has = await this.productService.hasAttribute('brand', body.name, id); // 唯一性校验(排除自身)
|
||||||
|
|
@ -390,7 +417,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/flavors')
|
@Post('/flavors')
|
||||||
async compatCreateFlavors(@Body() body: { title: string; name: string }) {
|
async compatCreateFlavors(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('flavor', body.name);
|
const has = await this.productService.hasAttribute('flavor', body.name);
|
||||||
if (has) return errorResponse('口味已存在');
|
if (has) return errorResponse('口味已存在');
|
||||||
|
|
@ -403,7 +430,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/flavors/:id')
|
@Put('/flavors/:id')
|
||||||
async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
async compatUpdateFlavors(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('flavor', body.name, id);
|
const has = await this.productService.hasAttribute('flavor', body.name, id);
|
||||||
|
|
@ -452,7 +479,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/strength')
|
@Post('/strength')
|
||||||
async compatCreateStrength(@Body() body: { title: string; name: string }) {
|
async compatCreateStrength(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('strength', body.name);
|
const has = await this.productService.hasAttribute('strength', body.name);
|
||||||
if (has) return errorResponse('规格已存在');
|
if (has) return errorResponse('规格已存在');
|
||||||
|
|
@ -465,7 +492,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/strength/:id')
|
@Put('/strength/:id')
|
||||||
async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
async compatUpdateStrength(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('strength', body.name, id);
|
const has = await this.productService.hasAttribute('strength', body.name, id);
|
||||||
|
|
@ -514,7 +541,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/size')
|
@Post('/size')
|
||||||
async compatCreateSize(@Body() body: { title: string; name: string }) {
|
async compatCreateSize(@Body() body: { title: string; name: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
const has = await this.productService.hasAttribute('size', body.name);
|
const has = await this.productService.hasAttribute('size', body.name);
|
||||||
if (has) return errorResponse('尺寸已存在');
|
if (has) return errorResponse('尺寸已存在');
|
||||||
|
|
@ -527,7 +554,7 @@ export class ProductController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/size/:id')
|
@Put('/size/:id')
|
||||||
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string }) {
|
async compatUpdateSize(@Param('id') id: number, @Body() body: { title?: string; name?: string; image?: string; shortName?: string }) {
|
||||||
try {
|
try {
|
||||||
if (body?.name) {
|
if (body?.name) {
|
||||||
const has = await this.productService.hasAttribute('size', body.name, id);
|
const has = await this.productService.hasAttribute('size', body.name, id);
|
||||||
|
|
@ -634,4 +661,16 @@ export class ProductController {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步库存 SKU 到产品单品
|
||||||
|
@ApiOkResponse({ description: '同步库存 SKU 到产品单品' })
|
||||||
|
@Post('/sync-stock')
|
||||||
|
async syncStockToProduct() {
|
||||||
|
try {
|
||||||
|
const data = await this.productService.syncStockToProduct();
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,911 @@
|
||||||
|
import { Controller, Get, Inject, Param, Query, Body, Post, Put, Del } from '@midwayjs/core';
|
||||||
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
|
import {
|
||||||
|
UnifiedMediaPaginationDTO,
|
||||||
|
UnifiedOrderDTO,
|
||||||
|
UnifiedOrderPaginationDTO,
|
||||||
|
UnifiedProductDTO,
|
||||||
|
UnifiedProductPaginationDTO,
|
||||||
|
UnifiedSearchParamsDTO,
|
||||||
|
UnifiedSubscriptionPaginationDTO,
|
||||||
|
UnifiedCustomerDTO,
|
||||||
|
UnifiedCustomerPaginationDTO,
|
||||||
|
} from '../dto/site-api.dto';
|
||||||
|
import { SiteApiService } from '../service/site-api.service';
|
||||||
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
|
import { ILogger } from '@midwayjs/core';
|
||||||
|
|
||||||
|
|
||||||
|
@Controller('/site-api')
|
||||||
|
export class SiteApiController {
|
||||||
|
@Inject()
|
||||||
|
siteApiService: SiteApiService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
logger: ILogger;
|
||||||
|
|
||||||
|
@Get('/:siteId/products')
|
||||||
|
@ApiOkResponse({ type: UnifiedProductPaginationDTO })
|
||||||
|
async getProducts(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取产品列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getProducts(query);
|
||||||
|
this.logger.info(`[Site API] 获取产品列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个产品`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取产品列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/products/export')
|
||||||
|
async exportProducts(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getProducts(query);
|
||||||
|
const header = ['id','name','type','status','sku','regular_price','sale_price','price','stock_status','stock_quantity'];
|
||||||
|
const rows = data.items.map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台特性:产品导出(特殊CSV,走平台服务)
|
||||||
|
@Get('/:siteId/products/export-special')
|
||||||
|
async exportProductsSpecial(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const site = await this.siteApiService.siteService.get(siteId, true);
|
||||||
|
if (site.type === 'woocommerce') {
|
||||||
|
const page = query.page || 1;
|
||||||
|
const per_page = query.per_page || 100;
|
||||||
|
const res = await this.siteApiService.wpService.getProducts(site, page, per_page);
|
||||||
|
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
|
||||||
|
const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
}
|
||||||
|
if (site.type === 'shopyy') {
|
||||||
|
const res = await this.siteApiService.shopyyService.getProducts(site, query.page || 1, query.per_page || 100);
|
||||||
|
const header = ['id','name','type','status','sku','price','stock_status','stock_quantity'];
|
||||||
|
const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
}
|
||||||
|
throw new Error('Unsupported site type for special export');
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/products/:id')
|
||||||
|
@ApiOkResponse({ type: UnifiedProductDTO })
|
||||||
|
async getProduct(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取单个产品开始, siteId: ${siteId}, productId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getProduct(id);
|
||||||
|
this.logger.info(`[Site API] 获取单个产品成功, siteId: ${siteId}, productId: ${id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取单个产品失败, siteId: ${siteId}, productId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/products')
|
||||||
|
@ApiOkResponse({ type: UnifiedProductDTO })
|
||||||
|
async createProduct(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: UnifiedProductDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 创建产品开始, siteId: ${siteId}, 产品名称: ${body.name}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.createProduct(body);
|
||||||
|
this.logger.info(`[Site API] 创建产品成功, siteId: ${siteId}, 产品ID: ${data.id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 创建产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/products/import')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importProducts(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { items?: any[]; csv?: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
let items = body.items || [];
|
||||||
|
if (!items.length && body.csv) {
|
||||||
|
const lines = body.csv.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
items = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createProduct(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平台特性:产品导入(特殊CSV,走平台服务)
|
||||||
|
@Post('/:siteId/products/import-special')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importProductsSpecial(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { csv?: string; items?: any[] }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const site = await this.siteApiService.siteService.get(siteId, true);
|
||||||
|
const csvText = body.csv || '';
|
||||||
|
const items = body.items || [];
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
if (site.type === 'woocommerce') {
|
||||||
|
// 解析 CSV 为对象数组(若传入 items 则优先 items)
|
||||||
|
let payloads = items;
|
||||||
|
if (!payloads.length && csvText) {
|
||||||
|
const lines = csvText.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
payloads = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const item of payloads) {
|
||||||
|
try {
|
||||||
|
const res = await this.siteApiService.wpService.createProduct(site, item);
|
||||||
|
created.push(res);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
}
|
||||||
|
if (site.type === 'shopyy') {
|
||||||
|
throw new Error('ShopYY 暂不支持特殊CSV导入');
|
||||||
|
}
|
||||||
|
throw new Error('Unsupported site type for special import');
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:siteId/products/:id')
|
||||||
|
@ApiOkResponse({ type: UnifiedProductDTO })
|
||||||
|
async updateProduct(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: UnifiedProductDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 更新产品开始, siteId: ${siteId}, productId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.updateProduct(id, body);
|
||||||
|
this.logger.info(`[Site API] 更新产品成功, siteId: ${siteId}, productId: ${id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 更新产品失败, siteId: ${siteId}, productId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:siteId/products/:productId/variations/:variationId')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async updateVariation(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('productId') productId: string,
|
||||||
|
@Param('variationId') variationId: string,
|
||||||
|
@Body() body: any
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 更新产品变体开始, siteId: ${siteId}, productId: ${productId}, variationId: ${variationId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.updateVariation(productId, variationId, body);
|
||||||
|
this.logger.info(`[Site API] 更新产品变体成功, siteId: ${siteId}, productId: ${productId}, variationId: ${variationId}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 更新产品变体失败, siteId: ${siteId}, productId: ${productId}, variationId: ${variationId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Del('/:siteId/products/:id')
|
||||||
|
@ApiOkResponse({ type: Boolean })
|
||||||
|
async deleteProduct(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 删除产品开始, siteId: ${siteId}, productId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const success = await adapter.deleteProduct(id);
|
||||||
|
this.logger.info(`[Site API] 删除产品成功, siteId: ${siteId}, productId: ${id}`);
|
||||||
|
return successResponse(success);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 删除产品失败, siteId: ${siteId}, productId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/products/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchProducts(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理产品开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
if (adapter.batchProcessProducts) {
|
||||||
|
const res = await adapter.batchProcessProducts(body);
|
||||||
|
this.logger.info(`[Site API] 批量处理产品成功, siteId: ${siteId}`);
|
||||||
|
return successResponse(res);
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
if (body.create?.length) {
|
||||||
|
for (const item of body.create) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createProduct(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'create', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
const id = item.id;
|
||||||
|
const data = await adapter.updateProduct(id, item);
|
||||||
|
updated.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
const ok = await adapter.deleteProduct(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理产品完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ created, updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/orders')
|
||||||
|
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
|
||||||
|
async getOrders(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取订单列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getOrders(query);
|
||||||
|
this.logger.info(`[Site API] 获取订单列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个订单`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取订单列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/orders/export')
|
||||||
|
async exportOrders(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getOrders(query);
|
||||||
|
const header = ['id','number','status','currency','total','customer_id','customer_name','email','date_created'];
|
||||||
|
const rows = data.items.map((o: any) => [o.id,o.number,o.status,o.currency,o.total,o.customer_id,o.customer_name,o.email,o.date_created]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/orders/:id')
|
||||||
|
@ApiOkResponse({ type: UnifiedOrderDTO })
|
||||||
|
async getOrder(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取单个订单开始, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getOrder(id);
|
||||||
|
this.logger.info(`[Site API] 获取单个订单成功, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取单个订单失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/orders')
|
||||||
|
@ApiOkResponse({ type: UnifiedOrderDTO })
|
||||||
|
async createOrder(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: any
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 创建订单开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.createOrder(body);
|
||||||
|
this.logger.info(`[Site API] 创建订单成功, siteId: ${siteId}, orderId: ${data.id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 创建订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/orders/import')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importOrders(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { items?: any[]; csv?: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
let items = body.items || [];
|
||||||
|
if (!items.length && body.csv) {
|
||||||
|
const lines = body.csv.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
items = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createOrder(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:siteId/orders/:id')
|
||||||
|
@ApiOkResponse({ type: Boolean })
|
||||||
|
async updateOrder(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: any
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 更新订单开始, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const ok = await adapter.updateOrder(id, body);
|
||||||
|
this.logger.info(`[Site API] 更新订单成功, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
return successResponse(ok);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 更新订单失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Del('/:siteId/orders/:id')
|
||||||
|
@ApiOkResponse({ type: Boolean })
|
||||||
|
async deleteOrder(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 删除订单开始, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const ok = await adapter.deleteOrder(id);
|
||||||
|
this.logger.info(`[Site API] 删除订单成功, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
return successResponse(ok);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 删除订单失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/orders/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchOrders(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理订单开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const created: any[] = [];
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
if (body.create?.length) {
|
||||||
|
for (const item of body.create) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createOrder(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'create', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
const id = item.id;
|
||||||
|
const ok = await adapter.updateOrder(id, item);
|
||||||
|
if (ok) updated.push(item);
|
||||||
|
else failed.push({ action: 'update', item, error: 'update failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
const ok = await adapter.deleteOrder(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理订单完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ created, updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理订单失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/orders/:id/notes')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async getOrderNotes(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取订单备注开始, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getOrderNotes(id);
|
||||||
|
this.logger.info(`[Site API] 获取订单备注成功, siteId: ${siteId}, orderId: ${id}, 共获取到 ${data.length} 条备注`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取订单备注失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/orders/:id/notes')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async createOrderNote(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: any
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 创建订单备注开始, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.createOrderNote(id, body);
|
||||||
|
this.logger.info(`[Site API] 创建订单备注成功, siteId: ${siteId}, orderId: ${id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 创建订单备注失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/subscriptions')
|
||||||
|
@ApiOkResponse({ type: UnifiedSubscriptionPaginationDTO })
|
||||||
|
async getSubscriptions(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取订阅列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getSubscriptions(query);
|
||||||
|
this.logger.info(`[Site API] 获取订阅列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个订阅`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取订阅列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/subscriptions/export')
|
||||||
|
async exportSubscriptions(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getSubscriptions(query);
|
||||||
|
const header = ['id','status','customer_id','billing_period','billing_interval','start_date','next_payment_date'];
|
||||||
|
const rows = data.items.map((s: any) => [s.id,s.status,s.customer_id,s.billing_period,s.billing_interval,s.start_date,s.next_payment_date]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/media')
|
||||||
|
@ApiOkResponse({ type: UnifiedMediaPaginationDTO })
|
||||||
|
async getMedia(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取媒体列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getMedia(query);
|
||||||
|
this.logger.info(`[Site API] 获取媒体列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个媒体`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取媒体列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/media/export')
|
||||||
|
async exportMedia(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getMedia(query);
|
||||||
|
const header = ['id','title','media_type','mime_type','source_url','date_created'];
|
||||||
|
const rows = data.items.map((m: any) => [m.id,m.title,m.media_type,m.mime_type,m.source_url,m.date_created]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Del('/:siteId/media/:id')
|
||||||
|
@ApiOkResponse({ type: Boolean })
|
||||||
|
async deleteMedia(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 删除媒体开始, siteId: ${siteId}, mediaId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const api: any = adapter as any;
|
||||||
|
if (api.deleteMedia) {
|
||||||
|
const success = await api.deleteMedia(id);
|
||||||
|
this.logger.info(`[Site API] 删除媒体成功, siteId: ${siteId}, mediaId: ${id}`);
|
||||||
|
return successResponse(success);
|
||||||
|
}
|
||||||
|
throw new Error('Media delete not supported');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 删除媒体失败, siteId: ${siteId}, mediaId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:siteId/media/:id')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async updateMedia(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: any
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 更新媒体开始, siteId: ${siteId}, mediaId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const api: any = adapter as any;
|
||||||
|
if (api.updateMedia) {
|
||||||
|
const res = await api.updateMedia(id, body);
|
||||||
|
this.logger.info(`[Site API] 更新媒体成功, siteId: ${siteId}, mediaId: ${id}`);
|
||||||
|
return successResponse(res);
|
||||||
|
}
|
||||||
|
throw new Error('Media update not supported');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 更新媒体失败, siteId: ${siteId}, mediaId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/media/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchMedia(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理媒体开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
const api: any = adapter as any;
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
if (!api.updateMedia) throw new Error('Media update not supported');
|
||||||
|
const res = await api.updateMedia(item.id, item);
|
||||||
|
updated.push(res);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
if (!api.deleteMedia) throw new Error('Media delete not supported');
|
||||||
|
const ok = await api.deleteMedia(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理媒体完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理媒体失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/customers')
|
||||||
|
@ApiOkResponse({ type: UnifiedCustomerPaginationDTO })
|
||||||
|
async getCustomers(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取客户列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getCustomers(query);
|
||||||
|
this.logger.info(`[Site API] 获取客户列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个客户`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取客户列表失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/customers/export')
|
||||||
|
async exportCustomers(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Query() query: UnifiedSearchParamsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getCustomers(query);
|
||||||
|
const header = ['id','email','first_name','last_name','fullname','username','phone'];
|
||||||
|
const rows = data.items.map((c: any) => [c.id,c.email,c.first_name,c.last_name,c.fullname,c.username,c.phone]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return successResponse({ csv });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/customers/:id')
|
||||||
|
@ApiOkResponse({ type: UnifiedCustomerDTO })
|
||||||
|
async getCustomer(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取单个客户开始, siteId: ${siteId}, customerId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getCustomer(id);
|
||||||
|
this.logger.info(`[Site API] 获取单个客户成功, siteId: ${siteId}, customerId: ${id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取单个客户失败, siteId: ${siteId}, customerId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/customers')
|
||||||
|
@ApiOkResponse({ type: UnifiedCustomerDTO })
|
||||||
|
async createCustomer(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: UnifiedCustomerDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 创建客户开始, siteId: ${siteId}, 客户邮箱: ${body.email}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.createCustomer(body);
|
||||||
|
this.logger.info(`[Site API] 创建客户成功, siteId: ${siteId}, customerId: ${data.id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 创建客户失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/customers/import')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async importCustomers(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { items?: any[]; csv?: string }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
let items = body.items || [];
|
||||||
|
if (!items.length && body.csv) {
|
||||||
|
const lines = body.csv.split(/\r?\n/).filter(Boolean);
|
||||||
|
const header = lines.shift()?.split(',') || [];
|
||||||
|
items = lines.map((line) => {
|
||||||
|
const cols = line.split(',');
|
||||||
|
const obj: any = {};
|
||||||
|
header.forEach((h, i) => (obj[h] = cols[i]));
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const created: any[] = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createCustomer(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return successResponse({ created, failed });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:siteId/customers/:id')
|
||||||
|
@ApiOkResponse({ type: UnifiedCustomerDTO })
|
||||||
|
async updateCustomer(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: UnifiedCustomerDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 更新客户开始, siteId: ${siteId}, customerId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.updateCustomer(id, body);
|
||||||
|
this.logger.info(`[Site API] 更新客户成功, siteId: ${siteId}, customerId: ${id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 更新客户失败, siteId: ${siteId}, customerId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Del('/:siteId/customers/:id')
|
||||||
|
@ApiOkResponse({ type: Boolean })
|
||||||
|
async deleteCustomer(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('id') id: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 删除客户开始, siteId: ${siteId}, customerId: ${id}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const success = await adapter.deleteCustomer(id);
|
||||||
|
this.logger.info(`[Site API] 删除客户成功, siteId: ${siteId}, customerId: ${id}`);
|
||||||
|
return successResponse(success);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 删除客户失败, siteId: ${siteId}, customerId: ${id}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/customers/batch')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async batchCustomers(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { create?: any[]; update?: any[]; delete?: Array<string | number> }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量处理客户开始, siteId: ${siteId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const created: any[] = [];
|
||||||
|
const updated: any[] = [];
|
||||||
|
const deleted: Array<string | number> = [];
|
||||||
|
const failed: any[] = [];
|
||||||
|
if (body.create?.length) {
|
||||||
|
for (const item of body.create) {
|
||||||
|
try {
|
||||||
|
const data = await adapter.createCustomer(item);
|
||||||
|
created.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'create', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.update?.length) {
|
||||||
|
for (const item of body.update) {
|
||||||
|
try {
|
||||||
|
const id = item.id;
|
||||||
|
const data = await adapter.updateCustomer(id, item);
|
||||||
|
updated.push(data);
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'update', item, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body.delete?.length) {
|
||||||
|
for (const id of body.delete) {
|
||||||
|
try {
|
||||||
|
const ok = await adapter.deleteCustomer(id);
|
||||||
|
if (ok) deleted.push(id);
|
||||||
|
else failed.push({ action: 'delete', id, error: 'delete failed' });
|
||||||
|
} catch (e) {
|
||||||
|
failed.push({ action: 'delete', id, error: (e as any).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.logger.info(`[Site API] 批量处理客户完成, siteId: ${siteId}`);
|
||||||
|
return successResponse({ created, updated, deleted, failed });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量处理客户失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,8 +15,8 @@ export class SubscriptionController {
|
||||||
@Post('/sync/:siteId')
|
@Post('/sync/:siteId')
|
||||||
async sync(@Param('siteId') siteId: number) {
|
async sync(@Param('siteId') siteId: number) {
|
||||||
try {
|
try {
|
||||||
await this.subscriptionService.syncSubscriptions(siteId);
|
const result = await this.subscriptionService.syncSubscriptions(siteId);
|
||||||
return successResponse(true);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || '同步失败');
|
return errorResponse(error?.message || '同步失败');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -128,4 +128,19 @@ export class TemplateController {
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary 回填缺失的测试数据
|
||||||
|
* @description 扫描数据库中所有模板,为缺失 testData 的记录生成并保存测试数据
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: Number, description: '成功回填的数量' })
|
||||||
|
@Post('/backfill-testdata')
|
||||||
|
async backfillTestData() {
|
||||||
|
try {
|
||||||
|
const count = await this.templateService.backfillMissingTestData();
|
||||||
|
return successResponse({ updated: count });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ export class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/add')
|
@Post('/add')
|
||||||
async addUser(@Body() body: { username: string; password: string; remark?: string }) {
|
async addUser(@Body() body: { username: string; password: string; email?: string; remark?: string }) {
|
||||||
const { username, password, remark } = body;
|
const { username, password, email, remark } = body;
|
||||||
try {
|
try {
|
||||||
// 新增用户(支持备注)
|
// 新增用户 支持邮箱与备注
|
||||||
await this.userService.addUser(username, password, remark);
|
await this.userService.addUser(username, password, remark, email);
|
||||||
return successResponse(true);
|
return successResponse(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
@ -60,22 +60,37 @@ export class UserController {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
email?: string;
|
||||||
isActive?: string;
|
isActive?: string;
|
||||||
isSuper?: string;
|
isSuper?: string;
|
||||||
isAdmin?: string;
|
isAdmin?: string;
|
||||||
|
sortField?: string;
|
||||||
|
sortOrder?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { current = 1, pageSize = 10, remark, username, isActive, isSuper, isAdmin } = query;
|
const { current = 1, pageSize = 10, remark, username, email, isActive, isSuper, isAdmin, sortField, sortOrder } = query;
|
||||||
// 将字符串布尔转换为真实布尔
|
// 将字符串布尔转换为真实布尔
|
||||||
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
|
const toBool = (v?: string) => (v === undefined ? undefined : v === 'true');
|
||||||
|
// 处理排序方向
|
||||||
|
const order = (sortOrder === 'ascend' || sortOrder === 'ASC') ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
// 列表移除密码字段
|
// 列表移除密码字段
|
||||||
const { items, total } = await this.userService.listUsers(current, pageSize, {
|
const { items, total } = await this.userService.listUsers(
|
||||||
|
current,
|
||||||
|
pageSize,
|
||||||
|
{
|
||||||
remark,
|
remark,
|
||||||
username,
|
username,
|
||||||
|
email,
|
||||||
isActive: toBool(isActive),
|
isActive: toBool(isActive),
|
||||||
isSuper: toBool(isSuper),
|
isSuper: toBool(isSuper),
|
||||||
isAdmin: toBool(isAdmin),
|
isAdmin: toBool(isAdmin),
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
field: sortField,
|
||||||
|
order,
|
||||||
|
}
|
||||||
|
);
|
||||||
const safeItems = (items || []).map((it: any) => {
|
const safeItems = (items || []).map((it: any) => {
|
||||||
const { password, ...rest } = it || {};
|
const { password, ...rest } = it || {};
|
||||||
return rest;
|
return rest;
|
||||||
|
|
@ -99,7 +114,7 @@ export class UserController {
|
||||||
// 更新用户(支持用户名/密码/权限/角色更新)
|
// 更新用户(支持用户名/密码/权限/角色更新)
|
||||||
@Post('/update/:id')
|
@Post('/update/:id')
|
||||||
async updateUser(
|
async updateUser(
|
||||||
@Body() body: { username?: string; password?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
|
@Body() body: { username?: string; password?: string; email?: string; isSuper?: boolean; isAdmin?: boolean; permissions?: string[]; remark?: string },
|
||||||
@Query('id') id?: number
|
@Query('id') id?: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import {
|
||||||
} from '@midwayjs/decorator';
|
} from '@midwayjs/decorator';
|
||||||
import { Context } from '@midwayjs/koa';
|
import { Context } from '@midwayjs/koa';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { WpProductService } from '../service/wp_product.service';
|
|
||||||
import { WPService } from '../service/wp.service';
|
|
||||||
import { SiteService } from '../service/site.service';
|
import { SiteService } from '../service/site.service';
|
||||||
import { OrderService } from '../service/order.service';
|
import { OrderService } from '../service/order.service';
|
||||||
|
|
||||||
|
|
@ -18,11 +17,7 @@ import { OrderService } from '../service/order.service';
|
||||||
export class WebhookController {
|
export class WebhookController {
|
||||||
private secret = 'YOONE24kd$kjcdjflddd';
|
private secret = 'YOONE24kd$kjcdjflddd';
|
||||||
|
|
||||||
@Inject()
|
// 平台服务保留按需注入
|
||||||
private readonly wpProductService: WpProductService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private readonly wpApiService: WPService;
|
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly orderService: OrderService;
|
private readonly orderService: OrderService;
|
||||||
|
|
@ -79,32 +74,10 @@ export class WebhookController {
|
||||||
switch (topic) {
|
switch (topic) {
|
||||||
case 'product.created':
|
case 'product.created':
|
||||||
case 'product.updated':
|
case 'product.updated':
|
||||||
// 变体更新
|
// 不再写入本地,平台事件仅确认接收
|
||||||
if (body.type === 'variation') {
|
|
||||||
const variation = await this.wpApiService.getVariation(
|
|
||||||
site,
|
|
||||||
body.parent_id,
|
|
||||||
body.id
|
|
||||||
);
|
|
||||||
this.wpProductService.syncVariation(
|
|
||||||
siteId,
|
|
||||||
body.parent_id,
|
|
||||||
variation
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const variations =
|
|
||||||
body.type === 'variable'
|
|
||||||
? await this.wpApiService.getVariations(site, body.id)
|
|
||||||
: [];
|
|
||||||
await this.wpProductService.syncProductAndVariations(
|
|
||||||
site.id,
|
|
||||||
body,
|
|
||||||
variations
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'product.deleted':
|
case 'product.deleted':
|
||||||
await this.wpProductService.delWpProduct(site.id, body.id);
|
// 不再写入本地,平台事件仅确认接收
|
||||||
break;
|
break;
|
||||||
case 'order.created':
|
case 'order.created':
|
||||||
case 'order.updated':
|
case 'order.updated':
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
Query,
|
Query,
|
||||||
Put,
|
Put,
|
||||||
Body,
|
Body,
|
||||||
|
Files,
|
||||||
|
Del,
|
||||||
} 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';
|
||||||
|
|
@ -14,12 +16,13 @@ import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import { BooleanRes, WpProductListRes } from '../dto/reponse.dto';
|
import { BooleanRes, WpProductListRes } from '../dto/reponse.dto';
|
||||||
import {
|
import {
|
||||||
QueryWpProductDTO,
|
QueryWpProductDTO,
|
||||||
SetConstitutionDTO,
|
|
||||||
UpdateVariationDTO,
|
UpdateVariationDTO,
|
||||||
UpdateWpProductDTO,
|
UpdateWpProductDTO,
|
||||||
|
BatchSyncProductsDTO,
|
||||||
|
BatchUpdateTagsDTO,
|
||||||
|
BatchUpdateProductsDTO,
|
||||||
} from '../dto/wp_product.dto';
|
} from '../dto/wp_product.dto';
|
||||||
import { WPService } from '../service/wp.service';
|
|
||||||
import { SiteService } from '../service/site.service';
|
|
||||||
import {
|
import {
|
||||||
ProductsRes,
|
ProductsRes,
|
||||||
} from '../dto/reponse.dto';
|
} from '../dto/reponse.dto';
|
||||||
|
|
@ -30,11 +33,70 @@ export class WpProductController {
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly wpProductService: WpProductService;
|
private readonly wpProductService: WpProductService;
|
||||||
|
|
||||||
@Inject()
|
// 平台服务保留按需注入
|
||||||
private readonly wpApiService: WPService;
|
|
||||||
|
|
||||||
@Inject()
|
@ApiOkResponse({
|
||||||
private readonly siteService: SiteService;
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Del('/:id')
|
||||||
|
async delete(@Param('id') id: number) {
|
||||||
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({
|
||||||
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Post('/import/:siteId')
|
||||||
|
async importProducts(@Param('siteId') siteId: number, @Files() files) {
|
||||||
|
try {
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
throw new Error('请上传文件');
|
||||||
|
}
|
||||||
|
await this.wpProductService.importProducts(siteId, files[0]);
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入失败:', error);
|
||||||
|
return errorResponse(error.message || '导入失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({
|
||||||
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Post('/setconstitution')
|
||||||
|
async setConstitution(@Body() body: any) {
|
||||||
|
try {
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message || '设置失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({
|
||||||
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Post('/batch-update')
|
||||||
|
async batchUpdateProducts(@Body() body: BatchUpdateProductsDTO) {
|
||||||
|
try {
|
||||||
|
await this.wpProductService.batchUpdateProducts(body);
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message || '批量更新失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({
|
||||||
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Post('/batch-update-tags')
|
||||||
|
async batchUpdateTags(@Body() body: BatchUpdateTagsDTO) {
|
||||||
|
try {
|
||||||
|
await this.wpProductService.batchUpdateTags(body.ids, body.tags);
|
||||||
|
return successResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message || '批量更新标签失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: BooleanRes,
|
type: BooleanRes,
|
||||||
|
|
@ -42,43 +104,37 @@ export class WpProductController {
|
||||||
@Post('/sync/:siteId')
|
@Post('/sync/:siteId')
|
||||||
async syncProducts(@Param('siteId') siteId: number) {
|
async syncProducts(@Param('siteId') siteId: number) {
|
||||||
try {
|
try {
|
||||||
await this.wpProductService.syncSite(siteId);
|
const result = await this.wpProductService.syncSite(siteId);
|
||||||
return successResponse(true);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return errorResponse('同步失败');
|
return errorResponse('同步失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOkResponse({
|
||||||
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Post('/batch-sync-to-site/:siteId')
|
||||||
|
async batchSyncToSite(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: BatchSyncProductsDTO
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.wpProductService.batchSyncToSite(siteId, body.productIds);
|
||||||
|
return successResponse(true, '批量同步成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量同步失败:', error);
|
||||||
|
return errorResponse(error.message || '批量同步失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: WpProductListRes,
|
type: WpProductListRes,
|
||||||
})
|
})
|
||||||
@Get('/list')
|
@Get('/list')
|
||||||
async getWpProducts(@Query() query: QueryWpProductDTO) {
|
async getWpProducts(@Query() query: QueryWpProductDTO) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 列表');
|
||||||
const data = await this.wpProductService.getProductList(query);
|
|
||||||
return successResponse(data);
|
|
||||||
} catch (error) {
|
|
||||||
return errorResponse(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOkResponse({
|
|
||||||
type: BooleanRes,
|
|
||||||
})
|
|
||||||
@Put('/:id/constitution')
|
|
||||||
async setConstitution(
|
|
||||||
@Param('id') id: number,
|
|
||||||
@Body()
|
|
||||||
body: SetConstitutionDTO
|
|
||||||
) {
|
|
||||||
const { isProduct, constitution } = body;
|
|
||||||
try {
|
|
||||||
await this.wpProductService.setConstitution(id, isProduct, constitution);
|
|
||||||
return successResponse(true);
|
|
||||||
} catch (error) {
|
|
||||||
return errorResponse(error.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
|
|
@ -97,6 +153,22 @@ export class WpProductController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建产品接口
|
||||||
|
* @param siteId 站点 ID
|
||||||
|
* @param body 创建数据
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({
|
||||||
|
type: BooleanRes,
|
||||||
|
})
|
||||||
|
@Post('/siteId/:siteId/products')
|
||||||
|
async createProduct(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: any
|
||||||
|
) {
|
||||||
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products 创建');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新产品接口
|
* 更新产品接口
|
||||||
* @param productId 产品 ID
|
* @param productId 产品 ID
|
||||||
|
|
@ -111,30 +183,19 @@ export class WpProductController {
|
||||||
@Param('productId') productId: string,
|
@Param('productId') productId: string,
|
||||||
@Body() body: UpdateWpProductDTO
|
@Body() body: UpdateWpProductDTO
|
||||||
) {
|
) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:id 更新');
|
||||||
const isDuplicate = await this.wpProductService.isSkuDuplicate(
|
|
||||||
body.sku,
|
|
||||||
siteId,
|
|
||||||
productId
|
|
||||||
);
|
|
||||||
if (isDuplicate) {
|
|
||||||
return errorResponse('SKU已存在');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const site = await this.siteService.get(siteId, true);
|
@ApiOkResponse({
|
||||||
const result = await this.wpApiService.updateProduct(
|
type: BooleanRes,
|
||||||
site,
|
})
|
||||||
productId,
|
@Post('/sync-to-product/:id')
|
||||||
body
|
async syncToProduct(@Param('id') id: number) {
|
||||||
);
|
try {
|
||||||
if (result) {
|
await this.wpProductService.syncToProduct(id);
|
||||||
this.wpProductService.updateWpProduct(siteId, productId, body);
|
return successResponse(true);
|
||||||
return successResponse(result, '产品更新成功');
|
|
||||||
}
|
|
||||||
return errorResponse('产品更新失败');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新产品失败:', error);
|
return errorResponse(error.message);
|
||||||
return errorResponse(error.message || '产品更新失败');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,37 +212,7 @@ export class WpProductController {
|
||||||
@Param('variationId') variationId: string,
|
@Param('variationId') variationId: string,
|
||||||
@Body() body: UpdateVariationDTO
|
@Body() body: UpdateVariationDTO
|
||||||
) {
|
) {
|
||||||
try {
|
return errorResponse('接口已废弃,请改用 /site-api/:siteId/products/:productId/variations/:variationId 更新');
|
||||||
const isDuplicate = await this.wpProductService.isSkuDuplicate(
|
|
||||||
body.sku,
|
|
||||||
siteId,
|
|
||||||
productId,
|
|
||||||
variationId
|
|
||||||
);
|
|
||||||
if (isDuplicate) {
|
|
||||||
return errorResponse('SKU已存在');
|
|
||||||
}
|
|
||||||
const site = await this.siteService.get(siteId, true);
|
|
||||||
const result = await this.wpApiService.updateVariation(
|
|
||||||
site,
|
|
||||||
productId,
|
|
||||||
variationId,
|
|
||||||
body
|
|
||||||
);
|
|
||||||
if (result) {
|
|
||||||
this.wpProductService.updateWpProductVaritation(
|
|
||||||
siteId,
|
|
||||||
productId,
|
|
||||||
variationId,
|
|
||||||
body
|
|
||||||
);
|
|
||||||
return successResponse(result, '产品变体更新成功');
|
|
||||||
}
|
|
||||||
return errorResponse('变体更新失败');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新变体失败:', error);
|
|
||||||
return errorResponse(error.message || '产品变体更新失败');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { SeederOptions } from 'typeorm-extension';
|
||||||
|
|
||||||
const options: DataSourceOptions & SeederOptions = {
|
const options: DataSourceOptions & SeederOptions = {
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
host: 'localhost',
|
host: '127.0.0.1',
|
||||||
port: 23306,
|
port: 23306,
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: '12345678',
|
password: '12345678',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddTestDataToTemplate1765275715762 implements MigrationInterface {
|
||||||
|
name = 'AddTestDataToTemplate1765275715762'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` DROP FOREIGN KEY \`FK_e93d8c42c9baf5a0dade42c59ae\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`template\` ADD \`testData\` text NULL COMMENT '测试数据JSON'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` ADD CONSTRAINT \`FK_e93d8c42c9baf5a0dade42c59ae\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` DROP FOREIGN KEY \`FK_e93d8c42c9baf5a0dade42c59ae\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`template\` DROP COLUMN \`testData\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` ADD CONSTRAINT \`FK_e93d8c42c9baf5a0dade42c59ae\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddSiteDescription1765330208213 implements MigrationInterface {
|
||||||
|
name = 'AddSiteDescription1765330208213'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -22,77 +22,77 @@ export default class DictSeeder implements Seeder {
|
||||||
const dictItemRepository = dataSource.getRepository(DictItem);
|
const dictItemRepository = dataSource.getRepository(DictItem);
|
||||||
|
|
||||||
const flavorsData = [
|
const flavorsData = [
|
||||||
{ name: 'bellini', title: 'Bellini', titleCn: '贝利尼' },
|
{ name: 'bellini', title: 'Bellini', titleCn: '贝利尼', shortName: 'BL' },
|
||||||
{ name: 'max-polarmint', title: 'Max Polarmint', titleCn: '马克斯薄荷' },
|
{ name: 'max-polarmint', title: 'Max Polarmint', titleCn: '马克斯薄荷', shortName: 'MP' },
|
||||||
{ name: 'blueberry', title: 'Blueberry', titleCn: '蓝莓' },
|
{ name: 'blueberry', title: 'Blueberry', titleCn: '蓝莓', shortName: 'BB' },
|
||||||
{ name: 'citrus', title: 'Citrus', titleCn: '柑橘' },
|
{ name: 'citrus', title: 'Citrus', titleCn: '柑橘', shortName: 'CT' },
|
||||||
{ name: 'wintergreen', title: 'Wintergreen', titleCn: '冬绿薄荷' },
|
{ name: 'wintergreen', title: 'Wintergreen', titleCn: '冬绿薄荷', shortName: 'WG' },
|
||||||
{ name: 'cool-mint', title: 'COOL MINT', titleCn: '清凉薄荷' },
|
{ name: 'cool-mint', title: 'COOL MINT', titleCn: '清凉薄荷', shortName: 'CM' },
|
||||||
{ name: 'juicy-peach', title: 'JUICY PEACH', titleCn: '多汁蜜桃' },
|
{ name: 'juicy-peach', title: 'JUICY PEACH', titleCn: '多汁蜜桃', shortName: 'JP' },
|
||||||
{ name: 'orange', title: 'ORANGE', titleCn: '橙子' },
|
{ name: 'orange', title: 'ORANGE', titleCn: '橙子', shortName: 'OR' },
|
||||||
{ name: 'peppermint', title: 'PEPPERMINT', titleCn: '胡椒薄荷' },
|
{ name: 'peppermint', title: 'PEPPERMINT', titleCn: '胡椒薄荷', shortName: 'PP' },
|
||||||
{ name: 'spearmint', title: 'SPEARMINT', titleCn: '绿薄荷' },
|
{ name: 'spearmint', title: 'SPEARMINT', titleCn: '绿薄荷', shortName: 'SM' },
|
||||||
{ name: 'strawberry', title: 'STRAWBERRY', titleCn: '草莓' },
|
{ name: 'strawberry', title: 'STRAWBERRY', titleCn: '草莓', shortName: 'SB' },
|
||||||
{ name: 'watermelon', title: 'WATERMELON', titleCn: '西瓜' },
|
{ name: 'watermelon', title: 'WATERMELON', titleCn: '西瓜', shortName: 'WM' },
|
||||||
{ name: 'coffee', title: 'COFFEE', titleCn: '咖啡' },
|
{ name: 'coffee', title: 'COFFEE', titleCn: '咖啡', shortName: 'CF' },
|
||||||
{ name: 'lemonade', title: 'LEMONADE', titleCn: '柠檬水' },
|
{ name: 'lemonade', title: 'LEMONADE', titleCn: '柠檬水', shortName: 'LN' },
|
||||||
{ name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷' },
|
{ name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷', shortName: 'AM' },
|
||||||
{ name: 'peach', title: 'PEACH', titleCn: '桃子' },
|
{ name: 'peach', title: 'PEACH', titleCn: '桃子', shortName: 'PC' },
|
||||||
{ name: 'mango', title: 'Mango', titleCn: '芒果' },
|
{ name: 'mango', title: 'Mango', titleCn: '芒果', shortName: 'MG' },
|
||||||
{ name: 'ice-wintergreen', title: 'ICE WINTERGREEN', titleCn: '冰冬绿薄荷' },
|
{ name: 'ice-wintergreen', title: 'ICE WINTERGREEN', titleCn: '冰冬绿薄荷', shortName: 'IWG' },
|
||||||
{ name: 'pink-lemonade', title: 'Pink Lemonade', titleCn: '粉红柠檬水' },
|
{ name: 'pink-lemonade', title: 'Pink Lemonade', titleCn: '粉红柠檬水', shortName: 'PLN' },
|
||||||
{ name: 'blackcherry', title: 'Blackcherry', titleCn: '黑樱桃' },
|
{ name: 'blackcherry', title: 'Blackcherry', titleCn: '黑樱桃', shortName: 'BC' },
|
||||||
{ name: 'fresh-mint', title: 'fresh mint', titleCn: '清新薄荷' },
|
{ name: 'fresh-mint', title: 'fresh mint', titleCn: '清新薄荷', shortName: 'FM' },
|
||||||
{ name: 'strawberry-lychee', title: 'Strawberry Lychee', titleCn: '草莓荔枝' },
|
{ name: 'strawberry-lychee', title: 'Strawberry Lychee', titleCn: '草莓荔枝', shortName: 'SBL' },
|
||||||
{ name: 'passion-fruit', title: 'Passion Fruit', titleCn: '百香果' },
|
{ name: 'passion-fruit', title: 'Passion Fruit', titleCn: '百香果', shortName: 'PF' },
|
||||||
{ name: 'banana-lce', title: 'Banana lce', titleCn: '香蕉冰' },
|
{ name: 'banana-lce', title: 'Banana lce', titleCn: '香蕉冰', shortName: 'BI' },
|
||||||
{ name: 'bubblegum', title: 'Bubblegum', titleCn: '泡泡糖' },
|
{ name: 'bubblegum', title: 'Bubblegum', titleCn: '泡泡糖', shortName: 'BG' },
|
||||||
{ name: 'mango-lce', title: 'Mango lce', titleCn: '芒果冰' },
|
{ name: 'mango-lce', title: 'Mango lce', titleCn: '芒果冰', shortName: 'MI' },
|
||||||
{ name: 'grape-lce', title: 'Grape lce', titleCn: '葡萄冰' },
|
{ name: 'grape-lce', title: 'Grape lce', titleCn: '葡萄冰', shortName: 'GI' },
|
||||||
{ name: 'apple', title: 'apple', titleCn: '苹果' },
|
{ name: 'apple', title: 'apple', titleCn: '苹果', shortName: 'AP' },
|
||||||
{ name: 'grape', title: 'grape', titleCn: '葡萄' },
|
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
|
||||||
{ name: 'cherry', title: 'cherry', titleCn: '樱桃' },
|
{ name: 'cherry', title: 'cherry', titleCn: '樱桃', shortName: 'CH' },
|
||||||
{ name: 'lemon', title: 'lemon', titleCn: '柠檬' },
|
{ name: 'lemon', title: 'lemon', titleCn: '柠檬', shortName: 'LM' },
|
||||||
{ name: 'razz', title: 'razz', titleCn: '覆盆子' },
|
{ name: 'razz', title: 'razz', titleCn: '覆盆子', shortName: 'RZ' },
|
||||||
{ name: 'pineapple', title: 'pineapple', titleCn: '菠萝' },
|
{ name: 'pineapple', title: 'pineapple', titleCn: '菠萝', shortName: 'PA' },
|
||||||
{ name: 'berry', title: 'berry', titleCn: '浆果' },
|
{ name: 'berry', title: 'berry', titleCn: '浆果', shortName: 'BR' },
|
||||||
{ name: 'fruit', title: 'fruit', titleCn: '水果' },
|
{ name: 'fruit', title: 'fruit', titleCn: '水果', shortName: 'FR' },
|
||||||
{ name: 'mint', title: 'mint', titleCn: '薄荷' },
|
{ name: 'mint', title: 'mint', titleCn: '薄荷', shortName: 'MT' },
|
||||||
{ name: 'menthol', title: 'menthol', titleCn: '薄荷醇' },
|
{ name: 'menthol', title: 'menthol', titleCn: '薄荷醇', shortName: 'MH' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const brandsData = [
|
const brandsData = [
|
||||||
{ name: 'yoone', title: 'Yoone', titleCn: '' },
|
{ name: 'yoone', title: 'Yoone', titleCn: '', shortName: 'YN' },
|
||||||
{ name: 'white-fox', title: 'White Fox', titleCn: '' },
|
{ name: 'white-fox', title: 'White Fox', titleCn: '', shortName: 'WF' },
|
||||||
{ name: 'zyn', title: 'ZYN', titleCn: '' },
|
{ name: 'zyn', title: 'ZYN', titleCn: '', shortName: 'ZN' },
|
||||||
{ name: 'zonnic', title: 'Zonnic', titleCn: '' },
|
{ name: 'zonnic', title: 'Zonnic', titleCn: '', shortName: 'ZC' },
|
||||||
{ name: 'zolt', title: 'Zolt', titleCn: '' },
|
{ name: 'zolt', title: 'Zolt', titleCn: '', shortName: 'ZT' },
|
||||||
{ name: 'velo', title: 'Velo', titleCn: '' },
|
{ name: 'velo', title: 'Velo', titleCn: '', shortName: 'VL' },
|
||||||
{ name: 'lucy', title: 'Lucy', titleCn: '' },
|
{ name: 'lucy', title: 'Lucy', titleCn: '', shortName: 'LC' },
|
||||||
{ name: 'egp', title: 'EGP', titleCn: '' },
|
{ name: 'egp', title: 'EGP', titleCn: '', shortName: 'EP' },
|
||||||
{ name: 'bridge', title: 'Bridge', titleCn: '' },
|
{ name: 'bridge', title: 'Bridge', titleCn: '', shortName: 'BR' },
|
||||||
{ name: 'zex', title: 'ZEX', titleCn: '' },
|
{ name: 'zex', title: 'ZEX', titleCn: '', shortName: 'ZX' },
|
||||||
{ name: 'sesh', title: 'Sesh', titleCn: '' },
|
{ name: 'sesh', title: 'Sesh', titleCn: '', shortName: 'SH' },
|
||||||
{ name: 'pablo', title: 'Pablo', titleCn: '' },
|
{ name: 'pablo', title: 'Pablo', titleCn: '', shortName: 'PB' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const strengthsData = [
|
const strengthsData = [
|
||||||
{ name: '2mg', title: '2MG', titleCn: '2毫克' },
|
{ name: '2mg', title: '2MG', titleCn: '2毫克', shortName: '2M' },
|
||||||
{ name: '4mg', title: '4MG', titleCn: '4毫克' },
|
{ name: '3mg', title: '3MG', titleCn: '3毫克', shortName: '3M' },
|
||||||
{ name: '3mg', title: '3MG', titleCn: '3毫克' },
|
{ name: '4mg', title: '4MG', titleCn: '4毫克', shortName: '4M' },
|
||||||
{ name: '6mg', title: '6MG', titleCn: '6毫克' },
|
{ name: '6mg', title: '6MG', titleCn: '6毫克', shortName: '6M' },
|
||||||
{ name: '6.5mg', title: '6.5MG', titleCn: '6.5毫克' },
|
{ name: '6.5mg', title: '6.5MG', titleCn: '6.5毫克', shortName: '6.5M' },
|
||||||
{ name: '9mg', title: '9MG', titleCn: '9毫克' },
|
{ name: '9mg', title: '9MG', titleCn: '9毫克', shortName: '9M' },
|
||||||
{ name: '12mg', title: '12MG', titleCn: '12毫克' },
|
{ name: '12mg', title: '12MG', titleCn: '12毫克', shortName: '12M' },
|
||||||
{ name: '16.5mg', title: '16.5MG', titleCn: '16.5毫克' },
|
{ name: '16.5mg', title: '16.5MG', titleCn: '16.5毫克', shortName: '16.5M' },
|
||||||
{ name: '18mg', title: '18MG', titleCn: '18毫克' },
|
{ name: '18mg', title: '18MG', titleCn: '18毫克', shortName: '18M' },
|
||||||
{ name: '30mg', title: '30MG', titleCn: '30毫克' },
|
{ name: '30mg', title: '30MG', titleCn: '30毫克', shortName: '30M' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 初始化语言字典
|
// 初始化语言字典
|
||||||
const locales = [
|
const locales = [
|
||||||
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文' },
|
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文', shortName: 'CN' },
|
||||||
{ name: 'en-us', title: 'English', titleCn: '英文' },
|
{ name: 'en-us', title: 'English', titleCn: '英文', shortName: 'EN' },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
|
|
@ -114,19 +114,19 @@ export default class DictSeeder implements Seeder {
|
||||||
// 添加中文翻译
|
// 添加中文翻译
|
||||||
let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
|
let item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: zhDict.id } } });
|
||||||
if (!item) {
|
if (!item) {
|
||||||
await dictItemRepository.save({ name: t.name, title: t.zh, titleCn: t.zh, dict: zhDict });
|
await dictItemRepository.save({ name: t.name, title: t.zh, titleCn: t.zh, shortName: t.zh.substring(0, 2).toUpperCase(), dict: zhDict });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加英文翻译
|
// 添加英文翻译
|
||||||
item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
|
item = await dictItemRepository.findOne({ where: { name: t.name, dict: { id: enDict.id } } });
|
||||||
if (!item) {
|
if (!item) {
|
||||||
await dictItemRepository.save({ name: t.name, title: t.en, titleCn: t.en, dict: enDict });
|
await dictItemRepository.save({ name: t.name, title: t.en, titleCn: t.en, shortName: t.en.substring(0, 2).toUpperCase(), dict: enDict });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const brandDict = await this.createOrFindDict(dictRepository, { name: 'brand', title: '品牌', titleCn: '品牌' });
|
const brandDict = await this.createOrFindDict(dictRepository, { name: 'brand', title: '品牌', titleCn: '品牌', shortName: 'BR' });
|
||||||
const flavorDict = await this.createOrFindDict(dictRepository, { name: 'flavor', title: '口味', titleCn: '口味' });
|
const flavorDict = await this.createOrFindDict(dictRepository, { name: 'flavor', title: '口味', titleCn: '口味', shortName: 'FL' });
|
||||||
const strengthDict = await this.createOrFindDict(dictRepository, { name: 'strength', title: '强度', titleCn: '强度' });
|
const strengthDict = await this.createOrFindDict(dictRepository, { name: 'strength', title: '强度', titleCn: '强度', shortName: 'ST' });
|
||||||
|
|
||||||
// 遍历品牌数据
|
// 遍历品牌数据
|
||||||
await this.seedDictItems(dictItemRepository, brandDict, brandsData);
|
await this.seedDictItems(dictItemRepository, brandDict, brandsData);
|
||||||
|
|
@ -144,13 +144,13 @@ export default class DictSeeder implements Seeder {
|
||||||
* @param dictInfo 字典信息
|
* @param dictInfo 字典信息
|
||||||
* @returns Dict 实例
|
* @returns Dict 实例
|
||||||
*/
|
*/
|
||||||
private async createOrFindDict(repo: any, dictInfo: { name: string; title: string; titleCn: string }): Promise<Dict> {
|
private async createOrFindDict(repo: any, dictInfo: { name: string; title: string; titleCn: string; shortName: string }): Promise<Dict> {
|
||||||
// 格式化 name
|
// 格式化 name
|
||||||
const formattedName = this.formatName(dictInfo.name);
|
const formattedName = this.formatName(dictInfo.name);
|
||||||
let dict = await repo.findOne({ where: { name: formattedName } });
|
let dict = await repo.findOne({ where: { name: formattedName } });
|
||||||
if (!dict) {
|
if (!dict) {
|
||||||
// 如果字典不存在,则使用格式化后的 name 创建新字典
|
// 如果字典不存在,则使用格式化后的 name 创建新字典
|
||||||
dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn });
|
dict = await repo.save({ name: formattedName, title: dictInfo.title, titleCn: dictInfo.titleCn, shortName: dictInfo.shortName });
|
||||||
}
|
}
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
@ -161,14 +161,14 @@ export default class DictSeeder implements Seeder {
|
||||||
* @param dict 字典实例
|
* @param dict 字典实例
|
||||||
* @param items 字典项数组
|
* @param items 字典项数组
|
||||||
*/
|
*/
|
||||||
private async seedDictItems(repo: any, dict: Dict, items: { name: string; title: string; titleCn: string }[]): Promise<void> {
|
private async seedDictItems(repo: any, dict: Dict, items: { name: string; title: string; titleCn: string; shortName: string }[]): Promise<void> {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
// 格式化 name
|
// 格式化 name
|
||||||
const formattedName = this.formatName(item.name);
|
const formattedName = this.formatName(item.name);
|
||||||
const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
|
const existingItem = await repo.findOne({ where: { name: formattedName, dict: { id: dict.id } } });
|
||||||
if (!existingItem) {
|
if (!existingItem) {
|
||||||
// 如果字典项不存在,则使用格式化后的 name 创建新字典项
|
// 如果字典项不存在,则使用格式化后的 name 创建新字典项
|
||||||
await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, dict });
|
await repo.save({ name: formattedName, title: item.title, titleCn: item.titleCn, shortName: item.shortName, dict });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Template } from '../../entity/template.entity';
|
||||||
export default class TemplateSeeder implements Seeder {
|
export default class TemplateSeeder implements Seeder {
|
||||||
/**
|
/**
|
||||||
* @method run
|
* @method run
|
||||||
* @description 执行数据填充操作.如果 product_sku 模板不存在,则创建它.
|
* @description 执行数据填充操作.如果模板不存在,则创建它;如果存在,则更新它.
|
||||||
* @param {DataSource} dataSource - 数据源实例,用于获取 repository.
|
* @param {DataSource} dataSource - 数据源实例,用于获取 repository.
|
||||||
* @param {SeederFactoryManager} factoryManager - Seeder 工厂管理器.
|
* @param {SeederFactoryManager} factoryManager - Seeder 工厂管理器.
|
||||||
*/
|
*/
|
||||||
|
|
@ -20,17 +20,53 @@ export default class TemplateSeeder implements Seeder {
|
||||||
// 获取 Template 实体的 repository
|
// 获取 Template 实体的 repository
|
||||||
const templateRepository = dataSource.getRepository(Template);
|
const templateRepository = dataSource.getRepository(Template);
|
||||||
|
|
||||||
// 检查名为 'product_sku' 的模板是否已存在
|
const templates = [
|
||||||
|
{
|
||||||
|
name: 'product.sku',
|
||||||
|
value: '<%= it.brand %>-<%=it.category%>-<%= it.flavor %>-<%= it.strength %>-<%= it.humidity %>',
|
||||||
|
description: '产品SKU模板',
|
||||||
|
testData: JSON.stringify({
|
||||||
|
brand: 'Brand',
|
||||||
|
category: 'Category',
|
||||||
|
flavor: 'Flavor',
|
||||||
|
strength: '10mg',
|
||||||
|
humidity: 'Dry',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'product.title',
|
||||||
|
value: '<%= it.brand %> <%= it.flavor %> <%= it.strength %> <%= it.humidity %>',
|
||||||
|
description: '产品标题模板',
|
||||||
|
testData: JSON.stringify({
|
||||||
|
brand: 'Brand',
|
||||||
|
flavor: 'Flavor',
|
||||||
|
strength: '10mg',
|
||||||
|
humidity: 'Dry',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of templates) {
|
||||||
|
// 检查模板是否已存在
|
||||||
const existingTemplate = await templateRepository.findOne({
|
const existingTemplate = await templateRepository.findOne({
|
||||||
where: { name: 'product_sku' },
|
where: { name: t.name },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果模板不存在,则创建并保存
|
if (existingTemplate) {
|
||||||
if (!existingTemplate) {
|
// 如果存在,则更新
|
||||||
|
existingTemplate.value = t.value;
|
||||||
|
existingTemplate.description = t.description;
|
||||||
|
existingTemplate.testData = t.testData;
|
||||||
|
await templateRepository.save(existingTemplate);
|
||||||
|
} else {
|
||||||
|
// 如果不存在,则创建并保存
|
||||||
const template = new Template();
|
const template = new Template();
|
||||||
template.name = 'product_sku';
|
template.name = t.name;
|
||||||
template.value = '{{brand}}-{{flavor}}-{{strength}}-{{humidity}}';
|
template.value = t.value;
|
||||||
|
template.description = t.description;
|
||||||
|
template.testData = t.testData;
|
||||||
await templateRepository.save(template);
|
await templateRepository.save(template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ export class CreateDictItemDTO {
|
||||||
@Rule(RuleType.string().allow('').allow(null))
|
@Rule(RuleType.string().allow('').allow(null))
|
||||||
titleCN?: string; // 字典项中文标题 (可选)
|
titleCN?: string; // 字典项中文标题 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow('').allow(null))
|
||||||
|
image?: string; // 图片 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow('').allow(null))
|
||||||
|
shortName?: string; // 简称 (可选)
|
||||||
|
|
||||||
@Rule(RuleType.number().required())
|
@Rule(RuleType.number().required())
|
||||||
dictId: number; // 所属字典的ID
|
dictId: number; // 所属字典的ID
|
||||||
}
|
}
|
||||||
|
|
@ -47,4 +53,10 @@ export class UpdateDictItemDTO {
|
||||||
@Rule(RuleType.string().allow(null))
|
@Rule(RuleType.string().allow(null))
|
||||||
value?: string; // 字典项值 (可选)
|
value?: string; // 字典项值 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow('').allow(null))
|
||||||
|
image?: string; // 图片 (可选)
|
||||||
|
|
||||||
|
@Rule(RuleType.string().allow('').allow(null))
|
||||||
|
shortName?: string; // 简称 (可选)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@ export class CreateProductDTO {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
shortDescription?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '产品 SKU', required: false })
|
@ApiProperty({ description: '产品 SKU', required: false })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|
@ -54,6 +58,10 @@ export class CreateProductDTO {
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
|
siteSkus?: string[];
|
||||||
|
|
||||||
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||||
@ApiProperty({ description: '属性列表', type: 'array' })
|
@ApiProperty({ description: '属性列表', type: 'array' })
|
||||||
@Rule(RuleType.array().required())
|
@Rule(RuleType.array().required())
|
||||||
|
|
@ -110,6 +118,10 @@ export class UpdateProductDTO {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
shortDescription?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '产品 SKU', required: false })
|
@ApiProperty({ description: '产品 SKU', required: false })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
sku?: string;
|
sku?: string;
|
||||||
|
|
@ -118,6 +130,10 @@ export class UpdateProductDTO {
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
|
siteSkus?: string[];
|
||||||
|
|
||||||
// 商品价格
|
// 商品价格
|
||||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number())
|
||||||
|
|
@ -139,6 +155,86 @@ export class UpdateProductDTO {
|
||||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
|
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
|
||||||
@Rule(RuleType.string().valid('single', 'bundle'))
|
@Rule(RuleType.string().valid('single', 'bundle'))
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
||||||
|
// 仅当 type 为 'bundle' 时,才需要提供 components
|
||||||
|
@ApiProperty({ description: '产品组成', type: 'array', required: false })
|
||||||
|
@Rule(
|
||||||
|
RuleType.array()
|
||||||
|
.items(
|
||||||
|
RuleType.object({
|
||||||
|
sku: RuleType.string().required(),
|
||||||
|
quantity: RuleType.number().required(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.when('type', {
|
||||||
|
is: 'bundle',
|
||||||
|
then: RuleType.array().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
components?: { sku: string; quantity: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO 用于批量更新产品属性
|
||||||
|
*/
|
||||||
|
export class BatchUpdateProductDTO {
|
||||||
|
@ApiProperty({ description: '产品ID列表', type: 'array', required: true })
|
||||||
|
@Rule(RuleType.array().items(RuleType.number()).required().min(1))
|
||||||
|
ids: number[];
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'ZYN 6MG WINTERGREEN', description: '产品名称', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品中文名称', required: false })
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
nameCn?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '产品描述', description: '产品描述', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '产品简短描述', description: '产品简短描述', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
shortDescription?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品 SKU', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
sku?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类ID (DictItem ID)', required: false })
|
||||||
|
@Rule(RuleType.number().optional())
|
||||||
|
categoryId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
|
siteSkus?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||||
|
@Rule(RuleType.number().optional())
|
||||||
|
price?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '促销价格', example: 99.99, required: false })
|
||||||
|
@Rule(RuleType.number().optional())
|
||||||
|
promotionPrice?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||||
|
@Rule(RuleType.array().optional())
|
||||||
|
attributes?: AttributeInputDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], required: false })
|
||||||
|
@Rule(RuleType.string().valid('single', 'bundle').optional())
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO 用于批量删除产品
|
||||||
|
*/
|
||||||
|
export class BatchDeleteProductDTO {
|
||||||
|
@ApiProperty({ description: '产品ID列表', type: 'array', required: true })
|
||||||
|
@Rule(RuleType.array().items(RuleType.number()).required().min(1))
|
||||||
|
ids: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -187,20 +283,3 @@ export class QueryProductDTO {
|
||||||
sortOrder?: string;
|
sortOrder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO 用于设置产品组成
|
|
||||||
*/
|
|
||||||
export class SetProductComponentsDTO {
|
|
||||||
@ApiProperty({ description: '产品组成', type: 'array', required: true })
|
|
||||||
@Rule(
|
|
||||||
RuleType.array()
|
|
||||||
.items(
|
|
||||||
RuleType.object({
|
|
||||||
sku: RuleType.string().required(),
|
|
||||||
quantity: RuleType.number().required(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.required()
|
|
||||||
)
|
|
||||||
components: { sku: string; quantity: number }[];
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
|
||||||
|
export class UnifiedPaginationDTO<T> {
|
||||||
|
@ApiProperty({ description: '列表数据' })
|
||||||
|
items: T[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总数', example: 100 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '当前页', example: 1 })
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
per_page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总页数', example: 5 })
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedImageDTO {
|
||||||
|
@ApiProperty({ description: '图片ID' })
|
||||||
|
id: number | string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '图片URL' })
|
||||||
|
src: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '图片名称' })
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '替代文本' })
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedProductDTO {
|
||||||
|
@ApiProperty({ description: '产品ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品名称' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品类型' })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品状态' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品SKU' })
|
||||||
|
sku: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '常规价格' })
|
||||||
|
regular_price: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '销售价格' })
|
||||||
|
sale_price: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '当前价格' })
|
||||||
|
price: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存状态' })
|
||||||
|
stock_status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存数量' })
|
||||||
|
stock_quantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品图片', type: [UnifiedImageDTO] })
|
||||||
|
images: UnifiedImageDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品标签', type: 'json' })
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品属性', type: 'json' })
|
||||||
|
attributes: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品变体', type: 'json' })
|
||||||
|
variations?: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
date_created: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
date_modified: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据(保留备用)', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedOrderDTO {
|
||||||
|
@ApiProperty({ description: '订单ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单号' })
|
||||||
|
number: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单状态' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '货币' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总金额' })
|
||||||
|
total: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
customer_id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户姓名' })
|
||||||
|
customer_name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户邮箱' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单项', type: 'json' })
|
||||||
|
line_items: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '销售项(兼容前端)', type: 'json' })
|
||||||
|
sales?: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单地址', type: 'json' })
|
||||||
|
billing: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '收货地址', type: 'json' })
|
||||||
|
shipping: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单地址全称' })
|
||||||
|
billing_full_address?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '收货地址全称' })
|
||||||
|
shipping_full_address?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '支付方式' })
|
||||||
|
payment_method: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
date_created: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedCustomerDTO {
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '名' })
|
||||||
|
first_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '姓' })
|
||||||
|
last_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '名字' })
|
||||||
|
fullname?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户名' })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '电话' })
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单地址', type: 'json' })
|
||||||
|
billing?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '收货地址', type: 'json' })
|
||||||
|
shipping?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedSubscriptionDTO {
|
||||||
|
@ApiProperty({ description: '订阅ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订阅状态' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
customer_id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '计费周期' })
|
||||||
|
billing_period: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '计费间隔' })
|
||||||
|
billing_interval: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '开始时间' })
|
||||||
|
start_date: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '下次支付时间' })
|
||||||
|
next_payment_date: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单项', type: 'json' })
|
||||||
|
line_items: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedMediaDTO {
|
||||||
|
@ApiProperty({ description: '媒体ID' })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标题' })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '媒体类型' })
|
||||||
|
media_type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'MIME类型' })
|
||||||
|
mime_type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '源URL' })
|
||||||
|
source_url: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
date_created: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO<UnifiedProductDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedProductDTO] })
|
||||||
|
items: UnifiedProductDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO<UnifiedOrderDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedOrderDTO] })
|
||||||
|
items: UnifiedOrderDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO<UnifiedCustomerDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedCustomerDTO] })
|
||||||
|
items: UnifiedCustomerDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO<UnifiedSubscriptionDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedSubscriptionDTO] })
|
||||||
|
items: UnifiedSubscriptionDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO<UnifiedMediaDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedMediaDTO] })
|
||||||
|
items: UnifiedMediaDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedSearchParamsDTO {
|
||||||
|
@ApiProperty({ description: '页码', example: 1 })
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
per_page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '搜索关键词' })
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '状态' })
|
||||||
|
status?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序字段' })
|
||||||
|
orderby?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序方式' })
|
||||||
|
order?: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
|
||||||
|
export class UnifiedPaginationDTO<T> {
|
||||||
|
@ApiProperty({ description: '列表数据' })
|
||||||
|
items: T[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总数', example: 100 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '当前页', example: 1 })
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
per_page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总页数', example: 5 })
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedImageDTO {
|
||||||
|
@ApiProperty({ description: '图片ID' })
|
||||||
|
id: number | string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '图片URL' })
|
||||||
|
src: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '图片名称' })
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '替代文本' })
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedProductDTO {
|
||||||
|
@ApiProperty({ description: '产品ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品名称' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品类型' })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品状态' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品SKU' })
|
||||||
|
sku: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '常规价格' })
|
||||||
|
regular_price: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '销售价格' })
|
||||||
|
sale_price: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '当前价格' })
|
||||||
|
price: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存状态' })
|
||||||
|
stock_status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存数量' })
|
||||||
|
stock_quantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品图片', type: [UnifiedImageDTO] })
|
||||||
|
images: UnifiedImageDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品标签', type: 'json' })
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品属性', type: 'json' })
|
||||||
|
attributes: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品变体', type: 'json' })
|
||||||
|
variations?: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
date_created: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
date_modified: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据(保留备用)', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedOrderDTO {
|
||||||
|
@ApiProperty({ description: '订单ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单号' })
|
||||||
|
number: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单状态' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '货币' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总金额' })
|
||||||
|
total: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
customer_id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户姓名' })
|
||||||
|
customer_name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户邮箱' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单项', type: 'json' })
|
||||||
|
line_items: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '销售项(兼容前端)', type: 'json' })
|
||||||
|
sales?: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单地址', type: 'json' })
|
||||||
|
billing: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '收货地址', type: 'json' })
|
||||||
|
shipping: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单地址全称' })
|
||||||
|
billing_full_address?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '收货地址全称' })
|
||||||
|
shipping_full_address?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '支付方式' })
|
||||||
|
payment_method: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
date_created: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedCustomerDTO {
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '名' })
|
||||||
|
first_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '姓' })
|
||||||
|
last_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '名字' })
|
||||||
|
fullname?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户名' })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '电话' })
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单地址', type: 'json' })
|
||||||
|
billing?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '收货地址', type: 'json' })
|
||||||
|
shipping?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedSubscriptionDTO {
|
||||||
|
@ApiProperty({ description: '订阅ID' })
|
||||||
|
id: string | number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订阅状态' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
customer_id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '计费周期' })
|
||||||
|
billing_period: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '计费间隔' })
|
||||||
|
billing_interval: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '开始时间' })
|
||||||
|
start_date: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '下次支付时间' })
|
||||||
|
next_payment_date: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单项', type: 'json' })
|
||||||
|
line_items: any[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'json' })
|
||||||
|
raw?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedMediaDTO {
|
||||||
|
@ApiProperty({ description: '媒体ID' })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标题' })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '媒体类型' })
|
||||||
|
media_type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'MIME类型' })
|
||||||
|
mime_type: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '源URL' })
|
||||||
|
source_url: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
date_created: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO<UnifiedProductDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedProductDTO] })
|
||||||
|
items: UnifiedProductDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO<UnifiedOrderDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedOrderDTO] })
|
||||||
|
items: UnifiedOrderDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO<UnifiedCustomerDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedCustomerDTO] })
|
||||||
|
items: UnifiedCustomerDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO<UnifiedSubscriptionDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedSubscriptionDTO] })
|
||||||
|
items: UnifiedSubscriptionDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO<UnifiedMediaDTO> {
|
||||||
|
@ApiProperty({ description: '列表数据', type: [UnifiedMediaDTO] })
|
||||||
|
items: UnifiedMediaDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedSearchParamsDTO {
|
||||||
|
@ApiProperty({ description: '页码', example: 1 })
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
per_page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '搜索关键词' })
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '状态' })
|
||||||
|
status?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序字段' })
|
||||||
|
orderby?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序方式' })
|
||||||
|
order?: string;
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,10 @@ export class SiteConfig {
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '描述' })
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
|
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'] })
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy'))
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -40,8 +44,10 @@ export class CreateSiteDTO {
|
||||||
consumerSecret?: string;
|
consumerSecret?: string;
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
token?: string;
|
token?: string;
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string().min(1))
|
||||||
name: string;
|
name: string;
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
description?: string;
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
||||||
type?: string;
|
type?: string;
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
|
|
@ -51,6 +57,11 @@ export class CreateSiteDTO {
|
||||||
@ApiProperty({ description: '区域' })
|
@ApiProperty({ description: '区域' })
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
areas?: string[];
|
areas?: string[];
|
||||||
|
|
||||||
|
// 绑定仓库
|
||||||
|
@ApiProperty({ description: '绑定仓库ID列表' })
|
||||||
|
@Rule(RuleType.array().items(RuleType.number()).optional())
|
||||||
|
stockPointIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateSiteDTO {
|
export class UpdateSiteDTO {
|
||||||
|
|
@ -62,8 +73,10 @@ export class UpdateSiteDTO {
|
||||||
consumerSecret?: string;
|
consumerSecret?: string;
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
token?: string;
|
token?: string;
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().min(1).optional())
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
description?: string;
|
||||||
@Rule(RuleType.boolean().optional())
|
@Rule(RuleType.boolean().optional())
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
||||||
|
|
@ -75,6 +88,11 @@ export class UpdateSiteDTO {
|
||||||
@ApiProperty({ description: '区域' })
|
@ApiProperty({ description: '区域' })
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
areas?: string[];
|
areas?: string[];
|
||||||
|
|
||||||
|
// 绑定仓库
|
||||||
|
@ApiProperty({ description: '绑定仓库ID列表' })
|
||||||
|
@Rule(RuleType.array().items(RuleType.number()).optional())
|
||||||
|
stockPointIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QuerySiteDTO {
|
export class QuerySiteDTO {
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,14 @@ export class CreateStockPointDTO {
|
||||||
@ApiProperty({ description: '区域' })
|
@ApiProperty({ description: '区域' })
|
||||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
areas?: string[];
|
areas?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '上游仓库点ID' })
|
||||||
|
@Rule(RuleType.number().optional())
|
||||||
|
upStreamStockPointId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '上游名称' })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
upStreamName?: string;
|
||||||
}
|
}
|
||||||
export class UpdateStockPointDTO extends CreateStockPointDTO {}
|
export class UpdateStockPointDTO extends CreateStockPointDTO {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ export class CreateTemplateDTO {
|
||||||
@ApiProperty({ description: '模板内容', required: true })
|
@ApiProperty({ description: '模板内容', required: true })
|
||||||
@Rule(RuleType.string().required())
|
@Rule(RuleType.string().required())
|
||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '测试数据JSON', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
testData?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateTemplateDTO {
|
export class UpdateTemplateDTO {
|
||||||
|
|
@ -19,4 +23,8 @@ export class UpdateTemplateDTO {
|
||||||
@ApiProperty({ description: '模板内容', required: true })
|
@ApiProperty({ description: '模板内容', required: true })
|
||||||
@Rule(RuleType.string().required())
|
@Rule(RuleType.string().required())
|
||||||
value: string;
|
value: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '测试数据JSON', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
testData?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,46 +13,58 @@ export class WpProductDTO extends WpProduct {
|
||||||
|
|
||||||
export class UpdateVariationDTO {
|
export class UpdateVariationDTO {
|
||||||
@ApiProperty({ description: '产品名称' })
|
@ApiProperty({ description: '产品名称' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string().optional())
|
||||||
name: string;
|
name?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'SKU' })
|
@ApiProperty({ description: 'SKU' })
|
||||||
@Rule(RuleType.string().allow(''))
|
@Rule(RuleType.string().allow('').optional())
|
||||||
sku: string;
|
sku?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '常规价格', type: Number })
|
@ApiProperty({ description: '常规价格', type: Number })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number().optional())
|
||||||
regular_price: number; // 常规价格
|
regular_price?: number; // 常规价格
|
||||||
|
|
||||||
@ApiProperty({ description: '销售价格', type: Number })
|
@ApiProperty({ description: '销售价格', type: Number })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number().optional())
|
||||||
sale_price: number; // 销售价格
|
sale_price?: number; // 销售价格
|
||||||
|
|
||||||
@ApiProperty({ description: '是否促销中', type: Boolean })
|
@ApiProperty({ description: '是否促销中', type: Boolean })
|
||||||
@Rule(RuleType.boolean())
|
@Rule(RuleType.boolean().optional())
|
||||||
on_sale: boolean; // 是否促销中
|
on_sale?: boolean; // 是否促销中
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateWpProductDTO {
|
export class UpdateWpProductDTO {
|
||||||
@ApiProperty({ description: '变体名称' })
|
@ApiProperty({ description: '变体名称' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string().optional())
|
||||||
name: string;
|
name?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'SKU' })
|
@ApiProperty({ description: 'SKU' })
|
||||||
@Rule(RuleType.string().allow(''))
|
@Rule(RuleType.string().allow('').optional())
|
||||||
sku: string;
|
sku?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '常规价格', type: Number })
|
@ApiProperty({ description: '常规价格', type: Number })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number().optional())
|
||||||
regular_price: number; // 常规价格
|
regular_price?: number; // 常规价格
|
||||||
|
|
||||||
@ApiProperty({ description: '销售价格', type: Number })
|
@ApiProperty({ description: '销售价格', type: Number })
|
||||||
@Rule(RuleType.number())
|
@Rule(RuleType.number().optional())
|
||||||
sale_price: number; // 销售价格
|
sale_price?: number; // 销售价格
|
||||||
|
|
||||||
@ApiProperty({ description: '是否促销中', type: Boolean })
|
@ApiProperty({ description: '是否促销中', type: Boolean })
|
||||||
@Rule(RuleType.boolean())
|
@Rule(RuleType.boolean().optional())
|
||||||
on_sale: boolean; // 是否促销中
|
on_sale?: boolean; // 是否促销中
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类列表', type: [String] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
|
categories?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签列表', type: [String] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID', required: false })
|
||||||
|
@Rule(RuleType.number().optional())
|
||||||
|
siteId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryWpProductDTO {
|
export class QueryWpProductDTO {
|
||||||
|
|
@ -75,24 +87,50 @@ export class QueryWpProductDTO {
|
||||||
@ApiProperty({ description: '产品状态', enum: ProductStatus })
|
@ApiProperty({ description: '产品状态', enum: ProductStatus })
|
||||||
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
|
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
|
||||||
status?: ProductStatus;
|
status?: ProductStatus;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'SKU列表', type: Array })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()).single())
|
||||||
|
skus?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SetConstitutionDTO {
|
export class BatchSyncProductsDTO {
|
||||||
@ApiProperty({ type: Boolean })
|
@ApiProperty({ description: '产品ID列表', type: [Number] })
|
||||||
@Rule(RuleType.boolean())
|
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||||
isProduct: boolean;
|
productIds: number[];
|
||||||
|
}
|
||||||
@ApiProperty({
|
|
||||||
description: '构成成分',
|
export class BatchUpdateTagsDTO {
|
||||||
type: 'array',
|
@ApiProperty({ description: '产品ID列表', type: [Number] })
|
||||||
items: {
|
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||||
type: 'object',
|
ids: number[];
|
||||||
properties: {
|
|
||||||
sku: { type: 'string' },
|
@ApiProperty({ description: '标签列表', type: [String] })
|
||||||
quantity: { type: 'number' },
|
@Rule(RuleType.array().items(RuleType.string()).required())
|
||||||
},
|
tags: string[];
|
||||||
},
|
}
|
||||||
})
|
|
||||||
@Rule(RuleType.array())
|
export class BatchUpdateProductsDTO {
|
||||||
constitution: { sku: string; quantity: number }[] | null;
|
@ApiProperty({ description: '产品ID列表', type: [Number] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.number()).required())
|
||||||
|
ids: number[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '常规价格', type: Number })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
regular_price?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '销售价格', type: Number })
|
||||||
|
@Rule(RuleType.number())
|
||||||
|
sale_price?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类列表', type: [String] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()))
|
||||||
|
categories?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签列表', type: [String] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.string()))
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '状态', enum: ProductStatus })
|
||||||
|
@Rule(RuleType.string().valid(...Object.values(ProductStatus)))
|
||||||
|
status?: ProductStatus;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ export class DictItem {
|
||||||
@Column({ nullable: true, comment: '字典项值' })
|
@Column({ nullable: true, comment: '字典项值' })
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, comment: '图片' })
|
||||||
|
image: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, comment: '简称' })
|
||||||
|
shortName: string;
|
||||||
|
|
||||||
// 排序
|
// 排序
|
||||||
@Column({ default: 0, comment: '排序' })
|
@Column({ default: 0, comment: '排序' })
|
||||||
sort: number;
|
sort: number;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { DictItem } from './dict_item.entity';
|
import { DictItem } from './dict_item.entity';
|
||||||
import { ProductStockComponent } from './product_stock_component.entity';
|
import { ProductStockComponent } from './product_stock_component.entity';
|
||||||
|
import { ProductSiteSku } from './product_site_sku.entity';
|
||||||
import { Category } from './category.entity';
|
import { Category } from './category.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
|
@ -49,6 +50,10 @@ export class Product {
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '产品简短描述', description: '产品简短描述' })
|
||||||
|
@Column({ nullable: true })
|
||||||
|
shortDescription?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'sku'})
|
@ApiProperty({ description: 'sku'})
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
sku: string;
|
sku: string;
|
||||||
|
|
@ -82,6 +87,10 @@ export class Product {
|
||||||
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
||||||
components: ProductStockComponent[];
|
components: ProductStockComponent[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 SKU 列表', type: ProductSiteSku, isArray: true })
|
||||||
|
@OneToMany(() => ProductSiteSku, (siteSku) => siteSku.product, { cascade: true })
|
||||||
|
siteSkus: ProductSiteSku[];
|
||||||
|
|
||||||
// 来源
|
// 来源
|
||||||
@ApiProperty({ description: '来源', example: '1' })
|
@ApiProperty({ description: '来源', example: '1' })
|
||||||
@Column({ default: 0 })
|
@Column({ default: 0 })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
import { Product } from './product.entity';
|
||||||
|
|
||||||
|
@Entity('product_site_sku')
|
||||||
|
export class ProductSiteSku {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 SKU' })
|
||||||
|
@Column({ length: 100, comment: '站点 SKU' })
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Product, product => product.siteSkus, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'productId' })
|
||||||
|
product: Product;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
productId: number;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
import { Area } from './area.entity';
|
import { Area } from './area.entity';
|
||||||
|
import { StockPoint } from './stock_point.entity';
|
||||||
|
|
||||||
@Entity('site')
|
@Entity('site')
|
||||||
export class Site {
|
export class Site {
|
||||||
|
|
@ -10,17 +11,20 @@ export class Site {
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
@Column({ length: 255, nullable: true })
|
||||||
consumerKey: string;
|
consumerKey?: string;
|
||||||
|
|
||||||
@Column({ length: 255, nullable: true })
|
@Column({ length: 255, nullable: true })
|
||||||
consumerSecret: string;
|
consumerSecret?: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
token: string;
|
token?: string;
|
||||||
|
|
||||||
@Column({ length: 255, unique: true })
|
@Column({ length: 255, unique: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@Column({ length: 255, nullable: true })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@Column({ length: 32, default: 'woocommerce' })
|
@Column({ length: 32, default: 'woocommerce' })
|
||||||
type: string; // 平台类型:woocommerce | shopyy
|
type: string; // 平台类型:woocommerce | shopyy
|
||||||
|
|
||||||
|
|
@ -33,4 +37,8 @@ export class Site {
|
||||||
@ManyToMany(() => Area)
|
@ManyToMany(() => Area)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
areas: Area[];
|
areas: Area[];
|
||||||
|
|
||||||
|
@ManyToMany(() => StockPoint, stockPoint => stockPoint.sites)
|
||||||
|
@JoinTable()
|
||||||
|
stockPoints: StockPoint[];
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Shipment } from './shipment.entity';
|
import { Shipment } from './shipment.entity';
|
||||||
import { Area } from './area.entity';
|
import { Area } from './area.entity';
|
||||||
|
import { Site } from './site.entity';
|
||||||
|
|
||||||
@Entity('stock_point')
|
@Entity('stock_point')
|
||||||
export class StockPoint extends BaseEntity {
|
export class StockPoint extends BaseEntity {
|
||||||
|
|
@ -54,7 +55,7 @@ export class StockPoint extends BaseEntity {
|
||||||
@Column({ default: 'uniuni' })
|
@Column({ default: 'uniuni' })
|
||||||
upStreamName: string;
|
upStreamName: string;
|
||||||
|
|
||||||
@Column()
|
@Column({ default: 0 })
|
||||||
upStreamStockPointId: number;
|
upStreamStockPointId: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
|
|
@ -79,4 +80,7 @@ export class StockPoint extends BaseEntity {
|
||||||
@ManyToMany(() => Area)
|
@ManyToMany(() => Area)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
areas: Area[];
|
areas: Area[];
|
||||||
|
|
||||||
|
@ManyToMany(() => Site, site => site.stockPoints)
|
||||||
|
sites: Site[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ export class Template {
|
||||||
@Column('text',{nullable: true,comment: "描述"})
|
@Column('text',{nullable: true,comment: "描述"})
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'string', nullable: true, description: '测试数据JSON' })
|
||||||
|
@Column('text', { nullable: true, comment: '测试数据JSON' })
|
||||||
|
testData?: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: true,
|
example: true,
|
||||||
description: '是否可删除',
|
description: '是否可删除',
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ export class User {
|
||||||
@Column({ type: 'simple-array', nullable: true })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
||||||
|
|
||||||
|
// 新增邮箱字段,可选且唯一
|
||||||
|
@Column({ unique: true, nullable: true })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
isSuper: boolean; // 超级管理员
|
isSuper: boolean; // 超级管理员
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,18 +101,4 @@ export class Variation {
|
||||||
})
|
})
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
description: '变体构成成分',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
sku: { type: 'string' },
|
|
||||||
quantity: { type: 'number' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@Column('json', { nullable: true, comment: '变体构成成分' })
|
|
||||||
constitution: { sku: string; quantity: number }[] | null;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export class WpProduct {
|
||||||
@Column({ type: 'int', nullable: true })
|
@Column({ type: 'int', nullable: true })
|
||||||
siteId: number;
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点信息', type: Site })
|
||||||
@ManyToOne(() => Site)
|
@ManyToOne(() => Site)
|
||||||
@JoinColumn({ name: 'siteId', referencedColumnName: 'id' })
|
@JoinColumn({ name: 'siteId', referencedColumnName: 'id' })
|
||||||
site: Site;
|
site: Site;
|
||||||
|
|
@ -223,18 +224,4 @@ export class WpProduct {
|
||||||
})
|
})
|
||||||
@UpdateDateColumn()
|
@UpdateDateColumn()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
description: '产品构成成分',
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
sku: { type: 'string' },
|
|
||||||
quantity: { type: 'number' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@Column('json', { nullable: true, comment: '产品构成成分' })
|
|
||||||
constitution: { sku: string; quantity: number }[] | null;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
// src/interface/platform.interface.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 电商平台抽象接口
|
||||||
|
* 定义所有平台必须实现的通用方法
|
||||||
|
*/
|
||||||
|
export interface IPlatformService {
|
||||||
|
/**
|
||||||
|
* 获取产品列表
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @returns 产品列表数据
|
||||||
|
*/
|
||||||
|
getProducts(site: any): Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品变体列表
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @returns 变体列表数据
|
||||||
|
*/
|
||||||
|
getVariations(site: any, productId: number): Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品变体详情
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param variationId 变体ID
|
||||||
|
* @returns 变体详情数据
|
||||||
|
*/
|
||||||
|
getVariation(site: any, productId: number, variationId: number): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单列表
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @returns 订单列表数据
|
||||||
|
*/
|
||||||
|
getOrders(siteId: number): Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单详情
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @returns 订单详情数据
|
||||||
|
*/
|
||||||
|
getOrder(siteId: string, orderId: string): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅列表(如果平台支持)
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @returns 订阅列表数据
|
||||||
|
*/
|
||||||
|
getSubscriptions?(siteId: number): Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建产品
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param data 产品数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
createProduct(site: any, data: any): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新产品
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
updateProduct(site: any, productId: string, data: any): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新产品状态
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param status 产品状态
|
||||||
|
* @param stockStatus 库存状态
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
updateProductStatus(site: any, productId: string, status: string, stockStatus: string): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新产品变体
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param variationId 变体ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
updateVariation(site: any, productId: string, variationId: string, data: any): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
updateOrder(site: any, orderId: string, data: Record<string, any>): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建物流信息
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param data 物流数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
createShipment(site: any, orderId: string, data: any): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除物流信息
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param trackingId 物流跟踪ID
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
deleteShipment(site: any, orderId: string, trackingId: string): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量处理产品
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @param data 批量操作数据
|
||||||
|
* @returns 处理结果
|
||||||
|
*/
|
||||||
|
batchProcessProducts(site: any, data: { create?: any[]; update?: any[]; delete?: any[] }): Promise<any>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import {
|
||||||
|
UnifiedMediaDTO,
|
||||||
|
UnifiedOrderDTO,
|
||||||
|
UnifiedPaginationDTO,
|
||||||
|
UnifiedProductDTO,
|
||||||
|
UnifiedSearchParamsDTO,
|
||||||
|
UnifiedSubscriptionDTO,
|
||||||
|
UnifiedCustomerDTO,
|
||||||
|
} from '../dto/site-api.dto';
|
||||||
|
|
||||||
|
export interface ISiteAdapter {
|
||||||
|
/**
|
||||||
|
* 获取产品列表
|
||||||
|
*/
|
||||||
|
getProducts(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedProductDTO>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个产品
|
||||||
|
*/
|
||||||
|
getProduct(id: string | number): Promise<UnifiedProductDTO>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单列表
|
||||||
|
*/
|
||||||
|
getOrders(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedOrderDTO>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个订单
|
||||||
|
*/
|
||||||
|
getOrder(id: string | number): Promise<UnifiedOrderDTO>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅列表
|
||||||
|
*/
|
||||||
|
getSubscriptions(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取媒体列表
|
||||||
|
*/
|
||||||
|
getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建产品
|
||||||
|
*/
|
||||||
|
createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新产品
|
||||||
|
*/
|
||||||
|
updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新产品变体
|
||||||
|
*/
|
||||||
|
updateVariation(productId: string | number, variationId: string | number, data: any): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单备注
|
||||||
|
*/
|
||||||
|
getOrderNotes(orderId: string | number): Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单备注
|
||||||
|
*/
|
||||||
|
createOrderNote(orderId: string | number, data: any): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除产品
|
||||||
|
*/
|
||||||
|
deleteProduct(id: string | number): Promise<boolean>;
|
||||||
|
|
||||||
|
batchProcessProducts?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
|
||||||
|
|
||||||
|
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
|
||||||
|
updateOrder(id: string | number, data: Partial<UnifiedOrderDTO>): Promise<boolean>;
|
||||||
|
deleteOrder(id: string | number): Promise<boolean>;
|
||||||
|
|
||||||
|
batchProcessOrders?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
|
||||||
|
|
||||||
|
getCustomers(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedCustomerDTO>>;
|
||||||
|
getCustomer(id: string | number): Promise<UnifiedCustomerDTO>;
|
||||||
|
createCustomer(data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
||||||
|
updateCustomer(id: string | number, data: Partial<UnifiedCustomerDTO>): Promise<UnifiedCustomerDTO>;
|
||||||
|
deleteCustomer(id: string | number): Promise<boolean>;
|
||||||
|
|
||||||
|
batchProcessCustomers?(data: { create?: any[]; update?: any[]; delete?: Array<string | number> }): Promise<any>;
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1 @@
|
||||||
import { FORMAT, ILogger, Logger } from '@midwayjs/core';
|
export {}
|
||||||
import { IJob, Job } from '@midwayjs/cron';
|
|
||||||
|
|
||||||
@Job({
|
|
||||||
cronTime: FORMAT.CRONTAB.EVERY_DAY,
|
|
||||||
runOnInit: true,
|
|
||||||
})
|
|
||||||
export class SyncProductJob implements IJob {
|
|
||||||
@Logger()
|
|
||||||
logger: ILogger;
|
|
||||||
|
|
||||||
onTick() {
|
|
||||||
}
|
|
||||||
onComplete?(result: any) {}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export class DictService {
|
||||||
|
|
||||||
// 生成并返回字典项的XLSX模板
|
// 生成并返回字典项的XLSX模板
|
||||||
getDictItemXLSXTemplate() {
|
getDictItemXLSXTemplate() {
|
||||||
const headers = ['name', 'title', 'titleCN', 'value', 'sort'];
|
const headers = ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'];
|
||||||
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
const ws = xlsx.utils.aoa_to_sheet([headers]);
|
||||||
const wb = xlsx.utils.book_new();
|
const wb = xlsx.utils.book_new();
|
||||||
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
||||||
|
|
@ -78,7 +78,7 @@ export class DictService {
|
||||||
const wsname = wb.SheetNames[0];
|
const wsname = wb.SheetNames[0];
|
||||||
const ws = wb.Sheets[wsname];
|
const ws = wb.Sheets[wsname];
|
||||||
// 支持titleCN字段的导入
|
// 支持titleCN字段的导入
|
||||||
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort'] }).slice(1);
|
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'] }).slice(1);
|
||||||
|
|
||||||
const items = data.map((row: any) => {
|
const items = data.map((row: any) => {
|
||||||
const item = new DictItem();
|
const item = new DictItem();
|
||||||
|
|
@ -86,6 +86,8 @@ export class DictService {
|
||||||
item.title = row.title;
|
item.title = row.title;
|
||||||
item.titleCN = row.titleCN; // 保存中文名称
|
item.titleCN = row.titleCN; // 保存中文名称
|
||||||
item.value = row.value;
|
item.value = row.value;
|
||||||
|
item.image = row.image;
|
||||||
|
item.shortName = row.shortName;
|
||||||
item.sort = row.sort || 0;
|
item.sort = row.sort || 0;
|
||||||
item.dict = dict;
|
item.dict = dict;
|
||||||
return item;
|
return item;
|
||||||
|
|
@ -168,6 +170,8 @@ export class DictService {
|
||||||
item.name = this.formatName(createDictItemDTO.name);
|
item.name = this.formatName(createDictItemDTO.name);
|
||||||
item.title = createDictItemDTO.title;
|
item.title = createDictItemDTO.title;
|
||||||
item.titleCN = createDictItemDTO.titleCN; // 保存中文名称
|
item.titleCN = createDictItemDTO.titleCN; // 保存中文名称
|
||||||
|
item.image = createDictItemDTO.image;
|
||||||
|
item.shortName = createDictItemDTO.shortName;
|
||||||
item.dict = dict;
|
item.dict = dict;
|
||||||
return this.dictItemModel.save(item);
|
return this.dictItemModel.save(item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ 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';
|
||||||
import { OrderItemOriginal } from '../entity/order_item_original.entity';
|
import { OrderItemOriginal } from '../entity/order_item_original.entity';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
@Provide()
|
@Provide()
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
|
|
||||||
|
|
@ -104,15 +106,35 @@ export class OrderService {
|
||||||
async syncOrders(siteId: number) {
|
async syncOrders(siteId: number) {
|
||||||
// 调用 WooCommerce API 获取订单
|
// 调用 WooCommerce API 获取订单
|
||||||
const orders = await this.wpService.getOrders(siteId);
|
const orders = await this.wpService.getOrders(siteId);
|
||||||
|
let successCount = 0;
|
||||||
|
let failureCount = 0;
|
||||||
for (const order of orders) {
|
for (const order of orders) {
|
||||||
|
try {
|
||||||
await this.syncSingleOrder(siteId, order);
|
await this.syncSingleOrder(siteId, order);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`同步订单 ${order.id} 失败:`, error);
|
||||||
|
failureCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
success: failureCount === 0,
|
||||||
|
successCount,
|
||||||
|
failureCount,
|
||||||
|
message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async syncOrderById(siteId: number, orderId: string) {
|
async syncOrderById(siteId: number, orderId: string) {
|
||||||
|
try {
|
||||||
// 调用 WooCommerce API 获取订单
|
// 调用 WooCommerce API 获取订单
|
||||||
const order = await this.wpService.getOrder(String(siteId), orderId);
|
const order = await this.wpService.getOrder(String(siteId), orderId);
|
||||||
await this.syncSingleOrder(siteId, order, true);
|
await this.syncSingleOrder(siteId, order, true);
|
||||||
|
return { success: true, message: '同步成功' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`同步订单 ${orderId} 失败:`, error);
|
||||||
|
return { success: false, message: `同步失败: ${error.message}` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 订单状态切换表
|
// 订单状态切换表
|
||||||
orderAutoNextStatusMap = {
|
orderAutoNextStatusMap = {
|
||||||
|
|
@ -397,40 +419,52 @@ export class OrderService {
|
||||||
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
|
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
|
||||||
}
|
}
|
||||||
if (!orderItem.sku) return;
|
if (!orderItem.sku) return;
|
||||||
let constitution;
|
const product = await this.productModel.findOne({
|
||||||
if (orderItem.externalVariationId === '0') {
|
|
||||||
const product = await this.wpProductModel.findOne({
|
|
||||||
where: { sku: orderItem.sku },
|
where: { sku: orderItem.sku },
|
||||||
|
relations: ['components'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!product) return;
|
if (!product) return;
|
||||||
constitution = product?.constitution;
|
|
||||||
} else {
|
|
||||||
const variation = await this.variationModel.findOne({
|
|
||||||
where: { sku: orderItem.sku },
|
|
||||||
});
|
|
||||||
if (!variation) return;
|
|
||||||
constitution = variation?.constitution;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(constitution)) return;
|
|
||||||
const orderSales: OrderSale[] = [];
|
const orderSales: OrderSale[] = [];
|
||||||
for (const item of constitution) {
|
|
||||||
|
if (product.components && product.components.length > 0) {
|
||||||
|
for (const comp of product.components) {
|
||||||
const baseProduct = await this.productModel.findOne({
|
const baseProduct = await this.productModel.findOne({
|
||||||
where: { sku: item.sku },
|
where: { sku: comp.sku },
|
||||||
});
|
});
|
||||||
|
if (baseProduct) {
|
||||||
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
||||||
orderId: orderItem.orderId,
|
orderId: orderItem.orderId,
|
||||||
siteId: orderItem.siteId,
|
siteId: orderItem.siteId,
|
||||||
externalOrderItemId: orderItem.externalOrderItemId,
|
externalOrderItemId: orderItem.externalOrderItemId,
|
||||||
productId: baseProduct.id,
|
productId: baseProduct.id,
|
||||||
name: baseProduct.name,
|
name: baseProduct.name,
|
||||||
quantity: item.quantity * orderItem.quantity,
|
quantity: comp.quantity * orderItem.quantity,
|
||||||
sku: item.sku,
|
sku: comp.sku,
|
||||||
isPackage: orderItem.name.toLowerCase().includes('package'),
|
isPackage: orderItem.name.toLowerCase().includes('package'),
|
||||||
});
|
});
|
||||||
orderSales.push(orderSaleItem);
|
orderSales.push(orderSaleItem);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
||||||
|
orderId: orderItem.orderId,
|
||||||
|
siteId: orderItem.siteId,
|
||||||
|
externalOrderItemId: orderItem.externalOrderItemId,
|
||||||
|
productId: product.id,
|
||||||
|
name: product.name,
|
||||||
|
quantity: orderItem.quantity,
|
||||||
|
sku: product.sku,
|
||||||
|
isPackage: orderItem.name.toLowerCase().includes('package'),
|
||||||
|
});
|
||||||
|
orderSales.push(orderSaleItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderSales.length > 0) {
|
||||||
await this.orderSaleModel.save(orderSales);
|
await this.orderSaleModel.save(orderSales);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async saveOrderRefunds({
|
async saveOrderRefunds({
|
||||||
siteId,
|
siteId,
|
||||||
|
|
@ -1666,4 +1700,201 @@ export class OrderService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportOrder(ids: number[]) {
|
||||||
|
// 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号
|
||||||
|
interface ExportData {
|
||||||
|
'日期': string;
|
||||||
|
'订单号': string;
|
||||||
|
'姓名地址': string;
|
||||||
|
'邮箱': string;
|
||||||
|
'号码': string;
|
||||||
|
'订单内容': string;
|
||||||
|
'盒数': number;
|
||||||
|
'换盒数': number;
|
||||||
|
'换货内容': string;
|
||||||
|
'快递号': string;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 空值检查
|
||||||
|
const dataSource = this.dataSourceManager.getDataSource('default');
|
||||||
|
|
||||||
|
// 优化事务使用
|
||||||
|
return await dataSource.transaction(async manager => {
|
||||||
|
// 准备查询条件
|
||||||
|
const whereCondition: any = {};
|
||||||
|
if (ids && ids.length > 0) {
|
||||||
|
whereCondition.id = In(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订单、订单项和物流信息
|
||||||
|
const orders = await manager.getRepository(Order).find({
|
||||||
|
where: whereCondition,
|
||||||
|
relations: ['shipment']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orders.length === 0) {
|
||||||
|
throw new Error('未找到匹配的订单');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有订单ID
|
||||||
|
const orderIds = orders.map(order => order.id);
|
||||||
|
|
||||||
|
// 获取所有订单项
|
||||||
|
const orderItems = await manager.getRepository(OrderItem).find({
|
||||||
|
where: {
|
||||||
|
orderId: In(orderIds)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按订单ID分组订单项
|
||||||
|
const orderItemsByOrderId = orderItems.reduce((acc, item) => {
|
||||||
|
if (!acc[item.orderId]) {
|
||||||
|
acc[item.orderId] = [];
|
||||||
|
}
|
||||||
|
acc[item.orderId].push(item);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, OrderItem[]>);
|
||||||
|
|
||||||
|
// 构建导出数据
|
||||||
|
const exportDataList: ExportData[] = orders.map(order => {
|
||||||
|
// 获取订单的订单项
|
||||||
|
const items = orderItemsByOrderId[order.id] || [];
|
||||||
|
|
||||||
|
// 计算总盒数
|
||||||
|
const boxCount = items.reduce((total, item) => total + item.quantity, 0);
|
||||||
|
|
||||||
|
// 构建订单内容
|
||||||
|
const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
|
||||||
|
|
||||||
|
// 构建姓名地址
|
||||||
|
const shipping = order.shipping;
|
||||||
|
const billing = order.billing;
|
||||||
|
const firstName = shipping?.first_name || billing?.first_name || '';
|
||||||
|
const lastName = shipping?.last_name || billing?.last_name || '';
|
||||||
|
const name = `${firstName} ${lastName}`.trim() || '';
|
||||||
|
const address = shipping?.address_1 || billing?.address_1 || '';
|
||||||
|
const address2 = shipping?.address_2 || billing?.address_2 || '';
|
||||||
|
const city = shipping?.city || billing?.city || '';
|
||||||
|
const state = shipping?.state || billing?.state || '';
|
||||||
|
const postcode = shipping?.postcode || billing?.postcode || '';
|
||||||
|
const country = shipping?.country || billing?.country || '';
|
||||||
|
const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`;
|
||||||
|
|
||||||
|
// 获取电话号码
|
||||||
|
const phone = shipping?.phone || billing?.phone || '';
|
||||||
|
|
||||||
|
// 获取快递号
|
||||||
|
const trackingNumber = order.shipment?.tracking_id || '';
|
||||||
|
|
||||||
|
// 暂时没有换货相关数据,默认为0和空字符串
|
||||||
|
const exchangeBoxCount = 0;
|
||||||
|
const exchangeContent = '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
'日期': order.date_created?.toISOString().split('T')[0] || '',
|
||||||
|
'订单号': order.externalOrderId || '',
|
||||||
|
'姓名地址': nameAddress,
|
||||||
|
'邮箱': order.customer_email || '',
|
||||||
|
'号码': phone,
|
||||||
|
'订单内容': orderContent,
|
||||||
|
'盒数': boxCount,
|
||||||
|
'换盒数': exchangeBoxCount,
|
||||||
|
'换货内容': exchangeContent,
|
||||||
|
'快递号': trackingNumber
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回CSV字符串内容给前端
|
||||||
|
const csvContent = await this.exportToCsv(exportDataList, { type: 'string' });
|
||||||
|
return csvContent;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`导出订单失败:${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出数据为CSV格式
|
||||||
|
* @param {any[]} data 数据数组
|
||||||
|
* @param {Object} options 配置选项
|
||||||
|
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
||||||
|
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
||||||
|
* @param {boolean} [options.writeFile=false] 是否写入文件
|
||||||
|
* @returns {string|Buffer} 根据type返回字符串或Buffer
|
||||||
|
*/
|
||||||
|
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
|
||||||
|
try {
|
||||||
|
// 检查数据是否为空
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
throw new Error('导出数据不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type = 'string', fileName, writeFile = false } = options;
|
||||||
|
|
||||||
|
// 生成表头
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
let csvContent = headers.join(',') + '\n';
|
||||||
|
|
||||||
|
// 处理数据行
|
||||||
|
data.forEach(item => {
|
||||||
|
const row = headers.map(key => {
|
||||||
|
const value = item[key as keyof any];
|
||||||
|
// 处理特殊字符
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// 转义双引号,将"替换为""
|
||||||
|
const escapedValue = value.replace(/"/g, '""');
|
||||||
|
// 如果包含逗号或换行符,需要用双引号包裹
|
||||||
|
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
|
||||||
|
return `"${escapedValue}"`;
|
||||||
|
}
|
||||||
|
return escapedValue;
|
||||||
|
}
|
||||||
|
// 处理日期类型
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return value.toISOString();
|
||||||
|
}
|
||||||
|
// 处理undefined和null
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}).join(',');
|
||||||
|
csvContent += row + '\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果需要写入文件
|
||||||
|
if (writeFile && fileName) {
|
||||||
|
// 获取当前用户目录
|
||||||
|
const userHomeDir = os.homedir();
|
||||||
|
|
||||||
|
// 构建目标路径(下载目录)
|
||||||
|
const downloadsDir = path.join(userHomeDir, 'Downloads');
|
||||||
|
|
||||||
|
// 确保下载目录存在
|
||||||
|
if (!fs.existsSync(downloadsDir)) {
|
||||||
|
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(downloadsDir, fileName);
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
fs.writeFileSync(filePath, csvContent, 'utf8');
|
||||||
|
|
||||||
|
console.log(`数据已成功导出至 ${filePath}`);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据类型返回不同结果
|
||||||
|
if (type === 'buffer') {
|
||||||
|
return Buffer.from(csvContent, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return csvContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导出CSV时出错:', error);
|
||||||
|
throw new Error(`导出CSV文件失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// src/service/platform.factory.ts
|
||||||
|
|
||||||
|
import { Provide, Scope, ScopeEnum, Inject } from '@midwayjs/core';
|
||||||
|
import { Site } from '../entity/site.entity';
|
||||||
|
import { IPlatformService } from '../interface/platform.interface';
|
||||||
|
import { WPService } from './wp.service';
|
||||||
|
import { ShopyyService } from './shopyy.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台服务工厂
|
||||||
|
* 根据站点类型创建对应的平台服务实例
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
export class PlatformFactory {
|
||||||
|
@Inject()
|
||||||
|
wpService: WPService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
shopyyService: ShopyyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据站点类型创建对应的平台服务实例
|
||||||
|
* @param site 站点配置信息
|
||||||
|
* @returns 平台服务实例
|
||||||
|
*/
|
||||||
|
createPlatformService(site: Site): IPlatformService {
|
||||||
|
switch (site.type) {
|
||||||
|
case 'woocommerce':
|
||||||
|
return this.wpService;
|
||||||
|
case 'shopyy':
|
||||||
|
return this.shopyyService;
|
||||||
|
case 'amazon':
|
||||||
|
// 这里需要引入并返回AmazonService实例
|
||||||
|
// 目前先返回WPService作为占位
|
||||||
|
return this.wpService;
|
||||||
|
default:
|
||||||
|
throw new Error(`不支持的平台类型: ${site.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { Inject, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import * as fs from 'fs';
|
||||||
import { In, Like, Not, Repository } from 'typeorm';
|
import { In, Like, Not, Repository } from 'typeorm';
|
||||||
import { Product } from '../entity/product.entity';
|
import { Product } from '../entity/product.entity';
|
||||||
import { paginate } from '../utils/paginate.util';
|
import { paginate } from '../utils/paginate.util';
|
||||||
import { PaginationParams } from '../interface';
|
import { PaginationParams } from '../interface';
|
||||||
|
import { parse } from 'csv-parse';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateProductDTO,
|
CreateProductDTO,
|
||||||
UpdateProductDTO,
|
UpdateProductDTO,
|
||||||
|
BatchUpdateProductDTO,
|
||||||
} from '../dto/product.dto';
|
} from '../dto/product.dto';
|
||||||
import {
|
import {
|
||||||
BrandPaginatedResponse,
|
BrandPaginatedResponse,
|
||||||
|
|
@ -25,6 +29,7 @@ import { StockService } from './stock.service';
|
||||||
import { Stock } from '../entity/stock.entity';
|
import { Stock } from '../entity/stock.entity';
|
||||||
import { StockPoint } from '../entity/stock_point.entity';
|
import { StockPoint } from '../entity/stock_point.entity';
|
||||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
|
import { ProductSiteSku } from '../entity/product_site_sku.entity';
|
||||||
import { Category } from '../entity/category.entity';
|
import { Category } from '../entity/category.entity';
|
||||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||||
|
|
||||||
|
|
@ -63,6 +68,9 @@ export class ProductService {
|
||||||
@InjectEntityModel(ProductStockComponent)
|
@InjectEntityModel(ProductStockComponent)
|
||||||
productStockComponentModel: Repository<ProductStockComponent>;
|
productStockComponentModel: Repository<ProductStockComponent>;
|
||||||
|
|
||||||
|
@InjectEntityModel(ProductSiteSku)
|
||||||
|
productSiteSkuModel: Repository<ProductSiteSku>;
|
||||||
|
|
||||||
@InjectEntityModel(Category)
|
@InjectEntityModel(Category)
|
||||||
categoryModel: Repository<Category>;
|
categoryModel: Repository<Category>;
|
||||||
|
|
||||||
|
|
@ -238,7 +246,8 @@ export class ProductService {
|
||||||
.createQueryBuilder('product')
|
.createQueryBuilder('product')
|
||||||
.leftJoinAndSelect('product.attributes', 'attribute')
|
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||||
.leftJoinAndSelect('attribute.dict', 'dict')
|
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||||
.leftJoinAndSelect('product.category', 'category');
|
.leftJoinAndSelect('product.category', 'category')
|
||||||
|
.leftJoinAndSelect('product.siteSkus', 'siteSku');
|
||||||
|
|
||||||
// 模糊搜索 name,支持多个关键词
|
// 模糊搜索 name,支持多个关键词
|
||||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
|
|
@ -362,7 +371,10 @@ export class ProductService {
|
||||||
|
|
||||||
// 如果提供了 categoryId,设置分类
|
// 如果提供了 categoryId,设置分类
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
categoryItem = await this.categoryModel.findOne({ where: { id: categoryId } });
|
categoryItem = await this.categoryModel.findOne({
|
||||||
|
where: { id: categoryId },
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
|
});
|
||||||
if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`);
|
if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,16 +382,23 @@ export class ProductService {
|
||||||
// 如果属性是分类,特殊处理
|
// 如果属性是分类,特殊处理
|
||||||
if (attr.dictName === 'category') {
|
if (attr.dictName === 'category') {
|
||||||
if (attr.id) {
|
if (attr.id) {
|
||||||
categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
|
categoryItem = await this.categoryModel.findOne({
|
||||||
|
where: { id: attr.id },
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
|
});
|
||||||
} else if (attr.name) {
|
} else if (attr.name) {
|
||||||
categoryItem = await this.categoryModel.findOneBy({ name: attr.name });
|
categoryItem = await this.categoryModel.findOne({
|
||||||
|
where: { name: attr.name },
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
|
});
|
||||||
} else if (attr.title) {
|
} else if (attr.title) {
|
||||||
// 尝试用 title 匹配 name 或 title
|
// 尝试用 title 匹配 name 或 title
|
||||||
categoryItem = await this.categoryModel.findOne({
|
categoryItem = await this.categoryModel.findOne({
|
||||||
where: [
|
where: [
|
||||||
{ name: attr.title },
|
{ name: attr.title },
|
||||||
{ title: attr.title }
|
{ title: attr.title }
|
||||||
]
|
],
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -411,13 +430,13 @@ export class ProductService {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const isExist = await qb.getOne();
|
const isExist = await qb.getOne();
|
||||||
if (isExist) throw new Error('产品已存在');
|
if (isExist) throw new Error('相同产品属性的产品已存在');
|
||||||
|
|
||||||
// 创建新产品实例(绑定属性与基础字段)
|
// 创建新产品实例(绑定属性与基础字段)
|
||||||
const product = new Product();
|
const product = new Product();
|
||||||
|
|
||||||
// 使用 merge 填充基础字段,排除特殊处理字段
|
// 使用 merge 填充基础字段,排除特殊处理字段
|
||||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = createProductDTO;
|
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = createProductDTO;
|
||||||
this.productModel.merge(product, simpleFields);
|
this.productModel.merge(product, simpleFields);
|
||||||
|
|
||||||
product.attributes = resolvedAttributes;
|
product.attributes = resolvedAttributes;
|
||||||
|
|
@ -431,19 +450,30 @@ export class ProductService {
|
||||||
if (sku) {
|
if (sku) {
|
||||||
product.sku = sku;
|
product.sku = sku;
|
||||||
} else {
|
} else {
|
||||||
const attributeMap: Record<string, string> = {};
|
product.sku = await this.templateService.render('product.sku', product);
|
||||||
for (const a of resolvedAttributes) {
|
|
||||||
if (a?.dict?.name && a?.name) attributeMap[a.dict.name] = a.name;
|
|
||||||
}
|
|
||||||
product.sku = await this.templateService.render('product_sku', {
|
|
||||||
brand: attributeMap['brand'] || '',
|
|
||||||
flavor: attributeMap['flavor'] || '',
|
|
||||||
strength: attributeMap['strength'] || '',
|
|
||||||
humidity: attributeMap['humidity'] || '',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.productModel.save(product);
|
const savedProduct = await this.productModel.save(product);
|
||||||
|
|
||||||
|
// 保存站点 SKU 列表
|
||||||
|
if (createProductDTO.siteSkus && createProductDTO.siteSkus.length > 0) {
|
||||||
|
const siteSkus = createProductDTO.siteSkus.map(code => {
|
||||||
|
const s = new ProductSiteSku();
|
||||||
|
s.code = code;
|
||||||
|
s.product = savedProduct;
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
await this.productSiteSkuModel.save(siteSkus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存组件信息
|
||||||
|
if (createProductDTO.components && createProductDTO.components.length > 0) {
|
||||||
|
await this.setProductComponents(savedProduct.id, createProductDTO.components);
|
||||||
|
// 重新加载带组件的产品
|
||||||
|
return await this.productModel.findOne({ where: { id: savedProduct.id }, relations: ['attributes', 'attributes.dict', 'category', 'components', 'siteSkus'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedProduct;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProduct(
|
async updateProduct(
|
||||||
|
|
@ -457,7 +487,7 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 merge 更新基础字段,排除特殊处理字段
|
// 使用 merge 更新基础字段,排除特殊处理字段
|
||||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, ...simpleFields } = updateProductDTO;
|
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = updateProductDTO;
|
||||||
this.productModel.merge(product, simpleFields);
|
this.productModel.merge(product, simpleFields);
|
||||||
|
|
||||||
// 处理分类更新
|
// 处理分类更新
|
||||||
|
|
@ -472,6 +502,23 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理站点 SKU 更新
|
||||||
|
if (updateProductDTO.siteSkus !== undefined) {
|
||||||
|
// 删除旧的 siteSkus
|
||||||
|
await this.productSiteSkuModel.delete({ productId: id });
|
||||||
|
|
||||||
|
// 如果有新的 siteSkus,则保存
|
||||||
|
if (updateProductDTO.siteSkus.length > 0) {
|
||||||
|
const siteSkus = updateProductDTO.siteSkus.map(code => {
|
||||||
|
const s = new ProductSiteSku();
|
||||||
|
s.code = code;
|
||||||
|
s.productId = id;
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
await this.productSiteSkuModel.save(siteSkus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理 SKU 更新
|
// 处理 SKU 更新
|
||||||
if (updateProductDTO.sku !== undefined) {
|
if (updateProductDTO.sku !== undefined) {
|
||||||
// 校验 SKU 唯一性(如变更)
|
// 校验 SKU 唯一性(如变更)
|
||||||
|
|
@ -532,9 +579,82 @@ export class ProductService {
|
||||||
|
|
||||||
// 保存更新后的产品
|
// 保存更新后的产品
|
||||||
const saved = await this.productModel.save(product);
|
const saved = await this.productModel.save(product);
|
||||||
|
|
||||||
|
// 处理组件更新
|
||||||
|
if (updateProductDTO.components !== undefined) {
|
||||||
|
// 如果 components 为空数组,则删除所有组件? setProductComponents 会处理
|
||||||
|
await this.setProductComponents(saved.id, updateProductDTO.components);
|
||||||
|
}
|
||||||
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async batchUpdateProduct(
|
||||||
|
batchUpdateProductDTO: BatchUpdateProductDTO
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { ids, ...updateData } = batchUpdateProductDTO;
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
throw new Error('未选择任何产品');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 updateData 中是否有复杂字段 (attributes, categoryId, type, sku)
|
||||||
|
// 如果包含复杂字段,需要复用 updateProduct 的逻辑
|
||||||
|
const hasComplexFields =
|
||||||
|
updateData.attributes !== undefined ||
|
||||||
|
updateData.categoryId !== undefined ||
|
||||||
|
updateData.type !== undefined ||
|
||||||
|
updateData.sku !== undefined;
|
||||||
|
|
||||||
|
if (hasComplexFields) {
|
||||||
|
// 循环调用 updateProduct
|
||||||
|
for (const id of ids) {
|
||||||
|
const updateDTO = new UpdateProductDTO();
|
||||||
|
// 复制属性
|
||||||
|
Object.assign(updateDTO, updateData);
|
||||||
|
await this.updateProduct(id, updateDTO);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 简单字段,直接批量更新以提高性能
|
||||||
|
// UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice
|
||||||
|
|
||||||
|
const simpleUpdate: any = {};
|
||||||
|
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
||||||
|
if (updateData.nameCn !== undefined) simpleUpdate.nameCn = updateData.nameCn;
|
||||||
|
if (updateData.description !== undefined) simpleUpdate.description = updateData.description;
|
||||||
|
if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription;
|
||||||
|
if (updateData.price !== undefined) simpleUpdate.price = updateData.price;
|
||||||
|
if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice;
|
||||||
|
|
||||||
|
if (Object.keys(simpleUpdate).length > 0) {
|
||||||
|
await this.productModel.update({ id: In(ids) }, simpleUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchDeleteProduct(ids: number[]): Promise<{ success: number; failed: number; errors: string[] }> {
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
throw new Error('未选择任何产品');
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
try {
|
||||||
|
await this.deleteProduct(id);
|
||||||
|
success++;
|
||||||
|
} catch (error) {
|
||||||
|
failed++;
|
||||||
|
errors.push(`ID ${id}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success, failed, errors };
|
||||||
|
}
|
||||||
|
|
||||||
// 获取产品的库存组成列表(表关联版本)
|
// 获取产品的库存组成列表(表关联版本)
|
||||||
async getProductComponents(productId: number): Promise<any[]> {
|
async getProductComponents(productId: number): Promise<any[]> {
|
||||||
// 条件判断:确保产品存在
|
// 条件判断:确保产品存在
|
||||||
|
|
@ -609,7 +729,9 @@ export class ProductService {
|
||||||
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
// 条件判断(单品 simple 不允许手动设置组成)
|
// 条件判断(单品 simple 不允许手动设置组成)
|
||||||
if (product.type === 'single') {
|
if (product.type === 'single') {
|
||||||
throw new Error('单品无需设置组成');
|
// 单品类型,直接清空关联的组成(如果有)
|
||||||
|
await this.productStockComponentModel.delete({ productId });
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const validItems = (items || [])
|
const validItems = (items || [])
|
||||||
|
|
@ -661,6 +783,41 @@ export class ProductService {
|
||||||
return await this.getProductComponents(productId);
|
return await this.getProductComponents(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 站点SKU绑定:覆盖式绑定一组站点SKU到产品
|
||||||
|
async bindSiteSkus(productId: number, codes: string[]): Promise<ProductSiteSku[]> {
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
const normalized = (codes || [])
|
||||||
|
.map(c => String(c).trim())
|
||||||
|
.filter(c => c.length > 0);
|
||||||
|
await this.productSiteSkuModel.delete({ productId });
|
||||||
|
if (normalized.length === 0) return [];
|
||||||
|
const entities = normalized.map(code => {
|
||||||
|
const e = new ProductSiteSku();
|
||||||
|
e.productId = productId;
|
||||||
|
e.code = code;
|
||||||
|
return e;
|
||||||
|
});
|
||||||
|
return await this.productSiteSkuModel.save(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点SKU绑定:按单个 code 绑定到指定产品(若已有则更新归属)
|
||||||
|
async bindProductBySiteSku(code: string, productId: number): Promise<ProductSiteSku> {
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
const skuCode = String(code || '').trim();
|
||||||
|
if (!skuCode) throw new Error('站点SKU不能为空');
|
||||||
|
const existing = await this.productSiteSkuModel.findOne({ where: { code: skuCode } });
|
||||||
|
if (existing) {
|
||||||
|
existing.productId = productId;
|
||||||
|
return await this.productSiteSkuModel.save(existing);
|
||||||
|
}
|
||||||
|
const e = new ProductSiteSku();
|
||||||
|
e.productId = productId;
|
||||||
|
e.code = skuCode;
|
||||||
|
return await this.productSiteSkuModel.save(e);
|
||||||
|
}
|
||||||
|
|
||||||
// 重复定义的 getProductList 已合并到前面的实现(移除重复)
|
// 重复定义的 getProductList 已合并到前面的实现(移除重复)
|
||||||
|
|
||||||
async updatenameCn(id: number, nameCn: string): Promise<Product> {
|
async updatenameCn(id: number, nameCn: string): Promise<Product> {
|
||||||
|
|
@ -681,30 +838,8 @@ export class ProductService {
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new Error(`产品 ID ${id} 不存在`);
|
throw new Error(`产品 ID ${id} 不存在`);
|
||||||
}
|
}
|
||||||
const sku = product.sku;
|
|
||||||
|
|
||||||
// 查询 wp_product 表中是否存在与该 SKU 关联的产品
|
// 不再阻塞于远端站点商品/变体的存在,删除仅按本地引用保护
|
||||||
const wpProduct = await this.wpProductModel
|
|
||||||
.createQueryBuilder('wp_product')
|
|
||||||
.where('JSON_CONTAINS(wp_product.constitution, :sku)', {
|
|
||||||
sku: JSON.stringify({ sku: sku }),
|
|
||||||
})
|
|
||||||
.getOne();
|
|
||||||
if (wpProduct) {
|
|
||||||
throw new Error('无法删除,请先删除关联的WP产品');
|
|
||||||
}
|
|
||||||
|
|
||||||
const variation = await this.variationModel
|
|
||||||
.createQueryBuilder('variation')
|
|
||||||
.where('JSON_CONTAINS(variation.constitution, :sku)', {
|
|
||||||
sku: JSON.stringify({ sku: sku }),
|
|
||||||
})
|
|
||||||
.getOne();
|
|
||||||
|
|
||||||
if (variation) {
|
|
||||||
console.log(variation);
|
|
||||||
throw new Error('无法删除,请先删除关联的WP变体');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除产品
|
// 删除产品
|
||||||
const result = await this.productModel.delete(id);
|
const result = await this.productModel.delete(id);
|
||||||
|
|
@ -1069,7 +1204,7 @@ export class ProductService {
|
||||||
// 通用属性:创建字典项
|
// 通用属性:创建字典项
|
||||||
async createAttribute(
|
async createAttribute(
|
||||||
dictName: string,
|
dictName: string,
|
||||||
payload: { title: string; name: string }
|
payload: { title: string; name: string; image?: string; shortName?: string }
|
||||||
): Promise<DictItem> {
|
): Promise<DictItem> {
|
||||||
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
const dict = await this.dictModel.findOne({ where: { name: dictName } });
|
||||||
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
|
if (!dict) throw new Error(`字典 ${dictName} 不存在`);
|
||||||
|
|
@ -1081,6 +1216,8 @@ export class ProductService {
|
||||||
const item = new DictItem();
|
const item = new DictItem();
|
||||||
item.title = payload.title;
|
item.title = payload.title;
|
||||||
item.name = payload.name;
|
item.name = payload.name;
|
||||||
|
item.image = payload.image;
|
||||||
|
item.shortName = payload.shortName;
|
||||||
item.dict = dict;
|
item.dict = dict;
|
||||||
return await this.dictItemModel.save(item);
|
return await this.dictItemModel.save(item);
|
||||||
}
|
}
|
||||||
|
|
@ -1088,12 +1225,14 @@ export class ProductService {
|
||||||
// 通用属性:更新字典项
|
// 通用属性:更新字典项
|
||||||
async updateAttribute(
|
async updateAttribute(
|
||||||
id: number,
|
id: number,
|
||||||
payload: { title?: string; name?: string }
|
payload: { title?: string; name?: string; image?: string; shortName?: string }
|
||||||
): Promise<DictItem> {
|
): Promise<DictItem> {
|
||||||
const item = await this.dictItemModel.findOne({ where: { id } });
|
const item = await this.dictItemModel.findOne({ where: { id } });
|
||||||
if (!item) throw new Error('字典项不存在');
|
if (!item) throw new Error('字典项不存在');
|
||||||
if (payload.title !== undefined) item.title = payload.title;
|
if (payload.title !== undefined) item.title = payload.title;
|
||||||
if (payload.name !== undefined) item.name = payload.name;
|
if (payload.name !== undefined) item.name = payload.name;
|
||||||
|
if (payload.image !== undefined) item.image = payload.image;
|
||||||
|
if (payload.shortName !== undefined) item.shortName = payload.shortName;
|
||||||
return await this.dictItemModel.save(item);
|
return await this.dictItemModel.save(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1190,26 +1329,6 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析组件信息 (component_*)
|
|
||||||
const componentsMap = new Map<string, { sku?: string; quantity?: number }>();
|
|
||||||
for (const key of Object.keys(rec)) {
|
|
||||||
const skuMatch = key.match(/^component_(\d+)_sku$/);
|
|
||||||
if (skuMatch) {
|
|
||||||
const idx = skuMatch[1];
|
|
||||||
if (!componentsMap.has(idx)) componentsMap.set(idx, {});
|
|
||||||
componentsMap.get(idx)!.sku = rec[key];
|
|
||||||
}
|
|
||||||
const qtyMatch = key.match(/^component_(\d+)_quantity$/);
|
|
||||||
if (qtyMatch) {
|
|
||||||
const idx = qtyMatch[1];
|
|
||||||
if (!componentsMap.has(idx)) componentsMap.set(idx, {});
|
|
||||||
componentsMap.get(idx)!.quantity = Number(rec[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const components = Array.from(componentsMap.values())
|
|
||||||
.filter(c => c.sku && c.quantity)
|
|
||||||
.map(c => ({ sku: c.sku!, quantity: c.quantity! }));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sku,
|
sku,
|
||||||
name: val(rec.name),
|
name: val(rec.name),
|
||||||
|
|
@ -1218,9 +1337,9 @@ export class ProductService {
|
||||||
price: num(rec.price),
|
price: num(rec.price),
|
||||||
promotionPrice: num(rec.promotionPrice),
|
promotionPrice: num(rec.promotionPrice),
|
||||||
type: val(rec.type),
|
type: val(rec.type),
|
||||||
|
siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
||||||
|
|
||||||
attributes: attributes.length > 0 ? attributes : undefined,
|
attributes: attributes.length > 0 ? attributes : undefined,
|
||||||
components: components.length > 0 ? components : undefined,
|
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1232,6 +1351,7 @@ export class ProductService {
|
||||||
dto.nameCn = data.nameCn;
|
dto.nameCn = data.nameCn;
|
||||||
dto.description = data.description;
|
dto.description = data.description;
|
||||||
dto.sku = data.sku;
|
dto.sku = data.sku;
|
||||||
|
if (data.siteSkus) dto.siteSkus = data.siteSkus;
|
||||||
|
|
||||||
// 数值类型转换
|
// 数值类型转换
|
||||||
if (data.price !== undefined) dto.price = Number(data.price);
|
if (data.price !== undefined) dto.price = Number(data.price);
|
||||||
|
|
@ -1244,8 +1364,7 @@ export class ProductService {
|
||||||
dto.attributes = Array.isArray(data.attributes) ? data.attributes : [];
|
dto.attributes = Array.isArray(data.attributes) ? data.attributes : [];
|
||||||
|
|
||||||
// 如果有组件信息,透传
|
// 如果有组件信息,透传
|
||||||
dto.type = data.type || data.components?.length? 'bundle':'single'
|
dto.type = data.type || 'single';
|
||||||
if (data.components) dto.components = data.components;
|
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
@ -1258,6 +1377,7 @@ export class ProductService {
|
||||||
if (data.nameCn !== undefined) dto.nameCn = data.nameCn;
|
if (data.nameCn !== undefined) dto.nameCn = data.nameCn;
|
||||||
if (data.description !== undefined) dto.description = data.description;
|
if (data.description !== undefined) dto.description = data.description;
|
||||||
if (data.sku !== undefined) dto.sku = data.sku;
|
if (data.sku !== undefined) dto.sku = data.sku;
|
||||||
|
if (data.siteSkus !== undefined) dto.siteSkus = data.siteSkus;
|
||||||
|
|
||||||
if (data.price !== undefined) dto.price = Number(data.price);
|
if (data.price !== undefined) dto.price = Number(data.price);
|
||||||
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
|
if (data.promotionPrice !== undefined) dto.promotionPrice = Number(data.promotionPrice);
|
||||||
|
|
@ -1266,6 +1386,7 @@ export class ProductService {
|
||||||
|
|
||||||
if (data.type !== undefined) dto.type = data.type;
|
if (data.type !== undefined) dto.type = data.type;
|
||||||
if (data.attributes !== undefined) dto.attributes = data.attributes;
|
if (data.attributes !== undefined) dto.attributes = data.attributes;
|
||||||
|
if (data.components !== undefined) dto.components = data.components;
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
@ -1295,6 +1416,7 @@ export class ProductService {
|
||||||
// 基础数据
|
// 基础数据
|
||||||
const rowData = [
|
const rowData = [
|
||||||
esc(p.sku),
|
esc(p.sku),
|
||||||
|
esc(p.siteSkus ? p.siteSkus.map(s => s.code).join(',') : ''),
|
||||||
esc(p.name),
|
esc(p.name),
|
||||||
esc(p.nameCn),
|
esc(p.nameCn),
|
||||||
esc(p.price),
|
esc(p.price),
|
||||||
|
|
@ -1329,7 +1451,7 @@ export class ProductService {
|
||||||
async exportProductsCSV(): Promise<string> {
|
async exportProductsCSV(): Promise<string> {
|
||||||
// 查询所有产品及其属性(包含字典关系)和组成
|
// 查询所有产品及其属性(包含字典关系)和组成
|
||||||
const products = await this.productModel.find({
|
const products = await this.productModel.find({
|
||||||
relations: ['attributes', 'attributes.dict', 'components'],
|
relations: ['attributes', 'attributes.dict', 'components', 'siteSkus'],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1358,6 +1480,7 @@ export class ProductService {
|
||||||
// 定义 CSV 表头(与导入字段一致)
|
// 定义 CSV 表头(与导入字段一致)
|
||||||
const baseHeaders = [
|
const baseHeaders = [
|
||||||
'sku',
|
'sku',
|
||||||
|
'siteSkus',
|
||||||
'name',
|
'name',
|
||||||
'nameCn',
|
'nameCn',
|
||||||
'price',
|
'price',
|
||||||
|
|
@ -1391,17 +1514,37 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 CSV 导入产品;存在则更新,不存在则创建
|
// 从 CSV 导入产品;存在则更新,不存在则创建
|
||||||
async importProductsCSV(buffer: Buffer): Promise<{ created: number; updated: number; errors: string[] }> {
|
async importProductsCSV(file: any): Promise<{ created: number; updated: number; errors: string[] }> {
|
||||||
|
let buffer: Buffer;
|
||||||
|
if (Buffer.isBuffer(file)) {
|
||||||
|
buffer = file;
|
||||||
|
} else if (file?.data) {
|
||||||
|
if (typeof file.data === 'string') {
|
||||||
|
buffer = fs.readFileSync(file.data);
|
||||||
|
} else {
|
||||||
|
buffer = file.data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('无效的文件输入');
|
||||||
|
}
|
||||||
|
|
||||||
// 解析 CSV(使用 csv-parse/sync 按表头解析)
|
// 解析 CSV(使用 csv-parse/sync 按表头解析)
|
||||||
const { parse } = await import('csv-parse/sync');
|
|
||||||
let records: any[] = [];
|
let records: any[] = [];
|
||||||
try {
|
try {
|
||||||
records = parse(buffer, {
|
records = await new Promise((resolve, reject) => {
|
||||||
|
parse(buffer, {
|
||||||
columns: true,
|
columns: true,
|
||||||
skip_empty_lines: true,
|
skip_empty_lines: true,
|
||||||
trim: true,
|
trim: true,
|
||||||
bom: true,
|
bom: true,
|
||||||
|
}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
})
|
||||||
console.log('Parsed records count:', records.length);
|
console.log('Parsed records count:', records.length);
|
||||||
if (records.length > 0) {
|
if (records.length > 0) {
|
||||||
console.log('First record keys:', Object.keys(records[0]));
|
console.log('First record keys:', Object.keys(records[0]));
|
||||||
|
|
@ -1422,10 +1565,7 @@ export class ProductService {
|
||||||
errors.push('缺少 SKU 的记录已跳过');
|
errors.push('缺少 SKU 的记录已跳过');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const { sku, components } = data;
|
const { sku } = data;
|
||||||
|
|
||||||
let currentProductId: number;
|
|
||||||
let currentProductType: string = data.type || 'single';
|
|
||||||
|
|
||||||
// 查找现有产品
|
// 查找现有产品
|
||||||
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
|
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
|
||||||
|
|
@ -1433,28 +1573,53 @@ export class ProductService {
|
||||||
if (!exist) {
|
if (!exist) {
|
||||||
// 创建新产品
|
// 创建新产品
|
||||||
const createDTO = this.prepareCreateProductDTO(data);
|
const createDTO = this.prepareCreateProductDTO(data);
|
||||||
const createdProduct = await this.createProduct(createDTO);
|
await this.createProduct(createDTO);
|
||||||
currentProductId = createdProduct.id;
|
|
||||||
currentProductType = createdProduct.type;
|
|
||||||
created += 1;
|
created += 1;
|
||||||
} else {
|
} else {
|
||||||
// 更新产品
|
// 更新产品
|
||||||
const updateDTO = this.prepareUpdateProductDTO(data);
|
const updateDTO = this.prepareUpdateProductDTO(data);
|
||||||
await this.updateProduct(exist.id, updateDTO);
|
await this.updateProduct(exist.id, updateDTO);
|
||||||
currentProductId = exist.id;
|
|
||||||
currentProductType = updateDTO.type || exist.type;
|
|
||||||
updated += 1;
|
updated += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 保存组件信息
|
|
||||||
if (currentProductType !== 'single' && components && components.length > 0) {
|
|
||||||
await this.setProductComponents(currentProductId, components);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
errors.push(e?.message || String(e));
|
errors.push(`产品${rec?.sku}导入失败:${e?.message || String(e)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { created, updated, errors };
|
return { created, updated, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将库存记录的 sku 添加到产品单品中
|
||||||
|
async syncStockToProduct(): Promise<{ added: number; errors: string[] }> {
|
||||||
|
// 1. 获取所有库存记录的 SKU (去重)
|
||||||
|
const stockSkus = await this.stockModel
|
||||||
|
.createQueryBuilder('stock')
|
||||||
|
.select('DISTINCT(stock.sku)', 'sku')
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
const skus = stockSkus.map(s => s.sku).filter(Boolean);
|
||||||
|
let added = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// 2. 遍历 SKU,检查并添加
|
||||||
|
for (const sku of skus) {
|
||||||
|
try {
|
||||||
|
const exist = await this.productModel.findOne({ where: { sku } });
|
||||||
|
if (!exist) {
|
||||||
|
const product = new Product();
|
||||||
|
product.sku = sku;
|
||||||
|
product.name = sku; // 默认使用 SKU 作为名称
|
||||||
|
product.type = 'single';
|
||||||
|
product.price = 0;
|
||||||
|
product.promotionPrice = 0;
|
||||||
|
await this.productModel.save(product);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(`SKU ${sku} 添加失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { added, errors };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,505 @@
|
||||||
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import { IPlatformService } from '../interface/platform.interface';
|
||||||
|
import { SiteService } from './site.service';
|
||||||
|
import { Site } from '../entity/site.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ShopYY平台服务实现
|
||||||
|
*/
|
||||||
|
@Provide()
|
||||||
|
export class ShopyyService implements IPlatformService {
|
||||||
|
@Inject()
|
||||||
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建ShopYY API请求URL
|
||||||
|
* @param baseUrl 基础URL
|
||||||
|
* @param endpoint API端点
|
||||||
|
* @returns 完整URL
|
||||||
|
*/
|
||||||
|
private buildURL(baseUrl: string, endpoint: string): string {
|
||||||
|
// ShopYY API URL格式:https://{shop}.shopyy.com/openapi/{version}/{endpoint}
|
||||||
|
const base = baseUrl.replace(/\/$/, '');
|
||||||
|
const end = endpoint.replace(/^\//, '');
|
||||||
|
return `${base}/${end}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建ShopYY API请求头
|
||||||
|
* @param site 站点配置
|
||||||
|
* @returns 请求头
|
||||||
|
*/
|
||||||
|
private buildHeaders(site: Site): Record<string, string> {
|
||||||
|
if (!site?.token) {
|
||||||
|
throw new Error(`获取站点${site?.name}数据,但失败,因为未设置站点令牌配置`)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
token: site.token || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送ShopYY API请求
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param endpoint API端点
|
||||||
|
* @param method 请求方法
|
||||||
|
* @param data 请求数据
|
||||||
|
* @param params 请求参数
|
||||||
|
* @returns 响应数据
|
||||||
|
*/
|
||||||
|
private async request(site: any, endpoint: string, method: string = 'GET', data: any = null, params: any = null): Promise<any> {
|
||||||
|
const url = this.buildURL(site.apiUrl, endpoint);
|
||||||
|
const headers = this.buildHeaders(site);
|
||||||
|
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
params,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios(config);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ShopYY API请求失败:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用分页获取资源
|
||||||
|
*/
|
||||||
|
public async fetchResourcePaged<T>(site: any, endpoint: string, params: Record<string, any> = {}) {
|
||||||
|
// 映射 params 字段: page -> page, per_page -> limit
|
||||||
|
const requestParams = {
|
||||||
|
...params,
|
||||||
|
page: params.page || 1,
|
||||||
|
limit: params.per_page || 20
|
||||||
|
};
|
||||||
|
const response = await this.request(site, endpoint, 'GET', null, requestParams);
|
||||||
|
if (response?.code !== 0) {
|
||||||
|
throw new Error(response?.msg)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
items: (response.data.list || []) as T,
|
||||||
|
total: response.data?.paginate?.total || 0,
|
||||||
|
totalPages: response.data?.paginate?.pageTotal || 0,
|
||||||
|
page: response.data?.paginate?.current || requestParams.page,
|
||||||
|
per_page: response.data?.paginate?.pagesize || requestParams.limit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY产品列表
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param page 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @returns 分页产品列表
|
||||||
|
*/
|
||||||
|
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
|
// ShopYY API: GET /products
|
||||||
|
const response = await this.request(site, 'products', 'GET', null, {
|
||||||
|
page,
|
||||||
|
page_size: pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: response.data || [],
|
||||||
|
total: response.meta?.pagination?.total || 0,
|
||||||
|
totalPages: response.meta?.pagination?.total_pages || 0,
|
||||||
|
page: response.meta?.pagination?.current_page || page,
|
||||||
|
per_page: response.meta?.pagination?.per_page || pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个ShopYY产品
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @returns 产品详情
|
||||||
|
*/
|
||||||
|
async getProduct(site: any, productId: string | number): Promise<any> {
|
||||||
|
// ShopYY API: GET /products/{id}
|
||||||
|
const response = await this.request(site, `products/${productId}`, 'GET');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY产品变体列表
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param page 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @returns 分页变体列表
|
||||||
|
*/
|
||||||
|
async getVariations(site: any, productId: number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
|
// ShopYY API: GET /products/{id}/variations
|
||||||
|
const response = await this.request(site, `products/${productId}/variations`, 'GET', null, {
|
||||||
|
page,
|
||||||
|
page_size: pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: response.data || [],
|
||||||
|
total: response.meta?.pagination?.total || 0,
|
||||||
|
totalPages: response.meta?.pagination?.total_pages || 0,
|
||||||
|
page: response.meta?.pagination?.current_page || page,
|
||||||
|
per_page: response.meta?.pagination?.per_page || pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY产品变体详情
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param variationId 变体ID
|
||||||
|
* @returns 变体详情
|
||||||
|
*/
|
||||||
|
async getVariation(site: any, productId: number, variationId: number): Promise<any> {
|
||||||
|
// ShopYY API: GET /products/{product_id}/variations/{variation_id}
|
||||||
|
const response = await this.request(site, `products/${productId}/variations/${variationId}`, 'GET');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY订单列表
|
||||||
|
* @param site 站点配置或站点ID
|
||||||
|
* @param page 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @returns 分页订单列表
|
||||||
|
*/
|
||||||
|
async getOrders(site: any | number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
|
// 如果传入的是站点ID,则获取站点配置
|
||||||
|
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
||||||
|
|
||||||
|
// ShopYY API: GET /orders
|
||||||
|
const response = await this.request(siteConfig, 'orders', 'GET', null, {
|
||||||
|
page,
|
||||||
|
page_size: pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: response.data || [],
|
||||||
|
total: response.meta?.pagination?.total || 0,
|
||||||
|
totalPages: response.meta?.pagination?.total_pages || 0,
|
||||||
|
page: response.meta?.pagination?.current_page || page,
|
||||||
|
per_page: response.meta?.pagination?.per_page || pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY订单详情
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @returns 订单详情
|
||||||
|
*/
|
||||||
|
async getOrder(siteId: string, orderId: string): Promise<any> {
|
||||||
|
const site = await this.siteService.get(Number(siteId));
|
||||||
|
|
||||||
|
// ShopYY API: GET /orders/{id}
|
||||||
|
const response = await this.request(site, `orders/${orderId}`, 'GET');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建ShopYY产品
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 产品数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
async createProduct(site: any, data: any): Promise<any> {
|
||||||
|
// ShopYY API: POST /products
|
||||||
|
const response = await this.request(site, 'products', 'POST', data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新ShopYY产品
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateProduct(site: any, productId: string, data: any): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: PUT /products/{id}
|
||||||
|
await this.request(site, `products/${productId}`, 'PUT', data);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新ShopYY产品失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新ShopYY产品状态
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param status 产品状态
|
||||||
|
* @param stockStatus 库存状态
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateProductStatus(site: any, productId: string, status: string, stockStatus: string): Promise<boolean> {
|
||||||
|
// ShopYY产品状态映射
|
||||||
|
const shopyyStatus = status === 'publish' ? 1 : 0;
|
||||||
|
const shopyyStockStatus = stockStatus === 'instock' ? 1 : 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.request(site, `products/${productId}`, 'PUT', {
|
||||||
|
status: shopyyStatus,
|
||||||
|
stock_status: shopyyStockStatus
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新ShopYY产品状态失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新ShopYY产品变体
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param variationId 变体ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateVariation(site: any, productId: string, variationId: string, data: any): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: PUT /products/{product_id}/variations/{variation_id}
|
||||||
|
await this.request(site, `products/${productId}/variations/${variationId}`, 'PUT', data);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新ShopYY产品变体失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新ShopYY订单
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateOrder(site: any, orderId: string, data: Record<string, any>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: PUT /orders/{id}
|
||||||
|
await this.request(site, `orders/${orderId}`, 'PUT', data);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新ShopYY订单失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建ShopYY物流信息
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param data 物流数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
async createShipment(site: any, orderId: string, data: any): Promise<any> {
|
||||||
|
// ShopYY API: POST /orders/{id}/shipments
|
||||||
|
const shipmentData = {
|
||||||
|
tracking_number: data.tracking_number,
|
||||||
|
carrier_code: data.carrier_code,
|
||||||
|
carrier_name: data.carrier_name,
|
||||||
|
shipping_method: data.shipping_method
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', shipmentData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除ShopYY物流信息
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param trackingId 物流跟踪ID
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
async deleteShipment(site: any, orderId: string, trackingId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: DELETE /orders/{order_id}/shipments/{tracking_id}
|
||||||
|
await this.request(site, `orders/${orderId}/shipments/${trackingId}`, 'DELETE');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除ShopYY物流信息失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY订单备注
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param page 页码
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @returns 分页订单备注列表
|
||||||
|
*/
|
||||||
|
async getOrderNotes(site: any, orderId: string | number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
|
// ShopYY API: GET /orders/{id}/notes
|
||||||
|
const response = await this.request(site, `orders/${orderId}/notes`, 'GET', null, {
|
||||||
|
page,
|
||||||
|
page_size: pageSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: response.data || [],
|
||||||
|
total: response.meta?.pagination?.total || 0,
|
||||||
|
totalPages: response.meta?.pagination?.total_pages || 0,
|
||||||
|
page: response.meta?.pagination?.current_page || page,
|
||||||
|
per_page: response.meta?.pagination?.per_page || pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建ShopYY订单备注
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param data 备注数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
async createOrderNote(site: any, orderId: string | number, data: any): Promise<any> {
|
||||||
|
// ShopYY API: POST /orders/{id}/notes
|
||||||
|
const noteData = {
|
||||||
|
note: data.note,
|
||||||
|
is_customer_note: data.is_customer_note || false
|
||||||
|
};
|
||||||
|
const response = await this.request(site, `orders/${orderId}/notes`, 'POST', noteData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建ShopYY订单
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 订单数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
async createOrder(site: any, data: any): Promise<any> {
|
||||||
|
// ShopYY API: POST /orders
|
||||||
|
const response = await this.request(site, 'orders', 'POST', data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除ShopYY订单
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
async deleteOrder(site: any, orderId: string | number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: DELETE /orders/{id}
|
||||||
|
await this.request(site, `orders/${orderId}`, 'DELETE');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除ShopYY订单失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量处理ShopYY产品
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 批量操作数据
|
||||||
|
* @returns 处理结果
|
||||||
|
*/
|
||||||
|
async batchProcessProducts(site: any, data: { create?: any[]; update?: any[]; delete?: any[] }): Promise<any> {
|
||||||
|
// ShopYY API: POST /products/batch
|
||||||
|
const response = await this.request(site, 'products/batch', 'POST', data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY客户列表
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param params 查询参数
|
||||||
|
* @returns 分页客户列表
|
||||||
|
*/
|
||||||
|
async fetchCustomersPaged(site: any, params: any): Promise<any> {
|
||||||
|
// ShopYY API: GET /customers
|
||||||
|
const { items, total, totalPages, page, per_page } =
|
||||||
|
await this.fetchResourcePaged<any>(site, 'customers/list', params);
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
per_page
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个ShopYY客户
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param customerId 客户ID
|
||||||
|
* @returns 客户详情
|
||||||
|
*/
|
||||||
|
async getCustomer(site: any, customerId: string | number): Promise<any> {
|
||||||
|
// ShopYY API: GET /customers/{id}
|
||||||
|
const response = await this.request(site, `customers/${customerId}`, 'GET');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建ShopYY客户
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 客户数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
async createCustomer(site: any, data: any): Promise<any> {
|
||||||
|
// ShopYY API: POST /customers
|
||||||
|
const customerData = {
|
||||||
|
firstname: data.first_name || '',
|
||||||
|
lastname: data.last_name || '',
|
||||||
|
email: data.email || '',
|
||||||
|
phone: data.phone || '',
|
||||||
|
password: data.password || ''
|
||||||
|
};
|
||||||
|
const response = await this.request(site, 'customers', 'POST', customerData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新ShopYY客户
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param customerId 客户ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateCustomer(site: any, customerId: string | number, data: any): Promise<any> {
|
||||||
|
// ShopYY API: PUT /customers/{id}
|
||||||
|
const customerData = {
|
||||||
|
firstname: data.first_name || '',
|
||||||
|
lastname: data.last_name || '',
|
||||||
|
email: data.email || '',
|
||||||
|
phone: data.phone || ''
|
||||||
|
};
|
||||||
|
const response = await this.request(site, `customers/${customerId}`, 'PUT', customerData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除ShopYY客户
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param customerId 客户ID
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
async deleteCustomer(site: any, customerId: string | number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: DELETE /customers/{id}
|
||||||
|
await this.request(site, `customers/${customerId}`, 'DELETE');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除ShopYY客户失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import { ShopyyAdapter } from '../adapter/shopyy.adapter';
|
||||||
|
import { WooCommerceAdapter } from '../adapter/woocommerce.adapter';
|
||||||
|
import { ISiteAdapter } from '../interface/site-adapter.interface';
|
||||||
|
import { ShopyyService } from './shopyy.service';
|
||||||
|
import { SiteService } from './site.service';
|
||||||
|
import { WPService } from './wp.service';
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
export class SiteApiService {
|
||||||
|
@Inject()
|
||||||
|
siteService: SiteService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
wpService: WPService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
shopyyService: ShopyyService;
|
||||||
|
|
||||||
|
async getAdapter(siteId: number): Promise<ISiteAdapter> {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) {
|
||||||
|
throw new Error(`Site ${siteId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (site.type === 'woocommerce') {
|
||||||
|
if (!site?.consumerKey || !site.consumerSecret || !site.apiUrl) {
|
||||||
|
throw new Error('站点配置缺少 consumerKey/consumerSecret/apiUrl');
|
||||||
|
}
|
||||||
|
return new WooCommerceAdapter(site, this.wpService);
|
||||||
|
} else if (site.type === 'shopyy') {
|
||||||
|
return new ShopyyAdapter(site, this.shopyyService);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported site type: ${site.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { Site } from '../entity/site.entity';
|
||||||
import { WpSite } from '../interface';
|
import { WpSite } from '../interface';
|
||||||
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
import { CreateSiteDTO, UpdateSiteDTO } from '../dto/site.dto';
|
||||||
import { Area } from '../entity/area.entity';
|
import { Area } from '../entity/area.entity';
|
||||||
|
import { StockPoint } from '../entity/stock_point.entity';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
@Scope(ScopeEnum.Singleton)
|
@Scope(ScopeEnum.Singleton)
|
||||||
|
|
@ -15,9 +16,17 @@ export class SiteService {
|
||||||
@InjectEntityModel(Area)
|
@InjectEntityModel(Area)
|
||||||
areaModel: Repository<Area>;
|
areaModel: Repository<Area>;
|
||||||
|
|
||||||
|
@InjectEntityModel(StockPoint)
|
||||||
|
stockPointModel: Repository<StockPoint>;
|
||||||
|
|
||||||
async syncFromConfig(sites: WpSite[] = []) {
|
async syncFromConfig(sites: WpSite[] = []) {
|
||||||
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
|
||||||
for (const siteConfig of sites) {
|
for (const siteConfig of sites) {
|
||||||
|
// 跳过name为空的站点配置
|
||||||
|
if (!siteConfig.name) {
|
||||||
|
console.warn('跳过空名称的站点配置');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// 按站点名称查询是否已存在记录
|
// 按站点名称查询是否已存在记录
|
||||||
const exist = await this.siteModel.findOne({
|
const exist = await this.siteModel.findOne({
|
||||||
where: { name: siteConfig.name },
|
where: { name: siteConfig.name },
|
||||||
|
|
@ -41,7 +50,7 @@ export class SiteService {
|
||||||
|
|
||||||
async create(data: CreateSiteDTO) {
|
async create(data: CreateSiteDTO) {
|
||||||
// 从 DTO 中分离出区域代码和其他站点数据
|
// 从 DTO 中分离出区域代码和其他站点数据
|
||||||
const { areas: areaCodes, ...restData } = data;
|
const { areas: areaCodes, stockPointIds, ...restData } = data;
|
||||||
const newSite = new Site();
|
const newSite = new Site();
|
||||||
Object.assign(newSite, restData);
|
Object.assign(newSite, restData);
|
||||||
|
|
||||||
|
|
@ -56,6 +65,16 @@ export class SiteService {
|
||||||
newSite.areas = [];
|
newSite.areas = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果传入了仓库点 ID,则查询并关联 StockPoint 实体
|
||||||
|
if (stockPointIds && stockPointIds.length > 0) {
|
||||||
|
const stockPoints = await this.stockPointModel.findBy({
|
||||||
|
id: In(stockPointIds.map(Number)),
|
||||||
|
});
|
||||||
|
newSite.stockPoints = stockPoints;
|
||||||
|
} else {
|
||||||
|
newSite.stockPoints = [];
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 save 方法保存实体及其关联关系
|
// 使用 save 方法保存实体及其关联关系
|
||||||
await this.siteModel.save(newSite);
|
await this.siteModel.save(newSite);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -63,11 +82,12 @@ export class SiteService {
|
||||||
|
|
||||||
async update(id: string | number, data: UpdateSiteDTO) {
|
async update(id: string | number, data: UpdateSiteDTO) {
|
||||||
// 从 DTO 中分离出区域代码和其他站点数据
|
// 从 DTO 中分离出区域代码和其他站点数据
|
||||||
const { areas: areaCodes, ...restData } = data;
|
const { areas: areaCodes, stockPointIds, ...restData } = data;
|
||||||
|
|
||||||
// 首先,根据 ID 查找要更新的站点实体
|
// 首先,根据 ID 查找要更新的站点实体
|
||||||
const siteToUpdate = await this.siteModel.findOne({
|
const siteToUpdate = await this.siteModel.findOne({
|
||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
|
relations: ['areas', 'stockPoints'],
|
||||||
});
|
});
|
||||||
if (!siteToUpdate) {
|
if (!siteToUpdate) {
|
||||||
// 如果找不到站点,则操作失败
|
// 如果找不到站点,则操作失败
|
||||||
|
|
@ -100,16 +120,28 @@ export class SiteService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果 DTO 中传入了 stockPointIds 字段(即使是空数组),也要更新关联关系
|
||||||
|
if (stockPointIds !== undefined) {
|
||||||
|
if (stockPointIds.length > 0) {
|
||||||
|
const stockPoints = await this.stockPointModel.findBy({
|
||||||
|
id: In(stockPointIds.map(Number)),
|
||||||
|
});
|
||||||
|
siteToUpdate.stockPoints = stockPoints;
|
||||||
|
} else {
|
||||||
|
siteToUpdate.stockPoints = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 save 方法保存实体及其更新后的关联关系
|
// 使用 save 方法保存实体及其更新后的关联关系
|
||||||
await this.siteModel.save(siteToUpdate);
|
await this.siteModel.save(siteToUpdate);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string | number, includeSecret = false) {
|
async get(id: string | number, includeSecret = false):Promise<Site> {
|
||||||
// 根据主键获取站点,并使用 relations 加载关联的 areas
|
// 根据主键获取站点,并使用 relations 加载关联的 areas
|
||||||
const site = await this.siteModel.findOne({
|
const site = await this.siteModel.findOne({
|
||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
relations: ['areas'],
|
relations: ['areas', 'stockPoints'],
|
||||||
});
|
});
|
||||||
if (!site) {
|
if (!site) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -119,7 +151,7 @@ export class SiteService {
|
||||||
return site;
|
return site;
|
||||||
}
|
}
|
||||||
// 默认不返回密钥,进行字段脱敏
|
// 默认不返回密钥,进行字段脱敏
|
||||||
const { consumerKey, consumerSecret, ...rest } = site;
|
const { ...rest } = site;
|
||||||
return rest;
|
return rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +193,7 @@ export class SiteService {
|
||||||
where,
|
where,
|
||||||
skip: (current - 1) * pageSize,
|
skip: (current - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
relations: ['areas'],
|
relations: ['areas', 'stockPoints'],
|
||||||
});
|
});
|
||||||
// 根据 includeSecret 决定是否脱敏返回密钥字段
|
// 根据 includeSecret 决定是否脱敏返回密钥字段
|
||||||
const data = includeSecret
|
const data = includeSecret
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,28 @@ export class SubscriptionService {
|
||||||
* - 从 WooCommerce 拉取订阅并逐条入库/更新
|
* - 从 WooCommerce 拉取订阅并逐条入库/更新
|
||||||
*/
|
*/
|
||||||
async syncSubscriptions(siteId: number) {
|
async syncSubscriptions(siteId: number) {
|
||||||
|
try {
|
||||||
const subs = await this.wpService.getSubscriptions(siteId);
|
const subs = await this.wpService.getSubscriptions(siteId);
|
||||||
|
let successCount = 0;
|
||||||
|
let failureCount = 0;
|
||||||
for (const sub of subs) {
|
for (const sub of subs) {
|
||||||
|
try {
|
||||||
await this.syncSingleSubscription(siteId, sub);
|
await this.syncSingleSubscription(siteId, sub);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`同步订阅 ${sub.id} 失败:`, error);
|
||||||
|
failureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: failureCount === 0,
|
||||||
|
successCount,
|
||||||
|
failureCount,
|
||||||
|
message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步订阅失败:', error);
|
||||||
|
return { success: false, successCount: 0, failureCount: 0, message: `同步失败: ${error.message}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@ import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { Template } from '../entity/template.entity';
|
import { Template } from '../entity/template.entity';
|
||||||
import { CreateTemplateDTO, UpdateTemplateDTO } from '../dto/template.dto';
|
import { CreateTemplateDTO, UpdateTemplateDTO } from '../dto/template.dto';
|
||||||
|
import { Eta } from 'eta';
|
||||||
|
import { generateTestDataFromEta } from '../utils/testdata.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @service TemplateService 模板服务
|
* @service TemplateService 模板服务
|
||||||
*/
|
*/
|
||||||
@Provide()
|
@Provide()
|
||||||
export class TemplateService {
|
export class TemplateService {
|
||||||
|
private eta = new Eta();
|
||||||
|
|
||||||
// 注入 Template 实体模型
|
// 注入 Template 实体模型
|
||||||
@InjectEntityModel(Template)
|
@InjectEntityModel(Template)
|
||||||
templateModel: Repository<Template>;
|
templateModel: Repository<Template>;
|
||||||
|
|
@ -48,6 +52,12 @@ export class TemplateService {
|
||||||
// 设置模板的名称和值
|
// 设置模板的名称和值
|
||||||
template.name = templateData.name;
|
template.name = templateData.name;
|
||||||
template.value = templateData.value;
|
template.value = templateData.value;
|
||||||
|
if (templateData.testData && templateData.testData.trim().length > 0) {
|
||||||
|
template.testData = templateData.testData;
|
||||||
|
} else {
|
||||||
|
const obj = generateTestDataFromEta(template.value);
|
||||||
|
template.testData = JSON.stringify(obj);
|
||||||
|
}
|
||||||
// 保存新模板到数据库
|
// 保存新模板到数据库
|
||||||
return this.templateModel.save(template);
|
return this.templateModel.save(template);
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +81,12 @@ export class TemplateService {
|
||||||
// 更新模板的名称和值
|
// 更新模板的名称和值
|
||||||
template.name = templateData.name;
|
template.name = templateData.name;
|
||||||
template.value = templateData.value;
|
template.value = templateData.value;
|
||||||
|
if (templateData.testData && templateData.testData.trim().length > 0) {
|
||||||
|
template.testData = templateData.testData;
|
||||||
|
} else {
|
||||||
|
const obj = generateTestDataFromEta(template.value);
|
||||||
|
template.testData = JSON.stringify(obj);
|
||||||
|
}
|
||||||
// 保存更新后的模板到数据库
|
// 保存更新后的模板到数据库
|
||||||
return this.templateModel.save(template);
|
return this.templateModel.save(template);
|
||||||
}
|
}
|
||||||
|
|
@ -111,17 +127,25 @@ export class TemplateService {
|
||||||
throw new Error(`模板 '${name}' 不存在`);
|
throw new Error(`模板 '${name}' 不存在`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取模板的原始内容
|
// 使用 Eta 渲染
|
||||||
let rendered = template.value;
|
return this.eta.renderString(template.value, data);
|
||||||
// 遍历数据对象,替换模板中的占位符
|
|
||||||
for (const key in data) {
|
|
||||||
// 创建一个正则表达式来匹配 {{key}}
|
|
||||||
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
||||||
// 执行替换操作
|
|
||||||
rendered = rendered.replace(regex, data[key]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回渲染后的字符串
|
/**
|
||||||
return rendered;
|
* 回填所有缺失 testData 的模板
|
||||||
|
* @returns 更新的模板数量
|
||||||
|
*/
|
||||||
|
async backfillMissingTestData(): Promise<number> {
|
||||||
|
const items = await this.templateModel.find({ where: { } });
|
||||||
|
let updated = 0;
|
||||||
|
for (const t of items) {
|
||||||
|
if (!t.testData || t.testData.trim().length === 0) {
|
||||||
|
const obj = generateTestDataFromEta(t.value);
|
||||||
|
t.testData = JSON.stringify(obj);
|
||||||
|
await this.templateModel.save(t);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,8 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增用户(支持可选备注)
|
// 新增用户(支持可选备注)
|
||||||
async addUser(username: string, password: string, remark?: string) {
|
async addUser(username: string, password: string, remark?: string, email?: string) {
|
||||||
|
// 条件判断 检查用户名是否已存在
|
||||||
const existingUser = await this.userModel.findOne({
|
const existingUser = await this.userModel.findOne({
|
||||||
where: { username },
|
where: { username },
|
||||||
});
|
});
|
||||||
|
|
@ -90,9 +91,17 @@ export class UserService {
|
||||||
throw new Error('用户已存在');
|
throw new Error('用户已存在');
|
||||||
}
|
}
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
// 条件判断 若提供邮箱则校验唯一性并赋值
|
||||||
|
if (email) {
|
||||||
|
const existingEmail = await this.userModel.findOne({ where: { email } });
|
||||||
|
if (existingEmail) {
|
||||||
|
throw new Error('邮箱已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
const user = this.userModel.create({
|
const user = this.userModel.create({
|
||||||
username,
|
username,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
...(email ? { email } : {}),
|
||||||
// 备注字段赋值(若提供)
|
// 备注字段赋值(若提供)
|
||||||
...(remark ? { remark } : {}),
|
...(remark ? { remark } : {}),
|
||||||
});
|
});
|
||||||
|
|
@ -106,24 +115,36 @@ export class UserService {
|
||||||
filters: {
|
filters: {
|
||||||
remark?: string;
|
remark?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
email?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
isSuper?: boolean;
|
isSuper?: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
|
} = {},
|
||||||
|
sorter: {
|
||||||
|
field?: string;
|
||||||
|
order?: 'ASC' | 'DESC';
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
// 条件判断:构造 where 条件
|
// 条件判断:构造 where 条件
|
||||||
const where: Record<string, any> = {};
|
const where: Record<string, any> = {};
|
||||||
if (filters.username) where.username = Like(`%${filters.username}%`); // 用户名精确匹配(如需模糊可改为 Like)
|
if (filters.username) where.username = Like(`%${filters.username}%`); // 用户名精确匹配(如需模糊可改为 Like)
|
||||||
|
// 条件判断 邮箱模糊搜索
|
||||||
|
if (filters.email) where.email = Like(`%${filters.email}%`);
|
||||||
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 按启用状态过滤
|
if (typeof filters.isActive === 'boolean') where.isActive = filters.isActive; // 按启用状态过滤
|
||||||
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
|
if (typeof filters.isSuper === 'boolean') where.isSuper = filters.isSuper; // 按超管过滤
|
||||||
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
|
if (typeof filters.isAdmin === 'boolean') where.isAdmin = filters.isAdmin; // 按管理员过滤
|
||||||
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 备注模糊搜索
|
if (filters.remark) where.remark = Like(`%${filters.remark}%`); // 备注模糊搜索
|
||||||
|
|
||||||
|
// 条件判断 支持邮箱排序字段
|
||||||
|
const validSortFields = ['id', 'username', 'email', 'isActive', 'isSuper', 'isAdmin', 'remark'];
|
||||||
|
const sortField = validSortFields.includes(sorter.field) ? sorter.field : 'id';
|
||||||
|
const sortOrder = sorter.order === 'ASC' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
const [items, total] = await this.userModel.findAndCount({
|
const [items, total] = await this.userModel.findAndCount({
|
||||||
where,
|
where,
|
||||||
skip: (current - 1) * pageSize,
|
skip: (current - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
order: { id: 'DESC' },
|
order: { [sortField]: sortOrder },
|
||||||
});
|
});
|
||||||
return { items, total, current, pageSize };
|
return { items, total, current, pageSize };
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +164,7 @@ export class UserService {
|
||||||
payload: {
|
payload: {
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
email?: string;
|
||||||
isSuper?: boolean;
|
isSuper?: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
permissions?: string[];
|
permissions?: string[];
|
||||||
|
|
@ -167,6 +189,13 @@ export class UserService {
|
||||||
user.password = await bcrypt.hash(payload.password, 10);
|
user.password = await bcrypt.hash(payload.password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 条件判断 若提供新邮箱且与原邮箱不同,进行唯一性校验
|
||||||
|
if (payload.email && payload.email !== user.email) {
|
||||||
|
const existEmail = await this.userModel.findOne({ where: { email: payload.email } });
|
||||||
|
if (existEmail) throw new Error('邮箱已存在');
|
||||||
|
user.email = payload.email;
|
||||||
|
}
|
||||||
|
|
||||||
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
// 条件判断:更新布尔与权限字段(若提供则覆盖)
|
||||||
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
|
if (typeof payload.isSuper === 'boolean') user.isSuper = payload.isSuper;
|
||||||
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
|
if (typeof payload.isAdmin === 'boolean') user.isAdmin = payload.isAdmin;
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ import WooCommerceRestApi, { WooCommerceRestApiVersion } from '@woocommerce/wooc
|
||||||
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 { SiteService } from './site.service';
|
import { SiteService } from './site.service';
|
||||||
|
import { IPlatformService } from '../interface/platform.interface';
|
||||||
|
import * as FormData from 'form-data';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class WPService {
|
export class WPService implements IPlatformService {
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly siteService: SiteService;
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
|
|
@ -44,6 +46,14 @@ export class WPService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用分页获取资源
|
||||||
|
*/
|
||||||
|
public async fetchResourcePaged<T>(site: any, resource: string, params: Record<string, any> = {}) {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
return this.sdkGetPage<T>(api, resource, params);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 SDK 获取单页数据,并返回数据与 totalPages
|
* 通过 SDK 获取单页数据,并返回数据与 totalPages
|
||||||
*/
|
*/
|
||||||
|
|
@ -64,13 +74,9 @@ export class WPService {
|
||||||
* 通过 SDK 聚合分页数据,返回全部数据
|
* 通过 SDK 聚合分页数据,返回全部数据
|
||||||
*/
|
*/
|
||||||
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
|
private async sdkGetAll<T>(api: WooCommerceRestApi, resource: string, params: Record<string, any> = {}, maxPages: number = 50): Promise<T[]> {
|
||||||
const result: T[] = [];
|
// 直接传入较大的per_page参数,一次性获取所有数据
|
||||||
for (let page = 1; page <= maxPages; page++) {
|
const { items } = await this.sdkGetPage<T>(api, resource, { ...params, per_page: 100 });
|
||||||
const { items, totalPages } = await this.sdkGetPage<T>(api, resource, { ...params, page });
|
return items;
|
||||||
result.push(...items);
|
|
||||||
if (page >= totalPages) break;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,14 +163,24 @@ export class WPService {
|
||||||
return allData;
|
return allData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProducts(site: any): Promise<WpProduct[]> {
|
async getProducts(site: any, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
const api = this.createApi(site, 'wc/v3');
|
const api = this.createApi(site, 'wc/v3');
|
||||||
return await this.sdkGetAll<WpProduct>(api, 'products');
|
return await this.sdkGetPage<WpProduct>(api, 'products', { page, per_page: pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVariations(site: any, productId: number): Promise<Variation[]> {
|
|
||||||
|
// 导出 WooCommerce 产品为特殊CSV(平台特性)
|
||||||
|
async exportProductsCsvSpecial(site: any, page: number = 1, pageSize: number = 100): Promise<string> {
|
||||||
|
const list = await this.getProducts(site, page, pageSize);
|
||||||
|
const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity'];
|
||||||
|
const rows = (list.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]);
|
||||||
|
const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n');
|
||||||
|
return csv;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVariations(site: any, productId: number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
const api = this.createApi(site, 'wc/v3');
|
const api = this.createApi(site, 'wc/v3');
|
||||||
return await this.sdkGetAll<Variation>(api, `products/${productId}/variations`);
|
return await this.sdkGetPage<Variation>(api, `products/${productId}/variations`, { page, per_page: pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVariation(
|
async getVariation(
|
||||||
|
|
@ -186,23 +202,23 @@ export class WPService {
|
||||||
const res = await api.get(`orders/${orderId}`);
|
const res = await api.get(`orders/${orderId}`);
|
||||||
return res.data as Record<string, any>;
|
return res.data as Record<string, any>;
|
||||||
}
|
}
|
||||||
async getOrders(siteId: number): Promise<Record<string, any>[]> {
|
async getOrders(site: any | number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
const site = await this.siteService.get(siteId);
|
// 如果传入的是站点ID,则获取站点配置
|
||||||
const api = this.createApi(site, 'wc/v3');
|
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
||||||
return await this.sdkGetAll<Record<string, any>>(api, 'orders');
|
const api = this.createApi(siteConfig, 'wc/v3');
|
||||||
|
return await this.sdkGetPage<Record<string, any>>(api, 'orders', { page, per_page: pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 WooCommerce Subscriptions
|
* 获取 WooCommerce Subscriptions
|
||||||
* 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions.
|
* 优先尝试 wc/v1/subscriptions(Subscriptions 插件提供),失败时回退 wc/v3/subscriptions.
|
||||||
* 返回所有分页合并后的订阅数组.
|
|
||||||
*/
|
*/
|
||||||
async getSubscriptions(siteId: number): Promise<Record<string, any>[]> {
|
async getSubscriptions(site: any | number, page: number = 1, pageSize: number = 100): Promise<any> {
|
||||||
const site = await this.siteService.get(siteId);
|
// 如果传入的是站点ID,则获取站点配置
|
||||||
|
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
||||||
// 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
|
// 优先使用 Subscriptions 命名空间 wcs/v1,失败回退 wc/v3
|
||||||
const api = this.createApi(site, 'wc/v3');
|
const api = this.createApi(siteConfig, 'wc/v3');
|
||||||
return await this.sdkGetAll<Record<string, any>>(api, 'subscriptions');
|
return await this.sdkGetPage<Record<string, any>>(api, 'subscriptions', { page, per_page: pageSize });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderRefund(
|
async getOrderRefund(
|
||||||
|
|
@ -217,12 +233,15 @@ export class WPService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderRefunds(
|
async getOrderRefunds(
|
||||||
siteId: string,
|
site: any | string,
|
||||||
orderId: number
|
orderId: number,
|
||||||
): Promise<Record<string, any>[]> {
|
page: number = 1,
|
||||||
const site = await this.siteService.get(siteId);
|
pageSize: number = 100
|
||||||
const api = this.createApi(site, 'wc/v3');
|
): Promise<any> {
|
||||||
return await this.sdkGetAll<Record<string, any>>(api, `orders/${orderId}/refunds`);
|
// 如果传入的是站点ID,则获取站点配置
|
||||||
|
const siteConfig = typeof site === 'string' ? await this.siteService.get(site) : site;
|
||||||
|
const api = this.createApi(siteConfig, 'wc/v3');
|
||||||
|
return await this.sdkGetPage<Record<string, any>>(api, `orders/${orderId}/refunds`, { page, per_page: pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderNote(
|
async getOrderNote(
|
||||||
|
|
@ -237,38 +256,42 @@ export class WPService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderNotes(
|
async getOrderNotes(
|
||||||
siteId: string,
|
site: any | string,
|
||||||
orderId: number
|
orderId: number,
|
||||||
): Promise<Record<string, any>[]> {
|
page: number = 1,
|
||||||
const site = await this.siteService.get(siteId);
|
pageSize: number = 100
|
||||||
const api = this.createApi(site, 'wc/v3');
|
): Promise<any> {
|
||||||
return await this.sdkGetAll<Record<string, any>>(api, `orders/${orderId}/notes`);
|
// 如果传入的是站点ID,则获取站点配置
|
||||||
|
const siteConfig = typeof site === 'string' ? await this.siteService.get(site) : site;
|
||||||
|
const api = this.createApi(siteConfig, 'wc/v3');
|
||||||
|
return await this.sdkGetPage<Record<string, any>>(api, `orders/${orderId}/notes`, { page, per_page: pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateData<T>(
|
|
||||||
endpoint: string,
|
/**
|
||||||
|
* 创建 WooCommerce 产品
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 产品数据
|
||||||
|
*/
|
||||||
|
async createProduct(
|
||||||
site: any,
|
site: any,
|
||||||
data: Record<string, any>
|
data: any
|
||||||
): Promise<Boolean> {
|
): Promise<any> {
|
||||||
const apiUrl = site.apiUrl;
|
const api = this.createApi(site, 'wc/v3');
|
||||||
const { consumerKey, consumerSecret } = site;
|
// 确保价格为字符串
|
||||||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
if (data.regular_price !== undefined && data.regular_price !== null) {
|
||||||
'base64'
|
data.regular_price = String(data.regular_price);
|
||||||
);
|
}
|
||||||
const config: AxiosRequestConfig = {
|
if (data.sale_price !== undefined && data.sale_price !== null) {
|
||||||
method: 'PUT',
|
data.sale_price = String(data.sale_price);
|
||||||
// 构建 URL,规避多/或少/问题
|
}
|
||||||
url: this.buildURL(apiUrl, '/wp-json', endpoint),
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${auth}`,
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
await axios.request(config);
|
const response = await api.post('products', data);
|
||||||
return true;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
console.error('创建产品失败:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,33 +304,108 @@ export class WPService {
|
||||||
site: any,
|
site: any,
|
||||||
productId: string,
|
productId: string,
|
||||||
data: UpdateWpProductDTO
|
data: UpdateWpProductDTO
|
||||||
): Promise<Boolean> {
|
): Promise<any> {
|
||||||
const { regular_price, sale_price, ...params } = data;
|
const { regular_price, sale_price, ...params } = data;
|
||||||
return await this.updateData(`/wc/v3/products/${productId}`, site, {
|
const api = this.createApi(site, 'wc/v3');
|
||||||
...params,
|
const updateData: any = { ...params };
|
||||||
regular_price: regular_price ? regular_price.toString() : null,
|
if (regular_price !== undefined && regular_price !== null) {
|
||||||
sale_price: sale_price ? sale_price.toString() : null,
|
updateData.regular_price = String(regular_price);
|
||||||
});
|
}
|
||||||
|
if (sale_price !== undefined && sale_price !== null) {
|
||||||
|
updateData.sale_price = String(sale_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.put(`products/${productId}`, updateData);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新产品失败:', error.response?.data || error.message);
|
||||||
|
throw new Error(`更新产品失败: ${error.response?.data?.message || error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新 WooCommerce 产品 上下架状态
|
* 更新 WooCommerce 产品 上下架状态
|
||||||
* @param productId 产品 ID
|
* @param productId 产品 ID
|
||||||
* @param status 状态
|
* @param status 状态
|
||||||
* @param stock_status 上下架状态
|
* @param stockStatus 库存状态
|
||||||
*/
|
*/
|
||||||
async updateProductStatus(
|
async updateProductStatus(
|
||||||
site: any,
|
site: any,
|
||||||
productId: string,
|
productId: string,
|
||||||
status: ProductStatus,
|
status: string,
|
||||||
stock_status: ProductStockStatus
|
stockStatus: string
|
||||||
): Promise<Boolean> {
|
): Promise<boolean> {
|
||||||
const res = await this.updateData(`/wc/v3/products/${productId}`, site, {
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
await api.put(`products/${productId}`, {
|
||||||
status,
|
status,
|
||||||
manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
|
manage_stock: false, // 为true的时候,用quantity控制库存,为false时,直接用stock_status控制
|
||||||
stock_status,
|
stock_status: stockStatus,
|
||||||
});
|
});
|
||||||
return res;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新产品上下架状态失败:', error.response?.data || error.message);
|
||||||
|
throw new Error(`更新产品上下架状态失败: ${error.response?.data?.message || error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 WooCommerce 产品库存
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品 ID
|
||||||
|
* @param quantity 库存数量
|
||||||
|
* @param stockStatus 库存状态
|
||||||
|
*/
|
||||||
|
async updateProductStock(
|
||||||
|
site: any,
|
||||||
|
productId: string,
|
||||||
|
quantity: number,
|
||||||
|
stockStatus: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
await api.put(`products/${productId}`, {
|
||||||
|
manage_stock: true,
|
||||||
|
stock_quantity: quantity,
|
||||||
|
stock_status: stockStatus,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新产品库存失败:', error.response?.data || error.message);
|
||||||
|
// throw new Error(`更新产品库存失败: ${error.response?.data?.message || error.message}`);
|
||||||
|
// 为了不打断批量同步,这里记录错误但不抛出
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 WooCommerce 产品变体库存
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品 ID
|
||||||
|
* @param variationId 变体 ID
|
||||||
|
* @param quantity 库存数量
|
||||||
|
* @param stockStatus 库存状态
|
||||||
|
*/
|
||||||
|
async updateProductVariationStock(
|
||||||
|
site: any,
|
||||||
|
productId: string,
|
||||||
|
variationId: string,
|
||||||
|
quantity: number,
|
||||||
|
stockStatus: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
await api.put(`products/${productId}/variations/${variationId}`, {
|
||||||
|
manage_stock: true,
|
||||||
|
stock_quantity: quantity,
|
||||||
|
stock_status: stockStatus,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新产品变体库存失败:', error.response?.data || error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -321,17 +419,24 @@ export class WPService {
|
||||||
productId: string,
|
productId: string,
|
||||||
variationId: string,
|
variationId: string,
|
||||||
data: Partial<UpdateVariationDTO>
|
data: Partial<UpdateVariationDTO>
|
||||||
): Promise<Boolean> {
|
): Promise<boolean> {
|
||||||
const { regular_price, sale_price, ...params } = data;
|
const { regular_price, sale_price, ...params } = data;
|
||||||
return await this.updateData(
|
const api = this.createApi(site, 'wc/v3');
|
||||||
`/wc/v3/products/${productId}/variations/${variationId}`,
|
const updateData: any = { ...params };
|
||||||
site,
|
if (regular_price !== undefined && regular_price !== null) {
|
||||||
{
|
updateData.regular_price = String(regular_price);
|
||||||
...params,
|
}
|
||||||
regular_price: regular_price ? regular_price.toString() : null,
|
if (sale_price !== undefined && sale_price !== null) {
|
||||||
sale_price: sale_price ? sale_price.toString() : null,
|
updateData.sale_price = String(sale_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.put(`products/${productId}/variations/${variationId}`, updateData);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新产品变体失败:', error.response?.data || error.message);
|
||||||
|
throw new Error(`更新产品变体失败: ${error.response?.data?.message || error.message}`);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -341,8 +446,15 @@ export class WPService {
|
||||||
site: any,
|
site: any,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
): Promise<Boolean> {
|
): Promise<boolean> {
|
||||||
return await this.updateData(`/wc/v3/orders/${orderId}`, site, data);
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
await api.put(`orders/${orderId}`, data);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新订单失败:', error.response?.data || error.message);
|
||||||
|
throw new Error(`更新订单失败: ${error.response?.data?.message || error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createShipment(
|
async createShipment(
|
||||||
|
|
@ -377,7 +489,7 @@ export class WPService {
|
||||||
site: any,
|
site: any,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
trackingId: string,
|
trackingId: string,
|
||||||
): Promise<Boolean> {
|
): Promise<boolean> {
|
||||||
const apiUrl = site.apiUrl;
|
const apiUrl = site.apiUrl;
|
||||||
const { consumerKey, consumerSecret } = site;
|
const { consumerKey, consumerSecret } = site;
|
||||||
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
||||||
|
|
@ -401,6 +513,266 @@ export class WPService {
|
||||||
Authorization: `Basic ${auth}`,
|
Authorization: `Basic ${auth}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return await axios.request(config);
|
try {
|
||||||
|
await axios.request(config);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除物流信息失败:', error.response?.data || error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量处理产品 (Create, Update, Delete)
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 批量操作数据 { create?: [], update?: [], delete?: [] }
|
||||||
|
*/
|
||||||
|
async batchProcessProducts(
|
||||||
|
site: any,
|
||||||
|
data: { create?: any[]; update?: any[]; delete?: any[] }
|
||||||
|
): Promise<any> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
const response = await api.post('products/batch', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量处理产品失败:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有产品分类
|
||||||
|
* @param site 站点配置
|
||||||
|
*/
|
||||||
|
async getCategories(site: any): Promise<any[]> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
return await this.sdkGetAll<any>(api, 'products/categories');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量处理产品分类
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data { create?: [], update?: [], delete?: [] }
|
||||||
|
*/
|
||||||
|
async batchProcessCategories(
|
||||||
|
site: any,
|
||||||
|
data: { create?: any[]; update?: any[]; delete?: any[] }
|
||||||
|
): Promise<any> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
const response = await api.post('products/categories/batch', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量处理产品分类失败:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有产品标签
|
||||||
|
* @param site 站点配置
|
||||||
|
*/
|
||||||
|
async getTags(site: any): Promise<any[]> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
return await this.sdkGetAll<any>(api, 'products/tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量处理产品标签
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data { create?: [], update?: [], delete?: [] }
|
||||||
|
*/
|
||||||
|
async batchProcessTags(
|
||||||
|
site: any,
|
||||||
|
data: { create?: any[]; update?: any[]; delete?: any[] }
|
||||||
|
): Promise<any> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
const response = await api.post('products/tags/batch', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量处理产品标签失败:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 WordPress 媒体库数据
|
||||||
|
* @param siteId 站点 ID
|
||||||
|
* @param page 页码
|
||||||
|
* @param perPage 每页数量
|
||||||
|
*/
|
||||||
|
async getMedia(siteId: number, page: number = 1, perPage: number = 20): Promise<{ items: any[], total: number, totalPages: number }> {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) {
|
||||||
|
throw new Error('站点不存在');
|
||||||
|
}
|
||||||
|
const endpoint = 'wp/v2/media';
|
||||||
|
const apiUrl = site.apiUrl;
|
||||||
|
const { consumerKey, consumerSecret } = site as any;
|
||||||
|
// 构建 URL,规避多/或少/问题
|
||||||
|
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
|
||||||
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
|
||||||
|
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: { Authorization: `Basic ${auth}` },
|
||||||
|
params: { page, per_page: perPage }
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = Number(response.headers['x-wp-total'] || 0);
|
||||||
|
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: response.data,
|
||||||
|
total,
|
||||||
|
totalPages
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传媒体文件
|
||||||
|
* @param siteId 站点 ID
|
||||||
|
* @param file 文件对象
|
||||||
|
*/
|
||||||
|
async createMedia(siteId: number, file: any): Promise<any> {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) {
|
||||||
|
throw new Error('站点不存在');
|
||||||
|
}
|
||||||
|
const endpoint = 'wp/v2/media';
|
||||||
|
const apiUrl = site.apiUrl;
|
||||||
|
const { consumerKey, consumerSecret } = site as any;
|
||||||
|
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
|
||||||
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
// 假设 file 是 MidwayJS 的 file 对象
|
||||||
|
// MidwayJS 上传文件通常在 tmp 目录,需要读取流
|
||||||
|
formData.append('file', fs.createReadStream(file.data), {
|
||||||
|
filename: file.filename,
|
||||||
|
contentType: file.mimeType,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Axios headers for multipart
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Basic ${auth}`,
|
||||||
|
'Content-Disposition': `attachment; filename=${file.filename}`,
|
||||||
|
...formData.getHeaders(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post(url, formData, { headers });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新媒体信息
|
||||||
|
* @param siteId 站点 ID
|
||||||
|
* @param mediaId 媒体 ID
|
||||||
|
* @param data 更新数据 (title, caption, description, alt_text)
|
||||||
|
*/
|
||||||
|
async updateMedia(siteId: number, mediaId: number, data: any): Promise<any> {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) {
|
||||||
|
throw new Error('站点不存在');
|
||||||
|
}
|
||||||
|
const endpoint = `wp/v2/media/${mediaId}`;
|
||||||
|
const apiUrl = site.apiUrl;
|
||||||
|
const { consumerKey, consumerSecret } = site as any;
|
||||||
|
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
|
||||||
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
|
||||||
|
|
||||||
|
const response = await axios.post(url, data, {
|
||||||
|
headers: { Authorization: `Basic ${auth}` },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除媒体文件
|
||||||
|
* @param siteId 站点 ID
|
||||||
|
* @param mediaId 媒体 ID
|
||||||
|
* @param force 是否强制删除(绕过回收站)
|
||||||
|
*/
|
||||||
|
async deleteMedia(siteId: number, mediaId: number, force: boolean = true): Promise<any> {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) {
|
||||||
|
throw new Error('站点不存在');
|
||||||
|
}
|
||||||
|
const endpoint = `wp/v2/media/${mediaId}`;
|
||||||
|
const apiUrl = site.apiUrl;
|
||||||
|
const { consumerKey, consumerSecret } = site as any;
|
||||||
|
const url = this.buildURL(apiUrl, '/wp-json', endpoint);
|
||||||
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');
|
||||||
|
|
||||||
|
const response = await axios.delete(url, {
|
||||||
|
headers: { Authorization: `Basic ${auth}` },
|
||||||
|
params: { force },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCustomers(siteId: number, page: number = 1, perPage: number = 20): Promise<{ items: any[], total: number, totalPages: number }> {
|
||||||
|
const site = await this.siteService.get(siteId);
|
||||||
|
if (!site) {
|
||||||
|
throw new Error(`Site ${siteId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (site.type === 'shopyy') {
|
||||||
|
return { items: [], total: 0, totalPages: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
return await this.sdkGetPage<any>(api, 'customers', { page, per_page: perPage });
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureTags(site: any, tagNames: string[]): Promise<{ id: number; name: string }[]> {
|
||||||
|
if (!tagNames || tagNames.length === 0) return [];
|
||||||
|
|
||||||
|
const allTags = await this.getTags(site);
|
||||||
|
const existingTagMap = new Map(allTags.map((t) => [t.name, t.id]));
|
||||||
|
const missingTags = tagNames.filter((name) => !existingTagMap.has(name));
|
||||||
|
|
||||||
|
if (missingTags.length > 0) {
|
||||||
|
const createPayload = missingTags.map((name) => ({ name }));
|
||||||
|
const createdTagsResult = await this.batchProcessTags(site, { create: createPayload });
|
||||||
|
if (createdTagsResult && createdTagsResult.create) {
|
||||||
|
createdTagsResult.create.forEach((t) => {
|
||||||
|
if (t.id && t.name) existingTagMap.set(t.name, t.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagNames
|
||||||
|
.map((name) => {
|
||||||
|
const id = existingTagMap.get(name);
|
||||||
|
return id ? { id, name } : null;
|
||||||
|
})
|
||||||
|
.filter((t) => t !== null) as { id: number; name: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureCategories(site: any, categoryNames: string[]): Promise<{ id: number; name: string }[]> {
|
||||||
|
if (!categoryNames || categoryNames.length === 0) return [];
|
||||||
|
|
||||||
|
const allCategories = await this.getCategories(site);
|
||||||
|
const existingCatMap = new Map(allCategories.map((c) => [c.name, c.id]));
|
||||||
|
const missingCategories = categoryNames.filter((name) => !existingCatMap.has(name));
|
||||||
|
|
||||||
|
if (missingCategories.length > 0) {
|
||||||
|
const createPayload = missingCategories.map((name) => ({ name }));
|
||||||
|
const createdCatsResult = await this.batchProcessCategories(site, { create: createPayload });
|
||||||
|
if (createdCatsResult && createdCatsResult.create) {
|
||||||
|
createdCatsResult.create.forEach((c) => {
|
||||||
|
if (c.id && c.name) existingCatMap.set(c.name, c.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return categoryNames
|
||||||
|
.map((name) => {
|
||||||
|
const id = existingCatMap.get(name);
|
||||||
|
return id ? { id, name } : null;
|
||||||
|
})
|
||||||
|
.filter((c) => c !== null) as { id: number; name: string }[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
|
import { ProductSiteSku } from '../entity/product_site_sku.entity';
|
||||||
import { Product } from '../entity/product.entity';
|
import { Product } from '../entity/product.entity';
|
||||||
import { Inject, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { parse } from 'csv-parse';
|
||||||
import { WPService } from './wp.service';
|
import { WPService } from './wp.service';
|
||||||
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, In } from 'typeorm';
|
||||||
import { Variation } from '../entity/variation.entity';
|
import { Variation } from '../entity/variation.entity';
|
||||||
import {
|
import {
|
||||||
QueryWpProductDTO,
|
QueryWpProductDTO,
|
||||||
UpdateVariationDTO,
|
UpdateVariationDTO,
|
||||||
UpdateWpProductDTO,
|
UpdateWpProductDTO,
|
||||||
|
BatchUpdateProductsDTO,
|
||||||
} 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';
|
import { SiteService } from './site.service';
|
||||||
|
|
||||||
|
import { StockService } from './stock.service';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class WpProductService {
|
export class WpProductService {
|
||||||
// 移除配置中的站点数组,统一从数据库获取站点信息
|
// 移除配置中的站点数组,统一从数据库获取站点信息
|
||||||
|
|
@ -23,12 +29,21 @@ export class WpProductService {
|
||||||
@Inject()
|
@Inject()
|
||||||
private readonly siteService: SiteService;
|
private readonly siteService: SiteService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private readonly stockService: StockService;
|
||||||
|
|
||||||
@InjectEntityModel(WpProduct)
|
@InjectEntityModel(WpProduct)
|
||||||
wpProductModel: Repository<WpProduct>;
|
wpProductModel: Repository<WpProduct>;
|
||||||
|
|
||||||
@InjectEntityModel(Variation)
|
@InjectEntityModel(Variation)
|
||||||
variationModel: Repository<Variation>;
|
variationModel: Repository<Variation>;
|
||||||
|
|
||||||
|
@InjectEntityModel(Product)
|
||||||
|
productModel: Repository<Product>;
|
||||||
|
|
||||||
|
@InjectEntityModel(ProductSiteSku)
|
||||||
|
productSiteSkuModel: Repository<ProductSiteSku>;
|
||||||
|
|
||||||
|
|
||||||
async syncAllSites() {
|
async syncAllSites() {
|
||||||
// 从数据库获取所有启用的站点,并逐站点同步产品与变体
|
// 从数据库获取所有启用的站点,并逐站点同步产品与变体
|
||||||
|
|
@ -44,8 +59,502 @@ export class WpProductService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private logToFile(msg: string, data?: any) {
|
||||||
|
const logFile = '/Users/zksu/Developer/work/workcode/API/debug_sync.log';
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
let content = `[${timestamp}] ${msg}`;
|
||||||
|
if (data !== undefined) {
|
||||||
|
content += ' ' + (typeof data === 'object' ? JSON.stringify(data) : String(data));
|
||||||
|
}
|
||||||
|
content += '\n';
|
||||||
|
try {
|
||||||
|
fs.appendFileSync(logFile, content);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to write to log file:', e);
|
||||||
|
}
|
||||||
|
console.log(msg, data || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchSyncToSite(siteId: number, productIds: number[]) {
|
||||||
|
this.logToFile(`[BatchSync] Starting sync to site ${siteId} for products:`, productIds);
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
const products = await this.productModel.find({
|
||||||
|
where: { id: In(productIds) },
|
||||||
|
});
|
||||||
|
this.logToFile(`[BatchSync] Found ${products.length} products in local DB`);
|
||||||
|
|
||||||
|
const batchData = {
|
||||||
|
create: [],
|
||||||
|
update: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const skuToProductMap = new Map<string, Product>();
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
const targetSku = (site.skuPrefix || '') + product.sku;
|
||||||
|
skuToProductMap.set(targetSku, product);
|
||||||
|
|
||||||
|
// Determine if we should create or update based on local WpProduct record
|
||||||
|
const existingWpProduct = await this.wpProductModel.findOne({
|
||||||
|
where: { siteId, sku: targetSku, on_delete: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const productData = {
|
||||||
|
name: product.name,
|
||||||
|
type: product.type === 'single' ? 'simple' : (product.type === 'bundle' ? 'bundle' : 'simple'),
|
||||||
|
regular_price: product.price ? String(product.price) : '0',
|
||||||
|
sale_price: product.promotionPrice ? String(product.promotionPrice) : '',
|
||||||
|
sku: targetSku,
|
||||||
|
status: 'publish', // Default to publish
|
||||||
|
// categories?
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingWpProduct) {
|
||||||
|
batchData.update.push({
|
||||||
|
id: existingWpProduct.externalProductId,
|
||||||
|
...productData
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
batchData.create.push(productData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logToFile(`[BatchSync] Payload - Create: ${batchData.create.length}, Update: ${batchData.update.length}`);
|
||||||
|
if (batchData.create.length > 0) this.logToFile('[BatchSync] Create Payload:', JSON.stringify(batchData.create));
|
||||||
|
if (batchData.update.length > 0) this.logToFile('[BatchSync] Update Payload:', JSON.stringify(batchData.update));
|
||||||
|
|
||||||
|
if (batchData.create.length === 0 && batchData.update.length === 0) {
|
||||||
|
this.logToFile('[BatchSync] No actions needed, skipping API call');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await this.wpApiService.batchProcessProducts(site, batchData);
|
||||||
|
this.logToFile('[BatchSync] API Success. Result:', JSON.stringify(result));
|
||||||
|
} catch (error) {
|
||||||
|
this.logToFile('[BatchSync] API Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process results to update local WpProduct and ProductSiteSku
|
||||||
|
const processResultItem = async (item: any, sourceList: any[], index: number) => {
|
||||||
|
const originalSku = sourceList[index]?.sku;
|
||||||
|
|
||||||
|
if (item.id) {
|
||||||
|
this.logToFile(`[BatchSync] Processing success item: ID=${item.id}, SKU=${item.sku}`);
|
||||||
|
let localProduct = skuToProductMap.get(item.sku);
|
||||||
|
|
||||||
|
// Fallback to original SKU if response SKU differs or lookup fails
|
||||||
|
if (!localProduct && originalSku) {
|
||||||
|
localProduct = skuToProductMap.get(originalSku);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localProduct) {
|
||||||
|
this.logToFile(`[BatchSync] Found local product ID=${localProduct.id} for SKU=${item.sku || originalSku}`);
|
||||||
|
const code = item.sku || originalSku;
|
||||||
|
const existingSiteSku = await this.productSiteSkuModel.findOne({
|
||||||
|
where: { productId: localProduct.id, code },
|
||||||
|
});
|
||||||
|
if (!existingSiteSku) {
|
||||||
|
this.logToFile(`[BatchSync] Creating ProductSiteSku for productId=${localProduct.id} code=${code}`);
|
||||||
|
await this.productSiteSkuModel.save({
|
||||||
|
productId: localProduct.id,
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.logToFile(`[BatchSync] ProductSiteSku already exists for productId=${localProduct.id} code=${code}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logToFile(`[BatchSync] Warning: Local product not found in map for SKU=${item.sku || originalSku}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync back to local WpProduct table
|
||||||
|
await this.syncProductAndVariations(siteId, item, []);
|
||||||
|
} else if (item.error) {
|
||||||
|
// Handle duplicated SKU error by linking to existing remote product
|
||||||
|
if (item.error.code === 'product_invalid_sku' && item.error.data && item.error.data.resource_id) {
|
||||||
|
const recoveredSku = item.error.data.unique_sku;
|
||||||
|
const resourceId = item.error.data.resource_id;
|
||||||
|
this.logToFile(`[BatchSync] Recovering from duplicate SKU error. SKU=${recoveredSku}, ID=${resourceId}`);
|
||||||
|
|
||||||
|
let localProduct = skuToProductMap.get(recoveredSku);
|
||||||
|
|
||||||
|
// Fallback to original SKU
|
||||||
|
if (!localProduct && originalSku) {
|
||||||
|
localProduct = skuToProductMap.get(originalSku);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localProduct) {
|
||||||
|
// Construct a fake product object to sync local DB
|
||||||
|
const fakeProduct = {
|
||||||
|
id: resourceId,
|
||||||
|
sku: recoveredSku, // Use the actual SKU on server
|
||||||
|
name: localProduct.name,
|
||||||
|
type: localProduct.type === 'single' ? 'simple' : (localProduct.type === 'bundle' ? 'bundle' : 'simple'),
|
||||||
|
status: 'publish',
|
||||||
|
regular_price: localProduct.price ? String(localProduct.price) : '0',
|
||||||
|
sale_price: localProduct.promotionPrice ? String(localProduct.promotionPrice) : '',
|
||||||
|
on_sale: !!localProduct.promotionPrice,
|
||||||
|
metadata: [],
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.syncProductAndVariations(siteId, fakeProduct as any, []);
|
||||||
|
this.logToFile(`[BatchSync] Successfully linked local product to existing remote product ID=${resourceId}`);
|
||||||
|
} catch (e) {
|
||||||
|
this.logToFile(`[BatchSync] Failed to link recovered product:`, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logToFile(`[BatchSync] Warning: Local product not found in map for recovered SKU=${recoveredSku} or original SKU=${originalSku}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logToFile(`[BatchSync] Item Error: SKU=${originalSku || 'unknown'}`, item.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logToFile(`[BatchSync] Unknown item format:`, item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.create) {
|
||||||
|
for (let i = 0; i < result.create.length; i++) {
|
||||||
|
await processResultItem(result.create[i], batchData.create, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.update) {
|
||||||
|
for (let i = 0; i < result.update.length; i++) {
|
||||||
|
await processResultItem(result.update[i], batchData.update, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchUpdateTags(ids: number[], tags: string[]) {
|
||||||
|
if (!ids || ids.length === 0 || !tags || tags.length === 0) return;
|
||||||
|
|
||||||
|
const products = await this.wpProductModel.find({
|
||||||
|
where: { id: In(ids) },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group by siteId
|
||||||
|
const productsBySite = new Map<number, WpProduct[]>();
|
||||||
|
for (const product of products) {
|
||||||
|
if (!productsBySite.has(product.siteId)) {
|
||||||
|
productsBySite.set(product.siteId, []);
|
||||||
|
}
|
||||||
|
productsBySite.get(product.siteId).push(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [siteId, siteProducts] of productsBySite) {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) continue;
|
||||||
|
|
||||||
|
const batchData = {
|
||||||
|
create: [],
|
||||||
|
update: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const product of siteProducts) {
|
||||||
|
const currentTags = product.tags || [];
|
||||||
|
// Add new tags, avoiding duplicates by name
|
||||||
|
const newTags = [...currentTags];
|
||||||
|
const tagsToAdd = [];
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (!newTags.some(t => t.name === tag)) {
|
||||||
|
const newTagObj = { name: tag, id: 0, slug: '' };
|
||||||
|
newTags.push(newTagObj);
|
||||||
|
tagsToAdd.push(newTagObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagsToAdd.length > 0) {
|
||||||
|
batchData.update.push({
|
||||||
|
id: product.externalProductId,
|
||||||
|
tags: newTags.map(t => (t.id ? { id: t.id } : { name: t.name })),
|
||||||
|
});
|
||||||
|
// Update local DB optimistically
|
||||||
|
// Generate slug simply
|
||||||
|
tagsToAdd.forEach(t => (t.slug = t.name.toLowerCase().replace(/\s+/g, '-')));
|
||||||
|
product.tags = newTags;
|
||||||
|
await this.wpProductModel.save(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchData.update.length > 0) {
|
||||||
|
await this.wpApiService.batchProcessProducts(site, batchData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchUpdateProducts(dto: BatchUpdateProductsDTO) {
|
||||||
|
const { ids, ...updates } = dto;
|
||||||
|
if (!ids || ids.length === 0) return;
|
||||||
|
|
||||||
|
const products = await this.wpProductModel.find({
|
||||||
|
where: { id: In(ids) },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group by siteId
|
||||||
|
const productsBySite = new Map<number, WpProduct[]>();
|
||||||
|
for (const product of products) {
|
||||||
|
if (!productsBySite.has(product.siteId)) {
|
||||||
|
productsBySite.set(product.siteId, []);
|
||||||
|
}
|
||||||
|
productsBySite.get(product.siteId).push(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [siteId, siteProducts] of productsBySite) {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) continue;
|
||||||
|
|
||||||
|
// Resolve Categories if needed
|
||||||
|
let categoryIds: number[] = [];
|
||||||
|
if (updates.categories && updates.categories.length > 0) {
|
||||||
|
// 1. Get all existing categories
|
||||||
|
const allCategories = await this.wpApiService.getCategories(site);
|
||||||
|
const existingCatMap = new Map(allCategories.map(c => [c.name, c.id]));
|
||||||
|
|
||||||
|
// 2. Identify missing categories
|
||||||
|
const missingCategories = updates.categories.filter(name => !existingCatMap.has(name));
|
||||||
|
|
||||||
|
// 3. Create missing categories
|
||||||
|
if (missingCategories.length > 0) {
|
||||||
|
const createPayload = missingCategories.map(name => ({ name }));
|
||||||
|
const createdCatsResult = await this.wpApiService.batchProcessCategories(site, { create: createPayload });
|
||||||
|
if (createdCatsResult && createdCatsResult.create) {
|
||||||
|
createdCatsResult.create.forEach(c => {
|
||||||
|
if (c.id && c.name) existingCatMap.set(c.name, c.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Collect all IDs
|
||||||
|
categoryIds = updates.categories
|
||||||
|
.map(name => existingCatMap.get(name))
|
||||||
|
.filter(id => id !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve Tags if needed
|
||||||
|
let tagIds: number[] = [];
|
||||||
|
if (updates.tags && updates.tags.length > 0) {
|
||||||
|
// 1. Get all existing tags
|
||||||
|
const allTags = await this.wpApiService.getTags(site);
|
||||||
|
const existingTagMap = new Map(allTags.map(t => [t.name, t.id]));
|
||||||
|
|
||||||
|
// 2. Identify missing tags
|
||||||
|
const missingTags = updates.tags.filter(name => !existingTagMap.has(name));
|
||||||
|
|
||||||
|
// 3. Create missing tags
|
||||||
|
if (missingTags.length > 0) {
|
||||||
|
const createPayload = missingTags.map(name => ({ name }));
|
||||||
|
const createdTagsResult = await this.wpApiService.batchProcessTags(site, { create: createPayload });
|
||||||
|
if (createdTagsResult && createdTagsResult.create) {
|
||||||
|
createdTagsResult.create.forEach(t => {
|
||||||
|
if (t.id && t.name) existingTagMap.set(t.name, t.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Collect all IDs
|
||||||
|
tagIds = updates.tags
|
||||||
|
.map(name => existingTagMap.get(name))
|
||||||
|
.filter(id => id !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchData = {
|
||||||
|
create: [],
|
||||||
|
update: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const product of siteProducts) {
|
||||||
|
const updateData: any = {
|
||||||
|
id: product.externalProductId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updates.regular_price) updateData.regular_price = String(updates.regular_price);
|
||||||
|
if (updates.sale_price) updateData.sale_price = String(updates.sale_price);
|
||||||
|
if (updates.status) updateData.status = updates.status;
|
||||||
|
|
||||||
|
if (categoryIds.length > 0) {
|
||||||
|
updateData.categories = categoryIds.map(id => ({ id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagIds.length > 0) {
|
||||||
|
updateData.tags = tagIds.map(id => ({ id }));
|
||||||
|
}
|
||||||
|
|
||||||
|
batchData.update.push(updateData);
|
||||||
|
|
||||||
|
// Optimistic update local DB
|
||||||
|
if (updates.regular_price) product.regular_price = updates.regular_price;
|
||||||
|
if (updates.sale_price) product.sale_price = updates.sale_price;
|
||||||
|
if (updates.status) product.status = updates.status as ProductStatus;
|
||||||
|
if (updates.categories) product.categories = updates.categories.map(c => ({ name: c, id: 0, slug: '' })); // simple mock
|
||||||
|
if (updates.tags) product.tags = updates.tags.map(t => ({ name: t, id: 0, slug: '' })); // simple mock
|
||||||
|
|
||||||
|
await this.wpProductModel.save(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchData.update.length > 0) {
|
||||||
|
await this.wpApiService.batchProcessProducts(site, batchData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importProducts(siteId: number, file: any) {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) throw new Error('站点不存在');
|
||||||
|
|
||||||
|
const parser = fs
|
||||||
|
.createReadStream(file.data)
|
||||||
|
.pipe(parse({
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
trim: true,
|
||||||
|
bom: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
let batch = [];
|
||||||
|
const batchSize = 50;
|
||||||
|
|
||||||
|
for await (const record of parser) {
|
||||||
|
batch.push(record);
|
||||||
|
if (batch.length >= batchSize) {
|
||||||
|
await this.processImportBatch(siteId, site, batch);
|
||||||
|
batch = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.length > 0) {
|
||||||
|
await this.processImportBatch(siteId, site, batch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processImportBatch(siteId: number, site: any, chunk: any[]) {
|
||||||
|
const batchData = {
|
||||||
|
create: [],
|
||||||
|
update: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const row of chunk) {
|
||||||
|
const sku = row['SKU'] || row['sku'];
|
||||||
|
if (!sku) continue;
|
||||||
|
|
||||||
|
const existingProduct = await this.wpProductModel.findOne({
|
||||||
|
where: { siteId, sku }
|
||||||
|
});
|
||||||
|
|
||||||
|
const productData: any = {
|
||||||
|
sku: sku,
|
||||||
|
name: row['Name'] || row['name'],
|
||||||
|
type: (row['Type'] || row['type'] || 'simple').toLowerCase(),
|
||||||
|
regular_price: row['Regular price'] || row['regular_price'],
|
||||||
|
sale_price: row['Sale price'] || row['sale_price'],
|
||||||
|
short_description: row['Short description'] || row['short_description'] || '',
|
||||||
|
description: row['Description'] || row['description'] || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (productData.regular_price) productData.regular_price = String(productData.regular_price);
|
||||||
|
if (productData.sale_price) productData.sale_price = String(productData.sale_price);
|
||||||
|
|
||||||
|
const imagesStr = row['Images'] || row['images'];
|
||||||
|
if (imagesStr) {
|
||||||
|
productData.images = imagesStr.split(',').map(url => ({ src: url.trim() }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingProduct) {
|
||||||
|
batchData.update.push({
|
||||||
|
id: existingProduct.externalProductId,
|
||||||
|
...productData
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
batchData.create.push(productData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchData.create.length > 0 || batchData.update.length > 0) {
|
||||||
|
try {
|
||||||
|
const result = await this.wpApiService.batchProcessProducts(site, batchData);
|
||||||
|
await this.syncBackFromBatchResult(siteId, result);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Batch process error during import:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncBackFromBatchResult(siteId: number, result: any) {
|
||||||
|
const processResultItem = async (item: any) => {
|
||||||
|
if (item.id) {
|
||||||
|
await this.syncProductAndVariations(siteId, item, []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.create) {
|
||||||
|
for (const item of result.create) {
|
||||||
|
await processResultItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.update) {
|
||||||
|
for (const item of result.update) {
|
||||||
|
await processResultItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 同步产品库存到 Site
|
||||||
|
async syncProductStockToSite(siteId: number, sku: string) {
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (!site) throw new Error('站点不存在');
|
||||||
|
|
||||||
|
// 获取站点绑定的仓库
|
||||||
|
if (!site.stockPoints || site.stockPoints.length === 0) {
|
||||||
|
console.log(`站点 ${siteId} 未绑定任何仓库,跳过库存同步`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取产品在这些仓库的总库存
|
||||||
|
const stockPointIds = site.stockPoints.map(sp => sp.id);
|
||||||
|
const stock = await this.stockService.stockModel
|
||||||
|
.createQueryBuilder('stock')
|
||||||
|
.select('SUM(stock.quantity)', 'total')
|
||||||
|
.where('stock.sku = :sku', { sku })
|
||||||
|
.andWhere('stock.stockPointId IN (:...stockPointIds)', { stockPointIds })
|
||||||
|
.getRawOne();
|
||||||
|
|
||||||
|
const quantity = stock && stock.total ? Number(stock.total) : 0;
|
||||||
|
const stockStatus = quantity > 0 ? ProductStockStatus.INSTOCK : ProductStockStatus.OUT_OF_STOCK;
|
||||||
|
|
||||||
|
// 查找对应的 WpProduct 以获取 externalProductId
|
||||||
|
const wpProduct = await this.wpProductModel.findOne({ where: { siteId, sku } });
|
||||||
|
if (wpProduct) {
|
||||||
|
// 更新 WooCommerce 库存
|
||||||
|
await this.wpApiService.updateProductStock(site, wpProduct.externalProductId, quantity, stockStatus);
|
||||||
|
|
||||||
|
// 更新本地 WpProduct 状态
|
||||||
|
wpProduct.stock_quantity = quantity;
|
||||||
|
wpProduct.stockStatus = stockStatus;
|
||||||
|
await this.wpProductModel.save(wpProduct);
|
||||||
|
} else {
|
||||||
|
// 尝试查找变体
|
||||||
|
const variation = await this.variationModel.findOne({ where: { siteId, sku } });
|
||||||
|
if (variation) {
|
||||||
|
await this.wpApiService.updateProductVariationStock(site, variation.externalProductId, variation.externalVariationId, quantity, stockStatus);
|
||||||
|
// 变体表目前没有 stock_quantity 字段,如果需要可以添加
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 同步一个网站
|
// 同步一个网站
|
||||||
async syncSite(siteId: number) {
|
async syncSite(siteId: number) {
|
||||||
|
try {
|
||||||
// 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
|
// 通过数据库获取站点并转换为 WpSite,用于后续 WooCommerce 同步
|
||||||
const site = await this.siteService.get(siteId, true);
|
const site = await this.siteService.get(siteId, true);
|
||||||
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
|
const externalProductIds = this.wpProductModel.createQueryBuilder('wp_product')
|
||||||
|
|
@ -63,7 +572,10 @@ export class WpProductService {
|
||||||
const excludeValues = [];
|
const excludeValues = [];
|
||||||
|
|
||||||
const products = await this.wpApiService.getProducts(site);
|
const products = await this.wpApiService.getProducts(site);
|
||||||
|
let successCount = 0;
|
||||||
|
let failureCount = 0;
|
||||||
for (const product of products) {
|
for (const product of products) {
|
||||||
|
try {
|
||||||
excludeValues.push(String(product.id));
|
excludeValues.push(String(product.id));
|
||||||
const variations =
|
const variations =
|
||||||
product.type === 'variable'
|
product.type === 'variable'
|
||||||
|
|
@ -71,6 +583,11 @@ export class WpProductService {
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
await this.syncProductAndVariations(site.id, product, variations);
|
await this.syncProductAndVariations(site.id, product, variations);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`同步产品 ${product.id} 失败:`, error);
|
||||||
|
failureCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredIds = externalIds.filter(id => !excludeValues.includes(id));
|
const filteredIds = externalIds.filter(id => !excludeValues.includes(id));
|
||||||
|
|
@ -87,6 +604,16 @@ export class WpProductService {
|
||||||
.where('wp_product.siteId = :siteId AND wp_product.externalProductId IN (:...filteredId)', { siteId, filteredId: filteredIds })
|
.where('wp_product.siteId = :siteId AND wp_product.externalProductId IN (:...filteredId)', { siteId, filteredId: filteredIds })
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
success: failureCount === 0,
|
||||||
|
successCount,
|
||||||
|
failureCount,
|
||||||
|
message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步站点产品失败:', error);
|
||||||
|
return { success: false, successCount: 0, failureCount: 0, message: `同步失败: ${error.message}` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 控制产品上下架
|
// 控制产品上下架
|
||||||
|
|
@ -130,11 +657,23 @@ export class WpProductService {
|
||||||
) {
|
) {
|
||||||
let existingProduct = await this.findProduct(siteId, productId);
|
let existingProduct = await this.findProduct(siteId, productId);
|
||||||
if (existingProduct) {
|
if (existingProduct) {
|
||||||
existingProduct.name = product.name;
|
if (product.name) existingProduct.name = product.name;
|
||||||
existingProduct.sku = product.sku;
|
if (product.sku !== undefined) existingProduct.sku = product.sku;
|
||||||
product.regular_price &&
|
if (product.regular_price !== undefined && product.regular_price !== null) {
|
||||||
(existingProduct.regular_price = product.regular_price);
|
existingProduct.regular_price = product.regular_price;
|
||||||
product.sale_price && (existingProduct.sale_price = product.sale_price);
|
}
|
||||||
|
if (product.sale_price !== undefined && product.sale_price !== null) {
|
||||||
|
existingProduct.sale_price = product.sale_price;
|
||||||
|
}
|
||||||
|
if (product.on_sale !== undefined) {
|
||||||
|
existingProduct.on_sale = product.on_sale;
|
||||||
|
}
|
||||||
|
if (product.tags) {
|
||||||
|
existingProduct.tags = product.tags as any;
|
||||||
|
}
|
||||||
|
if (product.categories) {
|
||||||
|
existingProduct.categories = product.categories as any;
|
||||||
|
}
|
||||||
await this.wpProductModel.save(existingProduct);
|
await this.wpProductModel.save(existingProduct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,10 +693,12 @@ export class WpProductService {
|
||||||
if (existingVariation) {
|
if (existingVariation) {
|
||||||
existingVariation.name = variation.name;
|
existingVariation.name = variation.name;
|
||||||
existingVariation.sku = variation.sku;
|
existingVariation.sku = variation.sku;
|
||||||
variation.regular_price &&
|
if (variation.regular_price !== undefined && variation.regular_price !== null) {
|
||||||
(existingVariation.regular_price = variation.regular_price);
|
existingVariation.regular_price = variation.regular_price;
|
||||||
variation.sale_price &&
|
}
|
||||||
(existingVariation.sale_price = variation.sale_price);
|
if (variation.sale_price !== undefined && variation.sale_price !== null) {
|
||||||
|
existingVariation.sale_price = variation.sale_price;
|
||||||
|
}
|
||||||
await this.variationModel.save(existingVariation);
|
await this.variationModel.save(existingVariation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,9 +716,12 @@ export class WpProductService {
|
||||||
existingProduct.status = product.status;
|
existingProduct.status = product.status;
|
||||||
existingProduct.type = product.type;
|
existingProduct.type = product.type;
|
||||||
existingProduct.sku = product.sku;
|
existingProduct.sku = product.sku;
|
||||||
product.regular_price &&
|
if (product.regular_price !== undefined && product.regular_price !== null && String(product.regular_price) !== '') {
|
||||||
(existingProduct.regular_price = product.regular_price);
|
existingProduct.regular_price = Number(product.regular_price);
|
||||||
product.sale_price && (existingProduct.sale_price = product.sale_price);
|
}
|
||||||
|
if (product.sale_price !== undefined && product.sale_price !== null && String(product.sale_price) !== '') {
|
||||||
|
existingProduct.sale_price = Number(product.sale_price);
|
||||||
|
}
|
||||||
existingProduct.on_sale = product.on_sale;
|
existingProduct.on_sale = product.on_sale;
|
||||||
existingProduct.metadata = product.metadata;
|
existingProduct.metadata = product.metadata;
|
||||||
existingProduct.tags = product.tags;
|
existingProduct.tags = product.tags;
|
||||||
|
|
@ -192,9 +736,9 @@ export class WpProductService {
|
||||||
name: product.name,
|
name: product.name,
|
||||||
type: product.type,
|
type: product.type,
|
||||||
...(product.regular_price
|
...(product.regular_price
|
||||||
? { regular_price: product.regular_price }
|
? { regular_price: Number(product.regular_price) }
|
||||||
: {}),
|
: {}),
|
||||||
...(product.sale_price ? { sale_price: product.sale_price } : {}),
|
...(product.sale_price ? { sale_price: Number(product.sale_price) } : {}),
|
||||||
on_sale: product.on_sale,
|
on_sale: product.on_sale,
|
||||||
metadata: product.metadata,
|
metadata: product.metadata,
|
||||||
tags: product.tags,
|
tags: product.tags,
|
||||||
|
|
@ -203,6 +747,8 @@ export class WpProductService {
|
||||||
await this.wpProductModel.save(existingProduct);
|
await this.wpProductModel.save(existingProduct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.ensureSiteSku(product.sku, siteId, product.type);
|
||||||
|
|
||||||
// 2. 处理变体同步
|
// 2. 处理变体同步
|
||||||
if (product.type === 'variable') {
|
if (product.type === 'variable') {
|
||||||
const currentVariations = await this.variationModel.find({
|
const currentVariations = await this.variationModel.find({
|
||||||
|
|
@ -219,6 +765,7 @@ export class WpProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const variation of variations) {
|
for (const variation of variations) {
|
||||||
|
await this.ensureSiteSku(variation.sku, siteId);
|
||||||
const existingVariation = await this.findVariation(
|
const existingVariation = await this.findVariation(
|
||||||
siteId,
|
siteId,
|
||||||
String(product.id),
|
String(product.id),
|
||||||
|
|
@ -264,6 +811,7 @@ export class WpProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncVariation(siteId: number, productId: string, variation: Variation) {
|
async syncVariation(siteId: number, productId: string, variation: Variation) {
|
||||||
|
await this.ensureSiteSku(variation.sku, siteId);
|
||||||
let existingProduct = await this.findProduct(siteId, String(productId));
|
let existingProduct = await this.findProduct(siteId, String(productId));
|
||||||
if (!existingProduct) return;
|
if (!existingProduct) return;
|
||||||
const existingVariation = await this.variationModel.findOne({
|
const existingVariation = await this.variationModel.findOne({
|
||||||
|
|
@ -303,7 +851,7 @@ export class WpProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProductList(param: QueryWpProductDTO) {
|
async getProductList(param: QueryWpProductDTO) {
|
||||||
const { current = 1, pageSize = 10, name, siteId, status } = param;
|
const { current = 1, pageSize = 10, name, siteId, status, skus } = param;
|
||||||
// 第一步:先查询分页的产品
|
// 第一步:先查询分页的产品
|
||||||
const where: any = {};
|
const where: any = {};
|
||||||
if (siteId) {
|
if (siteId) {
|
||||||
|
|
@ -317,6 +865,65 @@ export class WpProductService {
|
||||||
if (status) {
|
if (status) {
|
||||||
where.status = status;
|
where.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (skus && skus.length > 0) {
|
||||||
|
// 查找 WpProduct 中匹配的 SKU
|
||||||
|
const wpProducts = await this.wpProductModel.find({
|
||||||
|
select: ['id'],
|
||||||
|
where: { sku: In(skus), on_delete: false },
|
||||||
|
});
|
||||||
|
let ids = wpProducts.map(p => p.id);
|
||||||
|
|
||||||
|
// 查找 Variation 中匹配的 SKU,并获取对应的 WpProduct
|
||||||
|
const variations = await this.variationModel.find({
|
||||||
|
select: ['siteId', 'externalProductId'],
|
||||||
|
where: { sku: In(skus), on_delete: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (variations.length > 0) {
|
||||||
|
const variationParentConditions = variations.map(v => ({
|
||||||
|
siteId: v.siteId,
|
||||||
|
externalProductId: v.externalProductId,
|
||||||
|
on_delete: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 这里不能直接用 In,因为是 siteId 和 externalProductId 的组合键
|
||||||
|
// 可以用 OR 条件查询对应的 WpProduct ID
|
||||||
|
// 或者,更简单的是,如果我们能获取到 ids...
|
||||||
|
// 既然 variationParentConditions 可能是多个,我们可以分批查或者构造查询
|
||||||
|
|
||||||
|
// 使用 QueryBuilder 查 ID
|
||||||
|
if (variationParentConditions.length > 0) {
|
||||||
|
const qb = this.wpProductModel.createQueryBuilder('wp_product')
|
||||||
|
.select('wp_product.id');
|
||||||
|
|
||||||
|
qb.where('1=0'); // Start with false
|
||||||
|
|
||||||
|
variationParentConditions.forEach((cond, index) => {
|
||||||
|
qb.orWhere(`(wp_product.siteId = :siteId${index} AND wp_product.externalProductId = :epid${index} AND wp_product.on_delete = :del${index})`, {
|
||||||
|
[`siteId${index}`]: cond.siteId,
|
||||||
|
[`epid${index}`]: cond.externalProductId,
|
||||||
|
[`del${index}`]: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentProducts = await qb.getMany();
|
||||||
|
ids = [...ids, ...parentProducts.map(p => p.id)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
current,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
where.id = In([...new Set(ids)]);
|
||||||
|
}
|
||||||
|
|
||||||
where.on_delete = false;
|
where.on_delete = false;
|
||||||
|
|
||||||
const products = await this.wpProductModel.find({
|
const products = await this.wpProductModel.find({
|
||||||
|
|
@ -343,12 +950,12 @@ export class WpProductService {
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
Product,
|
Product,
|
||||||
'product',
|
'product',
|
||||||
'JSON_UNQUOTE(JSON_EXTRACT(wp_product.constitution, "$.sku")) = product.sku'
|
'wp_product.sku = product.sku'
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
Product,
|
Product,
|
||||||
'variation_product',
|
'variation_product',
|
||||||
'JSON_UNQUOTE(JSON_EXTRACT(variation.constitution, "$.sku")) = variation_product.sku'
|
'variation.sku = variation_product.sku'
|
||||||
)
|
)
|
||||||
.select([
|
.select([
|
||||||
'wp_product.*',
|
'wp_product.*',
|
||||||
|
|
@ -362,7 +969,6 @@ export class WpProductService {
|
||||||
'variation.regular_price as variation_regular_price',
|
'variation.regular_price as variation_regular_price',
|
||||||
'variation.sale_price as variation_sale_price',
|
'variation.sale_price as variation_sale_price',
|
||||||
'variation.on_sale as variation_on_sale',
|
'variation.on_sale as variation_on_sale',
|
||||||
'variation.constitution as variation_constitution',
|
|
||||||
'product.name as product_name', // 关联查询返回 product.name
|
'product.name as product_name', // 关联查询返回 product.name
|
||||||
'variation_product.name as variation_product_name', // 关联查询返回 variation 的产品 name
|
'variation_product.name as variation_product_name', // 关联查询返回 variation 的产品 name
|
||||||
])
|
])
|
||||||
|
|
@ -401,25 +1007,10 @@ export class WpProductService {
|
||||||
obj[key.replace('variation_', '')] = row[key];
|
obj[key.replace('variation_', '')] = row[key];
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
variation.constitution =
|
|
||||||
variation?.constitution?.map(item => {
|
|
||||||
const product = item.sku
|
|
||||||
? { ...item, name: row.variation_product_name }
|
|
||||||
: item;
|
|
||||||
return product;
|
|
||||||
}) || [];
|
|
||||||
|
|
||||||
product.variations.push(variation);
|
product.variations.push(variation);
|
||||||
}
|
}
|
||||||
|
|
||||||
product.constitution =
|
|
||||||
product?.constitution?.map(item => {
|
|
||||||
const productWithName = item.sku
|
|
||||||
? { ...item, name: row.product_name }
|
|
||||||
: item;
|
|
||||||
return productWithName;
|
|
||||||
}) || [];
|
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -472,31 +1063,11 @@ export class WpProductService {
|
||||||
return !!variationDuplicate;
|
return !!variationDuplicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async deleteById(id: number) {
|
||||||
* 设置产品或变体的构成成分
|
|
||||||
*/
|
|
||||||
async setConstitution(
|
|
||||||
id: number,
|
|
||||||
isProduct: boolean,
|
|
||||||
constitution: { sku: string; quantity: number }[]
|
|
||||||
): Promise<void> {
|
|
||||||
if (isProduct) {
|
|
||||||
// 更新产品的 constitution
|
|
||||||
const product = await this.wpProductModel.findOne({ where: { id } });
|
const product = await this.wpProductModel.findOne({ where: { id } });
|
||||||
if (!product) {
|
if (!product) throw new Error('产品不存在');
|
||||||
throw new Error(`未找到 ID 为 ${id} 的产品`);
|
await this.delWpProduct(product.siteId, product.externalProductId);
|
||||||
}
|
return true;
|
||||||
product.constitution = constitution;
|
|
||||||
await this.wpProductModel.save(product);
|
|
||||||
} else {
|
|
||||||
// 更新变体的 constitution
|
|
||||||
const variation = await this.variationModel.findOne({ where: { id } });
|
|
||||||
if (!variation) {
|
|
||||||
throw new Error(`未找到 ID 为 ${id} 的变体`);
|
|
||||||
}
|
|
||||||
variation.constitution = constitution;
|
|
||||||
await this.variationModel.save(variation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delWpProduct(siteId: number, productId: string) {
|
async delWpProduct(siteId: number, productId: string) {
|
||||||
|
|
@ -559,4 +1130,85 @@ export class WpProductService {
|
||||||
|
|
||||||
return await query.getMany();
|
return await query.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncToProduct(wpProductId: number) {
|
||||||
|
const wpProduct = await this.wpProductModel.findOne({ where: { id: wpProductId }, relations: ['site'] });
|
||||||
|
if (!wpProduct) throw new Error('WpProduct not found');
|
||||||
|
|
||||||
|
const sku = wpProduct.sku;
|
||||||
|
if (!sku) throw new Error('WpProduct has no SKU');
|
||||||
|
|
||||||
|
// Try to find by main SKU
|
||||||
|
let product = await this.productModel.findOne({ where: { sku } });
|
||||||
|
|
||||||
|
// If not found, try to remove prefix if site has one
|
||||||
|
if (!product && wpProduct.site && wpProduct.site.skuPrefix && sku.startsWith(wpProduct.site.skuPrefix)) {
|
||||||
|
const skuWithoutPrefix = sku.slice(wpProduct.site.skuPrefix.length);
|
||||||
|
product = await this.productModel.findOne({ where: { sku: skuWithoutPrefix } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still not found, try siteSkus
|
||||||
|
if (!product) {
|
||||||
|
const siteSku = await this.productSiteSkuModel.findOne({ where: { code: sku }, relations: ['product'] });
|
||||||
|
if (siteSku) {
|
||||||
|
product = siteSku.product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new Error('Local Product not found for SKU: ' + sku);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (wpProduct.regular_price) product.price = Number(wpProduct.regular_price);
|
||||||
|
if (wpProduct.sale_price) product.promotionPrice = Number(wpProduct.sale_price);
|
||||||
|
|
||||||
|
await this.productModel.save(product);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保 SKU 存在于 ProductSiteSku 中,并根据 WpProduct 类型更新 Product 类型
|
||||||
|
* @param sku
|
||||||
|
* @param siteId 站点ID,用于去除前缀
|
||||||
|
* @param wpType WpProduct 类型
|
||||||
|
*/
|
||||||
|
private async ensureSiteSku(sku: string, siteId?: number, wpType?: string) {
|
||||||
|
if (!sku) return;
|
||||||
|
// 查找本地产品
|
||||||
|
let product = await this.productModel.findOne({ where: { sku } });
|
||||||
|
|
||||||
|
if (!product && siteId) {
|
||||||
|
// 如果找不到且有 siteId,尝试去除前缀再查找
|
||||||
|
const site = await this.siteService.get(siteId, true);
|
||||||
|
if (site && site.skuPrefix && sku.startsWith(site.skuPrefix)) {
|
||||||
|
const skuWithoutPrefix = sku.slice(site.skuPrefix.length);
|
||||||
|
product = await this.productModel.findOne({ where: { sku: skuWithoutPrefix } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product) {
|
||||||
|
// 更新产品类型
|
||||||
|
if (wpType) {
|
||||||
|
// simple 对应 single, 其他对应 bundle
|
||||||
|
const targetType = wpType === 'simple' ? 'single' : 'bundle';
|
||||||
|
if (product.type !== targetType) {
|
||||||
|
product.type = targetType;
|
||||||
|
await this.productModel.save(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在 ProductSiteSku
|
||||||
|
const existingSiteSku = await this.productSiteSkuModel.findOne({
|
||||||
|
where: { productId: product.id, code: sku },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingSiteSku) {
|
||||||
|
await this.productSiteSkuModel.save({
|
||||||
|
productId: product.id,
|
||||||
|
code: sku,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
export function generateTestDataFromEta(template: string): Record<string, any> {
|
||||||
|
const data: Record<string, any> = {};
|
||||||
|
|
||||||
|
const tagRegex = /<%[\-=]?([\s\S]*?)%>/g;
|
||||||
|
const itPathRegex = /\bit\.([a-zA-Z0-9_$.\[\]]+)/g;
|
||||||
|
|
||||||
|
const setPath = (path: string) => {
|
||||||
|
const parts: Array<string | number> = [];
|
||||||
|
path.split('.').forEach((segment) => {
|
||||||
|
const arrMatch = segment.match(/^([a-zA-Z0-9_\$]+)(\[(\d+)\])?$/);
|
||||||
|
if (arrMatch) {
|
||||||
|
parts.push(arrMatch[1]);
|
||||||
|
if (arrMatch[3] !== undefined) {
|
||||||
|
parts.push(Number(arrMatch[3]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts.push(segment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let cursor: any = data;
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const key = parts[i];
|
||||||
|
const next = parts[i + 1];
|
||||||
|
const isArrayIndex = typeof key === 'number';
|
||||||
|
|
||||||
|
if (isArrayIndex) {
|
||||||
|
if (!Array.isArray(cursor)) {
|
||||||
|
cursor = [];
|
||||||
|
}
|
||||||
|
if (!cursor[key]) cursor[key] = {};
|
||||||
|
cursor = cursor[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next === undefined) {
|
||||||
|
// leaf default value
|
||||||
|
cursor[key as string] = cursor[key as string] ?? 'sample';
|
||||||
|
} else if (typeof next === 'number') {
|
||||||
|
if (!Array.isArray(cursor[key as string])) cursor[key as string] = [];
|
||||||
|
if (!cursor[key as string][next]) cursor[key as string][next] = {};
|
||||||
|
cursor = cursor[key as string][next];
|
||||||
|
} else {
|
||||||
|
if (cursor[key as string] == null || typeof cursor[key as string] !== 'object') {
|
||||||
|
cursor[key as string] = {};
|
||||||
|
}
|
||||||
|
cursor = cursor[key as string];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = tagRegex.exec(template)) !== null) {
|
||||||
|
const inside = m[1];
|
||||||
|
let mm: RegExpExecArray | null;
|
||||||
|
while ((mm = itPathRegex.exec(inside)) !== null) {
|
||||||
|
const raw = mm[1];
|
||||||
|
// ignore method calls like it.arr.forEach -> we only keep path before method
|
||||||
|
const cleaned = raw.replace(/\b(forEach|map|filter|reduce|find|some|every|slice|splice)\b.*$/, '');
|
||||||
|
if (cleaned) setPath(cleaned);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
|
|
||||||
import { App, IMidwayApplication, Inject } from '@midwayjs/core';
|
|
||||||
import { Framework } from '@midwayjs/koa';
|
|
||||||
import { Bootstrap } from '@midwayjs/bootstrap';
|
|
||||||
import { Product } from './src/entity/product.entity';
|
|
||||||
import { WpProduct } from './src/entity/wp_product.entity';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
||||||
|
|
||||||
@App()
|
|
||||||
class TestApp {
|
|
||||||
@InjectEntityModel(Product)
|
|
||||||
productModel: Repository<Product>;
|
|
||||||
|
|
||||||
@InjectEntityModel(WpProduct)
|
|
||||||
wpProductModel: Repository<WpProduct>;
|
|
||||||
|
|
||||||
async run() {
|
|
||||||
if (!this.productModel || !this.wpProductModel) {
|
|
||||||
console.log('Models not injected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const productCount = await this.productModel.count();
|
|
||||||
const wpProductCount = await this.wpProductModel.count();
|
|
||||||
console.log(`Product Count: ${productCount}`);
|
|
||||||
console.log(`WpProduct Count: ${wpProductCount}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Bootstrap.run().then(async () => {
|
|
||||||
const app = await Bootstrap.getApplication() as IMidwayApplication<any>;
|
|
||||||
const testApp = await app.getApplicationContext().getAsync(TestApp);
|
|
||||||
await testApp.run();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
@ -20,5 +20,5 @@
|
||||||
"inlineSources": true // ✅ 把源码嵌入 map 文件,方便 VS Code 还原
|
"inlineSources": true // ✅ 把源码嵌入 map 文件,方便 VS Code 还原
|
||||||
|
|
||||||
},
|
},
|
||||||
"exclude": ["*.js", "*.ts", "dist", "node_modules", "test"]
|
"exclude": ["*.js", "*.ts", "dist", "node_modules", "test", "scripts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue