| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- <?php
- // +----------------------------------------------------------------------
- // | saiadmin [ saiadmin快速开发框架 ]
- // +----------------------------------------------------------------------
- namespace plugin\saiadmin\app\logic\device;
- use plugin\saiadmin\app\model\merchant\MerchantDevice;
- use plugin\saiadmin\app\model\system\SystemUser;
- use plugin\saiadmin\basic\think\BaseLogic;
- use plugin\saiadmin\exception\ApiException;
- use plugin\saiadmin\service\OpenSpoutWriter;
- use support\Response;
- use support\think\Db;
- /**
- * 我的设备列表、搜索与导出。
- */
- class DeviceLogic extends BaseLogic
- {
- private const TYPE_NAMES = [
- 1 => '充电底座',
- 100 => 'K霸宝',
- 101 => '充电宝',
- 102 => 'K霸宝 Blue',
- 103 => '密码线充',
- 104 => '蓝牙线充',
- 105 => '密码蓝牙线充',
- 107 => '车载充电器',
- ];
- private const CATEGORY_TYPES = [
- 'vehicle' => [107],
- 'power-bank' => [1, 100, 101, 102, 103, 104, 105],
- ];
- public function __construct()
- {
- $this->model = new MerchantDevice();
- }
- /**
- * 当前代理管理的设备分页列表。
- */
- public function indexList(array $where): array
- {
- $page = max(1, (int) request()->input('page', 1));
- $limit = min(100, max(1, (int) request()->input('limit', 20)));
- $result = $this->buildListQuery($where)
- ->order('d.id', 'desc')
- ->paginate([
- 'page' => $page,
- 'list_rows' => $limit,
- ])
- ->toArray();
- $result['data'] = array_map(fn (array $row) => $this->formatDevice($row), $result['data'] ?? []);
- $result['summary'] = $this->summary($where);
- return $result;
- }
- /**
- * 当前账号下级代理管理的设备列表。
- *
- * 下级范围由 sa_system_dept 的 level 路径确定;设备仍以
- * sa_device.manage_agent_id 作为当前管理关系,避免把设备所有权
- * (belong_agent_id) 误当作可操作权限。
- */
- public function subordinateIndexList(array $where): array
- {
- $page = max(1, (int) request()->input('page', 1));
- $limit = min(100, max(1, (int) request()->input('limit', 20)));
- $result = $this->buildListQuery($where, true)
- ->order('d.id', 'desc')
- ->paginate([
- 'page' => $page,
- 'list_rows' => $limit,
- ])
- ->toArray();
- $result['data'] = array_map(fn (array $row) => $this->formatDevice($row), $result['data'] ?? []);
- $result['summary'] = $this->summary($where, true);
- return $result;
- }
- /**
- * 按当前搜索条件导出全部设备。
- */
- public function export(array $where): Response
- {
- $rows = $this->buildListQuery($where)->order('d.id', 'desc')->select()->toArray();
- $data = [];
- foreach ($rows as $row) {
- $device = $this->formatDevice($row);
- $data[] = [
- 'id' => $device['id'],
- 'name' => $this->excelSafe($device['name']),
- 'type_name' => $device['type_name'],
- 'device_sn' => $this->excelSafe($device['device_sn']),
- 'qr_code' => $this->excelSafe($device['qr_code']),
- 'model' => $this->excelSafe($device['version']),
- 'status_name' => $device['status_name'],
- 'signal' => $device['signal'],
- 'heartbeat' => $device['heartbeat'],
- 'distributed' => $device['distributed'] ? '已绑定' : '未绑定',
- 'merchant' => $this->excelSafe($device['merchant']),
- 'merchant_phone' => $this->excelSafe($device['merchant_phone']),
- 'manage_agent_id' => $device['manage_agent_id'],
- 'is_active' => $device['is_active'] ? '已激活' : '未激活',
- 'active_time' => $device['active_time'],
- 'distributed_at' => $device['distributed_at'],
- 'created_at' => $device['created_at'],
- ];
- }
- $fileName = '我的设备_' . date('Ymd_His') . '.xlsx';
- $writer = new OpenSpoutWriter($fileName);
- $writer->setWidth([10, 20, 18, 24, 22, 16, 12, 12, 12, 12, 22, 16, 14, 12, 20, 20, 20]);
- $writer->setHeader([
- '设备ID', '设备名称', '设备类型', '设备SN', '二维码/机器码', '设备型号',
- '运营状态', '设备信号', '心跳状态', '绑定状态', '商户名称', '商户电话',
- '管理代理ID', '激活状态', '激活时间', '绑定时间', '创建时间',
- ]);
- $writer->setData($data);
- return response()->download($writer->returnFile(), urlencode($fileName));
- }
- /**
- * 返回设备归属、当前管理者、商户绑定及二维码内容。
- */
- public function readDevice(int $deviceId, bool $subordinate = false): array
- {
- $device = $this->findAccessibleDevice($deviceId, $subordinate);
- $binding = Db::table('sa_merchant_device_binding')->alias('b')
- ->leftJoin('sa_merchant m', 'm.id = b.merchant_id AND m.delete_time IS NULL')
- ->where('b.device_id', $deviceId)
- ->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')
- ->find();
- $owner = $this->userSummary((int) ($device['belong_agent_id'] ?? 0));
- $manager = $this->userSummary((int) ($device['manage_agent_id'] ?? 0));
- $merchantOperator = $this->userSummary((int) ($binding['agent_id'] ?? 0));
- $code = (string) (($device['device_code'] ?? '') ?: ($device['goods_sn'] ?? ''));
- $qrBaseUrl = rtrim(trim((string) env('DEVICE_QR_BASE_URL', '')), '/');
- return [
- 'device' => array_merge($this->formatDevice($device), [
- 'qr_content' => $qrBaseUrl !== '' ? $qrBaseUrl . '/' . rawurlencode($code) : $code,
- 'can_switch_power' => (int) ($device['goods_type'] ?? 0) === 107,
- 'can_unbind' => !empty($binding),
- ]),
- 'owner' => $owner,
- 'manager' => $manager,
- 'merchant' => empty($binding) ? null : [
- 'id' => (int) $binding['merchant_id'],
- 'name' => (string) ($binding['merchant_name'] ?? ''),
- 'phone' => (string) ($binding['merchant_phone'] ?? ''),
- 'address' => (string) ($binding['merchant_address'] ?? ''),
- 'bind_time' => (string) ($binding['bind_time'] ?? ''),
- 'operator' => $merchantOperator,
- ],
- ];
- }
- /**
- * 沿用旧系统车辆控制协议:POST /cdy/tasks,task_type=switch,value=1/0。
- * 指令服务必须显式配置,避免在未接入硬件网关时伪造成功状态。
- */
- public function switchPower(int $deviceId, string $action, bool $subordinate = false): array
- {
- $device = $this->findAccessibleDevice($deviceId, $subordinate);
- if ((int) ($device['goods_type'] ?? 0) !== 107) {
- throw new ApiException('仅车载充电器支持供电和断电操作');
- }
- $endpoint = trim((string) env('DEVICE_COMMAND_ENDPOINT', ''));
- if ($endpoint === '') {
- throw new ApiException('设备指令服务未配置,无法向真实设备下发操作');
- }
- $value = $action === 'on' ? 1 : 0;
- $payload = [
- 'uid' => (string) (($device['goods_sn'] ?? '') ?: ($device['device_code'] ?? '')),
- 'task_type' => 'switch',
- 'value' => $value,
- ];
- $response = $this->postDeviceCommand($endpoint, $payload);
- $success = in_array((int) ($response['code'] ?? -1), [0, 200], true);
- if ($success && $value === 1 && isset($response['data']['queued']) && (int) $response['data']['queued'] !== 1) {
- $success = false;
- }
- $this->writeCommandLog($deviceId, $action, $payload, $response, $success);
- if (!$success) {
- throw new ApiException((string) ($response['msg'] ?? '设备指令下发失败'));
- }
- return [
- 'device_id' => $deviceId,
- 'action' => $action,
- 'queued' => $response['data']['queued'] ?? true,
- ];
- }
- /**
- * 从“我的设备”解除当前商户绑定,复用旧系统解绑语义并写入流水。
- */
- public function unbindDevice(int $deviceId, bool $subordinate = false): bool
- {
- $device = $this->findAccessibleDevice($deviceId, $subordinate);
- $operatorId = $this->currentAdminId();
- return (bool) Db::transaction(function () use ($device, $deviceId, $operatorId) {
- $binding = Db::table('sa_merchant_device_binding')->where('device_id', $deviceId)->lock(true)->find();
- if (empty($binding)) {
- throw new ApiException('该设备当前未铺货,无需解绑');
- }
- $now = date('Y-m-d H:i:s');
- Db::table('sa_merchant_device_binding')->where('device_id', $deviceId)->delete();
- Db::table('sa_merchant_device_binding_record')->insert([
- 'device_id' => $deviceId,
- 'device_sn' => (string) $binding['device_sn'],
- 'depend_type' => (int) $binding['depend_type'],
- 'merchant_id' => (int) $binding['merchant_id'],
- 'action_type' => 2,
- 'operator_id' => $operatorId,
- 'action_time' => $now,
- 'created_by' => $operatorId,
- 'create_time' => $now,
- ]);
- Db::table('sa_device')->where('id', $deviceId)->update([
- 'is_active' => 0,
- 'active_time' => null,
- 'updated_by' => $operatorId,
- 'update_time' => $now,
- ]);
- $merchantId = (int) $binding['merchant_id'];
- $count = Db::table('sa_merchant_device_binding')->where('merchant_id', $merchantId)->count();
- Db::table('sa_merchant')->where('id', $merchantId)->update([
- 'device_count' => $count,
- 'distributed' => $count > 0 ? 1 : 0,
- 'update_time' => $now,
- 'updated_by' => $operatorId,
- ]);
- return true;
- });
- }
- private function buildListQuery(array $where, bool $subordinate = false)
- {
- $query = Db::table('sa_device')->alias('d')
- ->leftJoin('sa_merchant_device_binding b', 'b.device_id = d.id')
- ->leftJoin('sa_merchant m', 'm.id = b.merchant_id AND m.delete_time IS NULL')
- ->leftJoin('sa_system_user owner', 'owner.id = d.belong_agent_id AND owner.delete_time IS NULL')
- ->leftJoin('sa_system_user manager', 'manager.id = d.manage_agent_id AND manager.delete_time IS NULL')
- ->whereNull('d.delete_time')
- ->field(implode(',', [
- 'd.id', 'd.goods_name', 'd.goods_type', 'd.goods_sn', 'd.device_code',
- 'd.model', 'd.status', 'd.belong_agent_id', 'd.manage_agent_id',
- 'd.is_locked', 'd.is_active', 'd.active_time', 'd.device_status',
- 'd.create_time', 'd.update_time', 'b.merchant_id', 'b.bind_time',
- 'm.name AS merchant_name', 'm.phone AS merchant_phone',
- 'owner.id AS owner_id', 'owner.username AS owner_username', 'owner.realname AS owner_realname', 'owner.phone AS owner_phone',
- 'manager.id AS manager_id', 'manager.username AS manager_username', 'manager.realname AS manager_realname', 'manager.phone AS manager_phone',
- ]));
- if ($subordinate) {
- $managerIds = $this->subordinateManagerIds();
- if ($managerIds === []) {
- $query->where('d.id', 0);
- } else {
- $query->whereIn('d.manage_agent_id', $managerIds);
- }
- } else {
- $currentId = $this->currentAdminId();
- if ($currentId !== 1) {
- $query->where('d.manage_agent_id', $currentId);
- }
- }
- $category = trim((string) ($where['category'] ?? ''));
- if ($category !== '' && $category !== 'all' && isset(self::CATEGORY_TYPES[$category])) {
- $query->whereIn('d.goods_type', self::CATEGORY_TYPES[$category]);
- }
- if (($where['goods_type'] ?? '') !== '') {
- $query->where('d.goods_type', (int) $where['goods_type']);
- }
- $merchant = trim((string) ($where['merchant'] ?? ''));
- if ($merchant !== '') {
- $query->whereLike('m.name', '%' . $merchant . '%');
- }
- $qrCode = trim((string) ($where['qr_code'] ?? ''));
- if ($qrCode !== '') {
- $query->whereLike('d.device_code', '%' . $qrCode . '%');
- }
- $deviceSn = trim((string) ($where['device_sn'] ?? ''));
- if ($deviceSn !== '') {
- $query->whereLike('d.goods_sn', '%' . $deviceSn . '%');
- }
- $signal = trim((string) ($where['signal'] ?? ''));
- if ($signal === 'online') {
- $query->where('d.device_status', 1);
- } elseif ($signal === 'offline') {
- $query->where('d.device_status', '<>', 1);
- }
- $deviceState = trim((string) ($where['device_state'] ?? ''));
- if ($deviceState === 'online') {
- $query->where('d.device_status', 1);
- } elseif ($deviceState === 'offline') {
- $query->where('d.device_status', '<>', 1);
- } elseif ($deviceState === 'bound') {
- $query->whereNotNull('b.device_id');
- } elseif ($deviceState === 'unbound') {
- $query->whereNull('b.device_id');
- }
- return $query;
- }
- private function findAccessibleDevice(int $deviceId, bool $subordinate = false): array
- {
- if ($deviceId <= 0) {
- throw new ApiException('请选择设备');
- }
- $query = Db::table('sa_device')->where('id', $deviceId)->whereNull('delete_time');
- if ($subordinate) {
- $managerIds = $this->subordinateManagerIds();
- if ($managerIds === []) {
- throw new ApiException('设备不存在或无权操作');
- }
- $query->whereIn('manage_agent_id', $managerIds);
- } elseif ($this->currentAdminId() !== 1) {
- $query->where('manage_agent_id', $this->currentAdminId());
- }
- $device = $query->find();
- if (empty($device)) {
- throw new ApiException('设备不存在或无权操作');
- }
- return $device;
- }
- private function userSummary(int $userId): ?array
- {
- if ($userId <= 0) {
- return null;
- }
- $user = SystemUser::where('id', $userId)->field('id,username,realname,phone')->findOrEmpty();
- if ($user->isEmpty()) {
- return ['id' => $userId, 'name' => '未找到账号', 'username' => '', 'phone' => ''];
- }
- return [
- 'id' => (int) $user->id,
- 'name' => (string) ($user->realname ?: $user->username),
- 'username' => (string) $user->username,
- 'phone' => (string) ($user->phone ?? ''),
- ];
- }
- private function postDeviceCommand(string $endpoint, array $payload): array
- {
- if (!function_exists('curl_init')) {
- throw new ApiException('当前 PHP 未启用 cURL,无法调用设备指令服务');
- }
- $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
- $curl = curl_init($endpoint);
- curl_setopt_array($curl, [
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => $body,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_CONNECTTIMEOUT => 5,
- CURLOPT_TIMEOUT => 10,
- CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
- ]);
- $raw = curl_exec($curl);
- $error = curl_error($curl);
- $statusCode = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
- curl_close($curl);
- if ($raw === false) {
- throw new ApiException('设备指令服务请求失败:' . ($error ?: '连接异常'));
- }
- $decoded = json_decode($raw, true);
- if (!is_array($decoded)) {
- throw new ApiException('设备指令服务返回了无效响应(HTTP ' . $statusCode . ')');
- }
- if ($statusCode < 200 || $statusCode >= 300) {
- $decoded['code'] = $decoded['code'] ?? $statusCode;
- $decoded['msg'] = $decoded['msg'] ?? '设备指令服务请求失败';
- }
- return $decoded;
- }
- private function writeCommandLog(int $deviceId, string $action, array $request, array $response, bool $success): void
- {
- Db::table('sa_device_command_log')->insert([
- 'device_id' => $deviceId,
- 'device_sn' => (string) ($request['uid'] ?? ''),
- 'command' => 'switch',
- 'action' => $action,
- 'request_payload' => json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
- 'response_payload' => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
- 'status' => $success ? 1 : 2,
- 'operator_id' => $this->currentAdminId(),
- 'create_time' => date('Y-m-d H:i:s'),
- ]);
- }
- private function summary(array $where, bool $subordinate = false): array
- {
- $categoryWhere = $where;
- $categoryWhere['category'] = 'all';
- $statusWhere = $where;
- $statusWhere['device_state'] = '';
- return [
- 'all' => (int) $this->buildListQuery($categoryWhere, $subordinate)->count(),
- 'vehicle' => (int) $this->buildListQuery(array_merge($categoryWhere, ['category' => 'vehicle']), $subordinate)->count(),
- 'power-bank' => (int) $this->buildListQuery(array_merge($categoryWhere, ['category' => 'power-bank']), $subordinate)->count(),
- 'online' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'online']), $subordinate)->count(),
- 'offline' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'offline']), $subordinate)->count(),
- 'bound' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'bound']), $subordinate)->count(),
- 'unbound' => (int) $this->buildListQuery(array_merge($statusWhere, ['device_state' => 'unbound']), $subordinate)->count(),
- ];
- }
- private function formatDevice(array $row): array
- {
- $type = (int) ($row['goods_type'] ?? 0);
- $deviceStatus = (int) ($row['device_status'] ?? 0);
- $merchantId = (int) ($row['merchant_id'] ?? 0);
- $qrCode = (string) (($row['device_code'] ?? '') ?: ($row['goods_sn'] ?? ''));
- $qrBaseUrl = rtrim(trim((string) env('DEVICE_QR_BASE_URL', '')), '/');
- $device = [
- 'id' => (int) $row['id'],
- 'category' => $type === 107 ? 'vehicle' : 'power-bank',
- 'name' => (string) ($row['goods_name'] ?? ''),
- 'goods_type' => $type,
- 'type_name' => self::TYPE_NAMES[$type] ?? '其他设备',
- 'qr_code' => $qrCode,
- 'qr_content' => $qrBaseUrl !== '' ? $qrBaseUrl . '/' . rawurlencode($qrCode) : $qrCode,
- 'device_sn' => (string) ($row['goods_sn'] ?? ''),
- 'borrowable' => '--',
- 'imei' => '--',
- 'version' => (string) (($row['model'] ?? '') ?: '--'),
- 'signal' => $deviceStatus === 1 ? '在线' : '离线',
- 'signal_value' => $deviceStatus === 1 ? '正常' : '无信号',
- 'connection_count' => 0,
- 'distributed' => $merchantId > 0,
- 'distributed_at' => (string) ($row['bind_time'] ?? ''),
- 'merchant_id' => $merchantId,
- 'merchant' => (string) (($row['merchant_name'] ?? '') ?: '--'),
- 'merchant_phone' => (string) ($row['merchant_phone'] ?? ''),
- 'wechat_orders' => 0,
- 'alipay_orders' => 0,
- 'amount' => '0.00',
- 'heartbeat' => $this->heartbeatName($deviceStatus),
- 'status' => (int) ($row['status'] ?? 0),
- 'status_name' => $this->statusName((int) ($row['status'] ?? 0)),
- 'manage_agent_id' => (int) ($row['manage_agent_id'] ?? 0),
- 'is_locked' => (int) ($row['is_locked'] ?? 0) === 1,
- 'is_active' => (int) ($row['is_active'] ?? 0) === 1,
- 'active_time' => (string) ($row['active_time'] ?? ''),
- 'created_at' => (string) ($row['create_time'] ?? ''),
- ];
- if (array_key_exists('owner_id', $row)) {
- $device['owner'] = $this->personFromRow($row, 'owner');
- $device['manager'] = $this->personFromRow($row, 'manager');
- }
- return $device;
- }
- private function statusName(int $status): string
- {
- return match ($status) {
- 1 => '运营中',
- 2 => '报修',
- default => '未启用',
- };
- }
- private function heartbeatName(int $status): string
- {
- return match ($status) {
- 1 => '正常',
- 2 => '维护',
- 3 => '异常',
- default => '掉线',
- };
- }
- private function currentAdminId(): int
- {
- return (int) ($this->adminInfo['id'] ?? 0);
- }
- /** @return int[] */
- private function subordinateManagerIds(): array
- {
- $currentId = $this->currentAdminId();
- if ($currentId <= 0) {
- return [];
- }
- if ($currentId === 1) {
- return array_map('intval', SystemUser::where('id', '<>', 1)
- ->whereNull('delete_time')->column('id'));
- }
- $dept = $this->adminInfo['deptList'] ?? [];
- if (empty($dept['id']) || !isset($dept['level'])) {
- return [];
- }
- $prefix = (string) $dept['level'] . (int) $dept['id'] . ',';
- $deptIds = Db::table('sa_system_dept')->whereNull('delete_time')
- ->whereLike('level', $prefix . '%')->column('id');
- if ($deptIds === []) {
- return [];
- }
- return array_map('intval', SystemUser::whereIn('dept_id', $deptIds)
- ->whereNull('delete_time')->column('id'));
- }
- private function personFromRow(array $row, string $prefix): ?array
- {
- $id = (int) ($row[$prefix . '_id'] ?? 0);
- if ($id <= 0) {
- return null;
- }
- $username = (string) ($row[$prefix . '_username'] ?? '');
- $realname = (string) ($row[$prefix . '_realname'] ?? '');
- return [
- 'id' => $id,
- 'name' => $realname !== '' ? $realname : ($username !== '' ? $username : '未找到账号'),
- 'username' => $username,
- 'phone' => (string) ($row[$prefix . '_phone'] ?? ''),
- ];
- }
- private function excelSafe(mixed $value): string
- {
- $value = (string) $value;
- return preg_match('/^[=+\-@]/u', $value) === 1 ? "'" . $value : $value;
- }
- }
|