Apache 2.0 License // +---------------------------------------------------------------------- namespace app\admin\controller\developer; use app\admin\model\developer\Generate; use app\AdminController; use app\common\model\system\AdminRules; use system\Form; use think\db\exception\DataNotFoundException; use think\db\exception\DbException; use think\db\exception\ModelNotFoundException; use think\facade\Db; use think\helper\Str; /** * 一键CURD管理 * * Class Curd * @package app\admin\controller\developer */ class Curd extends AdminController { /** * 数据表前缀 * * @var mixed */ public mixed $prefix = 'sa_'; /** * 获取菜单 * * @var array */ public array $menus = []; /** * 关联表信息 * * @var array */ public array $relation = []; /** * 函数体 * * @var array */ public array $methods = []; /** * 模板路径 * * @var string */ public string $templatePath = ''; /** * 模板文件 * * @var array */ public array $templateFiles = []; /** * 添加时间字段 * @var string */ protected string $createTimeField = 'create_time'; /** * 更新时间字段 * @var string */ protected string $updateTimeField = 'update_time'; /** * 软删除时间字段 * @var string */ protected string $deleteTimeField = 'delete_time'; /** * 过滤默认模板 * * @var array */ public array $filterMethod = ['index', 'add', 'edit', 'del', 'status']; /** * 限定特定组件 * * @var array */ public array $mustbeComponent = ['set', 'text', 'json']; /** * 修改器字段 * * @var array */ public array $modifyFieldAttr = ['set', 'text', 'json']; /** * 保留字段 * * @var string */ public string $keepField = 'status'; /** * 查询字段[SELECT] * * @var array */ public array $dropdown = ['radio', 'checkbox', 'select']; /** * COLS换行符 * * @var string */ public string $commaEol = ',' . PHP_EOL; /** * 受保护的表 * 禁止CURD操作 * @var array */ protected array $protectTable = [ "admin", "admin_access", "admin_group", "admin_rules", "company", "department", "dictionary", "generate", "jobs", "user", "user_group", "user_third", "user_validate" ]; /** * 类保留关键字 * * @var array */ protected array $internalKeywords = [ 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield', 'readonly', 'match', 'fn' ]; // 初始化操作 public function __construct() { parent::__construct(); $this->model = new Generate(); $this->prefix = function_exists('get_env') ? get_env('DATABASE_PREFIX') : getenv('DATABASE_PREFIX'); } /** * 生成CURD代码 * @return \support\Response * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ public function build(): \support\Response { $id = input('id'); $data = $this->model->find($id); if ($data['status'] && !$data['force']) { return $this->error('该表已经生成过了'); } $table = str_replace($this->prefix, '', $data['table']); if ($this->filterSystemTable($table)) { return $this->error('禁止操作系统表'); } // 命名空间 $replaces = []; $controller = $data['controller']; $module = $data['global'] ? 'common' : 'admin'; $element = ['controller', 'model', 'validate']; try { foreach ($element as $key => $item) { $result = $this->parseNameData($item == 'controller' ? 'admin' : $module, $controller, $key ? $table : '', $item); list($replaces[$item . 'Name'], $replaces[$item . 'Namespace'], $replaces[$item . 'File']) = $result; } $this->getTemplatePath($controller); list($this->menus, $this->methods, $this->templateFiles) = $this->getMenuMethods($data->toArray()); // 获取字段 $adviceField = []; $adviceSearch = []; $everySearch = []; // 字段属性值 $colsFields = []; $fieldAttrArr = []; // 表单设计 $formDesign = []; $formItem = []; $formType = $data['formType']; if (!empty($data['formDesign'])) { $formDesign = json_decode($data['formDesign'], true); } $this->tableFields = Db::name($table)->getFields(); $listFields = explode(',', $data['listField']); foreach ($this->tableFields as $key => $value) { $field = $value['name']; $comment = str_replace(':', ';', $value['comment']); if (empty($comment)) { return $this->error($field . " 字段注释不能为空"); } $this->tableFields[$key]['title'] = explode(';', $comment)[0]; // 是否存在状态字段 if ($field == $this->keepField) { $adviceSearch[] = $field; } // 获取字段类型 $everySearch[] = $field; $type = explode('(', $value['type'])[0]; // 限定组件类型 if (in_array($type, $this->mustbeComponent)) { $this->validComponent($field, $type, $formDesign); } if (in_array($type, $this->modifyFieldAttr)) { $fieldAttrArr[] = $this->getFieldAttrArr($field, $type); } if (empty($adviceField) || ($adviceField['type'] != 'varchar' && $type == 'varchar')) { $adviceField = [ 'field' => $field, 'type' => $type, ]; } if (in_array($field, $listFields)) { $colsFields[] = [ 'field' => $field, 'title' => '{:__("' . $this->tableFields[$key]['title'] . '")}', ]; } } // 推荐搜索片段 $adviceSearch[] = $adviceField['field']; $adviceSearchHtml = $this->getAdviceSearch($adviceSearch, $formDesign); // 获取全部搜索字段 $everySearch = array_diff($everySearch, $adviceSearch); $everySearchHtml = $this->getAdviceSearch($everySearch, $formDesign); $controller = substr($controller, 0, (strrpos($controller, '/') + 1)); $colsListArr = $this->getColsListFields($colsFields, $formDesign); $replaces['table'] = $table; $replaces['title'] = $data['title']; $replaces['pluginClass'] = $data['plugin']; $replaces['controller'] = $controller; $replaces['controllerDiy'] = $this->getMethodString($this->methods); $replaces['colsListArr'] = $colsListArr; $replaces['fieldAttrArr'] = implode(PHP_EOL . PHP_EOL, $fieldAttrArr); $replaces['adviceSearchHtml'] = $adviceSearchHtml; $replaces['everySearchHtml'] = $everySearchHtml; $replaces['relationMethodList'] = $this->getRelationMethodList($data['relation']); $replaces['FormArea'] = $data['width'] . ',' . $data['height']; $replaces['softDelete'] = array_key_exists($this->deleteTimeField, $this->tableFields) ? "use SoftDelete;" : ''; $replaces['softDeleteClassPath'] = array_key_exists($this->deleteTimeField, $this->tableFields) ? "use think\model\concern\SoftDelete;" : ''; $replaces['createTime'] = array_key_exists($this->createTimeField, $this->tableFields) ? "'$this->createTimeField'" : 'false'; $replaces['updateTime'] = array_key_exists($this->updateTimeField, $this->tableFields) ? "'$this->updateTimeField'" : 'false'; $replaces['deleteTime'] = array_key_exists($this->deleteTimeField, $this->tableFields) ? "'$this->deleteTimeField'" : 'false'; // 生成控制器/模型/验证器规则 foreach ($element as $index => $item) { if ($index == 0 && (!$data['create'] || !$data['listField'])) { continue; } $code = read_file($this->getStubTpl($item)); foreach ($replaces as $key => $value) { $code = str_replace("{%$key%}", $value, $code); } write_file($replaces[$item . 'File'], $code); } // 生成表单元素 $template = $formType ? 'add' : 'inside'; $formHtml = read_file($this->getStubTpl($template)); if (!empty($formDesign) && $data['listField']) { foreach ($formDesign as $key => $value) { $formItem[$key] = Form::itemElem($value, $formType); } $formItem = implode(PHP_EOL, $formItem); $formHtml = str_replace(['{formItems}', '{pluginClass}'], [$formItem, $replaces['pluginClass']], $formHtml); $formType && write_file($this->templatePath . 'add.html', $formHtml); } // 生成首页模板 $indexHtml = read_file($this->getStubTpl($formType ? 'index' : 'index_inside')); if (!empty($data['listField'])) { $replaces['editforms'] = $formType ? '' : $formHtml; foreach ($replaces as $key => $value) { $indexHtml = str_replace("{%$key%}", $value, $indexHtml); } if (empty($formDesign)) { $indexHtml = preg_replace('/(.*)/isU', '', $indexHtml); } write_file($this->templatePath . 'index.html', str_replace('{pluginClass}', $replaces['pluginClass'], $indexHtml)); } // 生成扩展模板 $extendHtml = read_file($this->getStubTpl('extend')); $extendHtml = str_replace('{pluginClass}', $replaces['pluginClass'], $extendHtml); foreach ($this->methods as $method) { write_file($this->templatePath . Str::snake($method) . '.html', $extendHtml); } // 生成CURD菜单 if ($data['create'] && !empty($data['listField'])) { AdminRules::createMenu([$this->menus], $replaces['pluginClass'] ?: $table, $data['pid']); } // 更新生成状态 $data->save(['status' => 1]); } catch (\Throwable $e) { return $this->error($e->getMessage()); } return $this->success('生成成功'); } /** * 清理内容 * @return mixed|void * @throws DataNotFoundException * @throws DbException * @throws ModelNotFoundException */ public function clear() { $id = input('id'); if (request()->isAjax()) { $data = $this->model->find($id); $table = str_replace($this->prefix, '', $data['table']); $controller = $data['controller']; try { $module = $data['global'] ? 'common' : 'admin'; $element = ['controller', 'model', 'validate']; foreach ($element as $key => $item) { $result = $this->parseNameData($item == 'controller' ? 'admin' : $module, $controller, $key ? $table : '', $item); $file = end($result); if (file_exists($file)) { unlink($file); } // 删除空文件夹 remove_empty_dir(dirname($file)); } list($this->menus, $this->methods, $this->templateFiles) = $this->getMenuMethods($data->toArray()); recursive_delete($this->getTemplatePath($controller)); AdminRules::disabled($table, true); $data->save(['status' => 0]); } catch (\Throwable $th) { return $this->error($th->getMessage()); } return $this->success('删除成功'); } } /** * 获取列表字段 * @param array $colsFields * @param array $formDesign * @return string */ public function getColsListFields(array $colsFields = [], array $formDesign = []): string { $colsListArr = []; foreach ($colsFields as $key => $value) { // 过滤删除字段 $colsLine = []; $colsField = $value['field']; $colsTitle = $value['title']; if ($colsField == $this->deleteTimeField) { continue; } // 获取每一列参数合集 $colsLine[] = "field:'$colsField'"; if ($colsField == $this->keepField) { $colsLine[] = "templet: '#columnStatus'"; } $item = $this->recursiveComponent($colsField, $formDesign); if (!empty($item) && is_array($item)) { $colsArr = ''; $colsTag = $item['tag']; if (in_array($colsTag, $this->dropdown)) { $colsArr = $item['options']; foreach ($colsArr as $index => $elem) { $colsArr[$index]['title'] = "{:__('" . $elem['title'] . "')}"; } $colsArr = json_encode($colsArr, JSON_UNESCAPED_UNICODE); $colsTpl = read_file($this->getStubTpl('list/' . $colsTag)); } else if ($colsTag == 'upload') { $colsTpl = read_file($this->getStubTpl('list/' . $item['uploadtype'])); } else { $colsTpl = read_file($this->getStubTpl('list/' . $colsTag)); } if (!empty($colsTpl)) { $colsLine[] = str_replace(['{colsArr}', '{field}'], [$colsArr, $colsField], $colsTpl); } } $colsLine[] = "title:'$colsTitle'"; $colsListArr[$key] = '{' . implode(',', $colsLine) . '}'; } $colsListArr = implode($this->commaEol, $colsListArr); return $colsListArr ? $colsListArr . ',' : $colsListArr; } /** * 获取修改器 * @param string|null $field * @param string|null $type * @param string $subTpl * @return array|false|string|string[] */ public function getFieldAttrArr(string $field = null, string $type = null, string $subTpl = 'change') { $tplPath = $subTpl . '/' . $type; $methods = read_file($this->getStubTpl($tplPath)); if (!empty($methods)) { $methods = str_replace('{%field%}', ucfirst($field), $methods); } return $methods; } /** * 验证组件 * @param string|null $field * @param string|null $type * @param array $data * @return mixed */ public function validComponent(string $field, string $type, array $data = []) { if (!$field || !$data) { return false; } $result = $this->recursiveComponent($field, $data); if (!empty($result)) { $tag = strtolower($result['tag']); switch ($type) { case 'set': if ($tag != 'checkbox') { return $this->error($field . ' 组件类型限定为checkbox'); } break; case 'json': if ($tag != 'json') { return $this->error($field . ' 组件类型限定为json'); } break; case 'text': // 限定TEXT字段类型必须为多文件上传 if ($tag != 'upload' || $result['uploadtype'] != 'multiple') { return $this->error($field . ' 字段类型为text时,组件类型限定为多文件上传'); } break; default: break; } } return false; } /** * 查找组件 * @param string $field * @param array $data * @return mixed */ public function recursiveComponent(string $field = '', array $data = []) { foreach ($data as $value) { if ($field == $value['name']) { return $value; } if (isset($value['children']) && $value['children']) { $subElem = $value['children']; foreach ($subElem as $child) { $item = $this->recursiveComponent($field, $child['children']); if (!empty($item)) { return $item; } } } } } /** * 搜索模板 * @param array $searchArr * @param array $formArr * @return false|string */ public function getAdviceSearch(array $searchArr = [], array $formArr = []) { if (!$searchArr) { return false; } $varData = ''; $searchHtml = []; foreach ($searchArr as $searchField) { if ($searchField == $this->deleteTimeField) { continue; } if ($searchField == $this->keepField) { $rhtml = read_file($this->getStubTpl('search/status')); } else if (in_array($searchField, [$this->createTimeField, $this->updateTimeField])) { $rhtml = read_file($this->getStubTpl('search/datetime')); } else { $result = $this->recursiveComponent($searchField, $formArr); if ($result && in_array($result['tag'], $this->dropdown)) { $varData = Form::validOptions($result['options']); $rhtml = read_file($this->getStubTpl('search/select')); } else if ($result && in_array($result['tag'], ['slider'])) { $rhtml = read_file($this->getStubTpl('search/slider')); $rhtml = str_replace( ['{default}', '{theme}', '{step}', '{max}', '{min}'], [$result['data_default'], $result['data_theme'], $result['data_step'], $result['data_max'], $result['data_min']], $rhtml ); } else if ($result && $result['tag'] == 'cascader') { $rhtml = read_file($this->getStubTpl('search/cascader')); } else if ($result && $result['tag'] == 'date') { $rhtml = read_file($this->getStubTpl('search/datetime')); } else if ($result && $result['tag'] == 'rate') { $rhtml = read_file($this->getStubTpl('search/rate')); $rhtml = str_replace(['{theme}', '{length}'], [$result['data_theme'], $result['data_length']], $rhtml); } else { $rhtml = read_file($this->getStubTpl('search/input')); } } $replace = [ 'field' => $searchField, 'title' => $this->tableFields[$searchField]['title'], 'varlist' => ucfirst($searchField) . '_list', 'vardata' => $varData, ]; foreach ($replace as $key => $value) { $rhtml = str_replace("{%$key%}", $value, $rhtml); } $searchHtml[] = $rhtml; } return implode(PHP_EOL . PHP_EOL, $searchHtml); } /** * 获取菜单函数 * @param array $data * @return array * @throws \Exception */ protected function getMenuMethods(array $data = []): array { if (empty($data) || !is_array($data)) { throw new \Exception("Error Params Request", 1); } if (!is_array($data['menus'])) { $data['menus'] = unserialize($data['menus']); } $MenuRules = [ 'title' => $data['title'], 'router' => $data['controller'], 'icon' => $data['icon'] ?: '', 'pid' => $data['pid'], 'auth' => $data['auth'], ]; foreach ($data['menus'] as $key => $value) { $MenuRules['children'][$key] = [ 'title' => $value['title'], 'router' => $value['router'], 'auth' => $value['auth'], 'type' => $value['type'], ]; $parse = explode(':', $value['route']); $parse = end($parse); if (!in_array($parse, $this->filterMethod)) { $this->methods[$key] = $parse; $this->templateFiles[$key] = Str::snake($parse); } } return [$MenuRules, $this->methods, $this->templateFiles]; } /** * 获取其他函数 * @param array $methods * @return string */ protected function getMethodString(array $methods = []): string { $outsMethod = PHP_EOL; foreach ($methods as $method) { if (!in_array($method, $this->filterMethod)) { $outsMethod .= str_replace('method', $method, read_file($this->getStubTpl('method'))); } } return $outsMethod; } /** * 获取关联表信息 * id style KEY * @param $relation * @return string * @throws \Exception */ protected function getRelationMethodList($relation): string { $relationString = PHP_EOL; if (!empty($relation) && !is_array($relation)) { $relation = unserialize($relation); foreach ($relation as $value) { if (!$value) { continue; } $table = str_replace($this->prefix, '', $value['table']); $schema = Db::query("SHOW TABLE STATUS LIKE '$table'"); $studly = Str::studly($table); // 直接判断是否存在 // 可提交遍历命名空间PR if (in_array($table,$this->protectTable)) { $studly = '\\app\\common\\model\\system\\' . $studly; } else { $namespace = '\\app\\admin\\model\\'.$studly; if (class_exists($namespace)) { $studly = $namespace; } } // 拼接关联语句 $localKey = $value['localKey']; $foreignKey = $value['foreignKey']; $str_relation = '$this->' . $value['style'] . '(' . $studly . '::Class,' . "'$foreignKey','$localKey')"; $bindField = []; if ($value['relationField']) { $bindField = explode(',', $value['relationField']); $bindField = array_unique(array_filter($bindField)); $str_relation .= '->bind(' . str_replace('"', '\'', json_encode($bindField)) . ')'; } try { $Comment = $schema[0]['Comment'] ?? $value['table']; $table = Str::camel($table); $relationString .= ' /**'; $relationString .= PHP_EOL . ' * 定义 ' . $Comment . ' 关联模型'; $relationString .= PHP_EOL . ' * @localKey ' . $localKey; $relationString .= PHP_EOL . ' * @bind ' . implode(',', $bindField); $relationString .= PHP_EOL . ' */'; $relationString .= PHP_EOL . ' public function ' . $table . '()'; $relationString .= PHP_EOL . ' {'; $relationString .= PHP_EOL . ' return ' . $str_relation . ';'; $relationString .= PHP_EOL . ' }'; $relationString .= PHP_EOL; } catch (\Throwable $th) { throw new \Exception($th->getMessage()); } } } return $relationString; } /** * 获取文件信息 * @param string $module * @param string $name * @param string $table * @param string $type * @return array * @throws \Exception */ protected function parseNameData(string $module, string $name, string $table = '', string $type = 'controller'): array { $array = str_replace(['.', '/', '\\'], '/', $name); $array = array_filter(explode('/', $array)); if (substr($name, 0 - strlen('/')) != '/') { array_pop($array); } $parseName = $type == 'controller' ? ucfirst(end($array)) : Str::studly($table); if (in_array(strtolower($parseName), $this->internalKeywords)) { throw new \Exception('类名称不能使用内置关键字' . $parseName); } array_pop($array); $appNamespace = "app\\{$module}\\$type" . ($array ? "\\" . implode("\\", $array) : ""); $parseFile = root_path() . $appNamespace . DIRECTORY_SEPARATOR . $parseName . '.php'; $parseFile = str_replace('\\', '/', $parseFile); return [$parseName, $appNamespace, $parseFile]; } /** * @param $table * @return bool */ protected function filterSystemTable($table): bool { if (in_array($table, $this->protectTable)) { return true; } return false; } /** * 获取模板文件 * @param [type] $name * @return string */ protected function getStubTpl($name): string { return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . $name . '.stub'; } /** * 获取代码模板 * @param [type] $name * @return string */ protected function getTemplatePath($name): string { $this->templatePath = root_path('app/admin/view'); $array = str_replace(['.', '/', '\\'], '/', $name); $array = array_filter(explode('/', $array)); if (substr($name, 0 - strlen('/')) != '/') { array_pop($array); } foreach ($array as $value) { $value = Str::snake($value); $this->templatePath .= $value . DIRECTORY_SEPARATOR; } return $this->templatePath; } }