# Finder 和数据列表开发完整指南

# 📋 概述

本指南涵盖了 OMS 系统中 Finder 组件和数据列表开发的完整流程,包括开发原理、字段访问规则、自定义列实现、以及完整的开发模板。

适用范围:所有OMS系统开发人员
重要性:⭐⭐⭐⭐⭐(必须掌握)

# ⚠️ 重要提醒

按钮和功能控制原则

  • 只有明确要求时才添加功能
  • 默认情况下不添加任何按钮(添加、编辑、删除、导入)
  • 默认情况下不实现任何操作方法(add、edit、create、update、delete)
  • 默认情况下不展示操作日志
  • 避免过度开发,按需实现功能

请严格按照此原则进行开发,确保系统的简洁性和可维护性。

# 🔍 核心概念

# 1. Finder 组件结构

Finder 是 ShopEx ERP 系统中的数据列表组件,支持:

  • 自定义列显示
  • 高级筛选
  • 批量操作
  • 数据导出

# 2. 字段访问规则

在 ShopEx ERP 系统的 Finder 组件中,$this->col_prefix 是一个重要的概念,用于区分不同来源的字段数据。正确使用 col_prefix 是避免数据访问错误的关键。

# $this->col_prefix 使用规则

需要使用的情况:

  • 当字段在 addon_cols 中定义时
  • 当字段来自关联查询或扩展查询时
  • 当字段不在主表的 dbschema 中,而是通过其他方式获取的

不需要使用的情况:

  • 当字段在主表的 dbschema 中设置了 in_list => true
  • 当字段是主表的普通字段时

# 具体判断方法

// 情况1:字段在 addon_cols 中定义
var $addon_cols = "store_id,shop_id";  // 这些字段需要加前缀
function column_edit($row){
    $store_id = $row[$this->col_prefix.'store_id'];  // 需要加前缀
    $shop_id = $row[$this->col_prefix.'shop_id'];    // 需要加前缀
}

// 情况2:字段在 dbschema 中设置了 in_list => true
// 在 dbschema 中:
'store_bn' => array(
    'type' => 'varchar(32)',
    'in_list' => true,  // 这个字段不需要加前缀
    'label' => '门店编码',
)
// 在 Finder 中:
function column_store_bn($row){
    return $row['store_bn'];  // 直接使用,不需要前缀
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 系统自动处理机制

// 系统会自动为 addon_cols 中的字段添加前缀
$object->col_prefix = '_' . $k . '_';  // 例如:_finder_
$sql[] = $col . ' as ' . $object->col_prefix . $col;  // 例如:store_id as _finder_store_id
1
2
3

# 实际应用示例

需要加前缀的例子:

// app/o2o/lib/finder/store.php
var $addon_cols = "store_id";
function column_edit($row){
    $store_id = $row[$this->col_prefix.'store_id'];  // 正确
}
1
2
3
4
5

不需要加前缀的例子:

// 当字段在 dbschema 中设置了 in_list => true
function column_region_coverage($row){
    $store_bn = $row['store_bn'];  // 直接使用,因为 store_bn 在 dbschema 中设置了 in_list => true
}
1
2
3
4

# 判断标准

  1. 检查 dbschema:如果字段在 dbschema 中设置了 in_list => true,则不需要前缀
  2. 检查 addon_cols:如果字段在 addon_cols 中定义,则需要前缀
  3. 检查字段来源:如果字段来自关联查询或扩展查询,通常需要前缀

# 最佳实践

  • 优先检查 dbschema:先看字段是否在 dbschema 中设置了 in_list => true
  • 查看 addon_cols:如果字段在 addon_cols 中,必须使用前缀
  • 测试验证:如果不确定,可以先用 print_r($row) 查看实际的字段名

# 🛠️ 开发实践

# 1. 基础 Finder 类结构

<?php
class app_finder_table
{
    // 定义需要额外查询的字段
    var $addon_cols = "field1,field2,field3";
    
    // 自定义列定义
    var $column_custom = '自定义列';
    var $column_custom_width = '120';
    var $column_custom_order = 1;
    
    // 自定义列实现
    function column_custom($row)
    {
        // 访问 addon_cols 中的字段需要使用前缀
        $field1 = $row[$this->col_prefix.'field1'];
        
        // 访问 dbschema 中 in_list => true 的字段直接使用
        $field2 = $row['field2'];
        
        return $result;
    }
    
    // 操作列
    var $column_edit = '操作';
    var $column_edit_width = '120';
    var $column_edit_order = 1;
    
    function column_edit($row)
    {
        $id = $row[$this->col_prefix.'id'];
        $finder_id = $_GET['_finder']['finder_id'];
        
        $button = sprintf(
            '<a href="index.php?app=app&ctl=admin_table&act=edit&p[0]=%s&finder_id=%s" target="dialog::{width:660,height:480,title:\'编辑\'}">编辑</a>',
            $id, $finder_id
        );
        
        return $button;
    }
}
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

# 2. 批量查询优化

重要:所有需要查询关联信息的Finder列都必须使用批量查询优化,避免 N+1 查询问题。

class app_finder_table
{
    protected $relatedData = [];
    
    var $column_relation = '关联信息';
    var $column_relation_width = '120';
    
    function column_relation($row, $list)
    {
        $relation_id = $row[$this->col_prefix.'relation_id'];
        
        if (!$this->relatedData) {
            // 批量查询所有关联数据
            $relationIds = array_column($list, $this->col_prefix.'relation_id');
            $relationModel = app::get('app')->model('relation');
            $this->relatedData = $relationModel->getList('id,name', array('id' => $relationIds));
            $this->relatedData = array_column($this->relatedData, null, 'id');
        }
        
        if (isset($this->relatedData[$relation_id])) {
            return $this->relatedData[$relation_id]['name'];
        }
        
        return '-';
    }
}
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

# 3. 高级筛选扩展

Finder高级筛选系统提供动态筛选条件生成、侧边栏弹窗、筛选条件管理等功能。

# 核心文件结构

app/desktop/view/finder/view/actions.html          # 高级筛选按钮
app/desktop/lib/finder/builder/filter.php          # 筛选逻辑处理
app/desktop/lib/finder/builder/filter/render.php   # 筛选界面渲染
app/desktop/view/finder/finder_filter.html         # 筛选界面模板
app/desktop/view/index.html                        # Side_R侧边栏组件
1
2
3
4
5

# 实现步骤

1. 启用高级筛选功能

  • 在finder配置中设置 use_buildin_filter = true
  • 参考文件: app/desktop/lib/finder/builder/view.php:15

2. 定义筛选字段

  • 在数据库模型中设置字段的 filtertype 属性
  • 支持类型: text, number, time, bool, region, textarea
  • 参考文件: app/desktop/lib/finder/builder/filter/render.php:32-105

3. 自定义筛选模板

  • 创建 app/{app}/view/finder/finder_panel_filter.html
  • 或使用默认模板 app/desktop/view/finder/finder_filter.html

4. 扩展筛选功能

  • 实现 extend_filter_{model_class} 服务
  • 参考搜索: kernel::servicelist('extend_filter_')

# 扩展筛选实现示例

// app/app/lib/finder/extend/filter/table.php
<?php
class app_finder_extend_filter_table
{
    function get_extend_colums()
    {
        $db['table'] = array(
            'columns' => array(
                'status' => array(
                    'type' => array(
                        1 => '启用',
                        2 => '禁用'
                    ),
                    'label' => '状态',
                    'editable' => false,
                    'filtertype' => 'normal',
                    'filterdefault' => true,
                    'in_list' => false,
                    'default_in_list' => false,
                ),
            )
        );
        return $db;
    }
}
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

# 关键代码片段

按钮HTML结构:

<a lnk="<{$url}>&action=filter" href="javascript:void(0);" id="finder-filter-action-<{$name}>">
    <{t}>高级筛选<{/t}>
</a>
1
2
3

Side_R弹窗初始化:

sr = new Side_R(this.get('lnk'),{
    width:180,
    isClear:false,
    title:'高级筛选(搜索)',
    trigger:$('finder-action-<{$name}>'),
    onShow:function(){
        // 显示逻辑
    },
    onHide:function(){
        // 隐藏逻辑
    }
});
1
2
3
4
5
6
7
8
9
10
11
12

筛选字段定义:

'columns' => array(
    'field_name' => array(
        'filtertype' => 'text',  // 筛选类型
        'filterdefault' => true, // 默认显示
        'label' => '字段名称',
        'type' => 'varchar'
    )
)
1
2
3
4
5
6
7
8

# 服务注册

<!-- app/app/services.xml -->
<service id="extend_filter_app_mdl_table">
    <class>app_finder_extend_filter_table</class>
</service>
1
2
3
4

# 搜索关键词

  • use_buildin_filter
  • Side_R
  • finder-filter-action
  • desktop_finder_builder_filter
  • filtertype
  • extend_filter_

# 参考代码

  • 完整实现: app/desktop/lib/finder/builder/filter/render.php:15-132
  • 按钮事件: app/desktop/view/finder/view/actions.html:52-85
  • 侧边栏组件: app/desktop/view/index.html:452-671
  • 测试文件: app/desktop/testcase/finder_filter_test.php
  • 测试数据: app/desktop/testcase/data/filter_data.json

# 📝 实际案例:门店覆盖区域功能

# 1. 功能概述

在系统门店管理下的门店列表中,为每个门店添加"覆盖区域"功能,允许设置门店的配送覆盖范围。

# 2. 实现步骤

# 步骤1:数据库结构

// app/logisticsmanager/dbschema/warehouse.php
'b_type' => array(
    'type' => 'tinyint(1)',
    'editable' => false,
    'label' => '业务类型',
    'default' => 1,
    'comment' => '1=仓库,2=门店,同ome_branch的b_type',
),
1
2
3
4
5
6
7
8

# 步骤2:Finder 扩展

// app/o2o/lib/finder/store.php
var $addon_cols = "store_id";

// 添加操作列按钮
function column_edit($row){
    $finder_id = $_GET['_finder']['finder_id'];
    $store_id = $row[$this->col_prefix.'store_id']; // 使用前缀访问 addon_cols 中的字段
    
    $button = sprintf(
        '<a href="index.php?app=o2o&ctl=admin_store&act=storeRegion&p[0]=%s&finder_id=%s" target="dialog::{width:800,height:600,title:\'门店覆盖区域\'}">覆盖区域</a>',
        $store_id, $finder_id
    );
    
    return $button;
}

// 添加覆盖范围显示列
public $column_region_coverage = '门店覆盖范围';
public $column_region_coverage_width = '150';
public $column_region_coverage_order = 5;

public function column_region_coverage($row)
{
    $store_bn = $row['store_bn']; // 直接使用,因为 store_bn 在 dbschema 中设置了 in_list => true
    
    // 查询门店的覆盖范围
    $warehouseMdl = app::get('logisticsmanager')->model('warehouse');
    $warehouse = $warehouseMdl->dump(array('branch_bn' => $store_bn, 'b_type' => 2), 'region_names');
    
    if ($warehouse && $warehouse['region_names']) {
        return $warehouse['region_names'];
    } else {
        return '';
    }
}
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

# 步骤3:控制器实现

// app/o2o/controller/admin/store.php
public function storeRegion($store_id)
{
    $storeMdl = app::get('o2o')->model('store');
    $store = $storeMdl->dump($store_id, 'store_id,store_bn,name,branch_id');
    
    if (!$store) {
        $this->splash('error', null, '门店不存在');
        return;
    }
    
    $this->pagedata['store'] = $store;
    
    // 查询现有覆盖区域
    $warehouseMdl = app::get('logisticsmanager')->model('warehouse');
    $existingWarehouse = $warehouseMdl->dump(array('branch_bn' => $store['store_bn'], 'b_type' => 2), '*');
    
    $warehouse = array('region_ids' => '', 'region_names' => '', 'warehouse_name' => '');
    if ($existingWarehouse) {
        $warehouse['id'] = $existingWarehouse['id'];
        $warehouse['warehouse_name'] = $existingWarehouse['warehouse_name'];
        
        // 处理区域数据
        $regionIds = $existingWarehouse['region_ids'];
        if (!empty($existingWarehouse['one_level_region_names'])) {
            $regionIds = explode(';', $regionIds);
            $ids = array();
            foreach ($regionIds as $key) {
                if (strrpos($key, ',')) {
                    $ids[] = substr($key, strrpos($key, ',') + 1);
                } else {
                    $ids[] = $key;
                }
            }
            $ids = implode(',', $ids);
        } else {
            $ids = $regionIds;
        }
        $warehouse['region_ids'] = addslashes(json_encode(explode(',', $ids)));
        
        // 查询区域名称
        if (!empty($ids)) {
            $regionsMdl = app::get('eccommon')->model('regions');
            $regionList = $regionsMdl->getList('local_name', array('region_id' => explode(',', $ids)));
            $regionNames = array();
            foreach ($regionList as $region) {
                $regionNames[] = $region['local_name'];
            }
            $warehouse['region_names'] = implode(',', $regionNames);
        }
    }
    
    $this->pagedata['warehouse'] = $warehouse;
    $this->display('admin/store/store_region.html');
}
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

# 步骤4:模板实现

<!-- app/o2o/view/admin/store/store_region.html -->
<form action="index.php?app=logisticsmanager&ctl=admin_warehouse&act=doSave&finder_id=<{$finder_id}>" method="post" id="store_region_form" class="tableform">
<div class="division">
    <table width="100%" cellspacing="0" cellpadding="0" border="0" align="center" >
    <tbody>
        <tr>
            <th>门店编码:</th> 
            <td>
               <input type="text" value="<{$store.store_bn}>" readonly style="background-color:#f5f5f5;">
            </td>
        </tr>
        <tr>
            <th>门店名称:</th> 
            <td>
               <input type="text" value="<{$store.name}>" readonly style="background-color:#f5f5f5;">
            </td>
        </tr>
        <tr>
            <th>区域仓名称:</th> 
            <td>
               <input type="text" name="warehouse_name" value="<{$warehouse.warehouse_name|default:''}>" vtype="required">
            </td>
        </tr>
        <{if $warehouse.region_ids}>
        <tr>
            <th>上次已选覆盖范围:</th> 
            <td>
               <input type="text" value="<{$warehouse.region_names}>" readonly style="background-color:#f5f5f5;">
            </td>
        </tr>
        <{/if}>
        <tr>
            <th>门店覆盖范围:</th> 
            <td>
            <{input type="address" id='p_region_id' name="p_region_id" data=$warehouse.region_ids }>
            </td>
        </tr>
    </tbody>
    </table>
</div>
<div class="table-action">
      <{button label="确定" class="btn-primary" type='submit' id='save_store_region'}> &nbsp; &nbsp;
      <{button label="关闭" class="btn-secondary" isCloseDialogBtn="true"}>
</div>
<input type='hidden' id='finder_id' name='finder_id' value='<{$finder_id}>'>
<input type='hidden' name='branch_id' value='<{$store.branch_id}>'>
<input type='hidden' name='b_type' value='2'>
<input type='hidden' name='id' value='<{$warehouse.id}>'>
</form>
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

# 🚨 常见错误和解决方案

# 1. 字段访问错误

错误示例:

var $addon_cols = "store_id";
function column_edit($row){
    $store_id = $row['store_id']; // 错误!应该使用 $this->col_prefix
}
1
2
3
4

正确示例:

var $addon_cols = "store_id";
function column_edit($row){
    $store_id = $row[$this->col_prefix.'store_id']; // 正确
}
1
2
3
4

# 2. 模板语法错误

错误示例:

<{input type="address" id="p_region_id" name="p_region_id" value=$warehouse.region_ids}>
1

正确示例:

<{input type="address" id='p_region_id' name="p_region_id" data=$warehouse.region_ids }>
1

# 3. 性能问题

避免 N+1 查询:

// 错误:每次调用都查询数据库
function column_relation($row){
    $relation = $this->relationModel->dump(array('id' => $row['relation_id']));
    return $relation['name'];
}

// 正确:批量查询优化
function column_relation($row, $list){
    if (!$this->relatedData) {
        $relationIds = array_column($list, 'relation_id');
        $this->relatedData = $this->relationModel->getList('id,name', array('id' => $relationIds));
        $this->relatedData = array_column($this->relatedData, null, 'id');
    }
    return $this->relatedData[$row['relation_id']]['name'] ?? '-';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 📋 完整开发指南

以下部分提供了从数据表定义到完整实现的完整开发指南,包含所有必要的代码示例和配置说明。

# 🗄️ 数据表定义 (dbschema)

# 基础表结构

<?php
$db['table_name'] = array(
    'columns' => array(
        // 主键字段(必需)
        'id' => array(
            'type'     => 'int unsigned',
            'required' => true,
            'width'    => 110,
            'hidden'   => true,
            'editable' => false,
            'pkey'     => true,
            'extra'    => 'auto_increment',
        ),
        
        // 创建时间字段(必需)
        'at_time' => array(
            'type'     => 'TIMESTAMP',
            'label'    => '创建时间',
            'default'  => 'CURRENT_TIMESTAMP',
            'width'    => 130,
            'editable' => false,
            'in_list'  => true,
            'order'    => 330,
            'filtertype' => 'normal',
        ),
        
        // 更新时间字段(必需)
        'up_time' => array(
            'type'     => 'TIMESTAMP',
            'label'    => '更新时间',
            'default'  => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
            'width'    => 130,
            'editable' => false,
            'in_list'  => true,
            'order'    => 340,
            'filtertype' => 'normal',
        ),
        
        // 注意:以下字段需要根据具体需求添加
        // 编码字段(可选,根据需求添加)
        // 'code' => array(
        //     'type'     => 'varchar(20)',
        //     'required' => true,
        //     'label'    => '编码',
        //     'editable' => false,
        //     'in_list'         => true,
        //     'default_in_list' => true,
        //     'searchtype'      => 'nequal',
        //     'filtertype'      => 'normal',
        //     'filterdefault'   => true,
        // ),
        
        // 名称字段(可选,根据需求添加)
        // 'name' => array(
        //     'type'     => 'varchar(255)',
        //     'required' => true,
        //     'label'    => '名称',
        //     'editable' => false,
        //     'is_title' => true,
        //     'in_list'         => true,
        //     'default_in_list' => true,
        //     'filtertype'      => 'normal',
        //     'filterdefault'   => true,
        // ),
        
        // 状态字段(可选,根据需求添加)
        // 'status' => array(
        //     'type'            => array(
        //         0 => '禁用',
        //         1 => '启用',
        //     ),
        //     'label'           => '状态',
        //     'width'           => 80,
        //     'default'         => 1,
        //     'in_list'         => true,
        //     'default_in_list' => true,
        //     'order'           => 10,
        //     'filtertype'      => 'normal',
        //     'filterdefault'   => true,
        // ),
    ),
    
    // 索引定义(根据实际字段调整)
    'index' => array(
        'ind_at_time' => array('columns' => array('at_time')),
        'ind_up_time' => array('columns' => array('up_time')),
    ),
    
    'comment' => '表注释',
    'charset' => 'utf8mb4',
    'engine'  => 'innodb',
);
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

# 字段类型说明

# 必需字段

字段名 字段类型 说明 用途
id int unsigned 主键字段 记录唯一标识
at_time TIMESTAMP 创建时间 记录创建时间
up_time TIMESTAMP 更新时间 记录更新时间

# 可选字段类型

字段类型 说明 示例
int unsigned 无符号整数 主键、外键
varchar(n) 可变长度字符串 名称、编码
tinyint(1) 布尔值/状态 启用/禁用
region 地区选择器 省市区
number 数字类型 数量、金额
money 金额类型 价格、金额
bool 布尔类型 是/否
table:table_name@app 外键关联 关联其他表
longtext 长文本 扩展字段、备注

注意:除了 idat_timeup_time 三个必需字段外,其他字段都需要根据具体业务需求来定义。

# 字段定义原则

# 数据库表key命名规范

重要:dbschema文件中的key必须与文件名保持一致,避免重复前缀。

// ✅ 正确:文件名 region_relation.php,key使用 region_relation
$db['region_relation'] = array(
    'columns' => array(
        // 字段定义...
    ),
);

// ❌ 错误:文件名 region_relation.php,key使用 tongyioil_region_relation
$db['tongyioil_region_relation'] = array(
    'columns' => array(
        // 字段定义...
    ),
);
1
2
3
4
5
6
7
8
9
10
11
12
13

原因:OMS系统会自动为数据库表添加APP前缀,最终表名会是 sdb_APP名_表名。如果key中已经包含APP名,会导致表名重复前缀。


# 📋 完整开发指南

以下部分提供了从数据表定义到完整实现的完整开发指南,包含所有必要的代码示例和配置说明。

# ⚠️ 重要提醒

按钮和功能控制原则

  • 只有明确要求时才添加功能
  • 默认情况下不添加任何按钮(添加、编辑、删除、导入)
  • 默认情况下不实现任何操作方法(add、edit、create、update、delete)
  • 默认情况下不展示操作日志
  • 避免过度开发,按需实现功能

请严格按照此原则进行开发,确保系统的简洁性和可维护性。

# 🗄️ 1. 数据表定义 (dbschema)

# 1.1 基础表结构

<?php
$db['table_name'] = array(
    'columns' => array(
        // 主键字段(必需)
        'id' => array(
            'type'     => 'int unsigned',
            'required' => true,
            'width'    => 110,
            'hidden'   => true,
            'editable' => false,
            'pkey'     => true,
            'extra'    => 'auto_increment',
        ),
        
        // 创建时间字段(必需)
        'at_time' => array(
            'type'     => 'TIMESTAMP',
            'label'    => '创建时间',
            'default'  => 'CURRENT_TIMESTAMP',
            'width'    => 130,
            'editable' => false,
            'in_list'  => true,
            'order'    => 330,
            'filtertype' => 'normal',
        ),
        
        // 更新时间字段(必需)
        'up_time' => array(
            'type'     => 'TIMESTAMP',
            'label'    => '更新时间',
            'default'  => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
            'width'    => 130,
            'editable' => false,
            'in_list'  => true,
            'order'    => 340,
            'filtertype' => 'normal',
        ),
        
        // 注意:以下字段需要根据具体需求添加
        // 编码字段(可选,根据需求添加)
        // 'code' => array(
        //     'type'     => 'varchar(20)',
        //     'required' => true,
        //     'label'    => '编码',
        //     'editable' => false,
        //     'in_list'         => true,
        //     'default_in_list' => true,
        //     'searchtype'      => 'nequal',
        //     'filtertype'      => 'normal',
        //     'filterdefault'   => true,
        // ),
        
        // 名称字段(可选,根据需求添加)
        // 'name' => array(
        //     'type'     => 'varchar(255)',
        //     'required' => true,
        //     'label'    => '名称',
        //     'editable' => false,
        //     'is_title' => true,
        //     'in_list'         => true,
        //     'default_in_list' => true,
        //     'filtertype'      => 'normal',
        //     'filterdefault'   => true,
        // ),
        
        // 状态字段(可选,根据需求添加)
        // 'status' => array(
        //     'type'            => array(
        //         0 => '禁用',
        //         1 => '启用',
        //     ),
        //     'label'           => '状态',
        //     'width'           => 80,
        //     'default'         => 1,
        //     'in_list'         => true,
        //     'default_in_list' => true,
        //     'order'           => 10,
        //     'filtertype'      => 'normal',
        //     'filterdefault'   => true,
        // ),
        
        // 删除状态字段(可选,根据需求添加)
        // 'delete' => array(
        //     'type'     => 'bool',
        //     'default'  => 'false',
        //     'editable' => false,
        //     'comment'  => '删除状态,可选值: false(否),true(是)',
        // ),
        
        // 扩展字段(可选,根据需求添加)
        // 'addon' => array(
        //     'type'     => 'longtext',
        //     'editable' => false,
        //     'label'    => '扩展字段',
        //     'comment'  => '扩展字段',
        // ),
    ),
    
    // 索引定义(根据实际字段调整)
    'index' => array(
        'ind_at_time' => array('columns' => array('at_time')),
        'ind_up_time' => array('columns' => array('up_time')),
        // 根据实际字段添加索引
        // 'ind_code'    => array('columns' => array('code')),
        // 'ind_status'  => array('columns' => array('status')),
    ),
    
    'comment' => '表注释',
    'charset' => 'utf8mb4',
    'engine'  => 'innodb',
);
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

# 1.2 字段类型说明

# 1.2.1 必需字段

字段名 字段类型 说明 用途
id int unsigned 主键字段 记录唯一标识
at_time TIMESTAMP 创建时间 记录创建时间
up_time TIMESTAMP 更新时间 记录更新时间

# 1.2.2 可选字段类型

字段类型 说明 示例
int unsigned 无符号整数 主键、外键
varchar(n) 可变长度字符串 名称、编码
tinyint(1) 布尔值/状态 启用/禁用
region 地区选择器 省市区
number 数字类型 数量、金额
money 金额类型 价格、金额
bool 布尔类型 是/否
table:table_name@app 外键关联 关联其他表
longtext 长文本 扩展字段、备注

注意:除了 idat_timeup_time 三个必需字段外,其他字段都需要根据具体业务需求来定义。

# 1.3 字段定义原则

# 1.3.1 数据库表key命名规范

重要:dbschema文件中的key必须与文件名保持一致,避免重复前缀。

// ✅ 正确:文件名 region_relation.php,key使用 region_relation
$db['region_relation'] = array(
    'columns' => array(
        // 字段定义...
    ),
);

// ❌ 错误:文件名 region_relation.php,key使用 tongyioil_region_relation
$db['tongyioil_region_relation'] = array(
    'columns' => array(
        // 字段定义...
    ),
);
1
2
3
4
5
6
7
8
9
10
11
12
13

原因:OMS系统会自动为数据库表添加APP前缀,最终表名会是 sdb_APP名_表名。如果key中已经包含APP名,会导致表名重复前缀。

# 1.3.2 必需字段

每个表都必须包含以下三个字段:

// 主键字段(必需)
'id' => array(
    'type'     => 'int unsigned',
    'required' => true,
    'pkey'     => true,
    'extra'    => 'auto_increment',
),

// 创建时间字段(必需)
'at_time' => array(
    'type'     => 'TIMESTAMP',
    'default'  => 'CURRENT_TIMESTAMP',
    'in_list'  => true,
    'order'    => 330,
),

// 更新时间字段(必需)
'up_time' => array(
    'type'     => 'TIMESTAMP',
    'default'  => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
    'in_list'  => true,
    'order'    => 340,
),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 1.3.2 可选字段

其他字段需要根据具体业务需求来定义,常见的可选字段包括:

  • 编码字段:用于业务标识
  • 名称字段:用于显示和搜索
  • 状态字段:用于控制记录状态
  • 删除标记:用于软删除
  • 扩展字段:用于存储额外信息

# 1.3.3 字段添加原则

  1. 按需添加:只有明确需要的字段才添加
  2. 避免冗余:不要添加不必要的字段
  3. 考虑扩展性:预留扩展字段空间
  4. 性能考虑:合理设置字段类型和索引

# 1.4 字段属性说明

属性 说明 可选值
type 字段类型 见上表
required 是否必填 true/false
label 显示标签 中文名称
editable 是否可编辑 true/false
in_list 是否在列表中显示 true/false
default_in_list 是否默认显示在列表 true/false
width 列宽度 数字
order 排序 数字
default 默认值 具体值
comment 注释说明 字符串
is_title 是否标题字段 true/false
filtertype 筛选类型 normal/advanced
filterdefault 是否默认筛选 true/false
searchtype 搜索类型 nequal/has/equal

# 1.5 店铺筛选字段标准规范

# 1.5.1 店铺筛选字段定义

重要:所有涉及店铺筛选的字段都必须遵循以下标准规范,确保系统命名和功能的一致性。

# 1.5.2 字段配置标准

// 店铺筛选字段标准配置
'shop_id' => array(
    'type' => 'varchar(32)',           // 字段类型
    'label' => '来源店铺',              // 统一标签名称
    'comment' => '来源店铺',            // 注释说明
    'width' => 100,                    // 列宽度
    'order' => 55,                     // 排序位置
    'editable' => false,               // 不可编辑
    'in_list' => true,                 // 在列表中显示
    // 注意:筛选配置通过扩展筛选器动态获取,不在dbschema中静态配置
),
1
2
3
4
5
6
7
8
9
10
11

# 1.5.3 筛选扩展文件标准

文件路径结构

app/{app_name}/lib/finder/extend/filter/{module}/{action}.php
1

类名格式

class {app_name}_finder_extend_filter_{module}_{action}
1

标准实现指南

<?php
class {app_name}_finder_extend_filter_{module}_{action} {
    function get_extend_colums() {
        // 获取所有店铺数据
        $shopName = array_column(app::get('ome')->model('shop')->getList('name,shop_id'),'name','shop_id');
        
        // 返回筛选字段配置
        $db['{table_name}'] = array(
            'columns' => array(
                'shop_id' => array(
                    'type' => $shopName,                    // 动态获取店铺数据
                    'label' => '来源店铺',                   // 统一标签名称
                    'width' => 100,
                    'editable' => false,
                    'in_list' => true,
                    'filtertype' => 'fuzzy_search_multiple', // 模糊搜索多选
                    'filterdefault' => true,                // 默认显示在筛选器
                    'order' => 55,
                ),
                // 其他筛选字段...
            )
        );
        return $db;
    }
}
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

# 1.5.4 服务注册标准

services.xml 中注册筛选扩展服务:

<service id="extend_filter_{app_name}_mdl_{table_name}">
    <class>{app_name}_finder_extend_filter_{module}_{action}</class>
</service>
1
2
3

示例

<service id="extend_filter_financebase_mdl_expenses_split">
    <class>financebase_finder_extend_filter_expenses_split</class>
</service>
1
2
3

# 1.5.5 命名规范

项目 标准 示例
字段名 shop_id shop_id
显示标签 来源店铺 来源店铺
注释说明 来源店铺 来源店铺
筛选类型 fuzzy_search_multiple fuzzy_search_multiple
文件路径 filter/{module}/{action}.php filter/expenses/split.php
类名 {app}_finder_extend_filter_{module}_{action} financebase_finder_extend_filter_expenses_split
服务ID extend_filter_{app}_mdl_{table} extend_filter_financebase_mdl_expenses_split

# 1.5.6 功能特点

  • 模糊搜索:支持输入店铺名称进行模糊搜索
  • 多选支持:支持选择多个店铺进行筛选
  • 动态数据:实时获取系统中的所有店铺数据
  • 统一体验:与订单管理等其他模块保持一致的筛选体验
  • 性能优化:使用批量查询避免 N+1 查询问题

# 1.5.7 实现步骤

  1. 创建筛选扩展文件

    • 按目录结构创建文件:app/{app}/lib/finder/extend/filter/{module}/{action}.php
    • 实现标准类结构和方法
  2. 配置动态数据获取

    • 使用 app::get('ome')->model('shop')->getList() 获取店铺数据
    • 使用 array_column() 建立店铺ID到名称的映射
  3. 注册服务

    • services.xml 中注册筛选扩展服务
    • 确保服务ID格式正确
  4. 测试验证

    • 验证筛选功能正常工作
    • 验证模糊搜索和多选功能
    • 验证与系统其他模块的一致性

# 1.5.8 注意事项

  • 标签统一:所有模块的店铺筛选字段都使用"来源店铺"标签
  • 功能一致:所有模块都使用 fuzzy_search_multiple 筛选类型
  • 数据同步:确保获取的店铺数据是最新的
  • 性能考虑:避免在筛选扩展中进行复杂的数据库查询
  • 扩展性:预留其他筛选字段的配置空间

# 🎛️ 2. 控制器实现 (controller)

# 2.1 基础控制器结构

<?php
class app_ctl_admin_table extends desktop_controller
{
    public $name = "模块名称";
    public $workground = "workground_id";

    /**
     * 列表页面
     */
    public function index()
    {
        $this->title = '列表标题';

        $params = array(
            'title'                  => $this->title,
            'use_buildin_new_dialog' => false,
            'use_buildin_set_tag'    => false,
            'use_buildin_recycle'    => false,
            'use_buildin_export'     => true,
            'use_buildin_import'     => false, // 默认不启用导入功能
            'use_buildin_filter'     => true,
            'use_view_tab'           => true,
            'base_filter'            => [], // 基础筛选条件,默认为空
            'actions'                => [
                // 注意:只有明确要求时才添加按钮
                // 添加按钮:只有明确要求时才添加
                // 导入按钮:只有明确要求时才添加
            ],
        );

        $this->finder('app_mdl_table', $params);
    }

    /**
     * 添加页面(只有明确要求时才实现)
     */
    // public function add()
    // {
    //     // 准备页面数据
    //     $this->pagedata['options'] = $this->getOptions();
    //     
    //     $this->display("admin/table/add.html");
    // }

    /**
     * 编辑页面(只有明确要求时才实现)
     */
    // public function edit($id)
    // {
    //     $data = app::get('app')->model('table')->dump($id);
    //     $this->pagedata['data'] = $data;
    //     $this->pagedata['options'] = $this->getOptions();
    //     
    //     $this->display("admin/table/edit.html");
    // }

    /**
     * 创建数据(只有明确要求时才实现)
     */
    // public function create()
    // {
    //     $this->begin();
    //     
    //     try {
    //         $service = kernel::single('app_table');
    //         $result = $service->create($_POST);
    //         
    //         if ($result[0]) {
    //             $this->end(true, '创建成功');
    //         } else {
    //             $this->end(false, $result[1]);
    //         }
    //     } catch (Exception $e) {
    //         $this->end(false, $e->getMessage());
    //     }
    // }

    /**
     * 更新数据(只有明确要求时才实现)
     */
    // public function update($id)
    // {
    //     $this->begin();
    //     
    //     try {
    //         $service = kernel::single('app_table');
    //         $result = $service->update($id, $_POST);
    //         
    //         if ($result[0]) {
    //             $this->end(true, '更新成功');
    //         } else {
    //             $this->end(false, $result[1]);
    //         }
    //     } catch (Exception $e) {
    //         $this->end(false, $e->getMessage());
    //     }
    // }

    /**
     * 删除数据(软删除,只有明确要求时才实现)
     */
    // public function delete($id)
    // {
    //     $this->begin();
    //     
    //     try {
    //         $service = kernel::single('app_table');
    //         $result = $service->delete($id);
    //         
    //         if ($result[0]) {
    //             $this->end(true, '删除成功');
    //         } else {
    //             $this->end(false, $result[1]);
    //         }
    //     } catch (Exception $e) {
    //         $this->end(false, $e->getMessage());
    //     }
    // }

    /**
     * 获取选项数据
     */
    private function getOptions()
    {
        return [
            'status' => [
                1 => '启用',
                2 => '禁用'
            ],
            'is_complete' => [
                0 => '否',
                1 => '是'
            ]
        ];
    }
}
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

# 2.2 高级筛选说明

/**
 * 高级筛选会自动根据 dbschema 中的字段配置生成
 * 
 * 筛选字段配置:
 * - filtertype: 'normal' - 普通筛选
 * - filterdefault: true - 默认显示在筛选器中
 * - searchtype: 'nequal' - 精确搜索, 'has' - 模糊搜索
 * 
 * 系统会根据这些配置自动生成筛选界面
 */
1
2
3
4
5
6
7
8
9
10

# 🔍 3. Finder实现 (lib/finder)

# 3.1 列定义和排序规范

# 3.1.1 列名称定义

重要:所有自定义列都必须定义列名称属性,格式为 public $column_列名 = '显示名称'

// ✅ 正确:定义列名称
public $column_edit = '操作';
public $column_status = '状态';
public $column_relation = '关联信息';

// ❌ 错误:未定义列名称
// 直接使用 column_edit() 方法而不定义 $column_edit
1
2
3
4
5
6
7

# 3.1.2 列排序定义

重要:所有自定义列都必须定义排序属性,格式为 public $column_列名_order = 数字

// ✅ 正确:定义列排序
public $column_edit_order = 1;        // 排在最前面
public $column_status_order = 10;     // 排在前面
public $column_relation_order = 20;   // 排在中间

// ❌ 错误:未定义列排序
// 直接使用 column_edit() 方法而不定义 $column_edit_order
1
2
3
4
5
6
7

# 3.1.3 排序规则

  • 数值越小,排序越靠前
  • 操作列通常设置为 1,排在最前面
  • 状态列通常设置为 10,排在前面
  • 关联信息列通常设置为 20+,排在中间
  • 时间列通常设置为 300+,排在后面

# 3.1.4 完整示例

class app_finder_table
{
    // 列名称定义
    public $column_edit = '操作';
    public $column_status = '状态';
    public $column_relation = '关联信息';
    
    // 列排序定义
    public $column_edit_order = 1;        // 操作列排在最前面
    public $column_status_order = 10;     // 状态列排在前面
    public $column_relation_order = 20;   // 关联信息列排在中间
    
    // 列方法实现
    public function column_edit($row) {
        // 操作列实现
    }
    
    public function column_status($row) {
        // 状态列实现
    }
    
    public function column_relation($row) {
        // 关联信息列实现
    }
}
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

# 3.2 基础Finder类

<?php
class app_finder_table
{
    var $addon_cols = "id";
    // 注意:只有明确要求时才添加操作列
    // var $column_edit = "操作";
    // var $column_edit_width = 120;
    // var $column_edit_order = 1;

    /**
     * 操作列(只有明确要求时才实现)
     */
    function column_edit($row)
    {
        $finder_id = $_GET['_finder']['finder_id'];
        $id = $row[$this->col_prefix.'id'];
        
        $button = '';
        
        // 编辑按钮:只有明确要求时才添加
        // $button .= sprintf(
        //     '<a href="index.php?app=app&ctl=admin_table&act=edit&p[0]=%s&finder_id=%s" 
        //         target="dialog::{width:660,height:480,title:\'编辑\'}">编辑</a>', 
        //     $id, $finder_id
        // );
        
        // 删除按钮:只有明确要求时才添加
        // $button .= sprintf(
        //     ' <a href="index.php?app=app&ctl=admin_table&act=delete&p[0]=%s&finder_id=%s" 
        //         onclick="return confirm(\'确定删除吗?\')">删除</a>', 
        //     $id, $finder_id
        // );
        
        return $button;
    }

    /**
     * 状态列格式化
     */
    function column_status($row)
    {
        $status = $row[$this->col_prefix.'status'];
        $status_map = [1 => '启用', 2 => '禁用'];
        
        return $status_map[$status] ?? '未知';
    }

    /**
     * 关联信息列 - 批量查询优化指南
     * 
     * 重要:所有需要查询关联信息的列都应该使用批量查询优化
     * 避免 N+1 查询问题,提升页面性能
     */
    var $column_relation = '关联信息';
    var $column_relation_width = "120";
    function column_relation($row, $list){
        $relation_id = $row[$this->col_prefix.'relation_id'];
        if($relation_id && $relation_id > 0){
            $relation_data = $this->_getRelationData($relation_id, $list);
            if($relation_data){
                return $relation_data['code'] . ' - ' . $relation_data['name'];
            } else {
                return '关联ID: ' . $relation_id;
            }
        } else {
            return '-';
        }
    }

    /**
     * 批量查询关联数据 - 标准指南方法
     * 
     * 使用静态缓存确保整个列表只查询一次数据库
     * 参考实现:app/ome/lib/finder/orders.php 中的 _getShop 方法
     * 
     * @param int $relation_id 关联ID
     * @param array $list 当前列表数据
     * @return array|null 关联数据
     */
    private function _getRelationData($relation_id, $list)
    {
        static $relationDataList;
        
        if (isset($relationDataList)) {
            return $relationDataList[$relation_id];
        }
        
        $relationDataList = [];
        $relation_ids = array();
        
        // 收集所有需要查询的关联ID
        foreach($list as $val) {
            $rid = $val[$this->col_prefix.'relation_id'];
            if($rid && $rid > 0) {
                $relation_ids[] = $rid;
            }
        }
        
        if($relation_ids) {
            $relation_ids = array_unique($relation_ids);
            $relationObj = app::get('app')->model('relation_table');
            $relationList = $relationObj->getList('id,code,name', array('id' => $relation_ids));
            $relationDataList = array_column($relationList, null, 'id');
        }
        
        return $relationDataList[$relation_id];
    }

    /**
     * 详情列 - 基本信息
     */
    public $detail_basic = '基本信息';
    public function detail_basic($id)
    {
        $render = app::get('app')->render();
        $data = app::get('app')->model('table')->dump($id);
        
        $render->pagedata['data'] = $data;
        return $render->fetch('admin/table/detail_basic.html');
    }

    /**
     * 详情列 - 操作日志(只有明确要求时才实现)
     */
    // public $detail_log = '操作日志';
    // public function detail_log($id)
    // {
    //     $render = app::get('app')->render();
    //     
    //     $logObj = app::get('ome')->model('operation_log');
    //     $logData = $logObj->read_log([
    //         'obj_id' => $id, 
    //         'obj_type' => 'table@app'
    //     ]);
    //
    //     foreach ($logData as $k => $v) {
    //         $logData[$k]['operate_time'] = date('Y-m-d H:i:s', $v['operate_time']);
    //     }
    //
    //     $render->pagedata['datalist'] = $logData;
    //     return $render->fetch('admin/table/detail_log.html');
    // }
}
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

# 3.2 批量查询优化规范

# 3.2.1 重要原则

所有需要查询关联信息的Finder列都必须使用批量查询优化,避免 N+1 查询问题。

# 3.2.2 Schema配置说明

重要:当在Finder类中定义了自定义列方法(如 column_relation)时,数据库schema中不需要设置以下属性:

  • 'in_list' => true - 在列表中显示
  • 'default_in_list' => true/false - 默认是否显示在列表中
  • 'width' => 120 - 列宽度

这些配置由Finder类中的方法自动处理,避免重复配置。

# 3.2.3 addon_cols配置说明

重要:所有自定义列方法中使用的字段都必须添加到 addon_cols 中,否则无法获取到字段数据。

// 示例:添加自定义列需要的字段
var $addon_cols = "branch_id,area_conf,defaulted,wms_id,cutoff_time,latest_delivery_time,parent_id";
1
2

常见错误:忘记在 addon_cols 中添加字段,导致 $row[$this->col_prefix.'field_name'] 为空。

# 3.2.4 性能对比

查询方式 10行数据 50行数据 100行数据 性能影响
单条查询 10次查询 50次查询 100次查询 严重
批量查询 1次查询 1次查询 1次查询 优秀

# 3.2.5 标准实现指南

/**
 * 关联信息列 - 批量查询优化指南
 * 
 * 重要:所有需要查询关联信息的列都应该使用批量查询优化
 * 避免 N+1 查询问题,提升页面性能
 */
var $column_relation = '关联信息';
var $column_relation_width = "120";
function column_relation($row, $list){
    $relation_id = $row[$this->col_prefix.'relation_id'];
    if($relation_id && $relation_id > 0){
        $relation_data = $this->_getRelationData($relation_id, $list);
        if($relation_data){
            return $relation_data['code'] . ' - ' . $relation_data['name'];
        } else {
            return '关联ID: ' . $relation_id;
        }
    } else {
        return '-';
    }
}

/**
 * 批量查询关联数据 - 标准指南方法
 * 
 * 使用静态缓存确保整个列表只查询一次数据库
 * 参考实现:app/ome/lib/finder/orders.php 中的 _getShop 方法
 * 
 * @param int $relation_id 关联ID
 * @param array $list 当前列表数据
 * @return array|null 关联数据
 */
private function _getRelationData($relation_id, $list)
{
    static $relationDataList;
    
    if (isset($relationDataList)) {
        return $relationDataList[$relation_id];
    }
    
    $relationDataList = [];
    $relation_ids = array();
    
    // 收集所有需要查询的关联ID
    foreach($list as $val) {
        $rid = $val[$this->col_prefix.'relation_id'];
        if($rid && $rid > 0) {
            $relation_ids[] = $rid;
        }
    }
    
    if($relation_ids) {
        $relation_ids = array_unique($relation_ids);
        $relationObj = app::get('app')->model('relation_table');
        $relationList = $relationObj->getList('id,code,name', array('id' => $relation_ids));
        $relationDataList = array_column($relationList, null, 'id');
    }
    
    return $relationDataList[$relation_id];
}
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

# 3.2.6 关键技术点

  1. 静态缓存:使用 static 关键字确保只查询一次
  2. 批量收集:遍历列表收集所有需要查询的ID
  3. 去重处理:使用 array_unique() 避免重复查询
  4. 索引映射:使用 array_column() 建立ID到数据的映射
  5. 方法签名:列方法必须接收 $list 参数

# 3.2.7 实际应用示例

仓库关联主仓(参考:app/ome/lib/finder/branch.php):

var $column_parent = '关联主仓';
var $column_parent_width = "120";
function column_parent($row, $list){
    $parent_id = $row[$this->col_prefix.'parent_id'];
    if($parent_id && $parent_id > 0){
        $parent_branch = $this->_getParentBranch($parent_id, $list);
        if($parent_branch){
            return $parent_branch['branch_bn'] . ' - ' . $parent_branch['name'];
        } else {
            return '主仓ID: ' . $parent_id;
        }
    } else {
        return '-';
    }
}

private function _getParentBranch($parent_id, $list)
{
    static $parentBranchList;
    
    if (isset($parentBranchList)) {
        return $parentBranchList[$parent_id];
    }
    
    $parentBranchList = [];
    $parent_ids = array();
    
    foreach($list as $val) {
        $pid = $val[$this->col_prefix.'parent_id'];
        if($pid && $pid > 0) {
            $parent_ids[] = $pid;
        }
    }
    
    if($parent_ids) {
        $parent_ids = array_unique($parent_ids);
        $branchObj = app::get('ome')->model('branch');
        $branchList = $branchObj->getList('branch_id,branch_bn,name', array('branch_id' => $parent_ids));
        $parentBranchList = array_column($branchList, null, 'branch_id');
    }
    
    return $parentBranchList[$parent_id];
}
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

订单店铺信息(参考:app/ome/lib/finder/orders.php):

private function _getShop($shop_id, $list)
{
    static $shopList;
    
    if (isset($shopList)) {
        return $shopList[$shop_id];
    }
    
    $shopList = app::get('ome')->model('shop')->getList('shop_id,shop_bn',[
        'shop_id' => array_column($list, $this->col_prefix.'shop_id'),
    ]);
    $shopList = array_column($shopList, null, 'shop_id');
    
    return $shopList[$shop_id];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.2.8 注意事项

  • 必须使用:所有需要查询关联信息的列都必须使用此指南
  • 方法签名:列方法必须包含 $list 参数
  • 静态缓存:必须使用 static 关键字
  • 去重处理:必须使用 array_unique() 去重
  • 索引映射:必须使用 array_column() 建立映射
  • 错误处理:提供友好的错误显示(如:关联ID不存在时显示ID)
  • addon_cols配置:确保所有使用的字段都添加到 addon_cols

# 3.3 筛选器说明

/**
 * 筛选器会自动根据 dbschema 中的字段配置生成
 * 
 * 不需要手动创建扩展筛选器类,系统会根据以下配置自动生成:
 * 
 * 字段配置示例:
 * 'code' => array(
 *     'filtertype' => 'normal',      // 筛选类型
 *     'filterdefault' => true,       // 默认显示在筛选器
 *     'searchtype' => 'nequal',      // 搜索类型
 * ),
 * 
 * 'status' => array(
 *     'filtertype' => 'normal',      // 筛选类型
 *     'filterdefault' => true,       // 默认显示在筛选器
 * ),
 * 
 * 'at_time' => array(
 *     'filtertype' => 'normal',      // 筛选类型
 * ),
 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 🍔 4. 菜单配置 (desktop.xml)

# 4.1 权限定义

<desktop>
    <permissions>
        <!-- 基础权限 -->
        <permission id="app_table_view">查看列表</permission>
        <permission id="app_table_add">添加数据</permission>
        <permission id="app_table_edit">编辑数据</permission>
        <permission id="app_table_delete">删除数据</permission>
        <permission id="app_table_import">导入数据</permission>
        <permission id="app_table_export">导出数据</permission>
        
        <!-- 特殊权限 -->
        <permission id="app_table_advanced">高级操作</permission>
        <permission id="app_table_audit">审核操作</permission>
    </permissions>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.2 菜单结构

    <!-- 工作台定义 -->
    <workground name="模块管理" id="app_center" order="100" icon="icon-module">
        
        <!-- 基础管理菜单组 -->
        <menugroup name="基础管理" en="basic-management">
            <menu controller='admin_table' 
                  action='index' 
                  permission='app_table_view' 
                  display='true' 
                  order='100'>数据列表</menu>
        </menugroup>
        
        <!-- 高级功能菜单组 -->
        <menugroup name="高级功能" en="advanced-features">
            <menu controller='admin_table' 
                  action='advanced' 
                  permission='app_table_advanced' 
                  display='true' 
                  order='200'>高级操作</menu>
        </menugroup>
        
        <!-- 系统设置菜单组 -->
        <menugroup name="系统设置" en="system-settings">
            <menu controller='admin_config' 
                  action='index' 
                  permission='app_config_manage' 
                  display='true' 
                  order='300'>配置管理</menu>
        </menugroup>
    </workground>
</desktop>
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

# 🔧 5. 业务逻辑层 (lib)

# 5.1 业务逻辑服务类

<?php
class app_table
{
    /**
     * 创建数据
     * @param array $data 表单数据
     * @return array [bool, string, array] [是否成功, 消息, 返回数据]
     */
    public function create($data)
    {
        // 数据验证
        $validate_result = $this->validate($data, 'create');
        if (!$validate_result[0]) {
            return $validate_result;
        }
        
        // 数据处理
        $process_result = $this->processData($data);
        if (!$process_result[0]) {
            return $process_result;
        }
        
        // 保存到数据库
        try {
            $model = app::get('app')->model('table');
            $id = $model->insert($process_result[1]);
            
            if ($id) {
                return [true, '创建成功', ['id' => $id]];
            } else {
                return [false, '创建失败'];
            }
        } catch (Exception $e) {
            return [false, '数据库操作失败: ' . $e->getMessage()];
        }
    }
    
    /**
     * 更新数据
     * @param int $id 记录ID
     * @param array $data 表单数据
     * @return array [bool, string] [是否成功, 消息]
     */
    public function update($id, $data)
    {
        // 验证记录是否存在
        $model = app::get('app')->model('table');
        $record = $model->dump($id);
        
        if (!$record) {
            return [false, '记录不存在'];
        }
        
        // 数据验证
        $validate_result = $this->validate($data, 'update', $id);
        if (!$validate_result[0]) {
            return $validate_result;
        }
        
        // 数据处理
        $process_result = $this->processData($data);
        if (!$process_result[0]) {
            return $process_result;
        }
        
        // 更新数据库
        try {
            $result = $model->update($process_result[1], ['id' => $id]);
            
            if ($result) {
                return [true, '更新成功'];
            } else {
                return [false, '更新失败'];
            }
        } catch (Exception $e) {
            return [false, '数据库操作失败: ' . $e->getMessage()];
        }
    }
    
    /**
     * 删除数据(软删除)
     * @param int $id 记录ID
     * @return array [bool, string] [是否成功, 消息]
     */
    public function delete($id)
    {
        // 验证记录是否存在
        $model = app::get('app')->model('table');
        $record = $model->dump($id);
        
        if (!$record) {
            return [false, '记录不存在'];
        }
        
        // 检查删除权限
        if (!$this->checkDeletePermission($record)) {
            return [false, '无权限删除此记录'];
        }
        
        // 执行软删除
        try {
            $result = $model->update(['delete' => 'true'], ['id' => $id]);
            
            if ($result) {
                return [true, '删除成功'];
            } else {
                return [false, '删除失败'];
            }
        } catch (Exception $e) {
            return [false, '数据库操作失败: ' . $e->getMessage()];
        }
    }
    
    /**
     * 数据验证
     * @param array $data 表单数据
     * @param string $action 操作类型:create/update
     * @param int $id 记录ID(更新时使用)
     * @return array [bool, string] [是否通过, 错误信息]
     */
    private function validate($data, $action = 'create', $id = null)
    {
        // 必填字段验证(根据实际字段调整)
        // if (empty($data['code'])) {
        //     return [false, '编码不能为空'];
        // }
        
        // if (empty($data['name'])) {
        //     return [false, '名称不能为空'];
        // }
        
        // 唯一性验证(根据实际字段调整)
        // $model = app::get('app')->model('table');
        // $existing = $model->db_dump(['code' => $data['code'], 'delete' => 'false'], 'id');
        // 
        // if ($action == 'update') {
        //     // 更新模式,排除当前记录
        //     if ($existing && $existing['id'] != $id) {
        //         return [false, '编码已存在'];
        //     }
        // } else {
        //     // 创建模式
        //     if ($existing) {
        //         return [false, '编码已存在'];
        //     }
        // }
        
        return [true, ''];
    }
    
    /**
     * 数据处理
     * @param array $data 原始数据
     * @return array [bool, array] [是否成功, 处理后的数据]
     */
    private function processData($data)
    {
        $processed = $data;
        
        // 数据清理(根据实际字段调整)
        // if (isset($processed['code'])) {
        //     $processed['code'] = trim($processed['code']);
        // }
        // if (isset($processed['name'])) {
        //     $processed['name'] = trim($processed['name']);
        // }
        
        // 设置默认值(根据实际字段调整)
        // if (empty($processed['status'])) {
        //     $processed['status'] = 1;
        // }
        
        return [true, $processed];
    }
    
    /**
     * 检查删除权限
     * @param array $record 记录数据
     * @return bool 是否有权限
     */
    private function checkDeletePermission($record)
    {
        // 根据业务规则检查删除权限
        // 例如:已启用的记录不能删除
        if ($record['status'] == 1) {
            return false;
        }
        
        return true;
    }
}
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

# 🔧 6. 服务配置 (services.xml)

# 6.1 服务定义

<services>
    <!-- Finder服务 -->
    <service id="desktop_finder.app_mdl_table">
        <class>app_finder_table</class> 
    </service>
    
    <!-- 扩展筛选器服务(可选,用于自定义筛选逻辑) -->
    <service id="extend_filter_app_mdl_table">
        <class>app_finder_extend_filter_table</class>
    </service>
    
    <!-- 业务逻辑服务 -->
    <service id="app_table">
        <class>app_table</class>
    </service>
    
    <!-- 操作日志服务 -->
    <service id="app_operation_log">
        <class>app_operation_log</class>
    </service>
</services>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 📊 7. 视图指南

# 7.1 列表页面指南

<!-- admin/table/index.html -->
<div class="table-container">
    <!-- 筛选器由系统根据 dbschema 自动生成 -->
    <!-- 数据表格由Finder自动生成 -->
</div>
1
2
3
4
5

# 7.2 添加/编辑页面指南

# 7.2.1 独立页面指南

<!-- admin/table/add.html / admin/table/edit.html -->
<div class="tableform">
    <form method="post" action="index.php?app=app&ctl=admin_table&act=save" id="table-form">
        <input type="hidden" name="id" value="<{$data.id}>">
        
        <h4>基本信息</h4>
        <div class="division">
            <table>
                <tbody>
                    <!-- 注意:以下字段需要根据实际需求添加 -->
                    <!-- 
                    <tr>
                        <th><em class="c-red">*</em>编码:</th>
                        <td>
                            <{if $data.code}>
                                <{$data.code}>
                                <input type="hidden" name="code" value="<{$data.code}>">
                            <{else}>
                                <input class="x-input" type="text" vtype="required" name="code" value="<{$data.code}>" size="40">
                            <{/if}>
                        </td>
                    </tr>
                    <tr>
                        <th><em class="c-red">*</em>名称:</th>
                        <td>
                            <{input type="text" vtype="required" name="name" value=$data.name size="40"}>
                        </td>
                    </tr>
                    <tr>
                        <th><em class="c-red">*</em>状态:</th>
                        <td>
                            <{input type="radio" vtype="requiredradio" name="status" value=$data.status|default:"1" options=array("1"=>"启用","0"=>"禁用") separator="&nbsp;"}>
                        </td>
                    </tr>
                    <tr>
                        <th>备注:</th>
                        <td>
                            <{input type="textarea" name="remark" value=$data.remark rows="3" cols="40"}>
                        </td>
                    </tr>
                    -->
                </tbody>
            </table>
        </div>
    </form>
</div>

<{area inject=".mainFoot"}>
<div class="table-action">
    <{button label="确定" class="btn-primary" id="savetable"}>
    <{button label="取消" class="btn-secondary" isCloseDialogBtn="true" }>
</div>
<{/area}>

<script>
$('savetable').addEvent('click', function(event) {
    if (!validate($('table-form'))) {
        return;
    }
    
    $('table-form').fireEvent('submit', {stop: $empty});
});

$('table-form').store('target', {
    onRequest: function() {
        $('savetable').set('disabled', 'true');
    },
    onComplete: function(resp) {
        resp = JSON.decode(resp);
        
        $('savetable').set('disabled', '');
        if (resp.error) return;
        
        $('table-form').getParent('.dialog').retrieve('instance').close();
        finderGroup['<{$env.get.finder_id}>'].refresh();
    }
});
</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

# 7.2.2 Dialog弹窗指南

重要:Dialog弹窗指南需要使用不同的结构和控制器方法。

控制器方法

// 编辑页面 - 使用display方法显示dialog内容
public function edit($id)
{
    if (!kernel::single('desktop_user')->has_permission('app.action.table.edit')) {
        $this->splash('error', '无权限');
    }
    
    if (empty($id)) {
        $this->splash('error', '参数错误');
    }

    $model = app::get('app')->model('table');
    $data = $model->db_dump(['id' => $id], '*');
    if (!$data) {
        $this->splash('error', '记录不存在');
    }

    $this->pagedata['data'] = $data;
    $this->display('admin/table/edit.html'); // 使用display方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Finder链接

// Finder中的编辑按钮链接
return '<a href="index.php?app=app&ctl=admin_table&act=edit&p[0]=' . $row['id'] . '" target="dialog::{title:\'编辑\',width:600,height:400}">编辑</a>';
1
2

Dialog指南结构

<!-- admin/table/edit.html - Dialog弹窗指南 -->
<div class="main">
    <div class="title">
        <h3>编辑信息</h3>
    </div>
    
    <form method="post" action="index.php?app=app&ctl=admin_table&act=update" id="editForm">
        <input type="hidden" name="id" value="<{$data.id}>">
        
        <table class="table table-bordered">
            <tr>
                <td class="text-right" style="width: 120px;">
                    <label class="control-label">字段名 <span class="text-danger">*</span></label>
                </td>
                <td>
                    <input type="text" name="field_name" class="form-control" required maxlength="50" 
                           value="<{$data.field_name}>" placeholder="请输入字段值">
                    <div class="help-block">字段说明</div>
                </td>
            </tr>
            <tr>
                <td class="text-right">
                    <label class="control-label">状态 <span class="text-danger">*</span></label>
                </td>
                <td>
                    <select name="status" class="form-control" required>
                        <option value="">请选择状态</option>
                        <option value="1" <{if $data.status == '1'}>selected<{/if}>>启用</option>
                        <option value="0" <{if $data.status == '0'}>selected<{/if}>>禁用</option>
                    </select>
                    <div class="help-block">状态说明</div>
                </td>
            </tr>
            <tr>
                <td class="text-right">
                    <label class="control-label">创建时间</label>
                </td>
                <td>
                    <input type="text" class="form-control" readonly value="<{$data.at_time|date_format:'%Y-%m-%d %H:%M:%S'}>">
                </td>
            </tr>
        </table>
        
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">保存</button>
            <button type="button" class="btn btn-default" onclick="parent.$.dialog.close();">取消</button>
        </div>
    </form>
</div>

<script>
$(document).ready(function() {
    // 表单验证
    $('#editForm').on('submit', function(e) {
        var field_name = $('input[name="field_name"]').val().trim();
        var status = $('select[name="status"]').val();
        
        if (!field_name || !status) {
            e.preventDefault();
            alert('请填写所有必填字段');
            return false;
        }
    });
});
</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

# 7.2.3 Dialog vs 独立页面对比

特性 Dialog弹窗 独立页面
控制器方法 $this->display() $this->page()
Finder链接 target="dialog::{...}" 普通链接
参数传递 &p[0]=123 &id=123
方法参数 public function edit($id) public function edit()
指南结构 <div class="main"> <div class="tableform">
按钮样式 普通按钮 系统按钮组件
关闭方式 parent.$.dialog.close() 页面跳转

# 7.3 详情页面指南

<!-- admin/table/detail_basic.html -->
<div class="tableform">
  <div class="division">
    <table width="100%" cellspacing="0" cellpadding="0" border="0">
      <tbody>
        <!-- 注意:以下字段需要根据实际需求添加 -->
        <!-- 
        <tr>
          <th>编码:</th>
          <td colspan="2"><{$data.code}></td>
        </tr>
        <tr>
          <th>名称:</th>
          <td colspan="2"><{$data.name}></td>
        </tr>
        <tr>
          <th>状态:</th>
          <td colspan="2"><{$data.status == 1 ? '启用' : '禁用'}></td>
        </tr>
        -->
        <tr>
          <th>创建时间:</th>
          <td colspan="2"><{$data.at_time}></td>
        </tr>
        <tr>
          <th>更新时间:</th>
          <td colspan="2"><{$data.up_time}></td>
        </tr>
      </tbody>
    </table>
  </div>
</div>
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

# 7.4 操作日志指南

<!-- admin/table/detail_log.html -->
<div class="log-container">
    <table class="log-table">
        <thead>
            <tr>
                <th>操作时间</th>
                <th>操作人</th>
                <th>操作类型</th>
                <th>操作说明</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach($datalist as $log): ?>
            <tr>
                <td><?php echo $log['operate_time']; ?></td>
                <td><?php echo $log['operate_user']; ?></td>
                <td><?php echo $log['operate_type']; ?></td>
                <td><?php echo $log['memo']; ?></td>
            </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 7.5 表单布局规范

# 7.5.1 布局原则

推荐使用2列布局,避免4列布局导致表单过宽:

<!-- ✅ 推荐:2列布局,适合对话框宽度 -->
<tr>
    <th>字段1:</th>
    <td><input type="text" name="field1" size="40"></td>
</tr>
<tr>
    <th>字段2:</th>
    <td><input type="text" name="field2" size="40"></td>
</tr>

<!-- ❌ 不推荐:4列布局,容易超出对话框宽度 -->
<tr>
    <th>字段1:</th>
    <td><input type="text" name="field1"></td>
    <th>字段2:</th>
    <td><input type="text" name="field2"></td>
</tr>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 7.5.2 字段宽度设置

  • 文本输入框: size="40" (适合对话框宽度)
  • 文本域: cols="40" (确保双引号包围)
  • 下拉选择: 默认宽度即可

# 7.5.3 对话框尺寸

控制器中推荐的对话框尺寸:

'target' => 'dialog::{width:660,height:480,title:\'添加\'}'
1

# 7.6 前端数据验证说明

# 7.6.1 验证类型 (vtype)

验证类型 说明 示例
required 必填字段 vtype="required"
number 数字类型 vtype="number"
digits 整数类型 vtype="digits"
unsignedint 正整数 vtype="unsignedint"
unsigned 非负数 vtype="unsigned"
positive 正数 vtype="positive"
alpha 字母 vtype="alpha"
alphanum 字母数字中文 vtype="alphanum"
email 邮箱格式 vtype="email"
url URL格式 vtype="url"
date 日期格式 vtype="date"
requiredradio 单选必选 vtype="requiredradio"
requiredcheckbox 多选必选 vtype="requiredcheckbox"

# 7.6.2 组合验证

<!-- 多个验证规则组合 -->
<input type="text" vtype="required&&email" name="email">

<!-- 必填且为数字 -->
<input type="text" vtype="required&&number" name="amount">

<!-- 必填且为正整数 -->
<input type="text" vtype="required&&unsignedint" name="quantity">
1
2
3
4
5
6
7
8

# 7.6.3 自定义错误提示

<!-- 使用 caution 属性自定义错误提示 -->
<input type="text" vtype="required" name="code" caution="请输入编码">

<!-- 使用 required 属性自动添加必填验证 -->
<input type="text" required name="name">
1
2
3
4
5

# 7.6.4 验证使用示例

// 表单验证
$('save-btn').addEvent('click', function(event) {
    if (!validate($('form-id'))) {
        return; // 验证失败,阻止提交
    }
    
    // 验证通过,提交表单
    $('form-id').fireEvent('submit', {stop: $empty});
});

// 单个字段验证
if (!validate($('field-id'))) {
    // 字段验证失败
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 🎯 8. 重要调用规范

# 8.1 业务逻辑层调用规范

# 8.1.1 控制器调用业务逻辑层

正确方式:使用 kernel::single() 实例化业务逻辑类

// ✅ 正确:使用 kernel::single() 实例化
$service = kernel::single('ome_book');  // 类名格式:app_类名
$result = $service->create($_POST);

// ❌ 错误:使用 app::get()->service() 方式
$service = app::get('ome')->service('book');
$result = $service->create($_POST);
1
2
3
4
5
6
7

注意事项

  • 使用 kernel::single('类名') 直接实例化
  • 类名格式:app_类名(如:ome_book
  • 不需要在 services.xml 中注册业务逻辑服务

# 8.1.2 数据库查询规范

查询单条记录:使用 db_dump() 方法

// ✅ 正确:使用 db_dump() 查询单条记录
$existing = $model->db_dump(['book_name' => $name, 'delete' => 'false'], 'id');

// ❌ 错误:使用 getRow() 方法
$existing = $model->getRow('id', ['book_name' => $name, 'delete' => 'false']);
1
2
3
4
5

方法参数说明

  • db_dump($filter, $cols)
    • 第一个参数:筛选条件数组
    • 第二个参数:要查询的字段(可以是字符串或数组)

# 8.1.3 标准返回格式

业务逻辑层方法必须返回标准格式:

// 成功返回格式
return [true, '成功消息', ['id' => $id]];

// 失败返回格式  
return [false, '失败原因'];

// 验证失败格式
return [false, '验证错误信息'];
1
2
3
4
5
6
7
8

# 8.2 架构分层调用规范

// 控制器层 → 业务逻辑层 → 模型层
class Controller {
    public function create() {
        // 1. 调用业务逻辑层
        $service = kernel::single('app_table');
        $result = $service->create($_POST);
        
        // 2. 处理返回结果
        if ($result[0]) {
            $this->end(true, $result[1]);
        } else {
            $this->end(false, $result[1]);
        }
    }
}

class BusinessLogic {
    public function create($data) {
        // 1. 数据验证
        $validate_result = $this->validate($data);
        if (!$validate_result[0]) {
            return $validate_result;
        }
        
        // 2. 数据处理
        $process_result = $this->processData($data);
        
        // 3. 调用模型层
        $model = app::get('app')->model('table');
        $id = $model->insert($process_result[1]);
        
        // 4. 返回标准格式
        return [true, '创建成功', ['id' => $id]];
    }
}
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

# 🎯 9. 使用步骤

# 9.1 创建新模块步骤

  1. 创建数据表定义

    • app/dbschema/ 下创建表定义文件
    • 重要:dbschema文件中的key必须与文件名保持一致(如:region_relation.php 文件中使用 $db['region_relation']
    • 必需字段idat_timeup_time(系统自动处理)
    • 可选字段:根据具体业务需求添加(如:名称、编码、状态等)
    • 配置筛选字段的 filtertypefilterdefault 属性
  2. 创建模型文件

    • app/model/ 下按目录格式创建模型类(如:app/model/region/relation.php
    • 继承基础模型类
  3. 创建业务逻辑层

    • app/lib/ 下按目录格式创建业务逻辑服务类(如:app/lib/region/relation.php
    • 实现数据验证、处理和数据库操作
    • 返回标准格式的结果 [bool, string, array]
  4. 创建控制器

    • app/controller/admin/ 下按目录格式创建控制器(如:app/controller/admin/region/relation.php
    • 重要:默认不实现任何操作方法(add、edit、create、update、delete)
    • 重要:默认不添加任何按钮(添加、编辑、删除、导入)
    • 设置 use_buildin_filter = true 启用自动筛选
    • 设置 use_buildin_import = false 默认不启用导入
  5. 创建Finder类

    • app/lib/finder/ 下按目录格式创建Finder类(如:app/lib/finder/region/relation.php
    • 重要:默认不实现操作列(column_edit)
    • 重要:默认不实现操作日志详情列(detail_log)
    • 重要:所有需要查询关联信息的列都必须使用批量查询优化指南
  6. 配置菜单权限

    • desktop.xml 中定义权限和菜单
    • 设置菜单分组和排序
  7. 配置服务

    • services.xml 中注册服务
    • 绑定Finder和业务逻辑类

重要提醒

  • 只有明确要求时才添加按钮和功能
  • 默认情况下只实现基础的列表展示功能
  • 避免过度开发,按需实现功能

# 9.2 权限控制要点

  • 每个操作都需要对应的权限ID
  • 在控制器中检查用户权限
  • 在菜单中绑定权限控制显示
  • 在Finder中根据权限显示不同操作按钮

# 9.3 性能优化建议

  • 批量查询优化:所有Finder列中的关联信息查询都必须使用批量查询指南,避免 N+1 查询问题
  • 合理设置数据库索引
  • 使用分页查询避免大量数据加载
  • 缓存常用数据减少数据库查询
  • 优化SQL查询语句

# 9.4 架构分层说明

  • 控制器层 (Controller):处理HTTP请求,调用业务逻辑层,返回响应
  • 业务逻辑层 (Lib):处理数据验证、业务规则、数据处理,调用模型层
  • 模型层 (Model):只负责数据库交互,如 save/insert/update/delete 等操作
  • 数据访问层 (Dbschema):定义数据库表结构和字段属性

标准返回格式

  • 成功:[true, '成功消息', ['id' => '主键ID']]
  • 失败:[false, '失败原因']

# 9.5 导入功能说明

OMS 系统采用基于 omecsv 框架的统一导入指南,参考 docs/cheatsheet/import-data-template.md

# 9.5.1 使用统一导入指南

// 控制器配置
$params = array(
    'use_buildin_import' => false, // 使用自定义导入指南
    'actions' => [
        [
            'label'  => '导入图书',
            'href'   => $this->url . '&act=displayImport',
            'target' => 'dialog::{width:660,height:400,title:\'导入图书\'}',
        ],
    ],
);

// 控制器方法
public function displayImport()
{
    $this->pagedata['type'] = 'book_import';
    $this->pagedata['extra_params'] = $_GET['extra_params'] ?? '';
    $this->display('admin/book/import.html');
}

public function exportTemplate()
{
    $importClass = kernel::single('ome_book_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
27
28
29

# 9.5.2 核心组件

  1. 导入处理类app/ome/lib/book/to/import.php

    • 实现 omecsv_data_split_interface 接口
    • 处理数据验证、格式转换、业务逻辑
  2. 导入界面指南app/ome/view/admin/book/import.html

    • 使用统一指南的HTML结构
    • 支持文件上传和指南下载
  3. 白名单注册app/omecsv/lib/split/whitelist.php

    'book_import' => [
        'name'  => '图书导入任务',
        'class' => 'ome_book_import',
    ],
    
    1
    2
    3
    4

# 9.5.3 特点

  • 智能处理:50条以内直接处理,超过则走队列
  • 大文件支持:文件切片,避免内存溢出
  • 数据验证:表头检查、必填字段、业务逻辑验证
  • 错误处理:详细错误信息、事务回滚、日志记录
  • 格式支持:CSV、XLS、XLSX格式

# 9.5.4 注意事项

  • 参考 docs/cheatsheet/import-data-template.md 完整实现
  • 查询单条数据时使用 db_dump($filter, $field) 方法
  • 实现事务管理确保数据一致性
  • 合理设置 max_direct_count 阈值

# 9.5 按钮和功能控制原则

# 9.5.1 基本原则

重要:只有明确要求时才添加功能,默认情况下不添加任何按钮和功能

# 9.5.2 控制器按钮控制

$params = array(
    'use_buildin_import' => false, // 默认不启用导入功能
    'actions' => [
        // 只有明确要求时才添加按钮
        // 添加按钮:只有明确要求时才添加
        // 导入按钮:只有明确要求时才添加
    ],
);
1
2
3
4
5
6
7
8

添加按钮示例(只有明确要求时):

'actions' => [
    [
        'label'  => '添加图书',
        'href'   => $this->url . '&act=add',
        'target' => 'dialog::{width:660,height:480,title:\'添加图书\'}',
    ],
],
1
2
3
4
5
6
7

导入按钮示例(只有明确要求时):

'actions' => [
    [
        'label'  => '导入图书',
        'href'   => $this->url . '&act=displayImport',
        'target' => 'dialog::{width:660,height:400,title:\'导入图书\'}',
    ],
],
1
2
3
4
5
6
7

# 9.5.3 Finder操作列控制

class app_finder_table
{
    // 只有明确要求时才添加操作列
    // var $column_edit = "操作";
    // var $column_edit_width = 120;
    // var $column_edit_order = 1;

    function column_edit($row)
    {
        $button = '';
        
        // 编辑按钮:只有明确要求时才添加
        // 删除按钮:只有明确要求时才添加
        
        return $button;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

编辑按钮示例(只有明确要求时):

$button .= sprintf(
    '<a href="index.php?app=app&ctl=admin_table&act=edit&p[0]=%s&finder_id=%s" 
        target="dialog::{width:660,height:480,title:\'编辑\'}">编辑</a>', 
    $id, $finder_id
);
1
2
3
4
5

删除按钮示例(只有明确要求时):

$button .= sprintf(
    ' <a href="index.php?app=app&ctl=admin_table&act=delete&p[0]=%s&finder_id=%s" 
        onclick="return confirm(\'确定删除吗?\')">删除</a>', 
    $id, $finder_id
);
1
2
3
4
5

# 9.5.4 操作日志控制

class app_finder_table
{
    // 只有明确要求时才展示操作日志
    // public $detail_log = '操作日志';
    // public function detail_log($id) { ... }
}
1
2
3
4
5
6

操作日志示例(只有明确要求时):

public $detail_log = '操作日志';
public function detail_log($id)
{
    $render = app::get('app')->render();
    
    $logObj = app::get('ome')->model('operation_log');
    $logData = $logObj->read_log([
        'obj_id' => $id, 
        'obj_type' => 'table@app'
    ]);

    foreach ($logData as $k => $v) {
        $logData[$k]['operate_time'] = date('Y-m-d H:i:s', $v['operate_time']);
    }

    $render->pagedata['datalist'] = $logData;
    return $render->fetch('admin/table/detail_log.html');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 9.5.5 控制器方法控制

class app_ctl_admin_table extends desktop_controller
{
    // 只有明确要求时才实现以下方法
    // public function add() { ... }           // 添加页面
    // public function edit($id) { ... }       // 编辑页面
    // public function create() { ... }        // 创建数据
    // public function update($id) { ... }     // 更新数据
    // public function delete($id) { ... }     // 删除数据
    // public function displayImport() { ... } // 导入页面
    // public function exportTemplate() { ... } // 导出指南
}
1
2
3
4
5
6
7
8
9
10
11

# 9.5.6 功能启用检查清单

在开发新模块时,请确认以下功能是否需要启用:

  • [ ] 添加功能:是否需要添加按钮和添加页面?
  • [ ] 编辑功能:是否需要编辑按钮和编辑页面?
  • [ ] 删除功能:是否需要删除按钮和删除逻辑?
  • [ ] 导入功能:是否需要导入按钮和导入处理?
  • [ ] 操作日志:是否需要展示操作日志详情列?
  • [ ] 快照功能:是否需要展示操作快照?

只有明确要求的功能才实现,避免过度开发。

# 9.6 指南语法规范

# 9.5.1 Smarty 指南语法

OMS 系统使用 Smarty 指南引擎,必须使用 Smarty 语法而不是 PHP 语法:

变量输出

<!-- ✅ 正确:Smarty 语法 -->
<{$data.field_name}>
<{$data.status == 1 ? '启用' : '禁用'}>

<!-- ❌ 错误:PHP 语法 -->
<?php echo $data['field_name']; ?>
<?php echo $data['status'] == 1 ? '启用' : '禁用'; ?>
1
2
3
4
5
6
7

循环语法

<!-- ✅ 正确:Smarty 循环 -->
<{foreach from=$datalist item=item}>
<tr>
  <th>字段名:</th>
  <td colspan="2"><{$item.field_name}></td>
</tr>
<{/foreach}>

<!-- ❌ 错误:PHP 循环 -->
<?php foreach($datalist as $item): ?>
<tr>
  <th>字段名:</th>
  <td colspan="2"><?php echo $item['field_name']; ?></td>
</tr>
<?php endforeach; ?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

条件判断

<!-- ✅ 正确:Smarty 条件 -->
<{if $data.status == 1}>
<td>启用</td>
<{else}>
<td>禁用</td>
<{/if}>

<!-- ❌ 错误:PHP 条件 -->
<?php if($data['status'] == 1): ?>
<td>启用</td>
<?php else: ?>
<td>禁用</td>
<?php endif; ?>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 9.5.2 详情页指南结构

详情页必须使用标准的 tableform + division + table 结构,纵向展示单条记录

<div class="tableform">
  <div class="division">
    <table width="100%" cellspacing="0" cellpadding="0" border="0">
      <tbody>
        <tr>
          <th>字段名:</th>
          <td colspan="2"><{$data.field_name}></td>
        </tr>
        <tr>
          <th>状态:</th>
          <td colspan="2"><{$data.status == 1 ? '启用' : '禁用'}></td>
        </tr>
      </tbody>
    </table>
  </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

详情页特点

  • 使用 <th> 标签显示字段名
  • 使用 <td colspan="2"> 显示字段值
  • 不使用 <thead> 结构
  • 纵向展示单条记录的各个字段

# 9.5.3 列表页指南结构

列表页(如操作日志列表)必须使用 tableform + division + table class="gridlist" 结构,横向展示多行数据

<div class="tableform">
  <div class="division">
    <table class="gridlist" width="100%" cellspacing="0" cellpadding="0" border="0">
      <thead>
        <tr>
          <th>操作时间</th>
          <th>操作人</th>
          <th>操作类型</th>
          <th>操作说明</th>
        </tr>
      </thead>
      <tbody>
        <{foreach from=$datalist item=log}>
        <tr>
          <td><{$log.operate_time}></td>
          <td><{$log.op_name}></td>
          <td><{$log.operation}></td>
          <td><{$log.memo}></td>
        </tr>
        <{/foreach}>
      </tbody>
    </table>
  </div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

列表页特点

  • 使用 tableform + division 外层容器
  • <table> 标签上添加 class="gridlist" 样式
  • 使用 <thead> 定义表头
  • 使用 <tbody> 循环展示多行数据
  • 每行使用 <td> 显示对应字段值
  • 横向展示多行记录

# 9.5.4 Dialog弹窗指南规范

重要:Dialog弹窗指南必须遵循特定的结构和语法规范。

指南结构

<!-- Dialog弹窗指南结构 -->
<div class="main">
    <div class="title">
        <h3>弹窗标题</h3>
    </div>
    
    <form method="post" action="..." id="formId">
        <!-- 表单内容 -->
    </form>
</div>
1
2
3
4
5
6
7
8
9
10

Smarty语法规范

<!-- ✅ 正确:Smarty变量输出 -->
<{$data.field_name}>
<{$data.status == '1' ? '启用' : '禁用'}>

<!-- ✅ 正确:Smarty条件判断 -->
<{if $data.status == '1'}>selected<{/if}>

<!-- ✅ 正确:Smarty日期格式化 -->
<{$data.at_time|date_format:'%Y-%m-%d %H:%M:%S'}>

<!-- ❌ 错误:PHP语法 -->
<?php echo $data['field_name']; ?>
<?php echo $data['status'] == '1' ? '启用' : '禁用'; ?>
1
2
3
4
5
6
7
8
9
10
11
12
13

控制器方法规范

// ✅ 正确:Dialog弹窗控制器方法
public function edit($id)
{
    // 权限检查
    if (!kernel::single('desktop_user')->has_permission('app.action.table.edit')) {
        $this->splash('error', '无权限');
    }
    
    // 参数验证
    if (empty($id)) {
        $this->splash('error', '参数错误');
    }

    // 数据查询
    $model = app::get('app')->model('table');
    $data = $model->db_dump(['id' => $id], '*');
    if (!$data) {
        $this->splash('error', '记录不存在');
    }

    // 显示指南
    $this->pagedata['data'] = $data;
    $this->display('admin/table/edit.html'); // 使用display方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Finder链接规范

// ✅ 正确:Dialog弹窗链接
return '<a href="index.php?app=app&ctl=admin_table&act=edit&p[0]=' . $row['id'] . '" target="dialog::{title:\'编辑\',width:600,height:400}">编辑</a>';

// ❌ 错误:独立页面链接
return '<a href="index.php?app=app&ctl=admin_table&act=edit&id=' . $row['id'] . '">编辑</a>';
1
2
3
4
5

# 9.5.5 常见错误避免

  1. 不要混用 PHP 和 Smarty 语法
  2. 不要使用自定义 CSS 类,使用系统标准结构
  3. 详情页不要使用 <thead> 结构,列表页才使用 <thead>
  4. 变量访问使用点号 . 而不是方括号 []
  5. 详情页用纵向展示,列表页用横向展示
  6. Dialog弹窗必须使用 display() 方法,独立页面使用 page() 方法
  7. Dialog弹窗参数使用 p[0] 格式,独立页面使用 id 格式

# 📝 10. 操作日志与快照规范

# 10.1 日志类型定义

新增模块时,必须在 app/ome/lib/operation/log.php 中定义日志类型:

# 10.1.1 在 get_operations() 方法中添加操作定义

// 在 $operations 数组中添加新模块的操作类型
'book_create' => array('name'=> '新建图书','type' => 'book@ome'),
'book_update' => array('name'=> '编辑图书','type' => 'book@ome'),
'book_delete' => array('name'=> '删除图书','type' => 'book@ome'),
1
2
3
4

格式说明

  • 键名:模块名_操作(如:book_create
  • name:中文显示名称
  • type表名@app名(如:book@ome

# 10.1.2 在 getType() 方法中添加类型映射

// 在 $type 数组中添加类型映射
'book@ome' => '图书',
1
2

# 10.1.3 在 getTypeMap() 方法中添加类型对应关系

// 在 $typeList 数组中添加类型对应关系
'book@ome' => array('book@ome'),
1
2

# 10.2 操作日志写入规范

# 10.2.1 标准日志写入方法

// 在业务逻辑层的 create/update/delete 方法中写入日志
public function create($data) {
    // ... 业务逻辑处理 ...
    
    // 写入操作日志
    write_log('book_create@ome', $id, '新建图书:' . $data['book_name']);
    
    return [true, '创建成功', ['id' => $id]];
}

public function update($id, $data) {
    // ... 业务逻辑处理 ...
    
    // 写入操作日志
    write_log('book_update@ome', $id, '编辑图书:' . $data['book_name']);
    
    return [true, '更新成功'];
}

public function delete($id) {
    // 获取删除前的数据用于日志
    $book = $this->model->db_dump(['id' => $id], '*');
    
    // ... 业务逻辑处理 ...
    
    // 写入操作日志
    write_log('book_delete@ome', $id, '删除图书:' . $book['book_name']);
    
    return [true, '删除成功'];
}
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

# 10.2.2 日志参数说明

write_log($type, $obj_id, $memo) 参数:

  • $type:日志类型,格式为 操作名@app名(如:book_create@ome
  • $obj_id:操作对象ID(如:图书ID)
  • $memo:操作描述,包含关键信息(如:图书名称)

# 10.3 快照写入规范

# 10.3.1 快照数据写入

createupdate 方法中,需要将操作前的数据写入快照表:

public function create($data) {
    // ... 业务逻辑处理 ...
    
    // 写入操作日志
    write_log('book_create@ome', $id, '新建图书:' . $data['book_name']);
    
    // 写入快照(新建时为空数据)
    $snapshot_data = [
        'operation_type' => 'create',
        'table_name' => 'sdb_ome_book',
        'record_id' => $id,
        'snapshoot' => json_encode([]), // 新建时为空
        'create_time' => time()
    ];
    app::get('ome')->model('operation_log_snapshoot')->insert($snapshot_data);
    
    return [true, '创建成功', ['id' => $id]];
}

public function update($id, $data) {
    // 获取更新前的数据
    $old_data = $this->model->db_dump(['id' => $id], '*');
    
    // ... 业务逻辑处理 ...
    
    // 写入操作日志
    write_log('book_update@ome', $id, '编辑图书:' . $data['book_name']);
    
    // 写入快照(更新前的数据)
    $snapshot_data = [
        'operation_type' => 'update',
        'table_name' => 'sdb_ome_book',
        'record_id' => $id,
        'snapshoot' => json_encode($old_data), // 更新前的完整数据
        'create_time' => time()
    ];
    app::get('ome')->model('operation_log_snapshoot')->insert($snapshot_data);
    
    return [true, '更新成功'];
}
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

# 10.3.2 快照表结构

快照数据写入 operation_log_snapshoot 表:

字段 类型 说明
operation_type varchar(20) 操作类型:create/update/delete
table_name varchar(50) 表名
record_id int 记录ID
snapshoot longtext 快照数据(JSON格式)
create_time int 创建时间

# 10.4 推荐实现流程

  1. 在 log.php 中定义日志类型

    • get_operations() 中添加操作定义
    • getType() 中添加类型映射
    • getTypeMap() 中添加类型对应关系
  2. 在业务逻辑层实现日志写入

    • create 方法:写入创建日志,快照为空数据
    • update 方法:写入更新日志,快照为更新前数据
    • delete 方法:写入删除日志,快照为删除前数据
  3. 日志内容规范

    • 包含操作类型和关键信息
    • 使用中文描述便于理解
    • 包含对象标识(如:图书名称)
  4. 快照数据规范

    • 使用 JSON 格式存储
    • 包含完整的记录数据
    • 记录操作类型和时间

# 10.5 快照展示功能实现

# 10.5.1 Finder 中添加快照链接

在 Finder 的 detail_log 方法中,为特定操作类型添加快照链接:

public function detail_log($id)
{
    $render = app::get('ome')->render();
    
    $logObj = app::get('ome')->model('operation_log');
    $logData = $logObj->read_log([
        'obj_id' => $id, 
        'obj_type' => 'book@ome'
    ]);

    $finder_id = $_GET['_finder']['finder_id'];
    foreach ($logData as $k => $v) {
        $logData[$k]['operate_time'] = date('Y-m-d H:i:s', $v['operate_time']);

        // 为新建和编辑操作添加快照链接
        if (in_array($v['operation'], ['新建图书', '编辑图书'])) {
            $logData[$k]['memo'] .= " <a href='index.php?app=ome&ctl=admin_book&act=show_history&p[0]={$v['log_id']}&finder_id={$finder_id}' onclick=\"window.open(this.href, '_blank', 'width=500,height:400'); return false;\">查看快照</a>";
        }
    }

    $render->pagedata['datalist'] = $logData;
    return $render->fetch('admin/book/detail_log.html');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 10.5.2 控制器中添加 show_history 方法

/**
 * 显示操作历史快照
 */
public function show_history($log_id)
{
    $render = app::get('ome')->render();
    
    // 获取日志信息
    $logObj = app::get('ome')->model('operation_log');
    $logInfo = $logObj->dump($log_id);
    
    // 获取快照数据
    $snapshotObj = app::get('ome')->model('operation_log_snapshoot');
    $snapshotData = $snapshotObj->db_dump(['log_id' => $log_id], '*');
    
    if ($snapshotData) {
        $snapshot = json_decode($snapshotData['snapshoot'], true);
        $render->pagedata['snapshot'] = $snapshot;
        $render->pagedata['operation_type'] = $snapshotData['operation_type'];
    } else {
        $render->pagedata['snapshot'] = [];
        $render->pagedata['operation_type'] = 'create';
    }
    
    $render->pagedata['log_info'] = $logInfo;
    
    return $render->fetch('admin/book/show_history.html');
}
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

# 10.5.3 快照展示指南

创建 show_history.html 指南,展示快照数据:

<div class="tableform">
  <div class="division">
    <table width="100%" cellspacing="0" cellpadding="0" border="0">
      <tbody>
        <tr>
          <th>操作类型:</th>
          <td colspan="2">
            <{if $operation_type == 'create'}>新建<{elseif $operation_type == 'update'}>编辑<{else}>其他<{/if}>
          </td>
        </tr>
        <tr>
          <th>操作时间:</th>
          <td colspan="2"><{$log_info.operate_time}></td>
        </tr>
        <tr>
          <th>操作人:</th>
          <td colspan="2"><{$log_info.op_name}></td>
        </tr>
        <{if $snapshot}>
        <tr>
          <th>快照数据:</th>
          <td colspan="2">
            <div style="max-height:300px;overflow-y:auto;border:1px solid #ccc;padding:10px;background:#f9f9f9;">
              <{if $operation_type == 'create'}>
                <p><strong>新建数据:</strong></p>
              <{elseif $operation_type == 'update'}>
                <p><strong>更新前数据:</strong></p>
              <{/if}>
              <table width="100%" cellspacing="0" cellpadding="5" border="0">
                <{foreach from=$snapshot key=field item=value}>
                <tr>
                  <td width="30%"><strong><{$field}>:</strong></td>
                  <td width="70%">
                    <{if $field == 'status'}>
                      <{$value == 1 ? '启用' : '禁用'}>
                    <{else}>
                      <{$value}>
                    <{/if}>
                  </td>
                </tr>
                <{/foreach}>
              </table>
            </div>
          </td>
        </tr>
        <{else}>
        <tr>
          <th>快照数据:</th>
          <td colspan="2">无快照数据</td>
        </tr>
        <{/if}>
      </tbody>
    </table>
  </div>
</div>
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

# 10.6 注意事项

  • 日志类型必须在 log.php 中定义,否则无法正确显示
  • 快照数据用于数据恢复和审计,必须完整准确
  • 日志描述要简洁明了,包含关键信息
  • 所有重要操作都要记录日志,确保可追溯性
  • 快照展示功能需要配合日志和快照写入功能一起使用

# 10.6 PHP 8.1+ 兼容性注意事项

# 10.6.1 insert() 方法参数传递

在 PHP 8.1+ 版本中,insert() 方法不能直接传递数组字面量,需要先赋值给变量:

// ❌ 错误:直接传递数组字面量
app::get('ome')->model('operation_log_snapshoot')->insert([
    'log_id' => $log_id,
    'snapshoot' => $snapshoot,
]);

// ✅ 正确:先赋值给变量再传递
$snapshot_data = [
    'log_id' => $log_id,
    'snapshoot' => $snapshoot,
];
app::get('ome')->model('operation_log_snapshoot')->insert($snapshot_data);
1
2
3
4
5
6
7
8
9
10
11
12

原因:PHP 8.1+ 不允许通过引用传递字面量,必须先赋值给变量再传递。

# 📋 重要开发规范总结

# splash方法正确用法

# 概述

在desktop框架中,splash方法用于显示错误或成功信息并重定向页面。

# 正确语法

$this->splash('类型', '重定向URL', '提示信息');
1

# 参数说明

  1. 第一个参数:消息类型

    • 'error' - 错误信息
    • 'success' - 成功信息
    • 'info' - 信息提示
  2. 第二个参数:重定向URL

    • 通常使用 $this->url
    • 可以是具体的URL字符串
  3. 第三个参数:提示信息

    • 要显示给用户的错误或成功信息

# 正确示例

// 错误信息
$this->splash('error', $this->url, '无权限');
$this->splash('error', $this->url, '参数错误或不支持全选');
$this->splash('error', $this->url, '记录不存在');

// 成功信息
$this->splash('success', $this->url, '操作成功');
1
2
3
4
5
6
7

# 错误用法

// ❌ 错误:将提示信息放在第二个参数
$this->splash('error', '无权限');

// ❌ 错误:参数顺序错误
$this->splash('error', '无权限', $this->url);
1
2
3
4
5

# 注意事项

  • 第二个参数必须是重定向URL,不能是提示信息
  • 第三个参数才是要显示给用户的提示信息
  • 使用 $this->url 作为重定向URL是最常见的做法

# 数据库开发规范

# 1. 文件目录结构规范

重要:所有目录都应该按照目录格式组织文件,保持项目结构的一致性。

// ✅ 正确的目录结构
app/tongyioil/
├── dbschema/
│   └── region_relation.php    # dbschema使用下划线命名
├── model/
│   └── region/
│       └── relation.php       # 按目录格式组织
├── lib/
│   ├── region/
│   │   └── relation.php       # 按目录格式组织
│   └── finder/
│       └── region/
│           └── relation.php   # 按目录格式组织
├── controller/admin/
│   └── region/
│       └── relation.php       # 按目录格式组织
└── view/admin/region/relation/
    └── detail_basic.html      # 按目录格式组织
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2. dbschema文件命名规范

重要:dbschema文件中的key必须与文件名保持一致,避免重复前缀。

// ✅ 正确:文件名 region_relation.php,key使用 region_relation
$db['region_relation'] = array(
    'columns' => array(
        // 字段定义...
    ),
);

// ❌ 错误:文件名 region_relation.php,key使用 tongyioil_region_relation
$db['tongyioil_region_relation'] = array(
    'columns' => array(
        // 字段定义...
    ),
);
1
2
3
4
5
6
7
8
9
10
11
12
13

原因:OMS系统会自动为数据库表添加APP前缀,最终表名会是 sdb_APP名_表名。如果key中已经包含APP名,会导致表名重复前缀。

# 2. 数据库查询规范

  • 使用 db_dump() 方法查询单条记录
  • 使用 insert() 方法插入数据
  • 使用 update() 方法更新数据
  • 使用 delete() 方法删除数据

# 3. 业务逻辑返回格式

  • 成功:[true, '成功消息', ['id' => '主键ID']]
  • 失败:[false, '失败原因']

# 4. 类名与文件路径映射规范

重要:OMS系统使用自动加载机制,类名必须与文件路径保持一致。

// ✅ 正确的类名和文件路径映射
// 类名: tongyioil_ctl_admin_region_relation
// 文件: app/tongyioil/controller/admin/region/relation.php

// 类名: tongyioil_mdl_region_relation  
// 文件: app/tongyioil/model/region/relation.php

// 类名: tongyioil_region_relation
// 文件: app/tongyioil/lib/region/relation.php

// 类名: tongyioil_finder_region_relation
// 文件: app/tongyioil/lib/finder/region/relation.php
1
2
3
4
5
6
7
8
9
10
11
12

自动加载规则

  • 类名中的下划线 _ 会被转换为目录分隔符 /
  • 控制器类:{app}_ctl_{path}app/{app}/controller/{path}.php
  • 模型类:{app}_mdl_{path}app/{app}/model/{path}.php
  • 业务逻辑类:{app}_{path}app/{app}/lib/{path}.php

# 5. Finder列定义规范

重要:所有Finder自定义列都必须定义名称和排序属性。

// ✅ 正确:定义列名称和排序
class app_finder_table
{
    // 列名称定义
    public $column_edit = '操作';
    public $column_status = '状态';
    
    // 列排序定义
    public $column_edit_order = 1;        // 操作列排在最前面
    public $column_status_order = 10;     // 状态列排在前面
    
    // 列方法实现
    public function column_edit($row) {
        // 操作列实现
    }
    
    public function column_status($row) {
        // 状态列实现
    }
}

// ❌ 错误:未定义列名称和排序
class app_finder_table
{
    // 缺少列名称和排序定义
    public function column_edit($row) {
        // 操作列实现
    }
}
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

列定义规则

  • 列名称public $column_列名 = '显示名称'
  • 列排序public $column_列名_order = 数字
  • 排序规则:数值越小,排序越靠前
  • 操作列:通常设置为 order = 1,排在最前面

# 6. 操作日志添加规范

# 6.1 操作日志添加步骤

第一步:定义操作类型app/{模块}/lib/operation/log.php 中添加操作定义:

function get_operations()
{
    $operations = array(
        '{模块名}' => array(
            // 业务操作定义
            '{操作名}' => array('name' => '操作显示名称', 'type' => '业务类型@{模块名}'),
        )
    );
    return $operations;
}
1
2
3
4
5
6
7
8
9
10

第二步:在业务代码中调用 在控制器或业务逻辑中调用操作日志:

// 记录操作日志
if ($old_value != $new_value) {
    $memo = '字段名:' . $old_value . ' → ' . $new_value;
    app::get('ome')->model('operation_log')->write_log('{操作名}@{模块名}', $id, $memo);
}
1
2
3
4
5

# 6.2 操作日志参数说明

  • 操作标识{操作名}@{模块名}(如:platform_coupon_setting_edit@tongyioil
  • 对象ID$id(被操作的记录ID)
  • 备注信息$memo(操作的具体内容描述)

# 6.3 操作日志使用场景

  • 数据修改操作记录
  • 重要业务操作追踪
  • 用户行为审计
  • 系统操作监控

# 6.4 操作日志示例

定义操作类型

'platform_coupon_setting_edit' => array('name' => '编辑平台优惠券配置', 'type' => 'platform_coupon_setting@tongyioil'),
1

调用操作日志

$old_customer = $record['customer'];
if ($old_customer != $customer) {
    $memo = '客户名称:' . $old_customer . ' → ' . $customer;
    app::get('ome')->model('operation_log')->write_log('platform_coupon_setting_edit@tongyioil', $id, $memo);
}
1
2
3
4
5

# 6.5 操作日志常见错误

  • 错误:缺少 @模块名 后缀

  • 正确:使用完整的操作标识格式

  • 错误:使用错误的模块(如 desktop

  • 正确:使用 ome 模块的操作日志

  • 错误:忘记定义操作类型

  • 正确:先在 operation/log.php 中定义操作类型


# 📚 相关文档

# ✅ 开发检查清单

  • [ ] 正确使用 $this->col_prefix 访问 addon_cols 中的字段
  • [ ] 实现批量查询优化,避免 N+1 查询问题
  • [ ] 使用正确的 Smarty 模板语法
  • [ ] 注册必要的服务(Finder、扩展筛选器等)
  • [ ] 实现适当的错误处理和用户反馈
  • [ ] 测试功能在不同数据量下的性能表现
  • [ ] 确保代码复用,避免重复实现相同功能
最后更新: 11/11/2025, 9:12:34 PM