seed_water_device_dashboard_demo.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. <?php
  2. /** Fixed demo batch. --apply writes once; subsequent runs preserve user edits and heartbeat aging. */
  3. declare(strict_types=1);
  4. require dirname(__DIR__) . '/vendor/autoload.php';
  5. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  6. \support\App::loadAllConfig(['route']);
  7. date_default_timezone_set('Asia/Shanghai');
  8. use plugin\saiadmin\app\model\water\{
  9. WaterModel, WaterDevice, WaterDeviceUsage, WaterEnterprise, WaterSku,
  10. WaterAccount, WaterOrder, WaterDelivery, WaterLedger
  11. };
  12. use plugin\saiadmin\app\service\{WaterBusiness, WaterDeviceDashboard};
  13. const DEVICE_DEMO_BATCH = 'TEST-WATER-DEVICES-20260911';
  14. function ensure(bool $condition, string $message): void
  15. {
  16. if (!$condition) {
  17. throw new RuntimeException($message);
  18. }
  19. }
  20. function protectedBalances(): array
  21. {
  22. $result = [];
  23. foreach ([WaterSku::class, WaterAccount::class, WaterOrder::class, WaterDelivery::class] as $model) {
  24. $result[$model] = hash('sha256', serialize($model::query()->orderBy('id')->get()->toArray()));
  25. }
  26. return $result;
  27. }
  28. $samples = [
  29. ['会议区饮水机', '南山', 'online', 0, 18.6, '1.2.0-test', ''],
  30. ['前台饮水机', '罗湖', 'online', 2, 23.2, '1.2.0-test', ''],
  31. ['研发区设备', '福田', 'online', 5, 31.8, '1.3.0-test', ''],
  32. ['茶水间设备', '宝安', 'offline', 26 * 60, null, '1.1.0-test', ''],
  33. ['西区饮水机', '南山', 'online', 1, 16.4, '1.1.0-test', ''],
  34. ['会议室设备', '罗湖', 'online', 3, 20.1, '1.2.0-test', ''],
  35. ['研发楼设备', '福田', 'online', 8, 28.6, '1.3.0-test', ''],
  36. ['接待区设备', '宝安', 'fault', 42, null, '1.1.0-test', '【测试】流量数据异常'],
  37. ['二楼饮水机', '南山', 'offline', 90, 0.0, '1.2.0-test', ''],
  38. ['员工区设备', '罗湖', 'fault', 4, 12.5, '1.3.0-test', '【测试】滤芯寿命告警'],
  39. ['待安装设备A', '福田', 'unconnected', null, null, '', ''],
  40. ['待安装设备B', '宝安', 'unconnected', null, null, '', ''],
  41. ];
  42. $apply = in_array('--apply', $argv, true);
  43. $result = ['applied' => false, 'planned_devices' => count($samples)];
  44. if ($apply) {
  45. $result = WaterModel::transaction(function () use ($samples): array {
  46. $sku = WaterSku::query()->where('name', '【测试0910】18.9L桶装水')->lockForUpdate()->first();
  47. ensure($sku && (int) $sku->status === 1, '缺少指定启用测试规格,停止写入');
  48. if (WaterLedger::query()->where(['kind' => 'demo_seed', 'reference' => DEVICE_DEMO_BATCH])->exists()) {
  49. return ['applied' => true, 'replayed' => true];
  50. }
  51. ensure(date('Y-m-d') === '2026-09-11', '固定日期批次仅允许在对应日期首次执行');
  52. ensure(!WaterDevice::query()->where('sn', 'like', DEVICE_DEMO_BATCH . '-%')->exists(), '批次SN已存在,停止避免覆盖');
  53. $enterprises = [];
  54. foreach (['南山', '罗湖', '福田', '宝安'] as $district) {
  55. $enterprise = WaterEnterprise::query()->where('status', 1)
  56. ->where('name', 'like', '【测试0911】%')->where('region', '深圳市' . $district . '区')
  57. ->orderBy('id')->first();
  58. ensure((bool) $enterprise, '缺少' . $district . '区的既有测试企业');
  59. $enterprises[$district] = (int) $enterprise->id;
  60. }
  61. $balances = protectedBalances();
  62. $oldDevices = WaterDevice::query()->orderBy('id')->get()->toArray();
  63. $oldIds = array_column($oldDevices, 'id');
  64. $oldUsageCount = WaterDeviceUsage::query()->count();
  65. $business = new WaterBusiness();
  66. $now = new DateTimeImmutable();
  67. $deviceIds = [];
  68. $usageRows = [];
  69. foreach ($samples as $index => [$name, $district, $state, $minutes, $todayLiters, $version, $fault]) {
  70. $id = $business->save('device', [
  71. 'name' => '【测试0911】' . $name,
  72. 'sn' => DEVICE_DEMO_BATCH . '-' . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT),
  73. 'enterprise_id' => $enterprises[$district],
  74. 'sku_id' => (int) $sku->id,
  75. 'firmware' => $version,
  76. ], 1)['id'];
  77. $deviceIds[] = $id;
  78. $lastSeen = $minutes === null ? null : $now->modify('-' . $minutes . ' minutes');
  79. $updates = [
  80. 'last_seen' => $lastSeen?->format('Y-m-d H:i:s'),
  81. 'fault' => $fault,
  82. 'today_liters' => $todayLiters,
  83. 'telemetry_date' => $todayLiters === null ? null : $now->format('Y-m-d'),
  84. ];
  85. // Historical samples are explicitly attached to TEST devices, never sent as live reports.
  86. if ($state !== 'unconnected') {
  87. for ($offset = 29; $offset >= 0; $offset--) {
  88. $day = $now->modify('-' . $offset . ' days')->format('Y-m-d');
  89. if ($day > $lastSeen->format('Y-m-d') || ($offset === 0 && $todayLiters === null)) {
  90. continue;
  91. }
  92. $liters = $offset === 0 ? $todayLiters
  93. : round((14 + $index * 1.7 + (29 - $offset) * 0.25) * (0.78 + (($offset + $index) % 7) * 0.06), 2);
  94. $reportedAt = min($day . ' 17:00:00', $lastSeen->format('Y-m-d H:i:s'));
  95. $usageRows[] = ['device_id' => $id, 'day' => $day, 'liters' => $liters, 'reported_at' => $reportedAt];
  96. if ($offset > 0 && $todayLiters === null) {
  97. $updates['today_liters'] = $liters;
  98. $updates['telemetry_date'] = $day;
  99. }
  100. }
  101. }
  102. WaterDevice::query()->where('id', $id)->update($updates);
  103. }
  104. WaterDeviceUsage::query()->insert($usageRows);
  105. $dashboard = new WaterDeviceDashboard($now);
  106. $first = $dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'limit' => 8]);
  107. $second = $dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'limit' => 8, 'page' => 2]);
  108. ensure($first['list']['total'] === 12 && count($first['list']['data']) === 8 && count($second['list']['data']) === 4, '分页验证失败');
  109. foreach (['online' => 6, 'offline' => 2, 'fault' => 2, 'unconnected' => 2] as $state => $count) {
  110. ensure($dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'status' => $state])['list']['total'] === $count, '状态验证失败:' . $state);
  111. }
  112. foreach ($enterprises as $district => $id) {
  113. ensure($dashboard->dashboard(['keyword' => DEVICE_DEMO_BATCH, 'region' => '深圳市' . $district . '区'])['list']['total'] === 3, '区域筛选验证失败');
  114. }
  115. ensure(count($first['trend']) === 30 && count(array_filter($first['trend'], fn ($row) => $row['liters'] !== null)) === 30, '趋势日期验证失败');
  116. ensure(WaterDeviceUsage::query()->count() === $oldUsageCount + count($usageRows), '历史记录数量异常');
  117. ensure(protectedBalances() === $balances, '原库存、权益、配送或订单发生变动,整批回滚');
  118. ensure(WaterDevice::query()->whereIn('id', $oldIds)->orderBy('id')->get()->toArray() === $oldDevices, '既有设备被修改,整批回滚');
  119. WaterLedger::query()->insert([
  120. 'kind' => 'demo_seed', 'reference' => DEVICE_DEMO_BATCH,
  121. 'sku_id' => (int) $sku->id, 'quantity' => 0, 'created_by' => 1,
  122. 'note' => '【测试】12台模拟设备及30天用量;非真实遥测,库存权益未变。设备ID:' . implode(',', $deviceIds),
  123. 'created_at' => $now->format('Y-m-d H:i:s'),
  124. ]);
  125. return ['applied' => true, 'replayed' => false, 'device_ids' => $deviceIds, 'usage_rows' => count($usageRows)];
  126. });
  127. }
  128. $dashboard = (new WaterDeviceDashboard())->dashboard(['keyword' => DEVICE_DEMO_BATCH]);
  129. $result['batch_devices'] = $dashboard['list']['total'];
  130. $result['global_summary'] = $dashboard['summary'];
  131. echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL;