DeviceLogic.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | saiadmin [ saiadmin快速开发框架 ]
  4. // +----------------------------------------------------------------------
  5. namespace plugin\saiadmin\app\logic\device;
  6. use plugin\saiadmin\app\model\merchant\MerchantDevice;
  7. use plugin\saiadmin\app\model\system\SystemUser;
  8. use plugin\saiadmin\basic\think\BaseLogic;
  9. use plugin\saiadmin\exception\ApiException;
  10. use plugin\saiadmin\service\OpenSpoutWriter;
  11. use support\Response;
  12. use support\think\Db;
  13. /**
  14. * 我的设备列表、搜索与导出。
  15. */
  16. class DeviceLogic extends BaseLogic
  17. {
  18. private const TYPE_NAMES = [
  19. 1 => '充电底座',
  20. 100 => 'K霸宝',
  21. 101 => '充电宝',
  22. 102 => 'K霸宝 Blue',
  23. 103 => '密码线充',
  24. 104 => '蓝牙线充',
  25. 105 => '密码蓝牙线充',
  26. 107 => '车载充电器',
  27. ];
  28. private const CATEGORY_TYPES = [
  29. 'vehicle' => [107],
  30. 'power-bank' => [1, 100, 101, 102, 103, 104, 105],
  31. ];
  32. public function __construct()
  33. {
  34. $this->model = new MerchantDevice();
  35. }
  36. /**
  37. * 当前代理管理的设备分页列表。
  38. */
  39. public function indexList(array $where): array
  40. {
  41. $page = max(1, (int) request()->input('page', 1));
  42. $limit = min(100, max(1, (int) request()->input('limit', 20)));
  43. $result = $this->buildListQuery($where)
  44. ->order('d.id', 'desc')
  45. ->paginate([
  46. 'page' => $page,
  47. 'list_rows' => $limit,
  48. ])
  49. ->toArray();
  50. $result['data'] = array_map(fn (array $row) => $this->formatDevice($row), $result['data'] ?? []);
  51. $result['summary'] = $this->summary($where);
  52. return $result;
  53. }
  54. /**
  55. * 当前账号下级代理管理的设备列表。
  56. *
  57. * 下级范围由 sa_system_dept 的 level 路径确定;设备仍以
  58. * sa_device.manage_agent_id 作为当前管理关系,避免把设备所有权
  59. * (belong_agent_id) 误当作可操作权限。
  60. */
  61. public function subordinateIndexList(array $where): array
  62. {
  63. $page = max(1, (int) request()->input('page', 1));
  64. $limit = min(100, max(1, (int) request()->input('limit', 20)));
  65. $result = $this->buildListQuery($where, true)
  66. ->order('d.id', 'desc')
  67. ->paginate([
  68. 'page' => $page,
  69. 'list_rows' => $limit,
  70. ])
  71. ->toArray();
  72. $result['data'] = array_map(fn (array $row) => $this->formatDevice($row), $result['data'] ?? []);
  73. $result['summary'] = $this->summary($where, true);
  74. return $result;
  75. }
  76. /**
  77. * 按当前搜索条件导出全部设备。
  78. */
  79. public function export(array $where): Response
  80. {
  81. $rows = $this->buildListQuery($where)->order('d.id', 'desc')->select()->toArray();
  82. $data = [];
  83. foreach ($rows as $row) {
  84. $device = $this->formatDevice($row);
  85. $data[] = [
  86. 'id' => $device['id'],
  87. 'name' => $this->excelSafe($device['name']),
  88. 'type_name' => $device['type_name'],
  89. 'device_sn' => $this->excelSafe($device['device_sn']),
  90. 'qr_code' => $this->excelSafe($device['qr_code']),
  91. 'model' => $this->excelSafe($device['version']),
  92. 'status_name' => $device['status_name'],
  93. 'signal' => $device['signal'],
  94. 'heartbeat' => $device['heartbeat'],
  95. 'distributed' => $device['distributed'] ? '已绑定' : '未绑定',
  96. 'merchant' => $this->excelSafe($device['merchant']),
  97. 'merchant_phone' => $this->excelSafe($device['merchant_phone']),
  98. 'manage_agent_id' => $device['manage_agent_id'],
  99. 'is_active' => $device['is_active'] ? '已激活' : '未激活',
  100. 'active_time' => $device['active_time'],
  101. 'distributed_at' => $device['distributed_at'],
  102. 'created_at' => $device['created_at'],
  103. ];
  104. }
  105. $fileName = '我的设备_' . date('Ymd_His') . '.xlsx';
  106. $writer = new OpenSpoutWriter($fileName);
  107. $writer->setWidth([10, 20, 18, 24, 22, 16, 12, 12, 12, 12, 22, 16, 14, 12, 20, 20, 20]);
  108. $writer->setHeader([
  109. '设备ID', '设备名称', '设备类型', '设备SN', '二维码/机器码', '设备型号',
  110. '运营状态', '设备信号', '心跳状态', '绑定状态', '商户名称', '商户电话',
  111. '管理代理ID', '激活状态', '激活时间', '绑定时间', '创建时间',
  112. ]);
  113. $writer->setData($data);
  114. return response()->download($writer->returnFile(), urlencode($fileName));
  115. }
  116. /**
  117. * 返回设备归属、当前管理者、商户绑定及二维码内容。
  118. */
  119. public function readDevice(int $deviceId, bool $subordinate = false): array
  120. {
  121. $device = $this->findAccessibleDevice($deviceId, $subordinate);
  122. $binding = Db::table('sa_merchant_device_binding')->alias('b')
  123. ->leftJoin('sa_merchant m', 'm.id = b.merchant_id AND m.delete_time IS NULL')
  124. ->where('b.device_id', $deviceId)
  125. ->field('b.merchant_id,b.bind_time,m.name AS merchant_name,m.phone AS merchant_phone,m.address AS merchant_address,m.agent_id')
  126. ->find();
  127. $owner = $this->userSummary((int) ($device['belong_agent_id'] ?? 0));
  128. $manager = $this->userSummary((int) ($device['manage_agent_id'] ?? 0));
  129. $merchantOperator = $this->userSummary((int) ($binding['agent_id'] ?? 0));
  130. $code = (string) (($device['device_code'] ?? '') ?: ($device['goods_sn'] ?? ''));
  131. $qrBaseUrl = rtrim(trim((string) env('DEVICE_QR_BASE_URL', '')), '/');
  132. return [
  133. 'device' => array_merge($this->formatDevice($device), [
  134. 'qr_content' => $qrBaseUrl !== '' ? $qrBaseUrl . '/' . rawurlencode($code) : $code,
  135. 'can_switch_power' => (int) ($device['goods_type'] ?? 0) === 107,
  136. 'can_unbind' => !empty($binding),
  137. ]),
  138. 'owner' => $owner,
  139. 'manager' => $manager,
  140. 'merchant' => empty($binding) ? null : [
  141. 'id' => (int) $binding['merchant_id'],
  142. 'name' => (string) ($binding['merchant_name'] ?? ''),
  143. 'phone' => (string) ($binding['merchant_phone'] ?? ''),
  144. 'address' => (string) ($binding['merchant_address'] ?? ''),
  145. 'bind_time' => (string) ($binding['bind_time'] ?? ''),
  146. 'operator' => $merchantOperator,
  147. ],
  148. ];
  149. }
  150. /**
  151. * 沿用旧系统车辆控制协议:POST /cdy/tasks,task_type=switch,value=1/0。
  152. * 指令服务必须显式配置,避免在未接入硬件网关时伪造成功状态。
  153. */
  154. public function switchPower(int $deviceId, string $action, bool $subordinate = false): array
  155. {
  156. $device = $this->findAccessibleDevice($deviceId, $subordinate);
  157. if ((int) ($device['goods_type'] ?? 0) !== 107) {
  158. throw new ApiException('仅车载充电器支持供电和断电操作');
  159. }
  160. $endpoint = trim((string) env('DEVICE_COMMAND_ENDPOINT', ''));
  161. if ($endpoint === '') {
  162. throw new ApiException('设备指令服务未配置,无法向真实设备下发操作');
  163. }
  164. $value = $action === 'on' ? 1 : 0;
  165. $payload = [
  166. 'uid' => (string) (($device['goods_sn'] ?? '') ?: ($device['device_code'] ?? '')),
  167. 'task_type' => 'switch',
  168. 'value' => $value,
  169. ];
  170. $response = $this->postDeviceCommand($endpoint, $payload);
  171. $success = in_array((int) ($response['code'] ?? -1), [0, 200], true);
  172. if ($success && $value === 1 && isset($response['data']['queued']) && (int) $response['data']['queued'] !== 1) {
  173. $success = false;
  174. }
  175. $this->writeCommandLog($deviceId, $action, $payload, $response, $success);
  176. if (!$success) {
  177. throw new ApiException((string) ($response['msg'] ?? '设备指令下发失败'));
  178. }
  179. return [
  180. 'device_id' => $deviceId,
  181. 'action' => $action,
  182. 'queued' => $response['data']['queued'] ?? true,
  183. ];
  184. }
  185. /**
  186. * 从“我的设备”解除当前商户绑定,复用旧系统解绑语义并写入流水。
  187. */
  188. public function unbindDevice(int $deviceId, bool $subordinate = false): bool
  189. {
  190. $device = $this->findAccessibleDevice($deviceId, $subordinate);
  191. $operatorId = $this->currentAdminId();
  192. return (bool) Db::transaction(function () use ($device, $deviceId, $operatorId) {
  193. $binding = Db::table('sa_merchant_device_binding')->where('device_id', $deviceId)->lock(true)->find();
  194. if (empty($binding)) {
  195. throw new ApiException('该设备当前未铺货,无需解绑');
  196. }
  197. $now = date('Y-m-d H:i:s');
  198. Db::table('sa_merchant_device_binding')->where('device_id', $deviceId)->delete();
  199. Db::table('sa_merchant_device_binding_record')->insert([
  200. 'device_id' => $deviceId,
  201. 'device_sn' => (string) $binding['device_sn'],
  202. 'depend_type' => (int) $binding['depend_type'],
  203. 'merchant_id' => (int) $binding['merchant_id'],
  204. 'action_type' => 2,
  205. 'operator_id' => $operatorId,
  206. 'action_time' => $now,
  207. 'created_by' => $operatorId,
  208. 'create_time' => $now,
  209. ]);
  210. Db::table('sa_device')->where('id', $deviceId)->update([
  211. 'is_active' => 0,
  212. 'active_time' => null,
  213. 'updated_by' => $operatorId,
  214. 'update_time' => $now,
  215. ]);
  216. $merchantId = (int) $binding['merchant_id'];
  217. $count = Db::table('sa_merchant_device_binding')->where('merchant_id', $merchantId)->count();
  218. Db::table('sa_merchant')->where('id', $merchantId)->update([
  219. 'device_count' => $count,
  220. 'distributed' => $count > 0 ? 1 : 0,
  221. 'update_time' => $now,
  222. 'updated_by' => $operatorId,
  223. ]);
  224. return true;
  225. });
  226. }
  227. private function buildListQuery(array $where, bool $subordinate = false)
  228. {
  229. $query = Db::table('sa_device')->alias('d')
  230. ->leftJoin('sa_merchant_device_binding b', 'b.device_id = d.id')
  231. ->leftJoin('sa_merchant m', 'm.id = b.merchant_id AND m.delete_time IS NULL')
  232. ->leftJoin('sa_system_user owner', 'owner.id = d.belong_agent_id AND owner.delete_time IS NULL')
  233. ->leftJoin('sa_system_user manager', 'manager.id = d.manage_agent_id AND manager.delete_time IS NULL')
  234. ->whereNull('d.delete_time')
  235. ->field(implode(',', [
  236. 'd.id', 'd.goods_name', 'd.goods_type', 'd.goods_sn', 'd.device_code',
  237. 'd.model', 'd.status', 'd.belong_agent_id', 'd.manage_agent_id',
  238. 'd.is_locked', 'd.is_active', 'd.active_time', 'd.device_status',
  239. 'd.create_time', 'd.update_time', 'b.merchant_id', 'b.bind_time',
  240. 'm.name AS merchant_name', 'm.phone AS merchant_phone',
  241. 'owner.id AS owner_id', 'owner.username AS owner_username', 'owner.realname AS owner_realname', 'owner.phone AS owner_phone',
  242. 'manager.id AS manager_id', 'manager.username AS manager_username', 'manager.realname AS manager_realname', 'manager.phone AS manager_phone',
  243. ]));
  244. if ($subordinate) {
  245. $managerIds = $this->subordinateManagerIds();
  246. if ($managerIds === []) {
  247. $query->where('d.id', 0);
  248. } else {
  249. $query->whereIn('d.manage_agent_id', $managerIds);
  250. }
  251. } else {
  252. $currentId = $this->currentAdminId();
  253. if ($currentId !== 1) {
  254. $query->where('d.manage_agent_id', $currentId);
  255. }
  256. }
  257. $category = trim((string) ($where['category'] ?? ''));
  258. if ($category !== '' && $category !== 'all' && isset(self::CATEGORY_TYPES[$category])) {
  259. $query->whereIn('d.goods_type', self::CATEGORY_TYPES[$category]);
  260. }
  261. if (($where['goods_type'] ?? '') !== '') {
  262. $query->where('d.goods_type', (int) $where['goods_type']);
  263. }
  264. $merchant = trim((string) ($where['merchant'] ?? ''));
  265. if ($merchant !== '') {
  266. $query->whereLike('m.name', '%' . $merchant . '%');
  267. }
  268. $qrCode = trim((string) ($where['qr_code'] ?? ''));
  269. if ($qrCode !== '') {
  270. $query->whereLike('d.device_code', '%' . $qrCode . '%');
  271. }
  272. $deviceSn = trim((string) ($where['device_sn'] ?? ''));
  273. if ($deviceSn !== '') {
  274. $query->whereLike('d.goods_sn', '%' . $deviceSn . '%');
  275. }
  276. $signal = trim((string) ($where['signal'] ?? ''));
  277. if ($signal === 'online') {
  278. $query->where('d.device_status', 1);
  279. } elseif ($signal === 'offline') {
  280. $query->where('d.device_status', '<>', 1);
  281. }
  282. $deviceState = trim((string) ($where['device_state'] ?? ''));
  283. if ($deviceState === 'online') {
  284. $query->where('d.device_status', 1);
  285. } elseif ($deviceState === 'offline') {
  286. $query->where('d.device_status', '<>', 1);
  287. } elseif ($deviceState === 'bound') {
  288. $query->whereNotNull('b.device_id');
  289. } elseif ($deviceState === 'unbound') {
  290. $query->whereNull('b.device_id');
  291. }
  292. return $query;
  293. }
  294. private function findAccessibleDevice(int $deviceId, bool $subordinate = false): array
  295. {
  296. if ($deviceId <= 0) {
  297. throw new ApiException('请选择设备');
  298. }
  299. $query = Db::table('sa_device')->where('id', $deviceId)->whereNull('delete_time');
  300. if ($subordinate) {
  301. $managerIds = $this->subordinateManagerIds();
  302. if ($managerIds === []) {
  303. throw new ApiException('设备不存在或无权操作');
  304. }
  305. $query->whereIn('manage_agent_id', $managerIds);
  306. } elseif ($this->currentAdminId() !== 1) {
  307. $query->where('manage_agent_id', $this->currentAdminId());
  308. }
  309. $device = $query->find();
  310. if (empty($device)) {
  311. throw new ApiException('设备不存在或无权操作');
  312. }
  313. return $device;
  314. }
  315. private function userSummary(int $userId): ?array
  316. {
  317. if ($userId <= 0) {
  318. return null;
  319. }
  320. $user = SystemUser::where('id', $userId)->field('id,username,realname,phone')->findOrEmpty();
  321. if ($user->isEmpty()) {
  322. return ['id' => $userId, 'name' => '未找到账号', 'username' => '', 'phone' => ''];
  323. }
  324. return [
  325. 'id' => (int) $user->id,
  326. 'name' => (string) ($user->realname ?: $user->username),
  327. 'username' => (string) $user->username,
  328. 'phone' => (string) ($user->phone ?? ''),
  329. ];
  330. }
  331. private function postDeviceCommand(string $endpoint, array $payload): array
  332. {
  333. if (!function_exists('curl_init')) {
  334. throw new ApiException('当前 PHP 未启用 cURL,无法调用设备指令服务');
  335. }
  336. $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
  337. $curl = curl_init($endpoint);
  338. curl_setopt_array($curl, [
  339. CURLOPT_POST => true,
  340. CURLOPT_POSTFIELDS => $body,
  341. CURLOPT_RETURNTRANSFER => true,
  342. CURLOPT_CONNECTTIMEOUT => 5,
  343. CURLOPT_TIMEOUT => 10,
  344. CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
  345. ]);
  346. $raw = curl_exec($curl);
  347. $error = curl_error($curl);
  348. $statusCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
  349. curl_close($curl);
  350. if ($raw === false) {
  351. throw new ApiException('设备指令服务请求失败:' . ($error ?: '连接异常'));
  352. }
  353. $decoded = json_decode($raw, true);
  354. if (!is_array($decoded)) {
  355. throw new ApiException('设备指令服务返回了无效响应(HTTP ' . $statusCode . ')');
  356. }
  357. if ($statusCode < 200 || $statusCode >= 300) {
  358. $decoded['code'] = $decoded['code'] ?? $statusCode;
  359. $decoded['msg'] = $decoded['msg'] ?? '设备指令服务请求失败';
  360. }
  361. return $decoded;
  362. }
  363. private function writeCommandLog(int $deviceId, string $action, array $request, array $response, bool $success): void
  364. {
  365. Db::table('sa_device_command_log')->insert([
  366. 'device_id' => $deviceId,
  367. 'device_sn' => (string) ($request['uid'] ?? ''),
  368. 'command' => 'switch',
  369. 'action' => $action,
  370. 'request_payload' => json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  371. 'response_payload' => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
  372. 'status' => $success ? 1 : 2,
  373. 'operator_id' => $this->currentAdminId(),
  374. 'create_time' => date('Y-m-d H:i:s'),
  375. ]);
  376. }
  377. private function summary(array $where, bool $subordinate = false): array
  378. {
  379. $categoryWhere = $where;
  380. $categoryWhere['category'] = 'all';
  381. $statusWhere = $where;
  382. $statusWhere['device_state'] = '';
  383. return [
  384. 'all' => (int) $this->buildListQuery($categoryWhere, $subordinate)->count(),
  385. 'vehicle' => (int) $this->buildListQuery(array_merge($categoryWhere, ['category' => 'vehicle']), $subordinate)->count(),
  386. 'power-bank' => (int) $this->buildListQuery(array_merge($categoryWhere, ['category' => 'power-bank']), $subordinate)->count(),
  387. 'online' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'online']), $subordinate)->count(),
  388. 'offline' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'offline']), $subordinate)->count(),
  389. 'bound' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'bound']), $subordinate)->count(),
  390. 'unbound' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'unbound']), $subordinate)->count(),
  391. ];
  392. }
  393. private function formatDevice(array $row): array
  394. {
  395. $type = (int) ($row['goods_type'] ?? 0);
  396. $deviceStatus = (int) ($row['device_status'] ?? 0);
  397. $merchantId = (int) ($row['merchant_id'] ?? 0);
  398. $qrCode = (string) (($row['device_code'] ?? '') ?: ($row['goods_sn'] ?? ''));
  399. $qrBaseUrl = rtrim(trim((string) env('DEVICE_QR_BASE_URL', '')), '/');
  400. $device = [
  401. 'id' => (int) $row['id'],
  402. 'category' => $type === 107 ? 'vehicle' : 'power-bank',
  403. 'name' => (string) ($row['goods_name'] ?? ''),
  404. 'goods_type' => $type,
  405. 'type_name' => self::TYPE_NAMES[$type] ?? '其他设备',
  406. 'qr_code' => $qrCode,
  407. 'qr_content' => $qrBaseUrl !== '' ? $qrBaseUrl . '/' . rawurlencode($qrCode) : $qrCode,
  408. 'device_sn' => (string) ($row['goods_sn'] ?? ''),
  409. 'borrowable' => '--',
  410. 'imei' => '--',
  411. 'version' => (string) (($row['model'] ?? '') ?: '--'),
  412. 'signal' => $deviceStatus === 1 ? '在线' : '离线',
  413. 'signal_value' => $deviceStatus === 1 ? '正常' : '无信号',
  414. 'connection_count' => 0,
  415. 'distributed' => $merchantId > 0,
  416. 'distributed_at' => (string) ($row['bind_time'] ?? ''),
  417. 'merchant_id' => $merchantId,
  418. 'merchant' => (string) (($row['merchant_name'] ?? '') ?: '--'),
  419. 'merchant_phone' => (string) ($row['merchant_phone'] ?? ''),
  420. 'wechat_orders' => 0,
  421. 'alipay_orders' => 0,
  422. 'amount' => '0.00',
  423. 'heartbeat' => $this->heartbeatName($deviceStatus),
  424. 'status' => (int) ($row['status'] ?? 0),
  425. 'status_name' => $this->statusName((int) ($row['status'] ?? 0)),
  426. 'manage_agent_id' => (int) ($row['manage_agent_id'] ?? 0),
  427. 'is_locked' => (int) ($row['is_locked'] ?? 0) === 1,
  428. 'is_active' => (int) ($row['is_active'] ?? 0) === 1,
  429. 'active_time' => (string) ($row['active_time'] ?? ''),
  430. 'created_at' => (string) ($row['create_time'] ?? ''),
  431. ];
  432. if (array_key_exists('owner_id', $row)) {
  433. $device['owner'] = $this->personFromRow($row, 'owner');
  434. $device['manager'] = $this->personFromRow($row, 'manager');
  435. }
  436. return $device;
  437. }
  438. private function statusName(int $status): string
  439. {
  440. return match ($status) {
  441. 1 => '运营中',
  442. 2 => '报修',
  443. default => '未启用',
  444. };
  445. }
  446. private function heartbeatName(int $status): string
  447. {
  448. return match ($status) {
  449. 1 => '正常',
  450. 2 => '维护',
  451. 3 => '异常',
  452. default => '掉线',
  453. };
  454. }
  455. private function currentAdminId(): int
  456. {
  457. return (int) ($this->adminInfo['id'] ?? 0);
  458. }
  459. /** @return int[] */
  460. private function subordinateManagerIds(): array
  461. {
  462. $currentId = $this->currentAdminId();
  463. if ($currentId <= 0) {
  464. return [];
  465. }
  466. if ($currentId === 1) {
  467. return array_map('intval', SystemUser::where('id', '<>', 1)
  468. ->whereNull('delete_time')->column('id'));
  469. }
  470. $dept = $this->adminInfo['deptList'] ?? [];
  471. if (empty($dept['id']) || !isset($dept['level'])) {
  472. return [];
  473. }
  474. $prefix = (string) $dept['level'] . (int) $dept['id'] . ',';
  475. $deptIds = Db::table('sa_system_dept')->whereNull('delete_time')
  476. ->whereLike('level', $prefix . '%')->column('id');
  477. if ($deptIds === []) {
  478. return [];
  479. }
  480. return array_map('intval', SystemUser::whereIn('dept_id', $deptIds)
  481. ->whereNull('delete_time')->column('id'));
  482. }
  483. private function personFromRow(array $row, string $prefix): ?array
  484. {
  485. $id = (int) ($row[$prefix . '_id'] ?? 0);
  486. if ($id <= 0) {
  487. return null;
  488. }
  489. $username = (string) ($row[$prefix . '_username'] ?? '');
  490. $realname = (string) ($row[$prefix . '_realname'] ?? '');
  491. return [
  492. 'id' => $id,
  493. 'name' => $realname !== '' ? $realname : ($username !== '' ? $username : '未找到账号'),
  494. 'username' => $username,
  495. 'phone' => (string) ($row[$prefix . '_phone'] ?? ''),
  496. ];
  497. }
  498. private function excelSafe(mixed $value): string
  499. {
  500. $value = (string) $value;
  501. return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value;
  502. }
  503. }