'充电底座', 100 => 'K霸宝', 101 => '充电宝', 102 => 'K霸宝 Blue', 103 => '密码线充', 104 => '蓝牙线充', 105 => '密码蓝牙线充', 107 => '车载充电器', ]; public function __construct() { $this->model = new SystemUser(); } public function indexList(array $where): array { $rows = $this->agentRows($where); $page = max(1, (int) request()->input('page', 1)); $limit = min(100, max(1, (int) request()->input('limit', 10))); return [ 'total' => count($rows), 'per_page' => $limit, 'current_page' => $page, 'last_page' => max(1, (int) ceil(count($rows) / $limit)), 'data' => array_slice($rows, ($page - 1) * $limit, $limit), ]; } public function export(array $where): Response { $data = []; foreach ($this->agentRows($where) as $row) { $data[] = [ 'id' => $row['id'], 'name' => $this->excelSafe($row['name']), 'phone' => $this->excelSafe($row['phone']), 'agent_type' => $row['agent_type'], 'region' => $this->excelSafe(implode(' ', array_filter([$row['province'], $row['city'], $row['district']]))), 'device_count' => $row['device_count'], 'subordinate_count' => $row['subordinate_count'], 'income' => (float) $row['income'], 'withdrawable' => (float) $row['withdrawable'], 'profit_rate' => $row['profit_rate'], 'status' => $row['status_name'], 'created_at' => $row['created_at'], ]; } $fileName = '代理数据_' . date('Ymd_His') . '.xlsx'; $writer = new OpenSpoutWriter($fileName); $writer->setWidth([10, 18, 18, 16, 32, 14, 14, 14, 14, 14, 12, 22]); $writer->setHeader([ '代理ID', '代理姓名', '手机号码', '代理类型', '运营区域', '设备数', '下级总数', '总收益', '可提现金额', '分润比例', '状态', '创建时间', ]); $writer->setData($data); return response()->download($writer->returnFile(), urlencode($fileName)); } public function readAgent(int $id): array { $agent = $this->findAccessibleAgent($id, false); $deptRows = SystemDept::whereNull('delete_time')->field('id,name,level')->select()->toArray(); $deptMap = []; foreach ($deptRows as $dept) { $deptMap[(int) $dept['id']] = $dept; } $dept = $deptMap[(int) $agent['dept_id']] ?? []; $region = $this->regionNames($dept, $deptMap); $currentDepth = $this->deptDepth($this->adminInfo['deptList'] ?? []); $type = $this->agentType($this->deptDepth($dept) - $currentDepth); $profile = Db::table('sa_agent_profile')->where('agent_id', $id)->find() ?: []; $products = $this->decodeList($profile['product_codes'] ?? null, self::PRODUCT_CODES); if ($products === []) { $products = self::PRODUCT_CODES; } $profitRates = $this->decodeMap($profile['profit_rates'] ?? null); foreach ($products as $product) { $profitRates[$product] = (float) ($profitRates[$product] ?? 0); } return [ 'id' => (int) $agent['id'], 'opening_type' => $type['code'], 'opening_type_name' => $type['name'], 'business_mode' => in_array($profile['business_mode'] ?? '', ['agent', 'employee'], true) ? (string) $profile['business_mode'] : 'agent', 'name' => (string) (($agent['realname'] ?? '') ?: $agent['username']), 'phone' => (string) ($agent['phone'] ?? ''), 'product_codes' => $products, 'profit_rates' => $profitRates, 'permissions' => $this->decodeList($profile['permissions'] ?? null, self::PERMISSION_CODES), 'province' => (string) (($profile['province'] ?? '') ?: ($region[0] === '--' ? '' : $region[0])), 'city' => (string) (($profile['city'] ?? '') ?: ($region[1] === '--' ? '' : $region[1])), 'district' => (string) (($profile['district'] ?? '') ?: ($region[2] === '--' ? '' : $region[2])), 'company_address' => (string) ($profile['company_address'] ?? ''), 'status' => (int) ($agent['status'] ?? 1), ]; } public function updateAgent(int $id, array $data): bool { $agent = $this->findAccessibleAgent($id, false); $name = trim((string) ($data['name'] ?? '')); $phone = trim((string) ($data['phone'] ?? '')); if ($name === '') { throw new ApiException('请输入代理姓名'); } if (!preg_match('/^1\d{10}$/', $phone)) { throw new ApiException('请输入正确的手机号码'); } if ($phone !== (string) ($agent['phone'] ?? '')) { $exists = SystemUser::where('phone', $phone)->where('id', '<>', $id)->whereNull('delete_time')->value('id'); if ($exists) { throw new ApiException('该手机号码已被其他账号使用'); } } $businessMode = (string) ($data['business_mode'] ?? ''); if (!in_array($businessMode, ['agent', 'employee'], true)) { throw new ApiException('运营模式参数错误'); } $products = $this->normalizeList($data['product_codes'] ?? [], self::PRODUCT_CODES); if ($products === []) { throw new ApiException('请至少选择一种运营产品'); } $permissions = $this->normalizeList($data['permissions'] ?? [], self::PERMISSION_CODES); $rawRates = is_array($data['profit_rates'] ?? null) ? $data['profit_rates'] : []; $profitRates = []; foreach ($products as $product) { $rate = $rawRates[$product] ?? null; if (!is_numeric($rate) || (float) $rate < 0 || (float) $rate > 100) { throw new ApiException('分润比例必须在 0 到 100 之间'); } $profitRates[$product] = round((float) $rate, 2); } $operatorId = $this->currentAdminId(); $now = date('Y-m-d H:i:s'); $profileData = [ 'business_mode' => $businessMode, 'product_codes' => json_encode($products, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'profit_rates' => json_encode($profitRates, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'permissions' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'province' => trim((string) ($data['province'] ?? '')), 'city' => trim((string) ($data['city'] ?? '')), 'district' => trim((string) ($data['district'] ?? '')), 'company_address' => trim((string) ($data['company_address'] ?? '')), 'updated_by' => $operatorId, 'update_time' => $now, ]; if ($profileData['province'] === '' || $profileData['city'] === '' || $profileData['district'] === '') { throw new ApiException('请选择完整的运营区域'); } $this->transaction(function () use ($id, $name, $phone, $operatorId, $now, $profileData) { Db::table('sa_system_user')->where('id', $id)->update([ 'realname' => $name, 'phone' => $phone, 'updated_by' => $operatorId, 'update_time' => $now, ]); $profile = Db::table('sa_agent_profile')->where('agent_id', $id)->lock(true)->find(); if ($profile) { Db::table('sa_agent_profile')->where('agent_id', $id)->update($profileData); } else { Db::table('sa_agent_profile')->insert(array_merge($profileData, [ 'agent_id' => $id, 'created_by' => $operatorId, 'create_time' => $now, ])); } }); UserInfoCache::clearUserInfo($id); return true; } /** * 禁用无关联资产和下级账号的代理。 * * 旧系统的“删除代理”只停用账号,不物理删除历史记录;这里保持该语义, * 同时阻止遗留设备、商户或下级账号的孤立关联。 */ public function destroyAgent(int $id): void { $agent = $this->findAccessibleAgent($id, false); if ((int) $agent['status'] !== 1) { throw new ApiException('代理已删除或已停用'); } $managedDeviceCount = (int) Db::table('sa_device')->whereNull('delete_time') ->where('manage_agent_id', $id)->count(); $ownedDeviceCount = (int) Db::table('sa_device')->whereNull('delete_time') ->where('belong_agent_id', $id)->count(); if ($managedDeviceCount > 0 || $ownedDeviceCount > 0) { throw new ApiException('该代理名下仍有设备,请先回收或转移设备后再删除'); } $merchantCount = (int) Db::table('sa_merchant')->whereNull('delete_time') ->where('agent_id', $id)->count(); if ($merchantCount > 0) { throw new ApiException('该代理名下仍有商户,请先转移商户后再删除'); } $dept = SystemDept::where('id', (int) $agent['dept_id'])->field('id,level')->find(); if ($dept) { $prefix = (string) $dept->level . (int) $dept->id . ','; $descendantDeptIds = SystemDept::whereLike('level', $prefix . '%')->column('id'); if ($descendantDeptIds !== []) { $subordinateCount = (int) SystemUser::whereIn('dept_id', $descendantDeptIds) ->whereNull('delete_time')->count(); if ($subordinateCount > 0) { throw new ApiException('该代理仍有下级代理,请先转移或删除下级代理后再删除'); } } } $updated = Db::table('sa_system_user')->where('id', $id)->where('status', 1) ->whereNull('delete_time')->update([ 'status' => 2, 'updated_by' => $this->currentAdminId(), 'update_time' => date('Y-m-d H:i:s'), ]); if ($updated !== 1) { throw new ApiException('代理删除失败,请刷新后重试'); } UserInfoCache::clearUserInfo($id); UserAuthCache::clearUserAuth($id); UserMenuCache::clearUserMenu($id); } public function readContract(int $id): array { $agent = $this->findAccessibleAgent($id, false); $contract = Db::table('sa_agent_contract')->where('agent_id', $id)->find() ?: []; return [ 'id' => $id, 'agent_name' => (string) (($agent['realname'] ?? '') ?: $agent['username']), 'phone' => (string) ($agent['phone'] ?? ''), 'contract_urls' => $this->decodeAttachmentUrls($contract['contract_urls'] ?? null), 'identity_urls' => $this->decodeAttachmentUrls($contract['identity_urls'] ?? null), 'extra_urls' => $this->decodeAttachmentUrls($contract['extra_urls'] ?? null), ]; } public function updateContract(int $id, array $data): array { $this->findAccessibleAgent($id, false); $contractUrls = $this->normalizeAttachmentUrls($data['contract_urls'] ?? []); $identityUrls = $this->normalizeAttachmentUrls($data['identity_urls'] ?? []); $extraUrls = $this->normalizeAttachmentUrls($data['extra_urls'] ?? []); $operatorId = $this->currentAdminId(); $now = date('Y-m-d H:i:s'); $contractData = [ 'contract_urls' => json_encode($contractUrls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'identity_urls' => json_encode($identityUrls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'extra_urls' => json_encode($extraUrls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'updated_by' => $operatorId, 'update_time' => $now, ]; $this->transaction(function () use ($id, $operatorId, $now, $contractData) { $contract = Db::table('sa_agent_contract')->where('agent_id', $id)->lock(true)->find(); if ($contract) { Db::table('sa_agent_contract')->where('agent_id', $id)->update($contractData); return; } Db::table('sa_agent_contract')->insert(array_merge($contractData, [ 'agent_id' => $id, 'created_by' => $operatorId, 'create_time' => $now, ])); }); return $this->readContract($id); } public function candidateDevices(array $where): array { $page = max(1, (int) request()->input('page', 1)); $limit = min(100, max(1, (int) request()->input('limit', 20))); $query = Db::table('sa_device')->alias('d') ->leftJoin('sa_merchant_device_binding b', 'b.device_id = d.id') ->whereNull('d.delete_time') ->whereNull('b.device_id') ->where('d.manage_agent_id', $this->currentAdminId()) ->field('d.id,d.goods_name,d.goods_type,d.goods_sn,d.device_code,d.model,d.device_status,d.create_time'); $category = trim((string) ($where['category'] ?? 'all')); if ($category === 'vehicle') { $query->where('d.goods_type', 107); } elseif ($category === 'power-bank') { $query->whereIn('d.goods_type', [1, 100, 101, 102, 103, 104, 105]); } if (($where['goods_type'] ?? '') !== '') { $query->where('d.goods_type', (int) $where['goods_type']); } $sn = trim((string) ($where['device_sn'] ?? '')); if ($sn !== '') { $query->whereLike('d.goods_sn', '%' . $sn . '%'); } $code = trim((string) ($where['qr_code'] ?? '')); if ($code !== '') { $query->whereLike('d.device_code', '%' . $code . '%'); } $result = $query->order('d.id', 'desc')->paginate([ 'page' => $page, 'list_rows' => $limit, ])->toArray(); $result['data'] = array_map(function (array $row) { $type = (int) $row['goods_type']; return [ 'id' => (int) $row['id'], 'name' => (string) $row['goods_name'], 'goods_type' => $type, 'type_name' => self::TYPE_NAMES[$type] ?? '其他设备', 'device_sn' => (string) $row['goods_sn'], 'qr_code' => (string) (($row['device_code'] ?? '') ?: $row['goods_sn']), 'model' => (string) (($row['model'] ?? '') ?: '--'), 'online' => (int) ($row['device_status'] ?? 0) === 1, 'created_at' => (string) ($row['create_time'] ?? ''), ]; }, $result['data'] ?? []); return $result; } public function assignDevices(int $agentId, array $deviceSns): array { $target = $this->findAccessibleAgent($agentId); $operatorId = $this->currentAdminId(); if ((int) $target['id'] === $operatorId) { throw new ApiException('不能给自己分配设备'); } return $this->moveDevices($operatorId, $agentId, $deviceSns, 1, '上级分配设备'); } public function recoverDevices(int $agentId, array $deviceSns): array { $this->findAccessibleAgent($agentId); return $this->moveDevices($agentId, $this->currentAdminId(), $deviceSns, 2, '上级回收设备'); } public function assignmentRecords(int $agentId, array $where): array { $this->findAccessibleAgent($agentId); $currentId = $this->currentAdminId(); $page = max(1, (int) request()->input('page', 1)); $limit = min(100, max(1, (int) request()->input('limit', 20))); $query = Db::table('sa_device_manager_record')->alias('r') ->leftJoin('sa_system_user last_user', 'last_user.id = r.last_agent_id') ->leftJoin('sa_system_user now_user', 'now_user.id = r.now_agent_id') ->whereIn('r.last_agent_id', [$currentId, $agentId]) ->whereIn('r.now_agent_id', [$currentId, $agentId]) ->field(implode(',', [ 'r.id', 'r.device_id', 'r.device_sn', 'r.goods_type', 'r.last_agent_id', 'r.now_agent_id', 'r.action_type', 'r.reason', 'r.operator_id', 'r.action_time', 'last_user.realname AS last_realname', 'last_user.username AS last_username', 'now_user.realname AS now_realname', 'now_user.username AS now_username', ])); if (($where['action_type'] ?? '') !== '') { $query->where('r.action_type', (int) $where['action_type']); } if (($where['goods_type'] ?? '') !== '') { $query->where('r.goods_type', (int) $where['goods_type']); } $sn = trim((string) ($where['device_sn'] ?? '')); if ($sn !== '') { $query->whereLike('r.device_sn', '%' . $sn . '%'); } $result = $query->order('r.id', 'desc')->paginate([ 'page' => $page, 'list_rows' => $limit, ])->toArray(); $result['data'] = array_map(function (array $row) { $type = (int) $row['goods_type']; return [ 'id' => (int) $row['id'], 'device_id' => (int) $row['device_id'], 'device_sn' => (string) $row['device_sn'], 'goods_type' => $type, 'type_name' => self::TYPE_NAMES[$type] ?? '其他设备', 'last_agent_id' => (int) $row['last_agent_id'], 'last_agent_name' => (string) (($row['last_realname'] ?? '') ?: ($row['last_username'] ?? '')), 'now_agent_id' => (int) $row['now_agent_id'], 'now_agent_name' => (string) (($row['now_realname'] ?? '') ?: ($row['now_username'] ?? '')), 'action_type' => (int) $row['action_type'], 'action_name' => (int) $row['action_type'] === 1 ? '分配' : '回收', 'reason' => (string) $row['reason'], 'action_time' => (string) $row['action_time'], ]; }, $result['data'] ?? []); return $result; } private function moveDevices(int $fromAgentId, int $toAgentId, array $deviceSns, int $actionType, string $reason): array { $sns = $this->normalizeSns($deviceSns); $operatorId = $this->currentAdminId(); return $this->transaction(function () use ($fromAgentId, $toAgentId, $sns, $actionType, $reason, $operatorId) { $devices = Db::table('sa_device')->whereIn('goods_sn', $sns) ->whereNull('delete_time')->lock(true)->select()->toArray(); $deviceMap = []; foreach ($devices as $device) { $deviceMap[(string) $device['goods_sn']] = $device; } $ids = array_map('intval', array_column($devices, 'id')); $boundIds = $ids === [] ? [] : array_map('intval', Db::table('sa_merchant_device_binding') ->whereIn('device_id', $ids)->column('device_id')); $boundMap = array_fill_keys($boundIds, true); $success = []; $notFound = []; $notManaged = []; $alreadyTarget = []; $distributed = []; $records = []; $now = date('Y-m-d H:i:s'); foreach ($sns as $sn) { if (!isset($deviceMap[$sn])) { $notFound[] = $sn; continue; } $device = $deviceMap[$sn]; $deviceId = (int) $device['id']; $managerId = (int) $device['manage_agent_id']; if ($managerId === $toAgentId) { $alreadyTarget[] = $sn; continue; } if ($managerId !== $fromAgentId) { $notManaged[] = $sn; continue; } if (isset($boundMap[$deviceId])) { $distributed[] = $sn; continue; } $updated = Db::table('sa_device')->where('id', $deviceId) ->where('manage_agent_id', $fromAgentId) ->update(['manage_agent_id' => $toAgentId, 'update_time' => $now]); if ($updated !== 1) { $notManaged[] = $sn; continue; } $records[] = [ 'device_id' => $deviceId, 'device_sn' => $sn, 'goods_type' => (int) $device['goods_type'], 'last_agent_id' => $fromAgentId, 'now_agent_id' => $toAgentId, 'action_type' => $actionType, 'reason' => $reason, 'operator_id' => $operatorId, 'action_time' => $now, 'created_by' => $operatorId, 'create_time' => $now, ]; $success[] = $sn; } if ($records !== []) { Db::table('sa_device_manager_record')->insertAll($records); } return [ 'message' => $success === [] ? '没有可处理的新设备' : ($actionType === 1 ? '成功分配 ' : '成功回收 ') . count($success) . ' 台设备', 'success_sn' => $success, 'not_find_sn' => $notFound, 'not_managed_sn' => $notManaged, 'already_target_sn' => $alreadyTarget, 'distributed_sn' => $distributed, 'all_action_sn' => $sns, ]; }); } private function agentRows(array $where): array { $query = $this->accessibleAgentQuery(); $name = trim((string) ($where['name'] ?? '')); if ($name !== '') { $query->whereLike('u.realname|u.username', '%' . $name . '%'); } $phone = trim((string) ($where['phone'] ?? '')); if ($phone !== '') { $query->whereLike('u.phone', '%' . $phone . '%'); } if (($where['status'] ?? '') !== '') { $query->where('u.status', (int) $where['status']); } $rawRows = $query->field('u.id,u.username,u.realname,u.phone,u.avatar,u.dept_id,u.status,u.create_time,d.name AS dept_name,d.level AS dept_level')->select()->toArray(); $ids = array_map('intval', array_column($rawRows, 'id')); if ($ids === []) { return []; } $deviceMetrics = []; foreach (Db::table('sa_device')->whereIn('manage_agent_id', $ids)->whereNull('delete_time') ->field('manage_agent_id,goods_type')->select()->toArray() as $device) { $id = (int) $device['manage_agent_id']; $deviceMetrics[$id]['total'] = ($deviceMetrics[$id]['total'] ?? 0) + 1; if ((int) $device['goods_type'] === 107) { $deviceMetrics[$id]['vehicle'] = ($deviceMetrics[$id]['vehicle'] ?? 0) + 1; } else { $deviceMetrics[$id]['charging'] = ($deviceMetrics[$id]['charging'] ?? 0) + 1; } } $merchantMetrics = []; foreach (Db::table('sa_merchant')->whereIn('agent_id', $ids)->whereNull('delete_time') ->field('agent_id,total_income,withdrawable_amount,profit_rate')->select()->toArray() as $merchant) { $id = (int) $merchant['agent_id']; $merchantMetrics[$id]['income'] = ($merchantMetrics[$id]['income'] ?? 0) + (float) $merchant['total_income']; $merchantMetrics[$id]['withdrawable'] = ($merchantMetrics[$id]['withdrawable'] ?? 0) + (float) $merchant['withdrawable_amount']; $merchantMetrics[$id]['profit_total'] = ($merchantMetrics[$id]['profit_total'] ?? 0) + (float) $merchant['profit_rate']; $merchantMetrics[$id]['merchant_count'] = ($merchantMetrics[$id]['merchant_count'] ?? 0) + 1; } $deptRows = SystemDept::whereNull('delete_time')->field('id,name,level')->select()->toArray(); $deptMap = []; foreach ($deptRows as $dept) { $deptMap[(int) $dept['id']] = $dept; } $allUsers = SystemUser::whereNull('delete_time')->field('id,dept_id')->select()->toArray(); $currentDepth = $this->deptDepth($this->adminInfo['deptList'] ?? []); $rows = []; foreach ($rawRows as $row) { $id = (int) $row['id']; $dept = $deptMap[(int) $row['dept_id']] ?? []; $type = $this->agentType($this->deptDepth($dept) - $currentDepth); if (($where['agent_type'] ?? '') !== '' && $where['agent_type'] !== $type['code']) { continue; } $region = $this->regionNames($dept, $deptMap); $merchant = $merchantMetrics[$id] ?? []; $merchantCount = (int) ($merchant['merchant_count'] ?? 0); $rows[] = [ 'id' => $id, 'name' => (string) (($row['realname'] ?? '') ?: $row['username']), 'username' => (string) $row['username'], 'phone' => (string) ($row['phone'] ?? ''), 'avatar' => (string) ($row['avatar'] ?? ''), 'agent_type' => $type['name'], 'agent_type_code' => $type['code'], 'province' => $region[0], 'city' => $region[1], 'district' => $region[2], 'dept_name' => (string) ($row['dept_name'] ?? ''), 'device_count' => (int) ($deviceMetrics[$id]['total'] ?? 0), 'vehicle_count' => (int) ($deviceMetrics[$id]['vehicle'] ?? 0), 'charging_count' => (int) ($deviceMetrics[$id]['charging'] ?? 0), 'subordinate_count' => $this->descendantUserCount($dept, $allUsers, $deptMap), 'income' => number_format((float) ($merchant['income'] ?? 0), 2, '.', ''), 'frozen' => '0.00', 'withdrawable' => number_format((float) ($merchant['withdrawable'] ?? 0), 2, '.', ''), 'profit_rate' => $merchantCount > 0 ? round((float) $merchant['profit_total'] / $merchantCount, 2) : 0, 'status' => (int) $row['status'], 'status_name' => (int) $row['status'] === 1 ? '有效' : '无效', 'created_at' => (string) ($row['create_time'] ?? ''), ]; } $sort = (string) ($where['sort'] ?? 'default'); usort($rows, function (array $a, array $b) use ($sort) { return match ($sort) { 'income_desc' => (float) $b['income'] <=> (float) $a['income'], 'income_asc' => (float) $a['income'] <=> (float) $b['income'], 'created_asc' => strcmp($a['created_at'], $b['created_at']), default => strcmp($b['created_at'], $a['created_at']) ?: ($b['id'] <=> $a['id']), }; }); return $rows; } private function accessibleAgentQuery() { $query = Db::table('sa_system_user')->alias('u') ->leftJoin('sa_system_dept d', 'd.id = u.dept_id') ->whereNull('u.delete_time') ->where('u.id', '<>', $this->currentAdminId()); if ($this->currentAdminId() !== 1) { $dept = $this->adminInfo['deptList'] ?? []; if (empty($dept['id']) || !isset($dept['level'])) { return $query->where('u.id', 0); } $prefix = (string) $dept['level'] . (int) $dept['id'] . ','; $deptIds = SystemDept::whereLike('level', $prefix . '%')->column('id'); if ($deptIds === []) { return $query->where('u.id', 0); } $query->whereIn('u.dept_id', $deptIds); } return $query; } private function findAccessibleAgent(int $id, bool $requireEnabled = true): array { $agent = $this->accessibleAgentQuery()->where('u.id', $id) ->when($requireEnabled, static fn ($query) => $query->where('u.status', 1)) ->field('u.id,u.realname,u.username,u.phone,u.dept_id,u.status')->find(); if (!$agent) { throw new ApiException($requireEnabled ? '代理不存在、已停用或无权操作' : '代理不存在或无权操作'); } return $agent; } private function normalizeList(mixed $value, array $allowed): array { if (!is_array($value)) { return []; } $values = array_values(array_unique(array_map('strval', $value))); return array_values(array_intersect($allowed, $values)); } private function decodeList(mixed $value, array $allowed): array { $decoded = is_string($value) ? json_decode($value, true) : $value; return $this->normalizeList($decoded, $allowed); } private function decodeMap(mixed $value): array { $decoded = is_string($value) ? json_decode($value, true) : $value; return is_array($decoded) ? $decoded : []; } private function normalizeAttachmentUrls(mixed $value): array { if (!is_array($value)) { throw new ApiException('合同附件参数格式错误'); } if (count($value) > 20) { throw new ApiException('每类合同资料最多上传 20 张图片'); } $urls = []; foreach ($value as $url) { if (!is_string($url)) { throw new ApiException('合同附件地址格式错误'); } $url = trim($url); if ($url === '') { continue; } if (mb_strlen($url) > 2048 || preg_match('/^javascript:/i', $url)) { throw new ApiException('合同附件地址不合法'); } $urls[] = $url; } return array_values(array_unique($urls)); } private function decodeAttachmentUrls(mixed $value): array { $decoded = is_string($value) ? json_decode($value, true) : $value; return is_array($decoded) ? $this->normalizeAttachmentUrls($decoded) : []; } private function normalizeSns(array $deviceSns): array { $sns = array_values(array_unique(array_filter(array_map( static fn ($sn) => trim((string) $sn), $deviceSns )))); if ($sns === []) { throw new ApiException('请至少输入一个设备 SN'); } if (count($sns) > 100) { throw new ApiException('单次最多处理 100 台设备'); } return $sns; } private function deptDepth(array $dept): int { $level = trim((string) ($dept['level'] ?? ''), ','); return ($level === '' ? 0 : count(explode(',', $level))) + (empty($dept['id']) ? 0 : 1); } private function agentType(int $depthDiff): array { if ($depthDiff <= 1) return ['code' => 'city-partner', 'name' => '城市合伙人']; if ($depthDiff === 2) return ['code' => 'agent', 'name' => '代理商']; return ['code' => 'salesperson', 'name' => '业务员']; } private function regionNames(array $dept, array $deptMap): array { if ($dept === []) return ['--', '--', '--']; $ids = array_filter(array_map('intval', explode(',', trim((string) ($dept['level'] ?? ''), ',')))); $ids[] = (int) $dept['id']; $names = array_values(array_filter(array_map(fn ($id) => $deptMap[$id]['name'] ?? '', $ids))); return [$names[0] ?? '--', $names[1] ?? '--', $names[2] ?? '--']; } private function descendantUserCount(array $dept, array $users, array $deptMap): int { if ($dept === []) return 0; $prefix = (string) ($dept['level'] ?? '') . (int) $dept['id'] . ','; $count = 0; foreach ($users as $user) { $userDept = $deptMap[(int) $user['dept_id']] ?? []; if ($userDept !== [] && str_starts_with((string) $userDept['level'], $prefix)) $count++; } return $count; } private function currentAdminId(): int { return (int) ($this->adminInfo['id'] ?? 0); } private function excelSafe(mixed $value): string { $value = (string) $value; return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value; } }