更新
This commit is contained in:
@@ -78,6 +78,19 @@ class WecomPromotionController extends BaseAdminController
|
||||
)));
|
||||
}
|
||||
|
||||
public function batchUpdatePools()
|
||||
{
|
||||
if (!$this->hasBasePagePermission()) {
|
||||
return $this->fail('权限不足');
|
||||
}
|
||||
|
||||
return $this->run(fn () => $this->success('分流方案配置已批量更新', WecomPromotionLogic::batchUpdatePools(
|
||||
$this->request->post(),
|
||||
$this->adminId,
|
||||
$this->adminInfo
|
||||
)));
|
||||
}
|
||||
|
||||
public function saveWidget()
|
||||
{
|
||||
if (!$this->hasPagePermission()) {
|
||||
|
||||
@@ -4,16 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\controller\qywx;
|
||||
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\qywx\CustomerLists;
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\adminapi\validate\qywx\CustomerValidate;
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\qywx\CustomerLists;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\adminapi\validate\qywx\CustomerValidate;
|
||||
|
||||
/**
|
||||
* 企业微信客户管理控制器
|
||||
*/
|
||||
class CustomerController extends BaseAdminController
|
||||
{
|
||||
class CustomerController extends BaseAdminController
|
||||
{
|
||||
private const DELETE_PERMISSION = 'qywx.customer/delete';
|
||||
|
||||
/**
|
||||
* @notes 客户列表
|
||||
*/
|
||||
@@ -25,16 +28,34 @@ class CustomerController extends BaseAdminController
|
||||
/**
|
||||
* @notes 同步企业微信客户
|
||||
*/
|
||||
public function sync()
|
||||
{
|
||||
public function sync()
|
||||
{
|
||||
$result = CustomerLogic::triggerBackgroundSync();
|
||||
if ($result === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
$msg = is_array($result) && isset($result['message']) ? (string) $result['message'] : '已提交同步';
|
||||
|
||||
return $this->success($msg, $result);
|
||||
}
|
||||
return $this->success($msg, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 删除一条本地企业微信客户同步记录
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
// 显式鉴权,避免权限菜单迁移漏执行时被通用中间件当成“未受控 URI”放行。
|
||||
if (!$this->canDeleteCustomer()) {
|
||||
return $this->fail('权限不足,无法删除企业微信客户');
|
||||
}
|
||||
|
||||
$params = (new CustomerValidate())->post()->goCheck('delete');
|
||||
if (!CustomerLogic::deleteCustomer((int) $params['id'])) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取统计信息
|
||||
@@ -84,13 +105,22 @@ class CustomerController extends BaseAdminController
|
||||
/**
|
||||
* @notes 保存同步设置
|
||||
*/
|
||||
public function saveSyncSettings()
|
||||
public function saveSyncSettings()
|
||||
{
|
||||
$params = (new CustomerValidate())->post()->goCheck('syncSettings');
|
||||
$result = CustomerLogic::saveSyncSettings($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
}
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
private function canDeleteCustomer(): bool
|
||||
{
|
||||
if ((int) ($this->adminInfo['root'] ?? 0) === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array(self::DELETE_PERMISSION, AuthLogic::getAuthByAdminId($this->adminId), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +227,7 @@ class AuthMiddleware
|
||||
'firstvisit.wecompromotion/uploadwelcomemedia',
|
||||
'firstvisit.wecompromotion/overview',
|
||||
'firstvisit.wecompromotion/savepool',
|
||||
'firstvisit.wecompromotion/batchupdatepools',
|
||||
'firstvisit.wecompromotion/savewidget',
|
||||
'firstvisit.wecompromotion/batchsetoperators',
|
||||
'firstvisit.wecompromotion/deletepool',
|
||||
|
||||
@@ -230,20 +230,49 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$query->whereRaw($effExpr . ' > 0 AND ' . $effExpr . ' <= ?', [$endTs]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function baseQuery()
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按客户当前跟进关系中的添加方式筛选。
|
||||
*
|
||||
* follow_users 由同步逻辑使用 json_encode 写入,匹配数字值及历史字符串值;
|
||||
* 数字后必须紧跟逗号或对象结束符,避免 add_way=1 误命中 16。
|
||||
*/
|
||||
private function applyAddWayFilter($query): void
|
||||
{
|
||||
if (!array_key_exists('add_way', $this->params) || $this->params['add_way'] === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$addWay = self::normalizeAddWay($this->params['add_way']);
|
||||
if ($addWay === null) {
|
||||
$query->whereRaw('1=0');
|
||||
return;
|
||||
}
|
||||
|
||||
$numberPrefix = '%"add_way":' . $addWay;
|
||||
$stringPrefix = '%"add_way":"' . $addWay;
|
||||
$query->where(function ($q) use ($numberPrefix, $stringPrefix) {
|
||||
$q->where('follow_users', 'like', $numberPrefix . ',%')
|
||||
->whereOr('follow_users', 'like', $numberPrefix . '}%')
|
||||
->whereOr('follow_users', 'like', $stringPrefix . '",%')
|
||||
->whereOr('follow_users', 'like', $stringPrefix . '"}%');
|
||||
});
|
||||
}
|
||||
|
||||
private function baseQuery()
|
||||
{
|
||||
$query = QywxExternalContact::where($this->searchWhere);
|
||||
|
||||
// 添加时间可能已按「跟进人+事件流水」收窄;此时不必再 LIKE follow_users
|
||||
$followAlreadyScoped = $this->applyAddTimeFilter($query);
|
||||
if (!$followAlreadyScoped) {
|
||||
$this->applyFollowUserFilter($query);
|
||||
}
|
||||
|
||||
// 标签筛选:JOIN 关系表按 tag_id 过滤;多个标签为 OR(命中任一即返回)。
|
||||
if (!$followAlreadyScoped) {
|
||||
$this->applyFollowUserFilter($query);
|
||||
}
|
||||
$this->applyAddWayFilter($query);
|
||||
|
||||
// 标签筛选:JOIN 关系表按 tag_id 过滤;多个标签为 OR(命中任一即返回)。
|
||||
// 走 zyt_qywx_external_contact_tag.idx_tag 索引,比 LIKE follow_users 快得多
|
||||
$tagIds = $this->normalizeTagIds();
|
||||
if ($tagIds !== []) {
|
||||
@@ -262,6 +291,154 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量读取当前页客户的加客渠道流水。
|
||||
*
|
||||
* 事件表是一对多关系,不能直接 JOIN 到分页主查询,否则会放大列表行数与 count。
|
||||
* 同一客户可能被不同员工重复添加,因此保留所有不同的非空 state,并按最近事件排序。
|
||||
*
|
||||
* @param string[] $externalUserids
|
||||
* @return array<string, array<int, array<string, mixed>>>
|
||||
*/
|
||||
private function loadAddChannelsByExternalUserid(array $externalUserids): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($externalUserids as $externalUserid) {
|
||||
$externalUserid = trim((string) $externalUserid);
|
||||
if ($externalUserid !== '') {
|
||||
$ids[$externalUserid] = true;
|
||||
}
|
||||
}
|
||||
$ids = array_keys($ids);
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$events = Db::name('qywx_external_contact_event')
|
||||
->where('change_type', 'add_external_contact')
|
||||
->where('state', '<>', '')
|
||||
->whereIn('external_userid', $ids)
|
||||
->field(['id', 'external_userid', 'user_id', 'state', 'event_time'])
|
||||
->order('event_time', 'desc')
|
||||
->order('id', 'desc')
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$poolIds = [];
|
||||
foreach ($events as $event) {
|
||||
$state = trim((string) ($event['state'] ?? ''));
|
||||
if (preg_match('/^zyt_pool:([1-9]\d*)$/D', $state, $matches) === 1) {
|
||||
$poolIds[(int) $matches[1]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$poolNamesById = [];
|
||||
if ($poolIds !== []) {
|
||||
// 历史渠道仍应显示已删除方案原来的名称,因此这里不限制 delete_time。
|
||||
$poolNamesById = Db::name('qywx_promotion_pool')
|
||||
->whereIn('id', array_keys($poolIds))
|
||||
->column('name', 'id');
|
||||
}
|
||||
|
||||
return self::projectAddChannelEvents($events, $poolNamesById);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $events 已按 event_time DESC, id DESC 排序
|
||||
* @param array<int|string, string> $poolNamesById
|
||||
* @return array<string, array<int, array<string, mixed>>>
|
||||
*/
|
||||
private static function projectAddChannelEvents(array $events, array $poolNamesById): array
|
||||
{
|
||||
$channelsByExternalUserid = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($events as $event) {
|
||||
$externalUserid = trim((string) ($event['external_userid'] ?? ''));
|
||||
$state = trim((string) ($event['state'] ?? ''));
|
||||
if ($externalUserid === '' || $state === '' || isset($seen[$externalUserid][$state])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$externalUserid][$state] = true;
|
||||
|
||||
$poolId = 0;
|
||||
if (preg_match('/^zyt_pool:([1-9]\d*)$/D', $state, $matches) === 1) {
|
||||
$poolId = (int) $matches[1];
|
||||
}
|
||||
$poolName = $poolId > 0 ? trim((string) ($poolNamesById[$poolId] ?? '')) : '';
|
||||
|
||||
$channelsByExternalUserid[$externalUserid][] = [
|
||||
'state' => $state,
|
||||
'label' => $poolName !== ''
|
||||
? $poolName
|
||||
: ($poolId > 0 ? '获客助手方案 #' . $poolId : $state),
|
||||
'source_type' => $poolId > 0 ? 'promotion_pool' : 'state',
|
||||
'pool_id' => $poolId,
|
||||
'user_id' => trim((string) ($event['user_id'] ?? '')),
|
||||
'event_time' => (int) ($event['event_time'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $channelsByExternalUserid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业微信客户详情 follow_user.add_way 的可读文案。
|
||||
*
|
||||
* add_way 是固定添加方式,state 是企业自定义渠道参数,两者不能混用。
|
||||
* 未识别的新枚举保留原值,避免后续企微扩展时页面退化成“未记录”。
|
||||
*/
|
||||
private static function addWayLabel(int $addWay): string
|
||||
{
|
||||
$labels = [
|
||||
0 => '未知添加方式',
|
||||
1 => '通过扫描二维码添加',
|
||||
2 => '通过搜索手机号添加',
|
||||
3 => '通过名片分享添加',
|
||||
4 => '通过群聊添加',
|
||||
5 => '通过手机通讯录添加',
|
||||
6 => '通过微信联系人添加',
|
||||
8 => '安装第三方应用时自动添加',
|
||||
9 => '通过搜索邮箱添加',
|
||||
10 => '通过视频号添加',
|
||||
11 => '通过日程参与人添加',
|
||||
12 => '通过会议参与人添加',
|
||||
13 => '通过微信好友添加',
|
||||
14 => '通过智慧硬件专属客服添加',
|
||||
15 => '通过上门服务客服添加',
|
||||
16 => '通过获客链接添加',
|
||||
17 => '通过定制开发添加',
|
||||
18 => '通过需求回复添加',
|
||||
21 => '通过第三方售前客服添加',
|
||||
22 => '通过可能的商务伙伴添加',
|
||||
24 => '通过接受微信好友申请添加',
|
||||
201 => '通过内部成员共享添加',
|
||||
202 => '通过管理员或负责人分配添加',
|
||||
];
|
||||
|
||||
return $labels[$addWay] ?? '其他添加方式(' . $addWay . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private static function normalizeAddWay($value): ?int
|
||||
{
|
||||
if (is_int($value)) {
|
||||
return $value >= 0 ? $value : null;
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
if ($value === '' || preg_match('/^\d+$/D', $value) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取列表
|
||||
*/
|
||||
@@ -278,7 +455,12 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
->toArray();
|
||||
|
||||
$wxUserids = [];
|
||||
$externalUserids = [];
|
||||
foreach ($lists as $item) {
|
||||
$externalUserid = trim((string) ($item['external_userid'] ?? ''));
|
||||
if ($externalUserid !== '') {
|
||||
$externalUserids[$externalUserid] = true;
|
||||
}
|
||||
$raw = json_decode($item['follow_users'] ?? '[]', true);
|
||||
if (!is_array($raw)) {
|
||||
continue;
|
||||
@@ -298,19 +480,25 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
if ($wxUserids !== []) {
|
||||
$adminNameByWx = Admin::whereIn('work_wechat_userid', $wxUserids)->column('name', 'work_wechat_userid');
|
||||
}
|
||||
$addChannelsByExternalUserid = $this->loadAddChannelsByExternalUserid(array_keys($externalUserids));
|
||||
|
||||
foreach ($lists as &$item) {
|
||||
$followUsers = json_decode($item['follow_users'] ?? '[]', true);
|
||||
$followUsers = is_array($followUsers) ? $followUsers : [];
|
||||
foreach ($followUsers as &$fu) {
|
||||
if (!is_array($fu)) {
|
||||
continue;
|
||||
}
|
||||
$wx = trim((string) ($fu['userid'] ?? ''));
|
||||
if ($wx !== '' && isset($adminNameByWx[$wx]) && $adminNameByWx[$wx] !== '') {
|
||||
$fu['admin_name'] = $adminNameByWx[$wx];
|
||||
}
|
||||
}
|
||||
foreach ($followUsers as &$fu) {
|
||||
if (!is_array($fu)) {
|
||||
continue;
|
||||
}
|
||||
$wx = trim((string) ($fu['userid'] ?? ''));
|
||||
if ($wx !== '' && isset($adminNameByWx[$wx]) && $adminNameByWx[$wx] !== '') {
|
||||
$fu['admin_name'] = $adminNameByWx[$wx];
|
||||
}
|
||||
$addWay = self::normalizeAddWay($fu['add_way'] ?? $fu['AddWay'] ?? null);
|
||||
if ($addWay !== null) {
|
||||
$fu['add_way'] = $addWay;
|
||||
$fu['add_way_label'] = self::addWayLabel($addWay);
|
||||
}
|
||||
}
|
||||
unset($fu);
|
||||
$item['follow_users'] = $followUsers;
|
||||
$followAdminIds = json_decode($item['follow_admin_ids'] ?? '[]', true);
|
||||
@@ -320,6 +508,10 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
$tags = json_decode((string) ($item['tags'] ?? '[]'), true);
|
||||
$item['tags'] = is_array($tags) ? $tags : [];
|
||||
|
||||
$externalUserid = trim((string) ($item['external_userid'] ?? ''));
|
||||
$item['add_channels'] = $addChannelsByExternalUserid[$externalUserid] ?? [];
|
||||
$item['add_channel_states'] = array_column($item['add_channels'], 'state');
|
||||
|
||||
$fromDb = (int) ($item['external_first_add_time'] ?? 0);
|
||||
$fromJson = CustomerLogic::minFollowCreatetime($followUsers);
|
||||
$item['external_first_add_time'] = $fromDb > 0 ? $fromDb : $fromJson;
|
||||
|
||||
@@ -225,7 +225,12 @@ class WecomPromotionLogic
|
||||
];
|
||||
}
|
||||
|
||||
public static function savePool(array $params, int $adminId, array $adminInfo): array
|
||||
public static function savePool(
|
||||
array $params,
|
||||
int $adminId,
|
||||
array $adminInfo,
|
||||
bool $syncImmediately = true
|
||||
): array
|
||||
{
|
||||
self::assertMemberDispatchSchema();
|
||||
$id = max(0, (int) ($params['id'] ?? 0));
|
||||
@@ -425,11 +430,13 @@ class WecomPromotionLogic
|
||||
$syncError = '';
|
||||
if (!$createdRemote) {
|
||||
QywxPromotionMemberSchedulerService::requestPoolSync($id, $linkId);
|
||||
try {
|
||||
(new QywxPromotionRangeSyncService())->syncPool($id);
|
||||
} catch (\Throwable $e) {
|
||||
// 本地方案和成员规则已保存;后台分钟任务会继续重试最新完整范围。
|
||||
$syncError = $e->getMessage();
|
||||
if ($syncImmediately) {
|
||||
try {
|
||||
(new QywxPromotionRangeSyncService())->syncPool($id);
|
||||
} catch (\Throwable $e) {
|
||||
// 本地方案和成员规则已保存;后台分钟任务会继续重试最新完整范围。
|
||||
$syncError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
$savedLink = Db::name('qywx_promotion_link')->where('id', $linkId)->find() ?: [];
|
||||
@@ -448,6 +455,152 @@ class WecomPromotionLogic
|
||||
// 前端据此确认标签、欢迎语等扩展配置已和方案一并提交并完成回读校验。
|
||||
'automation_saved' => $automation !== null,
|
||||
'sync_error' => $syncError,
|
||||
'sync_queued' => !$createdRemote && !$syncImmediately,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量局部更新分流方案。changes 只覆盖显式传入的字段;每个方案仍复用
|
||||
* savePool 的成员、自动化、素材和企业微信同步校验。
|
||||
*
|
||||
* @return array{pool_ids:list<int>,updated:int,failed:int,sync_error_count:int,sync_queued_count:int,results:list<array<string,mixed>>}
|
||||
*/
|
||||
public static function batchUpdatePools(array $params, int $adminId, array $adminInfo): array
|
||||
{
|
||||
self::assertMemberDispatchSchema();
|
||||
self::assertBasePagePermission($adminId, $adminInfo);
|
||||
$poolIds = self::normalizePositiveIds((array) ($params['pool_ids'] ?? []));
|
||||
if ($poolIds === []) {
|
||||
throw new RuntimeException('请至少选择一个分流方案');
|
||||
}
|
||||
if (count($poolIds) > 100) {
|
||||
throw new RuntimeException('单次最多设置 100 个分流方案');
|
||||
}
|
||||
|
||||
$changes = $params['changes'] ?? null;
|
||||
if (!is_array($changes)) {
|
||||
throw new RuntimeException('批量修改内容格式不正确');
|
||||
}
|
||||
$allowedFields = ['skip_verify', 'fallback_url', 'status', 'automation_config'];
|
||||
$unknownFields = array_diff(array_keys($changes), $allowedFields);
|
||||
if ($unknownFields !== []) {
|
||||
throw new RuntimeException('批量修改包含不支持的字段');
|
||||
}
|
||||
if ($changes === []) {
|
||||
throw new RuntimeException('请至少选择一项需要批量修改的配置');
|
||||
}
|
||||
if (array_key_exists('fallback_url', $changes) && !is_string($changes['fallback_url'])) {
|
||||
throw new RuntimeException('兜底获客助手链接格式不正确');
|
||||
}
|
||||
|
||||
$automationPatch = null;
|
||||
if (array_key_exists('automation_config', $changes)) {
|
||||
if (!is_array($changes['automation_config']) || $changes['automation_config'] === []) {
|
||||
throw new RuntimeException('自动化配置格式不正确');
|
||||
}
|
||||
$allowedAutomationFields = [
|
||||
'reception_mode', 'reception_schedule', 'backup_member_admin_ids',
|
||||
'tags_enabled', 'tag_ids', 'remark_enabled', 'remark_template',
|
||||
'description_enabled', 'description', 'welcome_mode', 'welcome',
|
||||
'welcome_schedule_enabled', 'welcome_schedule',
|
||||
];
|
||||
if (array_diff(array_keys($changes['automation_config']), $allowedAutomationFields) !== []) {
|
||||
throw new RuntimeException('自动化配置包含不支持的字段');
|
||||
}
|
||||
QywxPromotionConfig::assertInstalled();
|
||||
$automationPatch = $changes['automation_config'];
|
||||
}
|
||||
|
||||
// 必须在任何方案写入前完成整批权限校验,避免越权请求产生部分更新。
|
||||
$pools = [];
|
||||
foreach ($poolIds as $poolId) {
|
||||
$pools[$poolId] = self::assertScopedRow(
|
||||
'qywx_promotion_pool',
|
||||
$poolId,
|
||||
$adminId,
|
||||
$adminInfo,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$updated = 0;
|
||||
$failed = 0;
|
||||
$syncErrorCount = 0;
|
||||
$syncQueuedCount = 0;
|
||||
foreach ($poolIds as $poolId) {
|
||||
$pool = $pools[$poolId];
|
||||
$currentAutomation = QywxPromotionConfig::forPool($poolId);
|
||||
$memberAdminIds = self::normalizePositiveIds(Db::name('qywx_promotion_pool_member')
|
||||
->where('pool_id', $poolId)
|
||||
->whereNull('delete_time')
|
||||
->column('admin_id'));
|
||||
$primaryMemberAdminIds = array_values(array_diff(
|
||||
$memberAdminIds,
|
||||
self::normalizePositiveIds((array) ($currentAutomation['backup_member_admin_ids'] ?? []))
|
||||
));
|
||||
$officialLink = Db::name('qywx_promotion_link')
|
||||
->where('pool_id', $poolId)
|
||||
->whereNull('delete_time')
|
||||
->where('remote_link_id', '<>', '')
|
||||
->where('remote_status', '<>', 2)
|
||||
->order('id', 'desc')
|
||||
->find() ?: [];
|
||||
$saveParams = [
|
||||
'id' => $poolId,
|
||||
'name' => (string) ($pool['name'] ?? ''),
|
||||
'fallback_url' => array_key_exists('fallback_url', $changes)
|
||||
? trim($changes['fallback_url'])
|
||||
: (string) ($pool['fallback_url'] ?? ''),
|
||||
'status' => array_key_exists('status', $changes)
|
||||
? ((int) $changes['status'] === 1 ? 1 : 0)
|
||||
: (int) ($pool['status'] ?? 0),
|
||||
'member_admin_ids' => $primaryMemberAdminIds,
|
||||
'skip_verify' => array_key_exists('skip_verify', $changes)
|
||||
? ((int) $changes['skip_verify'] === 1 ? 1 : 0)
|
||||
: (int) ($officialLink['skip_verify'] ?? 0),
|
||||
];
|
||||
if ($automationPatch !== null) {
|
||||
$saveParams['automation_config'] = array_replace($currentAutomation, $automationPatch);
|
||||
}
|
||||
|
||||
try {
|
||||
// 批量操作只落本地并入同步队列,避免大量企微请求阻塞管理端 HTTP 请求。
|
||||
$saved = self::savePool($saveParams, $adminId, $adminInfo, false);
|
||||
$syncError = trim((string) ($saved['sync_error'] ?? ''));
|
||||
$updated++;
|
||||
if ($syncError !== '') {
|
||||
$syncErrorCount++;
|
||||
}
|
||||
$syncQueued = !empty($saved['sync_queued']);
|
||||
if ($syncQueued) {
|
||||
$syncQueuedCount++;
|
||||
}
|
||||
$results[] = [
|
||||
'id' => $poolId,
|
||||
'name' => (string) ($pool['name'] ?? ''),
|
||||
'success' => true,
|
||||
'sync_error' => $syncError,
|
||||
'sync_queued' => $syncQueued,
|
||||
];
|
||||
} catch (\Throwable $error) {
|
||||
$failed++;
|
||||
$results[] = [
|
||||
'id' => $poolId,
|
||||
'name' => (string) ($pool['name'] ?? ''),
|
||||
'success' => false,
|
||||
'error' => $error->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'pool_ids' => $poolIds,
|
||||
'updated' => $updated,
|
||||
'failed' => $failed,
|
||||
'sync_error_count' => $syncErrorCount,
|
||||
'sync_queued_count' => $syncQueuedCount,
|
||||
'results' => $results,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -722,6 +722,73 @@ class CustomerLogic extends BaseLogic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台手工删除一条本地同步记录。
|
||||
*
|
||||
* 仅按列表行主键软删除,不调用企业微信删除客户关系;兼容历史库中可能存在的重复
|
||||
* external_userid。只有该客户已无其他有效行时才清理共享的标签关系。
|
||||
*/
|
||||
public static function deleteCustomer(int $id): bool
|
||||
{
|
||||
if ($id <= 0) {
|
||||
self::$error = '客户参数错误';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$externalUserId = Db::transaction(static function () use ($id): string {
|
||||
$row = Db::name('qywx_external_contact')
|
||||
->where('id', $id)
|
||||
->whereNull('delete_time')
|
||||
->lock(true)
|
||||
->find();
|
||||
if (!$row) {
|
||||
throw new \DomainException('客户不存在或已删除');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
Db::name('qywx_external_contact')
|
||||
->where('id', $id)
|
||||
->whereNull('delete_time')
|
||||
->update([
|
||||
'delete_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
$externalUserId = trim((string) ($row['external_userid'] ?? ''));
|
||||
if ($externalUserId !== '') {
|
||||
$activeRows = (int) Db::name('qywx_external_contact')
|
||||
->where('external_userid', $externalUserId)
|
||||
->whereNull('delete_time')
|
||||
->count();
|
||||
if ($activeRows === 0) {
|
||||
Db::name('qywx_external_contact_tag')
|
||||
->where('external_userid', $externalUserId)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return $externalUserId;
|
||||
});
|
||||
|
||||
if ($externalUserId !== '') {
|
||||
MediaChannelService::forgetCurrentTagCatalogCache();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\DomainException $e) {
|
||||
self::$error = $e->getMessage();
|
||||
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('后台删除企业微信客户同步记录失败: ' . $e->getMessage());
|
||||
self::$error = '删除失败,请稍后重试';
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户联系「删除企业客户」等事件:本地软删除一行。
|
||||
*/
|
||||
|
||||
@@ -12,11 +12,15 @@ use app\common\validate\BaseValidate;
|
||||
class CustomerValidate extends BaseValidate
|
||||
{
|
||||
protected $rule = [
|
||||
'id' => 'require|integer|gt:0',
|
||||
'auto_sync' => 'require|boolean',
|
||||
'interval' => 'require|integer|between:3600,86400',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'id.require' => '请选择要删除的客户',
|
||||
'id.integer' => '客户参数格式错误',
|
||||
'id.gt' => '客户参数格式错误',
|
||||
'auto_sync.require' => '请选择是否自动同步',
|
||||
'auto_sync.boolean' => '自动同步参数格式错误',
|
||||
'interval.require' => '请选择同步间隔',
|
||||
@@ -31,4 +35,12 @@ class CustomerValidate extends BaseValidate
|
||||
{
|
||||
return $this->only(['auto_sync', 'interval']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 删除客户场景
|
||||
*/
|
||||
public function sceneDelete()
|
||||
{
|
||||
return $this->only(['id']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user