460 lines
13 KiB
TypeScript
460 lines
13 KiB
TypeScript
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 });
|
||
}
|
||
} |