zksu
/
API
forked from yoone/API
1
0
Fork 0
API/src/service/customer.service.ts

460 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Provide, Inject } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Order } from '../entity/order.entity';
import { Repository } from 'typeorm';
import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity';
import { SiteApiService } from './site-api.service';
import { UnifiedCustomerDTO, UnifiedSearchParamsDTO } from '../dto/site-api.dto';
import { SyncOperationResult, BatchErrorItem } from '../dto/batch.dto';
@Provide()
export class CustomerService {
@InjectEntityModel(Order)
orderModel: Repository<Order>;
@InjectEntityModel(CustomerTag)
customerTagModel: Repository<CustomerTag>;
@InjectEntityModel(Customer)
customerModel: Repository<Customer>;
@Inject()
siteApiService: SiteApiService;
/**
* 根据邮箱查找客户
*/
async findCustomerByEmail(email: string): Promise<Customer | null> {
return await this.customerModel.findOne({ where: { email } });
}
/**
* 将站点客户数据映射为本地客户实体数据
* 处理字段映射和数据转换,确保所有字段正确同步
*/
private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial<Customer> {
return {
site_id: siteId, // 使用站点ID而不是客户ID
origin_id: "" + siteCustomer.id,
email: siteCustomer.email,
first_name: siteCustomer.first_name,
last_name: siteCustomer.last_name,
fullname: siteCustomer.fullname || `${siteCustomer.first_name || ''} ${siteCustomer.last_name || ''}`.trim(),
username: siteCustomer.username || '',
phone: siteCustomer.phone || '',
avatar: siteCustomer.avatar,
billing: siteCustomer.billing,
shipping: siteCustomer.shipping,
raw: siteCustomer.raw || siteCustomer,
site_created_at: this.parseDate(siteCustomer.date_created),
site_updated_at: this.parseDate(siteCustomer.date_modified)
};
}
/**
* 解析日期字符串或时间戳
*/
private parseDate(dateValue: any): Date | null {
if (!dateValue) return null;
if (dateValue instanceof Date) {
return dateValue;
}
if (typeof dateValue === 'number') {
// 处理Unix时间戳秒或毫秒
return new Date(dateValue > 9999999999 ? dateValue : dateValue * 1000);
}
if (typeof dateValue === 'string') {
const date = new Date(dateValue);
return isNaN(date.getTime()) ? null : date;
}
return null;
}
/**
* 创建新客户
*/
async createCustomer(customerData: Partial<Customer>): Promise<Customer> {
const customer = this.customerModel.create(customerData);
return await this.customerModel.save(customer);
}
/**
* 更新客户信息
*/
async updateCustomer(id: number, customerData: Partial<Customer>): Promise<Customer> {
await this.customerModel.update(id, customerData);
return await this.customerModel.findOne({ where: { id } });
}
/**
* 创建或更新客户upsert
* 如果客户存在则更新,不存在则创建
*/
async upsertCustomer(
customerData: Partial<Customer>,
): Promise<{ customer: Customer; isCreated: boolean }> {
if(!customerData.email) throw new Error("客户邮箱不能为空");
// 首先尝试根据邮箱查找现有客户
const existingCustomer = await this.findCustomerByEmail(customerData.email);
if (existingCustomer) {
// 如果客户存在,更新客户信息
const updatedCustomer = await this.updateCustomer(existingCustomer.id, customerData);
return { customer: updatedCustomer, isCreated: false };
} else {
// 如果客户不存在,创建新客户
const newCustomer = await this.createCustomer(customerData);
return { customer: newCustomer, isCreated: true };
}
}
/**
* 批量创建或更新客户
* 使用事务确保数据一致性
*/
async upsertManyCustomers(
customersData: Array<Partial<Customer>>
): Promise<{
customers: Customer[];
created: number;
updated: number;
processed: number;
errors: BatchErrorItem[];
}> {
const results = {
customers: [],
created: 0,
updated: 0,
processed: 0,
errors: []
};
// 批量处理每个客户
for (const customerData of customersData) {
try {
const result = await this.upsertCustomer(customerData);
results.customers.push(result.customer);
if (result.isCreated) {
results.created++;
} else {
results.updated++;
}
results.processed++;
} catch (error) {
// 记录错误但不中断整个批量操作
results.errors.push({
identifier: customerData.email || String(customerData.id) || 'unknown',
error: error.message
});
}
}
return results;
}
/**
* 从站点同步客户数据
* 第一步调用adapter获取站点客户数据
* 第二步通过upsertManyCustomers保存这些客户
*/
async syncCustomersFromSite(
siteId: number,
params?: UnifiedSearchParamsDTO
): Promise<SyncOperationResult> {
try {
// 第一步:获取适配器并从站点获取客户数据
const adapter = await this.siteApiService.getAdapter(siteId);
const siteCustomersResult = await adapter.getCustomers(params || {});
// 第二步:将站点客户数据转换为客户实体数据
const customersData = siteCustomersResult.items.map(siteCustomer => {
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
});
// 第三步批量upsert客户数据
const upsertResult = await this.upsertManyCustomers(customersData);
return {
total: siteCustomersResult.total,
processed: upsertResult.customers.length,
synced: upsertResult.customers.length,
updated: upsertResult.updated,
created: upsertResult.created,
errors: upsertResult.errors
};
} catch (error) {
// 如果获取适配器或站点数据失败,抛出错误
throw new Error(`同步客户数据失败: ${error.message}`);
}
}
async getCustomerStatisticList(param: Record<string, any>) {
const {
current = 1,
pageSize = 10,
email,
tags,
sorterKey,
sorterValue,
state,
first_purchase_date,
customerId,
rate,
} = param;
const whereConds: string[] = [];
const havingConds: string[] = [];
// 邮箱搜索
if (email) {
whereConds.push(`o.customer_email LIKE '%${email}%'`);
}
// 省份搜索
if (state) {
whereConds.push(
`JSON_UNQUOTE(JSON_EXTRACT(o.billing, '$.state')) = '${state}'`
);
}
// customerId 过滤
if (customerId) {
whereConds.push(`c.id = ${Number(customerId)}`);
}
// rate 过滤
if (rate) {
whereConds.push(`c.rate = ${Number(rate)}`);
}
// tags 过滤
if (tags) {
const tagList = tags
.split(',')
.map(tag => `'${tag.trim()}'`)
.join(',');
havingConds.push(`
EXISTS (
SELECT 1 FROM customer_tag ct
WHERE ct.email = o.customer_email
AND ct.tag IN (${tagList})
)
`);
}
// 首次购买时间过滤
if (first_purchase_date) {
havingConds.push(
`DATE_FORMAT(MIN(o.date_paid), '%Y-%m') = '${first_purchase_date}'`
);
}
// 公用过滤
const baseQuery = `
${whereConds.length ? `WHERE ${whereConds.join(' AND ')}` : ''}
GROUP BY o.customer_email
${havingConds.length ? `HAVING ${havingConds.join(' AND ')}` : ''}
`;
// 主查询
const sql = `
SELECT
o.customer_email AS email,
MIN(o.date_created) AS date_created,
MIN(o.date_paid) AS first_purchase_date,
MAX(o.date_paid) AS last_purchase_date,
COUNT(DISTINCT o.id) AS orders,
SUM(o.total) AS total,
ANY_VALUE(o.shipping) AS shipping,
ANY_VALUE(o.billing) AS billing,
(
SELECT JSON_ARRAYAGG(tag)
FROM customer_tag ct
WHERE ct.email = o.customer_email
) AS tags,
c.id AS customerId,
c.rate AS rate,
yoone_stats.yoone_orders,
yoone_stats.yoone_total
FROM \`order\` o
LEFT JOIN customer c ON o.customer_email = c.email
LEFT JOIN (
SELECT
oo.customer_email,
COUNT(DISTINCT oi.orderId) AS yoone_orders,
SUM(oi.total) AS yoone_total
FROM order_item oi
JOIN \`order\` oo ON oi.orderId = oo.id
WHERE oi.name LIKE '%yoone%'
GROUP BY oo.customer_email
) yoone_stats ON yoone_stats.customer_email = o.customer_email
${baseQuery}
${sorterKey
? `ORDER BY ${sorterKey} ${sorterValue === 'descend' ? 'DESC' : 'ASC'}`
: 'ORDER BY orders ASC, yoone_total DESC'}
LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize}
`;
// 统计总数
const countSql = `
SELECT COUNT(*) AS total FROM (
SELECT o.customer_email
FROM \`order\` o
LEFT JOIN customer c ON o.customer_email = c.email
${baseQuery}
) AS sub
`;
const [items, countResult] = await Promise.all([
this.orderModel.query(sql),
this.orderModel.query(countSql),
]);
const total = countResult[0]?.total || 0;
return {
items,
total,
current,
pageSize,
};
}
/**
* 获取纯粹的客户列表(不包含订单统计信息)
* 支持基本的分页、搜索和排序功能
* 使用TypeORM查询构建器实现
*/
async getCustomerList(param: Record<string, any>): Promise<any>{
const {
current = 1,
pageSize = 10,
email,
firstName,
lastName,
phone,
state,
rate,
sorterKey,
sorterValue,
} = param;
// 创建查询构建器
const queryBuilder = this.customerModel
.createQueryBuilder('c')
.leftJoinAndSelect(
'customer_tag',
'ct',
'ct.email = c.email'
)
.select([
'c.id',
'c.email',
'c.first_name',
'c.last_name',
'c.fullname',
'c.username',
'c.phone',
'c.avatar',
'c.billing',
'c.shipping',
'c.rate',
'c.site_id',
'c.created_at',
'c.updated_at',
'c.site_created_at',
'c.site_updated_at',
'GROUP_CONCAT(ct.tag) as tags'
])
.groupBy('c.id');
// 邮箱搜索
if (email) {
queryBuilder.andWhere('c.email LIKE :email', { email: `%${email}%` });
}
// 姓名搜索
if (firstName) {
queryBuilder.andWhere('c.first_name LIKE :firstName', { firstName: `%${firstName}%` });
}
if (lastName) {
queryBuilder.andWhere('c.last_name LIKE :lastName', { lastName: `%${lastName}%` });
}
// 电话搜索
if (phone) {
queryBuilder.andWhere('c.phone LIKE :phone', { phone: `%${phone}%` });
}
// 省份搜索
if (state) {
queryBuilder.andWhere("JSON_UNQUOTE(JSON_EXTRACT(c.billing, '$.state')) = :state", { state });
}
// 评分过滤
if (rate !== undefined && rate !== null) {
queryBuilder.andWhere('c.rate = :rate', { rate: Number(rate) });
}
// 排序处理
if (sorterKey) {
const order = sorterValue === 'descend' ? 'DESC' : 'ASC';
queryBuilder.orderBy(`c.${sorterKey}`, order);
} else {
queryBuilder.orderBy('c.created_at', 'DESC');
}
// 分页
queryBuilder.skip((current - 1) * pageSize).take(pageSize);
// 执行查询
const [items, total] = await queryBuilder.getManyAndCount();
// 处理tags字段将逗号分隔的字符串转换为数组
const processedItems = items.map(item => {
const plainItem = JSON.parse(JSON.stringify(item));
plainItem.tags = plainItem.tags ? plainItem.tags.split(',').filter(tag => tag) : [];
return plainItem;
});
return {
items: processedItems,
total,
current,
pageSize,
};
}
async addTag(email: string, tag: string) {
const isExist = await this.customerTagModel.findOneBy({ email, tag });
if (isExist) throw new Error(`${tag}已存在`);
return await this.customerTagModel.save({ email, tag });
}
async delTag(email: string, tag: string) {
const isExist = await this.customerTagModel.findOneBy({ email, tag });
if (!isExist) throw new Error(`${tag}不存在`);
return await this.customerTagModel.delete({ email, tag });
}
async getTags() {
const tags = await this.customerTagModel
.createQueryBuilder('tag')
.select('DISTINCT tag.tag', 'tag')
.getRawMany();
return tags.map(t => t.tag);
}
async setRate(params: { id: number; rate: number }) {
return await this.customerModel.update(params.id, { rate: params.rate });
}
}