222 lines
9.5 KiB
PHP
222 lines
9.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\adminapi\logic\firstvisit;
|
||
|
||
use app\common\service\DataScope\DataScopeService;
|
||
use app\common\service\qywx\QywxCustomerAcquisitionCustomerService;
|
||
use RuntimeException;
|
||
use think\facade\Db;
|
||
|
||
/** 获客客户同步与数据权限统计。 */
|
||
class WecomAcquisitionCustomerLogic
|
||
{
|
||
/** @return array<string,mixed> */
|
||
public static function sync(array $params, int $adminId, array $adminInfo): array
|
||
{
|
||
$localLinkId = max(0, (int) ($params['promotion_link_id'] ?? $params['id'] ?? 0));
|
||
$query = Db::name('qywx_promotion_link')->alias('l')
|
||
->whereNull('l.delete_time')
|
||
->where('l.remote_link_id', '<>', '')
|
||
->where('l.remote_status', 1);
|
||
self::applyScope(
|
||
$query,
|
||
'l',
|
||
DataScopeService::getVisibleAdminIds($adminId, $adminInfo),
|
||
self::operatorPoolIds($adminId)
|
||
);
|
||
if ($localLinkId > 0) {
|
||
$query->where('l.id', $localLinkId);
|
||
}
|
||
$links = $query->field('l.id,l.remote_link_id')->order('l.id', 'asc')->limit(200)->select()->toArray();
|
||
if ($localLinkId > 0 && $links === []) {
|
||
throw new RuntimeException('获客链接不存在、已失效,或超出当前权限范围');
|
||
}
|
||
if ($links === []) {
|
||
throw new RuntimeException('当前数据范围内没有可同步的有效官方获客链接;已删除和历史手工链接不会参与客户同步,请先创建官方获客链接');
|
||
}
|
||
$service = new QywxCustomerAcquisitionCustomerService();
|
||
$result = ['links' => count($links), 'scanned' => 0, 'created' => 0, 'updated' => 0, 'failed' => 0, 'errors' => []];
|
||
foreach ($links as $link) {
|
||
try {
|
||
$one = $service->syncLink((string) $link['remote_link_id']);
|
||
$result['scanned'] += $one['scanned'];
|
||
$result['created'] += $one['created'];
|
||
$result['updated'] += $one['updated'];
|
||
} catch (\Throwable $e) {
|
||
$result['failed']++;
|
||
if (count($result['errors']) < 10) {
|
||
$result['errors'][] = (string) $link['remote_link_id'] . ':' . $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
public static function statistics(array $params, int $adminId, array $adminInfo): array
|
||
{
|
||
$page = max(1, (int) ($params['page_no'] ?? $params['page'] ?? 1));
|
||
$pageSize = min(100, max(1, (int) ($params['page_size'] ?? 20)));
|
||
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
|
||
$operatorPoolIds = self::operatorPoolIds($adminId);
|
||
$base = self::customerQuery($params, $visibleIds, $operatorPoolIds);
|
||
$total = (int) (clone $base)->count();
|
||
$rows = $base
|
||
->field('c.id,c.promotion_link_id,c.link_id,c.external_userid,c.userid,c.owner_admin_id,c.dept_id,c.state,c.chat_status,c.recv_msg_cnt,c.message_count_known,c.first_acquired_time,c.last_chat_time,c.last_sync_time,c.create_time,c.update_time,a.name as owner_name,d.name as dept_name,l.name as link_name,p.name as pool_name')
|
||
->order('c.last_chat_time', 'desc')->order('c.id', 'desc')
|
||
->page($page, $pageSize)->select()->toArray();
|
||
foreach ($rows as &$row) {
|
||
$row['external_userid_masked'] = self::maskIdentifier((string) ($row['external_userid'] ?? ''));
|
||
unset($row['external_userid']);
|
||
$row['has_messaged'] = (int) ($row['chat_status'] ?? 0) === 1;
|
||
$row['message_count_known'] = (int) ($row['message_count_known'] ?? 0);
|
||
$row['received_message_count'] = (int) ($row['recv_msg_cnt'] ?? 0);
|
||
}
|
||
unset($row);
|
||
|
||
$summaryQuery = self::customerQuery($params, $visibleIds, $operatorPoolIds);
|
||
$summaryRow = $summaryQuery->fieldRaw(
|
||
'COUNT(*) AS customer_count, '
|
||
. 'COALESCE(SUM(CASE WHEN c.message_count_known = 1 THEN c.recv_msg_cnt ELSE 0 END),0) AS recv_msg_cnt, '
|
||
. 'SUM(CASE WHEN c.chat_status = 1 THEN 1 ELSE 0 END) AS started_chat_count, '
|
||
. 'SUM(CASE WHEN c.message_count_known = 1 THEN 1 ELSE 0 END) AS message_count_known_count'
|
||
)->find() ?: [];
|
||
|
||
return [
|
||
'meta' => [
|
||
'scope_label' => DataScopeService::scopeLabel(DataScopeService::getEffectiveScope($adminInfo)),
|
||
'generated_at' => date('Y-m-d H:i:s'),
|
||
],
|
||
'summary' => [
|
||
'customer_count' => (int) ($summaryRow['customer_count'] ?? 0),
|
||
'started_chat_count' => (int) ($summaryRow['started_chat_count'] ?? 0),
|
||
'recv_msg_cnt' => (int) ($summaryRow['recv_msg_cnt'] ?? 0),
|
||
'received_message_count' => (int) ($summaryRow['recv_msg_cnt'] ?? 0),
|
||
'message_count_known_count' => (int) ($summaryRow['message_count_known_count'] ?? 0),
|
||
],
|
||
'lists' => $rows,
|
||
'count' => $total,
|
||
'page_no' => $page,
|
||
'page_size' => $pageSize,
|
||
];
|
||
}
|
||
|
||
private static function customerQuery(array $params, ?array $visibleIds, array $operatorPoolIds)
|
||
{
|
||
$query = Db::name('qywx_customer_acquisition_customer')->alias('c')
|
||
->leftJoin('admin a', 'a.id = c.owner_admin_id AND a.delete_time IS NULL')
|
||
->leftJoin('dept d', 'd.id = c.dept_id')
|
||
->leftJoin('qywx_promotion_link l', 'l.id = c.promotion_link_id')
|
||
->leftJoin('qywx_promotion_pool p', 'p.id = l.pool_id');
|
||
self::applyCustomerScope($query, $visibleIds, $operatorPoolIds);
|
||
$localLinkId = max(0, (int) ($params['promotion_link_id'] ?? 0));
|
||
if ($localLinkId > 0) {
|
||
$query->where('c.promotion_link_id', $localLinkId);
|
||
}
|
||
$userId = trim((string) ($params['userid'] ?? ''));
|
||
if ($userId !== '') {
|
||
$query->where('c.userid', $userId);
|
||
}
|
||
if (isset($params['chat_status']) && $params['chat_status'] !== '') {
|
||
$query->where('c.chat_status', max(0, (int) $params['chat_status']));
|
||
}
|
||
$keyword = trim((string) ($params['keyword'] ?? ''));
|
||
if ($keyword !== '') {
|
||
$query->whereLike('c.external_userid|c.userid|a.name|l.name', '%' . $keyword . '%');
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
/**
|
||
* 客户优先按实际承接成员做数据隔离;企微成员尚未绑定后台账号时,改按获客链接归属兜底。
|
||
*
|
||
* 企微 customer_list 的 state/customer_channel 允许为空,不能因此丢掉已返回的客户;
|
||
* 同时也不能直接放开全部 owner_admin_id=0 的记录,否则会跨部门泄露未映射客户。
|
||
*/
|
||
private static function applyCustomerScope($query, ?array $visibleIds, array $operatorPoolIds): void
|
||
{
|
||
if ($visibleIds === null) {
|
||
return;
|
||
}
|
||
if ($visibleIds === [] && $operatorPoolIds === []) {
|
||
$query->whereRaw('1 = 0');
|
||
return;
|
||
}
|
||
$ids = array_values(array_unique(array_map('intval', $visibleIds)));
|
||
$query->where(function ($scope) use ($ids, $operatorPoolIds): void {
|
||
if ($ids !== []) {
|
||
$scope->whereIn('c.owner_admin_id', $ids)
|
||
->whereOr(function ($unmapped) use ($ids): void {
|
||
$unmapped->where('c.owner_admin_id', 0)
|
||
->whereIn('l.owner_admin_id', $ids);
|
||
});
|
||
if ($operatorPoolIds !== []) {
|
||
$scope->whereOr('p.id', 'in', $operatorPoolIds);
|
||
}
|
||
return;
|
||
}
|
||
$scope->whereIn('p.id', $operatorPoolIds);
|
||
});
|
||
}
|
||
|
||
private static function applyScope($query, string $alias, ?array $visibleIds, array $operatorPoolIds): void
|
||
{
|
||
if ($visibleIds === null) {
|
||
return;
|
||
}
|
||
if ($visibleIds === [] && $operatorPoolIds === []) {
|
||
$query->whereRaw('1 = 0');
|
||
return;
|
||
}
|
||
$ids = array_values(array_unique(array_map('intval', $visibleIds)));
|
||
$query->where(function ($scope) use ($alias, $ids, $operatorPoolIds): void {
|
||
if ($ids !== []) {
|
||
$scope->whereIn($alias . '.owner_admin_id', $ids);
|
||
if ($operatorPoolIds !== []) {
|
||
$scope->whereOr($alias . '.pool_id', 'in', $operatorPoolIds);
|
||
}
|
||
return;
|
||
}
|
||
$scope->whereIn($alias . '.pool_id', $operatorPoolIds);
|
||
});
|
||
}
|
||
|
||
/** @return list<int> */
|
||
private static function operatorPoolIds(int $adminId): array
|
||
{
|
||
if ($adminId <= 0) {
|
||
return [];
|
||
}
|
||
$ids = Db::name('qywx_promotion_pool_operator')
|
||
->where('admin_id', $adminId)
|
||
->whereNull('delete_time')
|
||
->column('pool_id');
|
||
|
||
return array_values(array_unique(array_filter(array_map(
|
||
static fn ($value): int => (int) $value,
|
||
$ids
|
||
), static fn (int $value): bool => $value > 0)));
|
||
}
|
||
|
||
private static function maskIdentifier(string $value): string
|
||
{
|
||
$value = trim($value);
|
||
$length = mb_strlen($value);
|
||
if ($length <= 0) {
|
||
return '-';
|
||
}
|
||
if ($length <= 4) {
|
||
return mb_substr($value, 0, 1) . '***';
|
||
}
|
||
if ($length <= 8) {
|
||
return mb_substr($value, 0, 2) . '***' . mb_substr($value, -1);
|
||
}
|
||
|
||
return mb_substr($value, 0, 4) . '****' . mb_substr($value, -4);
|
||
}
|
||
}
|