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

529 lines
15 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 { Inject, Provide } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { SyncOperationResult, UnifiedPaginationDTO, UnifiedSearchParamsDTO, BatchOperationResult } from '../dto/api.dto';
import { UnifiedCustomerDTO } from '../dto/site-api.dto';
import { Customer } from '../entity/customer.entity';
import { CustomerTag } from '../entity/customer_tag.entity';
import { Order } from '../entity/order.entity';
import { SiteApiService } from './site-api.service';
import { CreateCustomerDTO, CustomerDTO, CustomerStatisticDTO, CustomerStatisticQueryParamsDTO } from '../dto/customer.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<CreateCustomerDTO> {
return {
site_id: siteId, // 使用站点ID而不是客户ID
site_created_at: this.parseDate(siteCustomer.date_created),
site_updated_at: this.parseDate(siteCustomer.date_modified),
origin_id: Number(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,
};
}
/**
* 解析日期字符串或时间戳
*/
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<BatchOperationResult> {
const results = {
total: customersData.length,
processed: 0,
created: 0,
updated: 0,
errors: []
};
// 批量处理每个客户
for (const customerData of customersData) {
try {
const result = await this.upsertCustomer(customerData);
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
});
results.processed++;
}
}
return results;
}
/**
* 从站点同步客户数据
* 第一步调用adapter获取站点客户数据
* 第二步通过upsertManyCustomers保存这些客户
*/
async syncCustomersFromSite(
siteId: number,
params?: UnifiedSearchParamsDTO
): Promise<SyncOperationResult> {
try {
// 第一步:获取适配器并从站点获取客户数据
const adapter = await this.siteApiService.getAdapter(siteId);
const siteCustomers = await adapter.getAllCustomers(params || {});
// 第二步:将站点客户数据转换为客户实体数据
const customersData = siteCustomers.map(siteCustomer => {
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
}).map(customer => ({
...customer,
origin_id: String(customer.origin_id),
}));
// 第三步批量upsert客户数据
const upsertResult = await this.upsertManyCustomers(customersData);
return {
total: siteCustomers.length,
processed: upsertResult.processed,
synced: upsertResult.processed,
updated: upsertResult.updated,
created: upsertResult.created,
errors: upsertResult.errors
};
} catch (error) {
// 如果获取适配器或站点数据失败,抛出错误
throw new Error(`同步客户数据失败: ${error.message}`);
}
}
/**
* 获取客户统计列表(包含订单统计信息)
* 支持分页、搜索和排序功能
* 使用原生SQL查询实现复杂的统计逻辑
*/
async getCustomerStatisticList(param: CustomerStatisticQueryParamsDTO): Promise<{
items: CustomerStatisticDTO[];
total: number;
current: number;
pageSize: number;
}> {
const {
page = 1,
per_page = 10,
search,
where,
orderBy,
} = param;
// 将page和per_page转换为current和pageSize
const current = page;
const pageSize = per_page;
const whereConds: string[] = [];
const havingConds: string[] = [];
// 全局搜索关键词
if (search) {
whereConds.push(`o.customer_email LIKE '%${search}%'`);
}
// where条件过滤
if (where) {
// 邮箱搜索
if (where.email) {
whereConds.push(`o.customer_email LIKE '%${where.email}%'`);
}
// customerId 过滤
if (where.customerId) {
whereConds.push(`c.id = ${Number(where.customerId)}`);
}
// rate 过滤
if (where.rate) {
whereConds.push(`c.rate = ${Number(where.rate)}`);
}
// tags 过滤
if (where.tags) {
const tagList = where.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 (where.first_purchase_date) {
havingConds.push(
`DATE_FORMAT(MIN(o.date_paid), '%Y-%m') = '${where.first_purchase_date}'`
);
}
}
// 公用过滤
const baseQuery = `
${whereConds.length ? `WHERE ${whereConds.join(' AND ')}` : ''}
GROUP BY o.customer_email
${havingConds.length ? `HAVING ${havingConds.join(' AND ')}` : ''}
`;
// 排序处理
let orderByClause = '';
if (orderBy) {
if (typeof orderBy === 'string') {
const [field, direction] = orderBy.split(':');
orderByClause = `ORDER BY ${field} ${direction === 'desc' ? 'DESC' : 'ASC'}`;
} else if (typeof orderBy === 'object') {
const orderClauses = Object.entries(orderBy).map(([field, direction]) =>
`${field} ${direction === 'desc' ? 'DESC' : 'ASC'}`
);
orderByClause = `ORDER BY ${orderClauses.join(', ')}`;
}
} else {
orderByClause = 'ORDER BY orders ASC, yoone_total DESC';
}
// 主查询
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}
${orderByClause}
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;
// 处理tags字段将JSON字符串转换为数组
const processedItems = items.map(item => {
if (item.tags) {
try {
item.tags = JSON.parse(item.tags);
} catch (e) {
item.tags = [];
}
} else {
item.tags = [];
}
return item;
});
return {
items: processedItems,
total,
current,
pageSize,
};
}
/**
* 获取纯粹的客户列表(不包含订单统计信息)
* 支持基本的分页、搜索和排序功能
* 使用TypeORM查询构建器实现
*/
async getCustomerList(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<CustomerDTO>>{
const {
page = 1,
per_page = 20,
where ={},
} = params;
// 查询客户列表和总数
const [customers, total] = await this.customerModel.findAndCount({
where,
// order: orderBy,
skip: (page - 1) * per_page,
take: per_page,
});
// 获取所有客户的邮箱列表
const emailList = customers.map(customer => customer.email);
// 查询所有客户的标签
let customerTagsMap: Record<string, string[]> = {};
if (emailList.length > 0) {
const customerTags = await this.customerTagModel
.createQueryBuilder('tag')
.select('tag.email', 'email')
.addSelect('tag.tag', 'tag')
.where('tag.email IN (:...emailList)', { emailList })
.getRawMany();
// 将标签按邮箱分组
customerTagsMap = customerTags.reduce((acc, item) => {
if (!acc[item.email]) {
acc[item.email] = [];
}
acc[item.email].push(item.tag);
return acc;
}, {} as Record<string, string[]>);
}
// 将标签合并到客户数据中
const customersWithTags = customers.map(customer => ({
...customer,
tags: customerTagsMap[customer.email] || []
}));
return {
items: customersWithTags,
total,
page,
per_page,
totalPages: Math.ceil(total / per_page),
};
}
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 });
}
/**
* 批量更新客户
* 每个客户可以有独立的更新字段
* 支持对多个客户进行统一化修改或分别更新
*/
async batchUpdateCustomers(
updateItems: Array<{ id: number; update_data: Partial<Customer> }>
): Promise<BatchOperationResult> {
const results = {
total: updateItems.length,
processed: 0,
updated: 0,
errors: []
};
// 批量处理每个客户的更新
for (const item of updateItems) {
try {
// 检查客户是否存在
const existingCustomer = await this.customerModel.findOne({ where: { id: item.id } });
if (!existingCustomer) {
throw new Error(`客户ID ${item.id} 不存在`);
}
// 更新客户信息
await this.updateCustomer(item.id, item.update_data);
results.updated++;
results.processed++;
} catch (error) {
// 记录错误但不中断整个批量操作
results.errors.push({
identifier: String(item.id),
error: error.message
});
results.processed++;
}
}
return results;
}
/**
* 批量删除客户
* 支持对多个客户进行批量删除操作
* 返回操作结果,包括成功和失败的数量
*/
async batchDeleteCustomers(ids: number[]): Promise<BatchOperationResult> {
const results = {
total: ids.length,
processed: 0,
updated: 0,
errors: []
};
// 批量处理每个客户的删除
for (const id of ids) {
try {
// 检查客户是否存在
const existingCustomer = await this.customerModel.findOne({ where: { id } });
if (!existingCustomer) {
throw new Error(`客户ID ${id} 不存在`);
}
// 删除客户
await this.customerModel.delete(id);
results.updated++;
results.processed++;
} catch (error) {
// 记录错误但不中断整个批量操作
results.errors.push({
identifier: String(id),
error: error.message
});
results.processed++;
}
}
return results;
}
}