# 统一导入数据执行模板

基于 omecsv 框架的统一数据导入模板,支持大文件切片处理、队列任务管理、文件格式验证等功能。

重要说明:此模板为统一的导入功能实现标准,使用此模板时不需要为每个业务单独生成具体的使用说明文档。所有实现细节、最佳实践和注意事项都已包含在此模板中。

# 目录结构

app/{app_name}/
├── controller/
│   └── admin/
│       └── {module}.php          # 控制器,包含导入界面和模板下载
├── lib/
│   └── {module}/
│       └── to/
│           └── import.php        # 导入数据处理类
├── view/
│   └── admin/
│       └── {module}/
│           └── import.html       # 导入界面模板
└── model/
    └── {module}.php              # 数据模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 核心组件

# 1. 接口定义

所有导入处理类必须实现 omecsv_data_split_interface 接口:

必需方法

  • process() - 数据处理主方法
  • checkFile() - 文件格式检查
  • getTitle() - 获取表头定义

可选方法

  • getConfig() - 配置信息(不实现时默认走队列处理)
  • is_split() - 切片检测(不实现时使用默认逻辑)
<?php
interface omecsv_data_split_interface
{
    /**
     * 数据处理主方法
     * @param int $cursor_id 队列ID
     * @param array $params 处理参数
     * @param array &$errmsg 错误信息
     * @return array [bool, string]
     */
    public function process($cursor_id, $params, &$errmsg);
    
    /**
     * 文件格式检查
     * @param string $file_name 文件名
     * @param string $file_type 文件类型
     * @param array $queue_data 队列数据
     * @return array [bool, string, array]
     */
    public function checkFile($file_name, $file_type, $queue_data);
    
    /**
     * 获取表头定义
     * @param mixed $filter 过滤条件
     * @param string $ioType 文件类型
     * @return array
     */
    public function getTitle($filter = null, $ioType = 'csv');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 2. 白名单配置

app/omecsv/lib/split/whitelist.php 中注册导入类型:

<?php
class omecsv_split_whitelist
{
    private $bill_type = [
        'your_import_type' => [
            'name'  => '你的导入任务名称',
            'class' => 'your_app_your_module_import',
        ],
        // ... 其他导入类型
    ];
    
    public function getBillType($type)
    {
        return $this->bill_type[$type];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 实现步骤

# 1. 控制器实现

<?php
class your_app_ctl_admin_your_module extends desktop_controller
{
    /**
     * 导入界面展示
     */
    public function displayImport()
    {
        $this->pagedata['type'] = 'your_import_type';
        $this->pagedata['extra_params'] = $_GET['extra_params'] ?? '';
        $this->display('admin/your_module/import.html');
    }
    
    /**
     * 导出模板
     */
    public function exportTemplate()
    {
        $importClass = kernel::single('your_app_your_module_import');
        $title = $importClass->getTitle();
        
        $data = [];
        $lib = kernel::single('omecsv_phpexcel');
        $lib->newExportExcel($data, '模板_' . date('Ymd'), 'xls', $title);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 2. 导入处理类实现

<?php
class your_app_your_module_import implements omecsv_data_split_interface
{
    public $column_num = 10; // 表头列数
    public $current_key = null; // 切片标识
    
    /**
     * 数据处理主方法
     */
    public function process($cursor_id, $params, &$errmsg)
    {
        set_time_limit(0);
        @ini_set('memory_limit', '128M');
        
        $oFunc = kernel::single('omecsv_func');
        $queueMdl = app::get('omecsv')->model('queue');
        
        $oFunc->writelog('处理任务-开始', 'import', $params);
        
        // 业务逻辑处理
        $data = $params['data'];
        $sdf = [];
        $offset = intval($data['offset']) + 1;
        $splitCount = 0;
        
        if ($data) {
            foreach ($data as $row) {
                $res = $this->getSdf($row, $offset, $params['title']);
                
                if ($res['status'] && $res['data']) {
                    $tmp = $res['data'];
                    $this->_formatData($tmp);
                    $sdf[] = $tmp;
                } elseif (!$res['status']) {
                    array_push($errmsg, $res['msg']);
                }
                
                if ($res['status']) {
                    $splitCount++;
                }
                $offset++;
            }
        }
        
        // 保存数据
        if ($sdf) {
            list($result, $msgList) = $this->saveData($sdf);
            if ($msgList) {
                $errmsg = array_merge($errmsg, $msgList);
            }
            $queueMdl->update(['split_count' => $splitCount], ['queue_id' => $cursor_id]);
        }
        
        $oFunc->writelog('处理任务-完成', 'import', 'Done');
        return [true];
    }
    
    /**
     * 文件格式检查
     */
    public function checkFile($file_name, $file_type, $queue_data)
    {
        $ioType = kernel::single('omecsv_io_split_' . $file_type);
        $rows = $ioType->getData($file_name, 0, -1);
        $title = array_values($this->getTitle());
        
        // 检查表头
        $plateTitle = $rows[0];
        foreach ($title as $v) {
            if (array_search($v, $plateTitle) === false) {
                return [false, '文件模板错误:列【' . $v . '】未包含在' . implode('、', $plateTitle)];
            }
        }
        
        // 检查数据
        $offset = 1;
        $sdf = [];
        $errmsg = [];
        foreach ($rows as $row) {
            $res = $this->getSdf($row, $offset, $rows[0]);
            if ($res['status'] && $res['data']) {
                $tmp = $res['data'];
                $this->_formatData($tmp);
                $sdf[] = $tmp;
            } elseif (!$res['status']) {
                array_push($errmsg, $res['msg']);
            }
            $offset++;
        }
        
        if ($errmsg) {
            return [false, '文件内容错误信息:' . implode(';', $errmsg)];
        }
        
        // 业务验证
        list($dataList, $errMsg) = $this->validateData($sdf, true);
        if ($errMsg) {
            return [false, '文件内容错误信息:' . implode(';', $errMsg)];
        }
        
        return [true, '文件模板匹配', $rows[0]];
    }
    
    /**
     * 获取表头定义
     */
    public function getTitle($filter = null, $ioType = 'csv')
    {
        $this->oSchema['csv'] = [
            '*:必填字段1' => 'field1',
            '*:必填字段2' => 'field2',
            '可选字段1' => 'field3',
            '可选字段2' => 'field4',
        ];
        
        $this->ioTitle[$ioType] = array_keys($this->oSchema[$ioType]);
        return $this->ioTitle[$ioType];
    }
    
    /**
     * 数据格式转换
     */
    public function getSdf($row, $offset, $title)
    {
        $row = array_map('trim', $row);
        $oSchema = array_flip($this->oSchema['csv']);
        
        $titleKey = [];
        foreach ($title as $k => $t) {
            $titleKey[$k] = array_search($t, $oSchema);
            if ($titleKey[$k] === false) {
                return ['status' => false, 'msg' => '未定义字段`' . $t . '`'];
            }
        }
        
        $res = ['status' => true, 'data' => [], 'msg' => ''];
        
        if ($this->column_num <= count($row) && $row[0] != '*:表头标识') {
            $tmp = array_combine($titleKey, $row);
            
            // 必填字段验证
            foreach ($tmp as $k => $v) {
                if (strpos($k, '*:') === 0 && !$v) {
                    $res['status'] = false;
                    $res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $oSchema[$k]);
                    return $res;
                }
            }
            
            $res['data'] = $tmp;
        }
        
        return $res;
    }
    
    /**
     * 数据格式化
     */
    public function _formatData(&$data)
    {
        foreach ($data as $k => $str) {
            $data[$k] = str_replace(["\r\n", "\r", "\n", "\t"], "", $str);
        }
    }
    
    /**
     * 数据验证
     */
    public function validateData($data, $is_check = true)
    {
        $errMsg = [];
        $validData = [];
        
        foreach ($data as $key => $item) {
            // 业务逻辑验证
            if (!$this->validateBusinessLogic($item)) {
                $errMsg[] = sprintf('数据验证失败,行号:%d', $key + 1);
                continue;
            }
            
            $validData[] = $item;
        }
        
        return [$validData, $errMsg];
    }
    
    /**
     * 业务逻辑验证
     */
    private function validateBusinessLogic($item)
    {
        // 实现具体的业务验证逻辑
        // 注意:查询单条数据时使用父类的 db_dump 方法,而不是 getRow
        // 正确用法:$model->db_dump($filter, $field)
        // 错误用法:$model->getRow($field, $filter)
        
        // 示例:检查编码是否重复
        // $existing = $this->model->db_dump(['code' => $item['code']], 'id');
        // if ($existing) {
        //     return false; // 编码已存在
        // }
        
        return true;
    }
    
    /**
     * 保存数据
     */
    public function saveData($contents)
    {
        $errMsg = [];
        
        foreach ($contents as $data) {
            // 开启事务
            kernel::database()->beginTransaction();
            
            try {
                // 保存主数据
                $result = $this->saveMainData($data);
                if (!$result) {
                    throw new Exception('保存主数据失败');
                }
                
                // 保存明细数据
                if (isset($data['items'])) {
                    $result = $this->saveDetailData($data['items']);
                    if (!$result) {
                        throw new Exception('保存明细数据失败');
                    }
                }
                
                // 提交事务
                kernel::database()->commit();
                
            } catch (Exception $e) {
                // 回滚事务
                kernel::database()->rollBack();
                $errMsg[] = $e->getMessage();
            }
        }
        
        return [true, $errMsg];
    }
    
    /**
     * 保存主数据
     */
    private function saveMainData($data)
    {
        // 实现主数据保存逻辑
        $model = app::get('your_app')->model('your_module');
        return $model->save($data);
    }
    
    /**
     * 保存明细数据
     */
    private function saveDetailData($items)
    {
        // 实现明细数据保存逻辑
        $model = app::get('your_app')->model('your_module_items');
        foreach ($items as $item) {
            if (!$model->save($item)) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * 切片检测
     */
    public function is_split($row)
    {
        $is_split = false;
        if ($row['0'] !== $this->current_key) {
            if ($this->current_key !== null) {
                $is_split = true;
            }
            $this->current_key = $row['0'];
        }
        return $is_split;
    }
    
    /**
     * 配置信息(可选方法)
     * 如果不实现此方法,系统将默认走队列处理
     * 实现此方法并设置 max_direct_count > 0 时,数据量小于等于此值将直接处理
     */
    public function getConfig($key = null)
    {
        $config = [
            'page_size' => 100, // 每页处理数量
            'max_direct_count' => 100, // 100条以内直接处理,超过则走队列
        ];
        return $key ? $config[$key] : $config;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298

# 3. 导入界面模板

<style>
    .msgbox {
        top: -400px !important;
        transition: top 0.5s ease-in-out;
    }
</style>

<div id="import-error" class="error" style="display:none;"></div>

<form action='index.php?app=omecsv&ctl=admin_split_import&act=treat&finder_id=<{$env.get.finder_id}>' 
      method='post' target="uploadframe" enctype="multipart/form-data" 
      class="tableform" id="import-form">
    
    <{toinput from=$env.post}>
    
    <div class="tableform" id="upload">
        <div class="division">
            <table border="0" cellpadding="0" cellspacing="0" class="">
                <tr>
                    <th>下载模板:</th>
                    <td style="text-align:left;">
                        <a target="_blank" href="index.php?app=your_app&ctl=admin_your_module&act=exportTemplate">
                            点击下载
                        </a>
                    </td>
                </tr>
            </table>
        </div>

        <div class="division">
            <table border="0" cellpadding="0" cellspacing="0" class="">
                <tr>
                    <th>导入文件:</th>
                    <td style="text-align:left;">
                        <input type='file' name='import_file' 
                               accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel" 
                               id='ImportCSV'/>
                    </td>
                    <td width="">
                        <h4><{t}>上传文件,支持(
                            <span style='color:red;font-size:16px;'>.csv</span><span style='color:red;font-size:16px;'>.xls</span><span style='color:red;font-size:16px;'>.xlsx</span>
                            )格式!<{/t}>
                        </h4>
                    </td>
                </tr>
            </table>
        </div>
    </div>

    <div class="table-action">
        <input type='hidden' name='type' value="<{$type}>"/>
        <input type='hidden' name='queue_data[extra_params]' value="<{$extra_params}>"/>
        <{button label='导入' id="ImportBtn" class="btn-primary" type="submit"}>
        <{button label='关闭' id="stopBtn" class="btn-secondary" type="button"}>
    </div>
    
    <div class="notice-inline">
        <span id="iMsg" style="color:red;"></span>
    </div>
    <div class="error" style="display: none;"></div>
</form>

<script>
    (function(){
        var state;
        $('ImportBtn').addEvent('click',function(e){
            if(state||!$('ImportCSV').value.length)return false;
            state=true;
            this.style.cursor='not-allowed';
        });
        
        $('ImportCSV').addEvent('change',function(e){
            state=false;
            $('ImportBtn').style.cursor='pointer';
        });
        
        var importError = document.getElementById('import-error');
        $('import-form').store('target', {
            onComplete:function(resp){
                state=false;
                $('ImportBtn').style.cursor='pointer';
                var res = JSON.parse(resp);
                if(res){
                    $('import-error').setHTML(res.error);
                    importError.style.display = 'block';
                }
            },
        });

        var finder = finderGroup['<{$env.get.finder_id}>'];
        $('stopBtn').addEvent('click', function(){
            try{
                var _dialogIns = this.getParent('.dialog').retrieve('instance');
            }catch(e){}
            if(_dialogIns){
                setTimeout(finder.refresh(),30000);
                _dialogIns.close();
            }
        });
    })();
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

# 使用流程

# 1. 注册导入类型

app/omecsv/lib/split/whitelist.php 中添加:

'your_import_type' => [
    'name'  => '你的导入任务名称',
    'class' => 'your_app_your_module_import',
],
1
2
3
4

# 2. 创建导入处理类

实现 omecsv_data_split_interface 接口的所有方法。

# 3. 创建控制器方法

添加导入界面展示和模板下载方法。

# 4. 创建界面模板

使用提供的HTML模板,修改相应的链接和参数。

# 5. 文档说明

注意:使用此统一模板时,不需要为每个业务单独生成具体的使用说明文档(如 shop-import-guide.md)。统一模板已经包含了完整的使用说明和最佳实践,可以直接参考使用。

# 核心特性

# 1. 智能处理方式

  • 直接处理:实现 getConfig() 方法并设置 max_direct_count > 0 时,数据量小于等于此值将直接处理
  • 队列处理:未实现 getConfig() 方法或数据量超过阈值时走后台队列,异步处理
  • 可通过 max_direct_count 配置直接处理的阈值

# 2. 大文件处理

  • 支持文件切片,避免内存溢出
  • 队列任务管理,支持后台处理
  • 支持CSV、XLS、XLSX格式

# 2. 数据验证

  • 表头格式验证
  • 必填字段检查
  • 业务逻辑验证
  • 数据格式转换

# 3. 错误处理

  • 详细的错误信息收集
  • 事务回滚机制
  • 日志记录功能

# 4. 性能优化

  • 内存限制设置
  • 执行时间控制
  • 批量数据处理

# 最佳实践

# 1. 数据验证

  • checkFile 中进行格式验证
  • validateData 中进行业务验证
  • 提供详细的错误信息
  • 重要:查询单条数据时使用父类的 db_dump 方法,而不是 getRow
    • 正确:$model->db_dump($filter, $field)
    • 错误:$model->getRow($field, $filter)

# 2. 事务管理

  • 使用数据库事务确保数据一致性
  • 出错时及时回滚
  • 记录操作日志

# 3. 性能优化

  • 合理设置切片大小
  • 避免在循环中进行数据库查询
  • 使用批量操作提高效率
  • 根据业务需求设置 max_direct_count 阈值
    • 简单数据:50-100条
    • 复杂数据:20-50条
    • 实时性要求高:10-20条
  • 可选配置getConfig() 方法为可选实现,不实现时默认走队列处理

# 4. 错误处理

  • 收集所有错误信息
  • 提供友好的错误提示
  • 记录详细的错误日志

# 注意事项

  1. 内存管理:大文件导入时注意内存使用
  2. 事务控制:确保数据一致性
  3. 错误处理:提供详细的错误信息
  4. 性能优化:合理设置处理参数
  5. 日志记录:记录关键操作步骤
  6. 数据查询:查询单条数据时使用父类的 db_dump 方法,参数顺序为 db_dump($filter, $field)
  7. 文档管理:使用此统一模板时,不需要为每个业务单独生成具体的使用说明文档,统一模板已包含完整说明
  8. 可选方法getConfig()is_split() 方法为可选实现,不实现时系统会使用默认逻辑(getConfig不实现时默认走队列)
最后更新: 11/11/2025, 9:12:34 PM