Files
zyt/server/app/adminapi/logic/firstvisit/WecomAcquisitionCustomerLogic.php
T
2026-09-01 10:42:16 +08:00

215 lines
9.4 KiB
PHP
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.
<?php
declare(strict_types=1);
namespace app\adminapi\logic\firstvisit;
use app\common\service\DataScope\DataScopeService;
use app\common\service\qywx\QywxCustomerAcquisitionCustomerService;
use app\common\service\qywx\QywxPromotionOperatorAccess;
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',
QywxPromotionOperatorAccess::visibleAdminIds($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)));
$hasBasePagePermission = QywxPromotionOperatorAccess::hasBasePagePermission($adminId, $adminInfo);
$visibleIds = QywxPromotionOperatorAccess::visibleAdminIds($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' => $hasBasePagePermission
? 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
{
return QywxPromotionOperatorAccess::activePoolIds($adminId);
}
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);
}
}