bianjunhui hai 3 días
pai
achega
7d930d876f

+ 48 - 4
packages/Longyi/Wholesale/README.md

@@ -9,8 +9,8 @@
 - ✅ 中英文提示文案
 - ✅ 后台 Sales 菜单新增「批发询盘」目录
 - ✅ 后台询盘列表(DataGrid:搜索 / 排序 / 分页)
-- ✅ 后台删除询盘记录
 - ✅ 基于角色的权限控制(ACL)
+- ✅ 历史数据导入命令(`php artisan wholesale:import-message`)
 
 ## 目录结构
 
@@ -20,8 +20,12 @@ Longyi/Wholesale/
 │   ├── Config/
 │   │   ├── admin-menu.php          # 后台菜单(sales.wholesale)
 │   │   └── acl.php                 # 权限配置
+│   ├── Console/
+│   │   └── Commands/
+│   │       └── ImportMessageData.php # 历史数据导入命令
 │   ├── Database/
-│   │   └── Migrations/             # message 表迁移
+│   │   ├── Migrations/             # message 表迁移
+│   │   └── message.sql             # 历史数据 SQL(Navicat 导出)
 │   ├── DataGrids/
 │   │   └── MessageDataGrid.php     # 后台列表
 │   ├── Helpers/
@@ -101,7 +105,48 @@ php artisan view:clear
 | `business_name` | varchar(300) | 公司名称 |
 | `business_role` | varchar(50) | 公司角色 |
 
-> 注意:表的主键为 `message_id`(非 `id`),且只有 `created_at` 字段(无 `updated_at`),模型已做相应适配(`$timestamps = false`)。
+> 注意:表的主键为 `message_id`(非 `id`),且只有 `created_at` 字段(无 `updated_at`),模型已做相应适配(`$timestamps = true`,`CREATED_AT = 'created_at'`,`UPDATED_AT = null`)。
+
+## 历史数据导入
+
+模块内置了一条 artisan 命令,用于将历史询盘数据(`src/Database/message.sql`,Navicat 导出的 MySQL 文件)批量导入到现有的 `message` 表。
+
+### 命令
+
+```bash
+php artisan wholesale:import-message
+```
+
+### 参数
+
+| 参数 | 说明 | 默认值 |
+|------|------|--------|
+| `--file` | SQL 文件路径 | 包内 `src/Database/message.sql` |
+| `--batch` | 每次批量插入的行数 | `500` |
+
+### 示例
+
+```bash
+# 使用默认文件、默认批大小导入
+php artisan wholesale:import-message
+
+# 指定 SQL 文件路径
+php artisan wholesale:import-message --file=/path/to/message.sql
+
+# 调整批量插入大小
+php artisan wholesale:import-message --batch=1000
+```
+
+### 行为说明
+
+1. **清空现有表**:导入前会先 `TRUNCATE` 现有 `message` 表,再导入 SQL 文件中的数据(全量替换)。
+2. **只解析 INSERT 语句**:跳过 `DROP TABLE`、`CREATE TABLE`、注释等语句,不会重建表结构。
+3. **字段映射**:SQL 文件中的 `Email`(大写)字段会正确映射到现有表的 `email`(小写)列。
+4. **类型处理**:
+   - `message_id` 转为整数,保留原始主键值;
+   - `NULL` 和空字符串转为 `null`;
+   - 支持 `\'`、`\r\n`、`\t` 等 SQL 转义字符。
+5. **容错**:列数不匹配的异常行会被跳过并输出告警,不中断整体导入。
 
 ## 前端 API
 
@@ -221,7 +266,6 @@ Content-Type: application/json
 
 ```
 GET  /admin/sales/wholesale       # 询盘列表
-DELETE /admin/sales/wholesale/{id} # 删除询盘
 ```
 
 ### 列表功能

+ 240 - 0
packages/Longyi/Wholesale/src/Console/Commands/ImportMessageData.php

@@ -0,0 +1,240 @@
+<?php
+
+namespace Longyi\Wholesale\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * ImportMessageData
+ *
+ * 将 Database/message.sql 中的 message 表数据导入到现有 message 表。
+ *
+ * - 清空现有表(TRUNCATE)
+ * - 解析 SQL 文件中的 INSERT 语句(跳过 DROP/CREATE/注释等)
+ * - 将数据映射到现有表结构(email 小写)
+ * - 批量导入
+ *
+ * 用法:php artisan wholesale:import-message [--file=...] [--batch=500]
+ */
+class ImportMessageData extends Command
+{
+    /**
+     * The name and signature of the console command.
+     */
+    protected $signature = 'wholesale:import-message
+                            {--file= : SQL 文件路径,默认使用包内 Database/message.sql}
+                            {--batch=500 : 每次批量插入的行数}
+                            {--truncate : 导入前清空现有表(默认开启,传 --no-truncate 可关闭)}';
+
+    /**
+     * The console command description.
+     */
+    protected $description = 'Import message data from message.sql into the message table';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle(): int
+    {
+        $file = $this->option('file') ?: __DIR__.'/../../Database/message.sql';
+        $batch = (int) $this->option('batch');
+
+        if (! file_exists($file)) {
+            $this->error("SQL file not found: {$file}");
+
+            return self::FAILURE;
+        }
+
+        // 1. 清空现有表
+        DB::table('message')->truncate();
+        $this->info('Existing message table truncated.');
+
+        // 2. 解析 INSERT 语句
+        $this->info('Parsing SQL file...');
+        $rows = $this->parseInserts($file);
+
+        if (empty($rows)) {
+            $this->error('No INSERT statements found in SQL file.');
+
+            return self::FAILURE;
+        }
+
+        $this->info("Found {$this->formatNumber(count($rows))} records.");
+
+        // 3. 批量导入
+        $this->info('Importing...');
+        $bar = $this->output->createProgressBar(count($rows));
+        $bar->start();
+
+        $columns = ['message_id', 'name', 'email', 'number', 'message', 'country', 'created_at', 'business_name', 'business_role'];
+
+        foreach (array_chunk($rows, $batch) as $chunk) {
+            DB::table('message')->insert($chunk);
+            $bar->advance(count($chunk));
+        }
+
+        $bar->finish();
+        $this->newLine();
+        $this->info('Import completed successfully.');
+
+        return self::SUCCESS;
+    }
+
+    /**
+     * 解析 SQL 文件,提取所有 INSERT 语句的数据行。
+     *
+     * @return array<int, array<string, mixed>>
+     */
+    private function parseInserts(string $file): array
+    {
+        $columns = ['message_id', 'name', 'email', 'number', 'message', 'country', 'created_at', 'business_name', 'business_role'];
+
+        $rows = [];
+
+        $handle = fopen($file, 'r');
+
+        if ($handle === false) {
+            return [];
+        }
+
+        while (($line = fgets($handle)) !== false) {
+            $line = trim($line);
+
+            // 只处理 INSERT INTO 语句
+            if (! preg_match('/^INSERT\s+INTO\s+`message`\s+VALUES\s*\((.*)\);\s*$/i', $line, $matches)) {
+                continue;
+            }
+
+            $values = $this->parseValues($matches[1]);
+
+            if ($values === null) {
+                $this->warn('Skipping malformed INSERT line: '.substr($line, 0, 80).'...');
+                continue;
+            }
+
+            // 列数校验:9 列(message_id, name, email, number, message, country, created_at, business_name, business_role)
+            if (count($values) !== count($columns)) {
+                $this->warn('Skipping INSERT with unexpected column count ('.count($values).'): '.substr($line, 0, 80).'...');
+                continue;
+            }
+
+            $row = array_combine($columns, $values);
+
+            // 类型处理:message_id 转 int,created_at 空字符串转 null
+            $row['message_id'] = (int) $row['message_id'];
+            if ($row['created_at'] === '') {
+                $row['created_at'] = null;
+            }
+
+            $rows[] = $row;
+        }
+
+        fclose($handle);
+
+        return $rows;
+    }
+
+    /**
+     * 解析 INSERT VALUES 括号内的内容,返回数组。
+     * 支持单引号转义(\')和 \r\n 等换行符。
+     *
+     * @return array<int, string|null>|null
+     */
+    private function parseValues(string $valuesString): ?array
+    {
+        $values = [];
+        $current = '';
+        $inString = false;
+        $length = strlen($valuesString);
+
+        for ($i = 0; $i < $length; $i++) {
+            $char = $valuesString[$i];
+
+            if ($char === "'") {
+                // 处理转义的单引号 \'
+                if ($inString && $i + 1 < $length && $valuesString[$i + 1] === "'") {
+                    $current .= "'";
+                    $i++;
+
+                    continue;
+                }
+
+                if ($inString) {
+                    // 字符串结束
+                    $inString = false;
+                } else {
+                    // 字符串开始
+                    $inString = true;
+                }
+
+                continue;
+            }
+
+            if ($char === '\\' && $inString && $i + 1 < $length) {
+                // 转义字符处理(\n \r \' 等)
+                $next = $valuesString[$i + 1];
+                $current .= $this->unescape($next);
+                $i++;
+
+                continue;
+            }
+
+            if ($char === ',' && ! $inString) {
+                // 字段分隔
+                $values[] = $this->normalizeValue($current);
+                $current = '';
+
+                continue;
+            }
+
+            $current .= $char;
+        }
+
+        // 最后一个字段
+        if ($current !== '' || count($values) < 9) {
+            $values[] = $this->normalizeValue($current);
+        }
+
+        return $values;
+    }
+
+    /**
+     * 处理转义字符。
+     */
+    private function unescape(string $char): string
+    {
+        $map = [
+            'n'  => "\n",
+            'r'  => "\r",
+            't'  => "\t",
+            '\\' => '\\',
+            "'"  => "'",
+            '0'  => "\0",
+        ];
+
+        return $map[$char] ?? $char;
+    }
+
+    /**
+     * 规范化字段值:NULL → null,去掉首尾空白。
+     */
+    private function normalizeValue(string $value): ?string
+    {
+        $value = trim($value);
+
+        if (strtoupper($value) === 'NULL') {
+            return null;
+        }
+
+        return $value;
+    }
+
+    /**
+     * 格式化数字(千分位)。
+     */
+    private function formatNumber(int $number): string
+    {
+        return number_format($number);
+    }
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 10950 - 0
packages/Longyi/Wholesale/src/Database/message.sql


+ 7 - 0
packages/Longyi/Wholesale/src/Providers/WholesaleServiceProvider.php

@@ -3,6 +3,7 @@
 namespace Longyi\Wholesale\Providers;
 
 use Illuminate\Support\ServiceProvider;
+use Longyi\Wholesale\Console\Commands\ImportMessageData;
 
 class WholesaleServiceProvider extends ServiceProvider
 {
@@ -28,6 +29,12 @@ class WholesaleServiceProvider extends ServiceProvider
         $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'wholesale');
 
         $this->loadViewsFrom(__DIR__.'/../Resources/views', 'wholesale');
+
+        if ($this->app->runningInConsole()) {
+            $this->commands([
+                ImportMessageData::class,
+            ]);
+        }
     }
 
     /**