This commit is contained in:
Your Name
2026-08-28 18:24:37 +08:00
parent 43ad07208f
commit ed48f8be31
383 changed files with 8673 additions and 2222 deletions
@@ -52,6 +52,19 @@ class WecomPromotionController extends BaseAdminController
)));
}
public function batchSetOperators()
{
if (!$this->hasPagePermission()) {
return $this->fail('权限不足');
}
return $this->run(fn () => $this->success('方案操作人已批量更新', WecomPromotionLogic::batchSetOperators(
$this->request->post(),
$this->adminId,
$this->adminInfo
)));
}
public function deletePool()
{
if (!$this->hasPagePermission()) {
@@ -20,7 +20,12 @@ class WecomAcquisitionCustomerLogic
->whereNull('l.delete_time')
->where('l.remote_link_id', '<>', '')
->where('l.remote_status', 1);
self::applyScope($query, 'l', DataScopeService::getVisibleAdminIds($adminId, $adminInfo));
self::applyScope(
$query,
'l',
DataScopeService::getVisibleAdminIds($adminId, $adminInfo),
self::operatorPoolIds($adminId)
);
if ($localLinkId > 0) {
$query->where('l.id', $localLinkId);
}
@@ -56,7 +61,8 @@ class WecomAcquisitionCustomerLogic
$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);
$base = self::customerQuery($params, $visibleIds);
$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')
@@ -71,7 +77,7 @@ class WecomAcquisitionCustomerLogic
}
unset($row);
$summaryQuery = self::customerQuery($params, $visibleIds);
$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, '
@@ -98,14 +104,14 @@ class WecomAcquisitionCustomerLogic
];
}
private static function customerQuery(array $params, ?array $visibleIds)
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);
self::applyCustomerScope($query, $visibleIds, $operatorPoolIds);
$localLinkId = max(0, (int) ($params['promotion_link_id'] ?? 0));
if ($localLinkId > 0) {
$query->where('c.promotion_link_id', $localLinkId);
@@ -131,35 +137,69 @@ class WecomAcquisitionCustomerLogic
* 企微 customer_list 的 state/customer_channel 允许为空,不能因此丢掉已返回的客户;
* 同时也不能直接放开全部 owner_admin_id=0 的记录,否则会跨部门泄露未映射客户。
*/
private static function applyCustomerScope($query, ?array $visibleIds): void
private static function applyCustomerScope($query, ?array $visibleIds, array $operatorPoolIds): void
{
if ($visibleIds === null) {
return;
}
if ($visibleIds === []) {
if ($visibleIds === [] && $operatorPoolIds === []) {
$query->whereRaw('1 = 0');
return;
}
$ids = array_values(array_unique(array_map('intval', $visibleIds)));
$query->where(function ($scope) use ($ids): void {
$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);
});
$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): void
private static function applyScope($query, string $alias, ?array $visibleIds, array $operatorPoolIds): void
{
if ($visibleIds === null) {
return;
}
if ($visibleIds === []) {
if ($visibleIds === [] && $operatorPoolIds === []) {
$query->whereRaw('1 = 0');
return;
}
$query->whereIn($alias . '.owner_admin_id', array_values(array_unique(array_map('intval', $visibleIds))));
$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
@@ -21,12 +21,14 @@ class WecomPromotionLogic
public static function overview(int $adminId, array $adminInfo, string $domain): array
{
self::assertMemberDispatchSchema();
self::assertPoolOperatorSchema();
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
$operatorPoolIds = self::operatorPoolIds($adminId);
$poolsQuery = Db::name('qywx_promotion_pool')->alias('p')
->leftJoin('admin u', 'u.id = p.owner_admin_id')
->leftJoin('dept d', 'd.id = p.dept_id')
->whereNull('p.delete_time');
self::applyOwnerScope($poolsQuery, 'p', $visibleIds);
self::applyPoolAccessScope($poolsQuery, 'p', $visibleIds, $operatorPoolIds);
$pools = $poolsQuery
->field('p.id,p.name,p.public_key,p.status,p.fallback_url,p.widget_config_json,p.click_count,p.owner_admin_id,p.dept_id,p.create_time,p.update_time,u.name as owner_name,d.name as dept_name')
->order('p.id', 'desc')
@@ -75,7 +77,10 @@ class WecomPromotionLogic
}
unset($link);
$memberOptions = self::memberOptions($adminId, $adminInfo);
$poolMemberAdminIds = self::poolMemberAdminIds($poolIds);
$memberOptions = self::memberOptions($adminId, $adminInfo, $poolMemberAdminIds);
$operatorOptions = self::operatorOptions($adminId, $adminInfo);
$operatorsByPool = self::poolOperators($poolIds);
$adminIdByUserId = [];
$memberOptionByAdminId = [];
foreach ($memberOptions as $member) {
@@ -153,6 +158,15 @@ class WecomPromotionLogic
$pool['legacy_link_count'] = $legacyCount;
$pool['member_admin_ids'] = array_values(array_unique($memberAdminIds));
$pool['member_rules'] = $memberRules;
$pool['operators'] = $operatorsByPool[(int) $pool['id']] ?? [];
$pool['operator_admin_ids'] = array_values(array_map(
static fn (array $operator): int => (int) $operator['id'],
$pool['operators']
));
$pool['is_shared_with_me'] = in_array($adminId, $pool['operator_admin_ids'], true);
$pool['can_operate'] = true;
$pool['can_manage_access'] = self::ownerInScope((int) ($pool['owner_admin_id'] ?? 0), $visibleIds);
$pool['can_delete'] = $pool['can_manage_access'];
$pool['dispatch_sync'] = $sync;
$pool['skip_verify'] = (int) ($officialLink['skip_verify'] ?? 0);
$pool['migration_state'] = count($officialLinks) > 1
@@ -171,6 +185,7 @@ class WecomPromotionLogic
return [
'meta' => [
'admin_id' => $adminId,
'scope_label' => DataScopeService::scopeLabel(DataScopeService::getEffectiveScope($adminInfo)),
'generated_at' => date('Y-m-d H:i:s'),
],
@@ -184,6 +199,7 @@ class WecomPromotionLogic
'pools' => $pools,
'links' => $links,
'member_options' => $memberOptions,
'operator_options' => $operatorOptions,
'department_options' => DeptLogic::getAllDataScoped($adminId, $adminInfo),
'customer_acquisition_link_example' => QywxCustomerAcquisitionLinkService::example(),
];
@@ -204,7 +220,7 @@ class WecomPromotionLogic
if (!QywxCustomerAcquisitionLinkService::isAllowed($fallback, true)) {
throw new RuntimeException('兜底链接必须是企业微信获客助手生成的 HTTPS 链接');
}
$members = self::resolveMembers((array) ($params['member_admin_ids'] ?? []), $adminId, $adminInfo);
$members = self::resolveMembers((array) ($params['member_admin_ids'] ?? []), $adminId, $adminInfo, $id);
$userIds = array_values(array_column($members, 'userid'));
$eligibleUserIds = self::eligibleSelectedUserIds($id, $members);
$skipVerify = (int) ($params['skip_verify'] ?? 0) === 1 ? 1 : 0;
@@ -388,7 +404,7 @@ class WecomPromotionLogic
?QywxCustomerAcquisitionApiService $api = null
): void
{
self::assertScopedRow('qywx_promotion_pool', $id, $adminId, $adminInfo);
self::assertScopedRow('qywx_promotion_pool', $id, $adminId, $adminInfo, false);
$links = Db::name('qywx_promotion_link')
->where('pool_id', $id)
->order('id', 'asc')
@@ -479,6 +495,10 @@ class WecomPromotionLogic
'delete_time' => $now,
'update_time' => $now,
]);
Db::name('qywx_promotion_pool_operator')->where('pool_id', $id)->whereNull('delete_time')->update([
'delete_time' => $now,
'update_time' => $now,
]);
Db::name('qywx_promotion_range_sync')->where('pool_id', $id)->update([
'status' => 4,
'next_retry' => 0,
@@ -490,6 +510,139 @@ class WecomPromotionLogic
});
}
/**
* 批量添加或移除分流方案共享操作人。
* 共享操作人可访问并编辑方案;删除方案和继续授权仍受 owner 数据范围控制。
*
* @return array{action:string,pool_ids:list<int>,operator_admin_ids:list<int>,affected:int}
*/
public static function batchSetOperators(array $params, int $adminId, array $adminInfo): array
{
self::assertPoolOperatorSchema();
$poolIds = self::normalizePositiveIds((array) ($params['pool_ids'] ?? []));
$operatorAdminIds = self::normalizePositiveIds((array) (
$params['operator_admin_ids'] ?? $params['admin_ids'] ?? []
));
$action = strtolower(trim((string) ($params['action'] ?? 'grant')));
if (!in_array($action, ['grant', 'revoke'], true)) {
throw new RuntimeException('批量设置方式仅支持添加或移除操作人');
}
if ($poolIds === []) {
throw new RuntimeException('请至少选择一个分流方案');
}
if (count($poolIds) > 100) {
throw new RuntimeException('单次最多设置 100 个分流方案');
}
if ($operatorAdminIds === []) {
throw new RuntimeException('请至少选择一名操作人');
}
if (count($operatorAdminIds) > 100) {
throw new RuntimeException('单次最多设置 100 名操作人');
}
if (in_array($adminId, $operatorAdminIds, true)) {
throw new RuntimeException('不能将当前账号设置为自己的共享操作人');
}
$pools = [];
foreach ($poolIds as $poolId) {
$pools[$poolId] = self::assertScopedRow(
'qywx_promotion_pool',
$poolId,
$adminId,
$adminInfo,
false
);
}
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
if ($visibleIds !== null) {
foreach ($operatorAdminIds as $operatorAdminId) {
if (!in_array($operatorAdminId, $visibleIds, true)) {
throw new RuntimeException('选择的操作人超出当前角色或部门的数据范围');
}
}
}
$adminQuery = Db::name('admin')->whereIn('id', $operatorAdminIds)->whereNull('delete_time');
if ($action === 'grant') {
$adminQuery->where('disable', 0);
}
$operatorAdmins = $adminQuery->field('id,root')->select()->toArray();
$existingAdminIds = self::normalizePositiveIds(array_column($operatorAdmins, 'id'));
if (count($existingAdminIds) !== count($operatorAdminIds)) {
throw new RuntimeException($action === 'grant'
? '选择的操作人不存在或账号已被禁用'
: '选择的操作人不存在');
}
if ($action === 'grant') {
$pagePermissionAdminIds = self::promotionPagePermissionAdminIdSet($operatorAdminIds);
foreach ($operatorAdmins as $operatorAdmin) {
if ((int) ($operatorAdmin['root'] ?? 0) !== 1
&& !isset($pagePermissionAdminIds[(int) ($operatorAdmin['id'] ?? 0)])) {
throw new RuntimeException('选择的操作人尚未获得企业微信获客助手页面权限');
}
}
}
$now = time();
$affected = Db::transaction(function () use (
$action,
$poolIds,
$pools,
$operatorAdminIds,
$adminId,
$now
): int {
$changed = 0;
foreach ($poolIds as $poolId) {
$ownerAdminId = (int) ($pools[$poolId]['owner_admin_id'] ?? 0);
foreach ($operatorAdminIds as $operatorAdminId) {
// 方案归属人天然拥有权限,无需写入共享关系。
if ($operatorAdminId === $ownerAdminId) {
continue;
}
$query = Db::name('qywx_promotion_pool_operator')
->where('pool_id', $poolId)
->where('admin_id', $operatorAdminId);
$existing = (clone $query)->lock(true)->find();
if ($action === 'grant') {
$data = [
'granted_by_admin_id' => $adminId,
'delete_time' => null,
'update_time' => $now,
];
if ($existing) {
if ($existing['delete_time'] !== null) {
$changed++;
}
$query->update($data);
} else {
Db::name('qywx_promotion_pool_operator')->insert($data + [
'pool_id' => $poolId,
'admin_id' => $operatorAdminId,
'create_time' => $now,
]);
$changed++;
}
continue;
}
if ($existing && $existing['delete_time'] === null) {
$query->update(['delete_time' => $now, 'update_time' => $now]);
$changed++;
}
}
}
return $changed;
});
return [
'action' => $action,
'pool_ids' => $poolIds,
'operator_admin_ids' => $operatorAdminIds,
'affected' => $affected,
];
}
private static function isRemoteLinkAlreadyMissing(\Throwable $error): bool
{
$message = strtolower($error->getMessage());
@@ -637,7 +790,11 @@ class WecomPromotionLogic
$visibleAdminIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
$visibleUserIds = null;
if ($visibleAdminIds !== null) {
$visibleUserIds = array_fill_keys(array_column(self::memberOptions($adminId, $adminInfo), 'userid'), true);
$visibleUserIds = array_fill_keys(array_column(self::memberOptions(
$adminId,
$adminInfo,
self::poolMemberAdminIds([$poolId])
), 'userid'), true);
}
$api = new QywxCustomerAcquisitionApiService();
@@ -704,7 +861,11 @@ class WecomPromotionLogic
$remote = self::normaliseRemoteLink($api->getLink($remoteLinkId), $remoteLinkId);
$visibleUserIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo) === null
? null
: array_fill_keys(array_column(self::memberOptions($adminId, $adminInfo), 'userid'), true);
: array_fill_keys(array_column(self::memberOptions(
$adminId,
$adminInfo,
self::poolMemberAdminIds([(int) ($row['pool_id'] ?? 0)])
), 'userid'), true);
if (!self::canSeeRemoteLink($remote, $visibleUserIds)) {
throw new RuntimeException('该获客链接已不在当前角色或部门的数据范围内');
}
@@ -749,8 +910,8 @@ class WecomPromotionLogic
]);
}
/** @return list<array{id:int,name:string,userid:string,display_dept_id:int,dept_ids:list<int>,dept_names:list<string>}> */
private static function memberOptions(int $adminId, array $adminInfo): array
/** @return list<array{id:int,name:string,disable:int,can_grant:bool,display_dept_id:int,dept_ids:list<int>,dept_names:list<string>}> */
private static function operatorOptions(int $adminId, array $adminInfo): array
{
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
if ($visibleIds === []) {
@@ -758,29 +919,89 @@ class WecomPromotionLogic
}
$query = Db::name('admin')->alias('a')
->whereNull('a.delete_time')
->where('a.disable', 0)
->where('a.work_wechat_userid', '<>', '');
->where('a.id', '<>', $adminId);
if ($visibleIds !== null) {
$query->whereIn('a.id', $visibleIds);
}
$admins = $query->field('a.id,a.name,a.root,a.disable')->order('a.disable', 'asc')->order('a.name', 'asc')->order('a.id', 'asc')->select()->toArray();
if ($admins === []) {
return [];
}
$adminIds = self::normalizePositiveIds(array_column($admins, 'id'));
$departments = self::adminDepartmentMaps($adminIds);
$pagePermissionAdminIds = self::promotionPagePermissionAdminIdSet($adminIds);
$result = [];
foreach ($admins as $admin) {
$aid = (int) $admin['id'];
$deptIds = array_values(array_unique(array_filter($departments[$aid]['ids'] ?? [])));
$hasPagePermission = (int) ($admin['root'] ?? 0) === 1
|| isset($pagePermissionAdminIds[$aid]);
$result[] = [
'id' => $aid,
'name' => (string) ($admin['name'] ?? ('账号 ' . $aid)),
'disable' => (int) ($admin['disable'] ?? 0),
'can_grant' => (int) ($admin['disable'] ?? 0) === 0 && $hasPagePermission,
'display_dept_id' => (int) ($deptIds[0] ?? 0),
'dept_ids' => $deptIds,
'dept_names' => array_values(array_unique($departments[$aid]['names'] ?? [])),
];
}
return $result;
}
/** @param list<int> $poolIds @return array<int,list<array{id:int,name:string,disable:int,dept_names:list<string>}>> */
private static function poolOperators(array $poolIds): array
{
if ($poolIds === []) {
return [];
}
$rows = Db::name('qywx_promotion_pool_operator')->alias('po')
->join('admin a', 'a.id = po.admin_id')
->whereIn('po.pool_id', $poolIds)
->whereNull('po.delete_time')
->whereNull('a.delete_time')
->field('po.pool_id,a.id,a.name,a.disable')
->order('a.name', 'asc')
->order('a.id', 'asc')
->select()->toArray();
$departments = self::adminDepartmentMaps(self::normalizePositiveIds(array_column($rows, 'id')));
$result = [];
foreach ($rows as $row) {
$aid = (int) ($row['id'] ?? 0);
$result[(int) $row['pool_id']][] = [
'id' => $aid,
'name' => (string) ($row['name'] ?? ('账号 ' . $aid)),
'disable' => (int) ($row['disable'] ?? 0),
'dept_names' => array_values(array_unique($departments[$aid]['names'] ?? [])),
];
}
return $result;
}
/** @return list<array{id:int,name:string,userid:string,display_dept_id:int,dept_ids:list<int>,dept_names:list<string>}> */
private static function memberOptions(int $adminId, array $adminInfo, array $extraAdminIds = []): array
{
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
$allowedIds = $visibleIds === null
? null
: self::normalizePositiveIds(array_merge($visibleIds, $extraAdminIds));
if ($allowedIds === []) {
return [];
}
$query = Db::name('admin')->alias('a')
->whereNull('a.delete_time')
->where('a.disable', 0)
->where('a.work_wechat_userid', '<>', '');
if ($allowedIds !== null) {
$query->whereIn('a.id', $allowedIds);
}
$admins = $query->field('a.id,a.name,a.work_wechat_userid')->order('a.id', 'asc')->select()->toArray();
if ($admins === []) {
return [];
}
$adminIds = array_map('intval', array_column($admins, 'id'));
$deptRows = Db::name('admin_dept')->alias('ad')
->leftJoin('dept d', 'd.id = ad.dept_id')
->whereIn('ad.admin_id', $adminIds)
->field('ad.admin_id,ad.dept_id,d.name as dept_name')
->order('ad.dept_id', 'asc')->select()->toArray();
$departments = [];
foreach ($deptRows as $row) {
$aid = (int) ($row['admin_id'] ?? 0);
$departments[$aid]['ids'][] = (int) ($row['dept_id'] ?? 0);
if (trim((string) ($row['dept_name'] ?? '')) !== '') {
$departments[$aid]['names'][] = (string) $row['dept_name'];
}
}
$departments = self::adminDepartmentMaps(self::normalizePositiveIds(array_column($admins, 'id')));
$result = [];
$seenUserIds = [];
@@ -808,14 +1029,15 @@ class WecomPromotionLogic
}
/** @return list<array{id:int,name:string,userid:string,display_dept_id:int,dept_ids:list<int>,dept_names:list<string>}> */
private static function resolveMembers(array $adminIds, int $adminId, array $adminInfo): array
private static function resolveMembers(array $adminIds, int $adminId, array $adminInfo, int $poolId = 0): array
{
$requested = array_values(array_unique(array_filter(array_map('intval', $adminIds))));
if ($requested === []) {
throw new RuntimeException('请至少选择一名当前角色或部门范围内的获客成员');
}
$available = [];
foreach (self::memberOptions($adminId, $adminInfo) as $member) {
$existingMemberAdminIds = $poolId > 0 ? self::poolMemberAdminIds([$poolId]) : [];
foreach (self::memberOptions($adminId, $adminInfo, $existingMemberAdminIds) as $member) {
$available[(int) $member['id']] = $member;
}
$members = [];
@@ -1033,9 +1255,8 @@ class WecomPromotionLogic
$existing = Db::name('qywx_promotion_link')->where('remote_link_id', $remoteLinkId)->find();
$remoteData = self::remoteColumns($remote, $now);
if ($existing) {
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
if ($visibleIds !== null && !in_array((int) ($existing['owner_admin_id'] ?? 0), $visibleIds, true)) {
throw new RuntimeException('该链接已归属其他数据范围');
if ((int) ($existing['pool_id'] ?? 0) !== (int) ($pool['id'] ?? 0)) {
throw new RuntimeException('该企业微信链接已归属其他分流方案');
}
$remoteData['delete_time'] = null;
Db::name('qywx_promotion_link')->where('id', (int) $existing['id'])->update($remoteData);
@@ -1146,37 +1367,183 @@ class WecomPromotionLogic
return $encoded === false ? '[]' : $encoded;
}
private static function assertScopedRow(string $table, int $id, int $adminId, array $adminInfo): array
private static function assertScopedRow(
string $table,
int $id,
int $adminId,
array $adminInfo,
bool $allowOperator = true
): array
{
if ($id <= 0) {
throw new RuntimeException('数据不存在');
}
$query = Db::name($table)->where('id', $id)->whereNull('delete_time');
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
if ($visibleIds !== null) {
if ($visibleIds === []) {
throw new RuntimeException('无权访问该数据');
}
$query->whereIn('owner_admin_id', $visibleIds);
}
$row = $query->find();
$row = Db::name($table)->where('id', $id)->whereNull('delete_time')->find();
if (!$row) {
throw new RuntimeException('数据不存在或已删除');
}
$visibleIds = DataScopeService::getVisibleAdminIds($adminId, $adminInfo);
$pool = $table === 'qywx_promotion_pool'
? $row
: ($table === 'qywx_promotion_link'
? Db::name('qywx_promotion_pool')
->where('id', (int) ($row['pool_id'] ?? 0))
->whereNull('delete_time')
->find()
: null);
if ($pool !== null) {
if (self::ownerInScope((int) ($pool['owner_admin_id'] ?? 0), $visibleIds)) {
return $row;
}
if ($allowOperator && self::isPoolOperator((int) ($pool['id'] ?? 0), $adminId)) {
return $row;
}
throw new RuntimeException($allowOperator
? '无权访问或操作该分流方案'
: '只有方案原有管理范围内的账号可以执行此操作');
}
if (!self::ownerInScope((int) ($row['owner_admin_id'] ?? 0), $visibleIds)) {
throw new RuntimeException('数据不存在或超出当前权限范围');
}
return $row;
}
private static function applyOwnerScope($query, string $alias, ?array $visibleIds): void
/** @param list<int>|null $visibleIds */
private static function ownerInScope(int $ownerAdminId, ?array $visibleIds): bool
{
if ($visibleIds === null) {
return true;
}
return in_array($ownerAdminId, $visibleIds, true);
}
/** @param list<int>|null $visibleIds @param list<int> $operatorPoolIds */
private static function applyPoolAccessScope(
$query,
string $alias,
?array $visibleIds,
array $operatorPoolIds
): void {
if ($visibleIds === null) {
return;
}
if ($visibleIds === []) {
if ($visibleIds === [] && $operatorPoolIds === []) {
$query->whereRaw('1 = 0');
return;
}
$query->whereIn($alias . '.owner_admin_id', $visibleIds);
$query->where(function ($scope) use ($alias, $visibleIds, $operatorPoolIds): void {
if ($visibleIds !== []) {
$scope->whereIn($alias . '.owner_admin_id', $visibleIds);
if ($operatorPoolIds !== []) {
$scope->whereOr($alias . '.id', 'in', $operatorPoolIds);
}
return;
}
$scope->whereIn($alias . '.id', $operatorPoolIds);
});
}
/** @return list<int> */
private static function operatorPoolIds(int $adminId): array
{
if ($adminId <= 0) {
return [];
}
return self::normalizePositiveIds(Db::name('qywx_promotion_pool_operator')
->where('admin_id', $adminId)
->whereNull('delete_time')
->column('pool_id'));
}
private static function isPoolOperator(int $poolId, int $adminId): bool
{
if ($poolId <= 0 || $adminId <= 0) {
return false;
}
return Db::name('qywx_promotion_pool_operator')
->where('pool_id', $poolId)
->where('admin_id', $adminId)
->whereNull('delete_time')
->count() > 0;
}
/** @param list<int> $poolIds @return list<int> */
private static function poolMemberAdminIds(array $poolIds): array
{
$poolIds = self::normalizePositiveIds($poolIds);
if ($poolIds === []) {
return [];
}
return self::normalizePositiveIds(Db::name('qywx_promotion_pool_member')
->whereIn('pool_id', $poolIds)
->whereNull('delete_time')
->column('admin_id'));
}
/** @param list<int> $adminIds @return array<int,array{ids:list<int>,names:list<string>}> */
private static function adminDepartmentMaps(array $adminIds): array
{
if ($adminIds === []) {
return [];
}
$rows = Db::name('admin_dept')->alias('ad')
->leftJoin('dept d', 'd.id = ad.dept_id')
->whereIn('ad.admin_id', $adminIds)
->field('ad.admin_id,ad.dept_id,d.name as dept_name')
->order('ad.dept_id', 'asc')->select()->toArray();
$departments = [];
foreach ($rows as $row) {
$aid = (int) ($row['admin_id'] ?? 0);
$departments[$aid]['ids'][] = (int) ($row['dept_id'] ?? 0);
if (trim((string) ($row['dept_name'] ?? '')) !== '') {
$departments[$aid]['names'][] = (string) $row['dept_name'];
}
}
return $departments;
}
/** @param list<int> $adminIds @return array<int,true> */
private static function promotionPagePermissionAdminIdSet(array $adminIds): array
{
if ($adminIds === []) {
return [];
}
$menuIds = self::normalizePositiveIds(Db::name('system_menu')
->where('perms', 'firstvisit.wecomPromotion/overview')
->where('is_disable', 0)
->column('id'));
if ($menuIds === []) {
return [];
}
$roleIds = self::normalizePositiveIds(Db::name('system_role_menu')
->whereIn('menu_id', $menuIds)
->column('role_id'));
if ($roleIds === []) {
return [];
}
$permittedAdminIds = self::normalizePositiveIds(Db::name('admin_role')
->whereIn('role_id', $roleIds)
->whereIn('admin_id', $adminIds)
->column('admin_id'));
return array_fill_keys($permittedAdminIds, true);
}
/** @return list<int> */
private static function normalizePositiveIds(array $ids): array
{
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 primaryDeptId(int $adminId): int
@@ -1210,6 +1577,17 @@ class WecomPromotionLogic
}
}
private static function assertPoolOperatorSchema(): void
{
try {
Db::name('qywx_promotion_pool_operator')->limit(1)->find();
} catch (\Throwable $e) {
throw new RuntimeException(
'分流方案共享操作人数据表尚未安装,请先执行 server/sql/1.9.20260828/add_wecom_promotion_pool_operators.sql'
);
}
}
private static function mask(string $value): string
{
$length = strlen($value);