load(); \support\App::loadAllConfig(['route']); date_default_timezone_set('Asia/Shanghai'); use plugin\saiadmin\app\model\water\{ WaterModel, WaterDevice, WaterDeviceUsage, WaterEnterprise, WaterSku, WaterAccount, WaterOrder, WaterDelivery, WaterLedger }; use plugin\saiadmin\app\service\{WaterBusiness, WaterDeviceDashboard}; const DEVICE_DEMO_BATCH = 'TEST-WATER-DEVICES-20260911'; function ensure(bool $condition, string $message): void { if (!$condition) { throw new RuntimeException($message); } } function protectedBalances(): array { $result = []; foreach ([WaterSku::class, WaterAccount::class, WaterOrder::class, WaterDelivery::class] as $model) { $result[$model] = hash('sha256', serialize($model::query()->orderBy('id')->get()->toArray())); } return $result; } $samples = [ ['会议区饮水机', '南山', 'online', 0, 18.6, '1.2.0-test', ''], ['前台饮水机', '罗湖', 'online', 2, 23.2, '1.2.0-test', ''], ['研发区设备', '福田', 'online', 5, 31.8, '1.3.0-test', ''], ['茶水间设备', '宝安', 'offline', 26 * 60, null, '1.1.0-test', ''], ['西区饮水机', '南山', 'online', 1, 16.4, '1.1.0-test', ''], ['会议室设备', '罗湖', 'online', 3, 20.1, '1.2.0-test', ''], ['研发楼设备', '福田', 'online', 8, 28.6, '1.3.0-test', ''], ['接待区设备', '宝安', 'fault', 42, null, '1.1.0-test', '【测试】流量数据异常'], ['二楼饮水机', '南山', 'offline', 90, 0.0, '1.2.0-test', ''], ['员工区设备', '罗湖', 'fault', 4, 12.5, '1.3.0-test', '【测试】滤芯寿命告警'], ['待安装设备A', '福田', 'unconnected', null, null, '', ''], ['待安装设备B', '宝安', 'unconnected', null, null, '', ''], ]; $apply = in_array('--apply', $argv, true); $result = ['applied' => false, 'planned_devices' => count($samples)]; if ($apply) { $result = WaterModel::transaction(function () use ($samples): array { $sku = WaterSku::query()->where('name', '【测试0910】18.9L桶装水')->lockForUpdate()->first(); ensure($sku && (int) $sku->status === 1, '缺少指定启用测试规格,停止写入'); if (WaterLedger::query()->where(['kind' => 'demo_seed', 'reference' => DEVICE_DEMO_BATCH])->exists()) { return ['applied' => true, 'replayed' => true]; } ensure(date('Y-m-d') === '2026-09-11', '固定日期批次仅允许在对应日期首次执行'); ensure(!WaterDevice::query()->where('sn', 'like', DEVICE_DEMO_BATCH . '-%')->exists(), '批次SN已存在,停止避免覆盖'); $enterprises = []; foreach (['南山', '罗湖', '福田', '宝安'] as $district) { $enterprise = WaterEnterprise::query()->where('status', 1) ->where('name', 'like', '【测试0911】%')->where('region', '深圳市' . $district . '区') ->orderBy('id')->first(); ensure((bool) $enterprise, '缺少' . $district . '区的既有测试企业'); $enterprises[$district] = (int) $enterprise->id; } $balances = protectedBalances(); $oldDevices = WaterDevice::query()->orderBy('id')->get()->toArray(); $oldIds = array_column($oldDevices, 'id'); $oldUsageCount = WaterDeviceUsage::query()->count(); $business = new WaterBusiness(); $now = new DateTimeImmutable(); $deviceIds = []; $usageRows = []; foreach ($samples as $index => [$name, $district, $state, $minutes, $todayLiters, $version, $fault]) { $id = $business->save('device', [ 'name' => '【测试0911】' . $name, 'sn' => DEVICE_DEMO_BATCH . '-' . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT), 'enterprise_id' => $enterprises[$district], 'sku_id' => (int) $sku->id, 'firmware' => $version, ], 1)['id']; $deviceIds[] = $id; $lastSeen = $minutes === null ? null : $now->modify('-' . $minutes . ' minutes'); $updates = [ 'last_seen' => $lastSeen?->format('Y-m-d H:i:s'), 'fault' => $fault, 'today_liters' => $todayLiters, 'telemetry_date' => $todayLiters === null ? null : $now->format('Y-m-d'), ]; // Historical samples are explicitly attached to TEST devices, never sent as live reports. if ($state !== 'unconnected') { for ($offset = 29; $offset >= 0; $offset--) { $day = $now->modify('-' . $offset . ' days')->format('Y-m-d'); if ($day > $lastSeen->format('Y-m-d') || ($offset === 0 && $todayLiters === null)) { continue; } $liters = $offset === 0 ? $todayLiters : round((14 + $index * 1.7 + (29 - $offset) * 0.25) * (0.78 + (($offset + $index) % 7) * 0.06), 2); $reportedAt = min($day . ' 17:00:00', $lastSeen->format('Y-m-d H:i:s')); $usageRows[] = ['device_id' => $id, 'day' => $day, 'liters' => $liters, 'reported_at' => $reportedAt]; if ($offset > 0 && $todayLiters === null) { $updates['today_liters'] = $liters; $updates['telemetry_date'] = $day; } } } WaterDevice::query()->where('id', $id)->update($updates); } WaterDeviceUsage::query()->insert($usageRows); $dashboard = new WaterDeviceDashboard($now); $first = $dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'limit' => 8]); $second = $dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'limit' => 8, 'page' => 2]); ensure($first['list']['total'] === 12 && count($first['list']['data']) === 8 && count($second['list']['data']) === 4, '分页验证失败'); foreach (['online' => 6, 'offline' => 2, 'fault' => 2, 'unconnected' => 2] as $state => $count) { ensure($dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'status' => $state])['list']['total'] === $count, '状态验证失败:' . $state); } foreach ($enterprises as $district => $id) { ensure($dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'region' => '深圳市' . $district . '区'])['list']['total'] === 3, '区域筛选验证失败'); } ensure(count($first['trend']) === 30 && count(array_filter($first['trend'], fn ($row) => $row['liters'] !== null)) === 30, '趋势日期验证失败'); ensure(WaterDeviceUsage::query()->count() === $oldUsageCount + count($usageRows), '历史记录数量异常'); ensure(protectedBalances() === $balances, '原库存、权益、配送或订单发生变动,整批回滚'); ensure(WaterDevice::query()->whereIn('id', $oldIds)->orderBy('id')->get()->toArray() === $oldDevices, '既有设备被修改,整批回滚'); WaterLedger::query()->insert([ 'kind' => 'demo_seed', 'reference' => DEVICE_DEMO_BATCH, 'sku_id' => (int) $sku->id, 'quantity' => 0, 'created_by' => 1, 'note' => '【测试】12台模拟设备及30天用量;非真实遥测,库存权益未变。设备ID:' . implode(',', $deviceIds), 'created_at' => $now->format('Y-m-d H:i:s'), ]); return ['applied' => true, 'replayed' => false, 'device_ids' => $deviceIds, 'usage_rows' => count($usageRows)]; }); } $dashboard = (new WaterDeviceDashboard())->dashboard(['keyword' => DEVICE_DEMO_BATCH]); $result['batch_devices'] = $dashboard['list']['total']; $result['global_summary'] = $dashboard['summary']; echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL;