# `xls2crud.py` 技术文档

> **工具名称**：`xls2crud.py`  
> **功能描述**：从 Excel 模型文件（`.xlsx`）自动生成基于模板的 CRUD 前后端界面代码，支持数据浏览器、新增、编辑、删除、查询及关联子表操作。  
> **适用场景**：快速构建基于数据库模型的 Web 应用前端 UI 和后端接口描述。

---

## 目录

- [1. 简介](#1-简介)
- [2. 依赖说明](#2-依赖说明)
- [3. 使用方式](#3-使用方式)
- [4. 核心函数说明](#4-核心函数说明)
  - [`build_dbdesc`](#builddbdescmodels_dir--dict)
  - [`subtable2toolbar`](#subtable2toolbardesc)
  - [`build_crud_ui`](#build_crud_uicrud_data-dict-dbdesc-dict)
  - [`build_table_crud_ui`](#build_table_crud_uiuidir-str-desc-dict---none)
  - [`field_list`](#field_listdesc-dict---list)
  - [`get_code_desc`](#get_code_desffield-dict-desc-dict---dict)
  - [`setup_ui_info`](#setup_ui_infofielddict-confidential_fieldslist---dict)
  - [`construct_get_data_sql`](#construct_get_data_sqldesc-dict---str)
  - 其他辅助函数
- [5. 模板系统与输出结构](#5-模板系统与输出结构)
- [6. 配置文件格式 (`json_file`)](#6-配置文件格式-json_file)
- [7. 输出目录结构示例](#7-输出目录结构示例)
- [8. 注意事项与限制](#8-注意事项与限制)

---

## 1. 简介

本脚本通过读取 `.xlsx` 表格定义的数据模型和 JSON 配置文件，自动为指定数据表生成完整的 CRUD 界面组件：

- 数据浏览页（`index.ui`）
- 新增表单（`add_*.dspy`）
- 编辑表单（`update_*.dspy`）
- 删除逻辑（`delete_*.dspy`）
- 数据获取接口（`get_*.dspy`）
- 关联字段状态检查（`check_changed.dspy`）

所有输出均基于预设的 Jinja-like 模板渲染，并支持动态参数替换。

---

## 2. 依赖说明

```python
import os
import sys
import codecs
import json
import argparse

from appPublic.dictObject import DictObject
from xlsxData import xlsxFactory
from appPublic.folderUtils import listFile, _mkdir
from appPublic.myTE import MyTemplateEngine
from tmpls import (
    data_browser_tmpl,
    get_data_tmpl,
    data_new_tmpl,
    data_update_tmpl,
    data_delete_tmpl,
    check_changed_tmpls
)
from appPublic.argsConvert import ArgsConvert
```

### 外部模块说明

| 模块 | 功能 |
|------|------|
| `appPublic.dictObject` | 将字典转换为可点式访问的对象（类似 JavaScript 的对象） |
| `xlsxData.xlsxFactory` | 解析 `.xlsx` 文件并提取结构化数据 |
| `appPublic.folderUtils.listFile`, `_mkdir` | 文件遍历与目录创建工具 |
| `appPublic.myTE.MyTemplateEngine` | 轻量级模板引擎，支持变量替换 |
| `tmpls.*` | 内置字符串形式的模板内容（HTML/UI/DSPY） |
| `appPublic.argsConvert` | 支持 `${var}$` 或 `[[var]]` 形式的变量替换 |

---

## 3. 使用方式

### 命令行语法

```bash
python xls2crud.py [-m models_dir] [-o output_dir] modulename json_file [...]
```

### 参数说明

| 参数 | 是否必需 | 说明 |
|------|----------|------|
| `modulename` | ✅ 是 | 当前模块名，用于数据库上下文解析 |
| `json_file` | ✅ 是 | 至少一个 CRUD 配置 JSON 文件路径 |
| `-m`, `--models_dir` | ❌ 否 | `.xlsx` 模型文件所在目录（可在 JSON 中覆盖） |
| `-o`, `--output_dir` | ❌ 否 | 输出根目录，默认为当前路径 |

### 示例命令

```bash
python xls2crud.py -m ./models -o ./ui_output user_module user_config.json role_config.json
```

---

## 4. 核心函数说明

### `build_dbdesc(models_dir: str) -> dict`

#### 功能
扫描指定目录下的所有 `.xlsx` 文件，将其解析为统一的数据库描述对象。

#### 输入
- `models_dir`: 字符串或列表，包含模型文件路径

#### 输出
- `dict`: 键为表名（来自 `summary[0].name`），值为该表的完整描述对象（含字段、编码表、子表等）

#### 流程
1. 遍历目录中所有 `.xlsx` 文件
2. 使用 `xlsxFactory` 加载每个文件
3. 提取其 `get_data()` 结果
4. 取第一项 summary 的 name 作为表名注册进 `db_desc`

---

### `subtable2toolbar(desc)`

#### 功能
将 `desc.subtables` 中定义的子表信息转化为工具栏按钮及弹窗绑定事件。

#### 参数
- `desc`: 当前表的描述对象（DictObject）

#### 行为
- 若无 `toolbar`，初始化为空对象
- 为每个子表添加：
  - 工具栏图标按钮（SVG 图标 + label）
  - 绑定点击事件打开 PopupWindow
  - POST 请求跳转至对应 URL（支持相对路径）
  - 自动映射主键 `id` 和引用控件 `referer_widget`
  - 支持额外字段映射（via `mapping`）

#### 示例生成的 bind 结构
```json
{
  "wid": "self",
  "event": "child_table",
  "actiontype": "urlwidget",
  "target": "PopupWindow",
  "popup_options": {
    "title": "子表标题",
    "width": "70%",
    "height": "70%"
  },
  "params_mapping": {
    "mapping": { "id": "parent_id", "referer_widget": "referer_widget" }
  },
  "options": {
    "method": "POST",
    "url": "{{entire_url('../child_table')}}"
  }
}
```

---

### `build_crud_ui(crud_data: dict, dbdesc: dict)`

#### 功能
根据传入的配置和数据库描述，驱动整个 CRUD 界面生成流程。

#### 参数
- `crud_data`: 来自 JSON 文件的配置对象（DictObject）
- `dbdesc`: 所有表的元数据集合

#### 步骤
1. 获取目标表名 `tblname`
2. 更新 `desc` 参数（如权限、标题等）
3. 调用 `subtable2toolbar(desc)` 添加子表按钮
4. 如果存在 `relation` 字段，则启用“行选中联动”机制
5. 渲染 `binds` 为 JSON 字符串
6. 调用 `build_table_crud_ui()` 开始生成具体文件

---

### `build_table_crud_ui(uidir: str, desc: dict) -> None`

#### 功能
协调生成所有与当前表相关的 UI 文件。

#### 行为
- 创建输出目录（若不存在）
- 生成：
  - `index.ui`（数据浏览器）
  - `get_{tblname}.dspy`（数据获取）
- 若有外键关系（`desc.relation`）：
  - 仅生成 `check_changed.dspy`
- 否则生成标准三件套：
  - `add_*.dspy`
  - `update_*.dspy`
  - `delete_*.dspy`

---

### `field_list(desc: dict) -> list`

#### 功能
构造适用于前端展示的字段列表，包含类型、宽度、标签、UI 类型等信息。

#### 逻辑
1. 遍历 `desc.fields`
2. 判断是否是码表字段（在 `codes` 中定义）
   - 是 → 调用 `get_code_desc`
   - 否 → 调用 `setup_ui_info`
3. 应用 `browserfields.alters` 进行个性化覆盖
4. 返回最终字段数组

---

### `get_code_desc(field: dict, desc: dict) -> dict`

#### 功能
处理码表字段（如性别、状态），将其转换为下拉选择框或远程数据源字段。

#### 输出字段说明
| 属性 | 说明 |
|------|------|
| `uitype` | `'code'` 表示码表类型 |
| `valueField` / `textField` | 实际值与显示文本字段名 |
| `params` | 请求码表数据所需参数（dbname, table, cond 等） |
| `dataurl` | 固定为 `/appbase/get_code.dspy` |

#### 特性支持
- 支持条件表达式中的变量替换（如 `${dept_id}$`）
- 使用 `ArgsConvert` 解析并注入运行时变量

---

### `setup_ui_info(field:dict, confidential_fields=[]) ->dict`

#### 功能
为普通字段设置默认 UI 显示属性。

#### 类型映射规则

| 数据库类型 | `uitype` | 说明 |
|-----------|---------|------|
| `date` | `date` | 日期输入框 |
| `time` | `time` | 时间输入框 |
| `int`, `short`, etc. | `int` | 整数输入 |
| `float`, `double`, `decimal` | `float` | 浮点数输入 |
| `text` | `text` | 多行文本 |
| 其他 | `str` | 默认字符串 |
| 名称以 `_date` 结尾 | `date` | 智能识别 |
| 在 `confidential_fields` 中 | `password` | 密码掩码 |

#### 宽度计算
- `cwidth = min(max(length, 4), 18)`
- 最小 4，最大 18（单位字符宽）

---

### `construct_get_data_sql(desc: dict) -> str`

#### 功能
构建用于获取主表及其码表连接数据的 SQL 查询语句。

#### 分支逻辑

##### A. 存在外键关联且有码表
生成 LEFT JOIN 查询，判断是否存在外键记录（`has_xxx` 标志位），并带出码表文本。

```sql
SELECT '${param_field}$' AS param_field,
       CASE WHEN b.param_field IS NULL THEN 0 ELSE 1 END has_param_field,
       a.value AS code_field,
       a.text AS code_field_text
FROM code_table a
LEFT JOIN (SELECT * FROM main_table WHERE param_field = ${param_field}$) b
  ON a.value = b.code_field;
```

##### B. 无码表
直接查询主表 + 占位符 `[[filterstr]]`

```sql
SELECT * FROM table_name WHERE 1=1 [[filterstr]]
```

##### C. 有一般码表（非外键）
构建多个 LEFT JOIN 子查询，别名为 `b`, `c`, ...，拼接成完整查询。

```sql
SELECT a.*, b.type_text, c.status_text
FROM (SELECT * FROM main WHERE 1=1 [[filterstr]]) a
LEFT JOIN (SELECT value AS status, text AS status_text FROM status_codes WHERE 1=1) c
  ON a.status = c.status;
```

---

### 其他辅助函数

| 函数 | 功能 |
|------|------|
| `alter_field(field, desc)` | 使用 `desc.browserfields.alters[name]` 修改特定字段属性 |
| `filter_backslash(s)` | 替换字符串中的 `\\/` 为 `/`，防止模板路径错误 |
| `build_data_browser(pat, desc)` | 渲染 `data_browser_tmpl` → `index.ui` |
| `build_data_new` / `update` / `delete` | 分别生成增删改页面 |
| `build_get_data` | 生成数据查询接口 `.dspy` 文件 |
| `build_check_changed` | 生成用于检测外键关联变化的特殊接口 |

---

## 5. 模板系统与输出结构

### 模板来源

所有模板来自 `tmpls` 模块，包括：

| 模板变量 | 用途 |
|--------|------|
| `data_browser_tmpl` | 主列表界面（index.ui） |
| `get_data_tmpl` | 数据获取接口 |
| `data_new_tmpl` | 新增页面 |
| `data_update_tmpl` | 编辑页面 |
| `data_delete_tmpl` | 删除确认/执行页面 |
| `check_changed_tmpls` | 外键关联状态检查接口 |

### 模板语法支持

- `{{ var }}`：变量插入
- `${ var }$`：环境变量占位符（由 `ArgsConvert` 替换）
- `[[ filterstr ]]`：SQL 过滤条件注入点

---

## 6. 配置文件格式 (`json_file`)

```json
{
  "tblname": "user",
  "alias": "user_manage",
  "params": {
    "title": "用户管理",
    "pagesize": 20,
    "confidential_fields": ["password"],
    "relation": {
      "param_field": "org_id",
      "outter_field": "id",
      "table": "organization"
    },
    "codes": [
      {
        "field": "status",
        "table": "sys_codes",
        "valuefield": "c_value",
        "textfield": "c_text",
        "cond": "ctype='USER_STATUS'"
      }
    ],
    "subtables": [
      {
        "subtable": "user_role",
        "title": "角色分配",
        "field": "user_id",
        "url": "../user_role",
        "mapping": {}
      }
    ],
    "toolbar": null,
    "binds": [],
    "browserfields": {
      "alters": {
        "name": { "label": "姓名", "uitype": "str", "cwidth": 20 }
      }
    }
  },
  "models_dir": "./models"
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `tblname` | string | 主表名（必须存在于 `.xlsx` 中） |
| `alias` | string | 输出目录名（可选，默认为 `tblname`） |
| `params` | object | 表级配置对象 |
| `params.title` | string | 页面标题 |
| `params.confidential_fields` | array | 需隐藏显示的字段（变为密码框） |
| `params.relation` | object | 外键关联信息（启用 check_changed） |
| `params.codes` | array | 码表配置 |
| `params.subtables` | array | 子表配置（生成工具栏按钮） |
| `params.browserfields.alters` | object | 字段显示定制 |
| `models_dir` | string | .xlsx 模型目录（优先级高于命令行） |

---

## 7. 输出目录结构示例

假设命令为：

```bash
python xls2crud.py -o ./dist -m ./models user_mod user_conf.json
```

且 `user_conf.json` 中 `"alias": "users"`，`"tblname": "user"`

则输出结构如下：

```
./dist/
└── users/
    ├── index.ui                         # 数据浏览器
    ├── add_user.dspy                    # 新增页面
    ├── update_user.dspy                 # 编辑页面
    ├── delete_user.dspy                 # 删除页面
    ├── get_user.dspy                    # 数据获取接口
    └── check_changed.dspy               # （如果有 relation）状态检查接口
```

---

## 8. 注意事项与限制

### ⚠️ 已知限制

1. **Excel 文件要求**
   - 必须包含 `summary` 工作表
   - 第一行 `summary[0].name` 作为表名
   - `fields` 表格需定义字段元信息（name, type, title, length...）

2. **模板路径问题**
   - 模板内使用 `{{entire_url(...)}}` 构造绝对路径，请确保前端框架支持此函数

3. **SQL 注入防护**
   - `[[filterstr]]` 不做转义，应由调用端保证安全

4. **并发写入风险**
   - 多个 JSON 文件同时处理同一张表可能导致文件覆盖

5. **编码要求**
   - 所有 `.xlsx` 和 `.json` 文件必须为 UTF-8 编码

### ✅ 最佳实践建议

- 使用 `alias` 区分不同视图的输出路径
- 在 `alters` 中统一设置中文标签和列宽
- 对敏感字段显式加入 `confidential_fields`
- 使用 `cond` + 变量实现动态码表过滤

---

## 版本信息

- 创建时间：未知
- 最后更新：根据代码注释推测近期维护
- 维护者：内部团队（`appPublic`, `xslxDATA` 为私有库）

---

> 📝 文档版本：v1.0  
> ✅ 审核状态：已完成初稿  
> 💬 如需扩展模板字段或增加校验逻辑，请联系开发组。