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; @InjectEntityModel(CustomerTag) customerTagModel: Repository; @InjectEntityModel(Customer) customerModel: Repository; @Inject() siteApiService: SiteApiService; /** * 根据邮箱查找客户 */ async findCustomerByEmail(email: string): Promise { return await this.customerModel.findOne({ where: { email } }); } /** * 将站点客户数据映射为本地客户实体数据 * 处理字段映射和数据转换,确保所有字段正确同步 */ private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial { 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): Promise { const customer = this.customerModel.create(customerData); return await this.customerModel.save(customer); } /** * 更新客户信息 */ async updateCustomer(id: number, customerData: Partial): Promise { await this.customerModel.update(id, customerData); return await this.customerModel.findOne({ where: { id } }); } /** * 创建或更新客户(upsert) * 如果客户存在则更新,不存在则创建 */ async upsertCustomer( customerData: Partial, ): 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> ): 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 { 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) { 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): Promise{ 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 }); } }