Files
nl-admin-api/app/Service/core/CodeGenerationService.php
李琦 a1abba3835
Some checks failed
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
表前缀
2026-01-23 17:13:23 +08:00

710 lines
23 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Service\core;
use App\Models\CodeGenerationModel;
use App\Service\common\UtilsService;
use App\Service\MenuService;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class CodeGenerationService
{
const TEXT_TYPE = [
'LONGTEXT',
'TEXT',
'longtext',
'text',
];
const INT_TYPE = [
'INT',
'TINYINT',
'BIGINT',
'SMALLINT',
'FLOAT',
'DECIMAL',
'int',
'tinyint',
'bigint',
'smallint',
'float',
'decimal',
];
private static mixed $_instance;
private ?UtilsService $utils;
private string $uniqid = '';
private string $tempDir = '';
// 大驼峰
private string $upperCamelCase = '';
// 小驼峰
private string $lowerCamelCase = '';
// 下划线
private string $underLineCase = '';
private string $classComment = '';
// - 连接符
private string $dashCase = '';
/**
* @var string 建表sql语句
*/
private string $sql = '';
private array $param = [];
private string $selectField = '"id"';
private string $insertField = '';
private string $updateField = '';
private string $viewForm = '';
private string $viewSearch = '';
private string $searchField = '';
private string $viewData = '';
private array $tmpFile = [];
public function __construct()
{
$this->utils = UtilsService::getInstance();
}
/**
* 获取代码生成记录列表
* @return array
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function list(): array
{
// ds(json_encode());
$page = request()->get('page', 1);
$pageSize = request()->get('pageSize', 20);
$title = request()->get('title', '');
$query = CodeGenerationModel::where('deleted_at', 0);
// 如果有标题搜索条件
if (!empty($title)) {
$query->where('title', 'like', '%' . $title . '%');
}
$result = $query->orderBy('id', 'desc')
->paginate($pageSize, ['id', 'title', 'file_name', 'params', 'created_at', 'updated_at'], 'page', $page)
->toArray();
foreach ($result['data'] as &$item) {
$item['params'] = json_decode($item['params'], true);
}
// /www/sites/nl-admin-api/index/nl-admin-api/storage/app/public/zip/code-generation/企业信息-69290eaaac899.zip
// www\sites\nl-admin-api\index\nl-admin-api\storage\app\public\zip\code-generation\企业信息-69290eaaac899.zip
return [
'page' => $result['current_page'],
'size' => $result['per_page'],
'page_count' => $result['last_page'],
'total' => $result['total'],
'items' => $result['data'],
];
}
/**
* 获取实例
* @return null|static
*/
public static function getInstance(): null|static
{
$name = get_called_class();
if (!isset(self::$_instance[$name])) {
self::$_instance[$name] = new static();
}
return self::$_instance[$name];
}
/**
* 下载文件
* @param $id
* @return BinaryFileResponse
* @throws Exception
*/
public function download($id)
{
$model = CodeGenerationModel::find($id);
if (!$model) {
$this->utils->errorThrow('文件不存在');
}
// 返回下载 $path 文件流
return response()->download($model['path'], $model['file_name'], ['Content-Type' => 'application/zip']);
}
/**
* 下载文件信息
* @param $id
* @return mixed
* @throws Exception
*/
public function downloadInfo($id)
{
$model = CodeGenerationModel::find($id);
if (!$model) {
$this->utils->errorThrow('文件不存在');
}
// 返回下载 $path 文件流
return $model;
}
/**
* 创建临时文件夹,复制模板文件,压缩并文件信息
*/
public function downloadUrl()
{
try {
// 将后端模板文件内容写入临时目录中
$tempFilePath = "{$this->tempDir}/api/{$this->upperCamelCase}Controller.php";
Storage::put($tempFilePath, $this->tmpFile['server']['controller']);
$tempFilePath = "{$this->tempDir}/api/{$this->upperCamelCase}Service.php";
Storage::put($tempFilePath, $this->tmpFile['server']['service']);
$tempFilePath = "{$this->tempDir}/api/{$this->upperCamelCase}Model.php";
Storage::put($tempFilePath, $this->tmpFile['server']['model']);
// 创建ZipService实例
$zip = new ZipService();
// 生成压缩文件的路径
$zipFile = storage_path("/app/public/zip/code-generation/{$this->classComment}-{$this->uniqid}.zip");
// 被压缩文件夹的路径
$path = Storage::path($this->tempDir);
// 执行文件压缩
$zip->zip($zipFile, $path);
$insertModel = CodeGenerationModel::create([
'title' => $this->classComment,
'file_name' => "{$this->classComment}-{$this->uniqid}.zip",
'path' => $zipFile,
'params' => json_encode($this->param),
'created_at' => time(),
'updated_at' => time(),
]);
// 删除Store/app/www
Storage::deleteDirectory('www');
return [
'id' => $insertModel->id,
'file_name' => $insertModel->file_name
];
} catch (\Exception $e) {
return response()->json([
'error' => '生成模板文件失败',
'message' => $e->getMessage()
], 500);
}
}
/**
* 初始化参数
* @return void
*/
private function init()
{
// 生成uuid
$this->uniqid = uniqid();
// 处理类名、表名
$this->upperCamelCase = ucfirst($this->param['class_name']);
$this->lowerCamelCase = lcfirst($this->param['class_name']);
$this->underLineCase = strtolower(preg_replace('/([A-Z])/', '_$1', $this->param['class_name']));
$this->dashCase = str_replace('_', '-', $this->underLineCase);
$this->classComment = $this->param['class_comment'];
// 生成文件
// 检查/app/public/zip/code-generation/目录是否存在,如果不存在则创建
if (!Storage::exists('zip/code-generation')) {
Storage::makeDirectory('zip/code-generation');
}
// 生成唯一的临时目录名
$this->tempDir = 'temp/templates/' . $this->upperCamelCase . '-' . $this->uniqid;
Storage::makeDirectory($this->tempDir);
}
/**
* 代码生成主逻辑
*
* @param $params
* @return mixed
* @throws Exception
*/
public function generate($params): mixed
{
$this->param = $params;
DB::beginTransaction();
try {
// 初始化信息
$this->init();
// 生成数据库
$this->genDatabase();
// 生成控制器
$this->genController();
// 生成服务层
$this->genService();
// 生成模型层
$this->genModel();
// 生成路由
$this->genRoute();
// 执行sql
$this->querySql();
// 生成视图
$this->genViews();
// 生成菜单
$this->genMenu();
DB::commit();
} catch (Exception $e) {
DB::rollBack();
// ds([
// 'error' => '生成代码失败',
// 'message' => $e->getMessage(),
// 'line' => $e->getLine(),
// 'file' => $e->getFile(),
// ]);
$this->utils->errorThrow($e->getMessage());
}
return $this->downloadUrl();
}
/**
* 生成 数据库表
*
* @return true
* @throws Exception
*/
public function genDatabase(): bool
{
$tableName = config('database.prefix', 'nl_') . $this->underLineCase;
// 默认sql语句
$sql = "CREATE TABLE `{$tableName}` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', ";
$sqlTime =
" `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0正常 1禁用',"
. " `created_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间',"
. "`updated_at` int(11) NOT NULL DEFAULT '0' COMMENT '修改时间',"
. "`deleted_at` int(11) NOT NULL DEFAULT '0' COMMENT '删除时间',";
$sqlEnd = "PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='{$this->classComment}表';";
foreach ($this->param['field'] as $v) {
// 处理所需的字段
$this->checkSelectField($v);
// 处理备注
if (empty($v['comment'])) $v['comment'] = "''";
// 处理是否为空
$v['not_null'] = !$v['not_null'] ? '' : 'NOT';
// 处理默认值
if (array_key_exists('default', $v) && isset($v['default'])) {
$v['default'] = 'DEFAULT ' . "'{$v['default']}'";
} else {
$v['default'] = (strpos($v['type'], 'INT') || in_array($v['type'], self::INT_TYPE)) ? 'DEFAULT ' . 0 : 'DEFAULT ' . '\'\'';
}
if (in_array($v['type'], self::TEXT_TYPE)) {
// 编写sql语句
$sql .= " {$v['name']} {$v['type']} {$v['not_null']} NULL COMMENT '{$v['comment']}', ";
} else {
// 编写sql语句
$sql .= " {$v['name']} {$v['type']}({$v['type_length']}) {$v['default']} {$v['not_null']} NULL COMMENT '{$v['comment']}', ";
}
}
$this->sql = $sql . $sqlTime . $sqlEnd;
return true;
}
/**
* 修改api配置文件
*
* @return void
* @throws Exception
*/
private function genRoute(): void
{
$apiPath = app_path('../routes/api.php');
$apiFile = file_get_contents($apiPath, 'y');
$newApi =
"'{$this->dashCase}' => \App\Http\Controllers\Api\CodeGeneration\\{$this->upperCamelCase}Controller::class, // {$this->classComment} \r\n // 需要登录的路由生成地址";
$updateNewFile = str_replace('// 需要登录的路由生成地址', $newApi, $apiFile);
$updateFile = file_put_contents($apiPath, $updateNewFile);
if (!$updateFile) $this->utils->errorThrow('写入api文件出现了错误');
}
/**
* 生成控制器文件
* @return void
*/
private function genController(): void
{
$this->strReplaceTmp(
app_path('template/api/TemplateController.php'),
app_path("Http/Controllers/Api/CodeGeneration/{$this->upperCamelCase}Controller.php"),
'server',
'controller'
);
}
/**
* 生成服务层文件
* @return void
*/
private function genService(): void
{
$this->strReplaceTmp(
app_path('template/api/TemplateService.php'),
app_path("Service/CodeGeneration/{$this->upperCamelCase}Service.php"),
'server',
'service'
);
}
/**
* 生成模型层文件
* @return void
*/
private function genModel(): void
{
$this->strReplaceTmp(
app_path('template/api/TemplateModel.php'),
app_path("Models/{$this->upperCamelCase}Model.php"),
'server',
'model'
);
}
/**
* 生成前端部分文件
* @return void
*/
private function genViews(): void
{
$appPath = app_path();
$viewList = [
'view-api' => 'api/index.ts',
'view-modal' => 'components/modal.vue',
'view-form' => 'config/form.ts',
'view-search' => 'config/search.ts',
'view-table' => 'config/table.ts',
'view-index' => 'index.vue',
];
foreach ($viewList as $k => $v) {
$this->strReplaceTmp(
$appPath. '/template/view/' . $v,
$this->tempDir. "/view/{$this->dashCase}/" . $v,
'view',
$k
);
}
}
/**
* 生成菜单/授权
*
* @return void
* @throws Exception
*/
private function genMenu(): void
{
MenuService::getInstance()->create([
'title' => $this->classComment,
'icon' => $this->param['icon']?? '',
'name' => $this->upperCamelCase,
'path' => "/{$this->dashCase}",
'component' => "/my-gen/{$this->dashCase}/index",
'pid' => $this->param['pid'],
'sort' => $this->param['sort'],
]);
}
/**
* 模板内容替换
*
* @param $template
* @param $path
* @param $type
* @param $fileKey
* @return void
*/
private function strReplaceTmp($template, $path, $type, $fileKey): void
{
$tmpFile = file_get_contents($template);
$newFile = str_replace('upperCamelCase', $this->upperCamelCase, $tmpFile);
$newFile = str_replace('lowerCamelCase', $this->lowerCamelCase, $newFile);
$newFile = str_replace('dashCase', $this->dashCase, $newFile);
$newFile = str_replace('模板名称', $this->classComment, $newFile);
switch ($fileKey) {
case 'service':
$newFile = str_replace("'tmpSelectField'", $this->selectField . ', "status", "created_at", "updated_at", "deleted_at"', $newFile);
$newFile = str_replace("tmpSearchField", $this->searchField . ',status=', $newFile);
break;
case 'controller':
$newFile = str_replace("'temInsertField'", '"' . $this->insertField . '"', $newFile);
$newFile = str_replace("'temUpdateField'", '"' . $this->updateField . '"', $newFile);
break;
// case 'export':
// $newFile = str_replace("'tmpExportField'", $this->exportField, $newFile);
// $newFile = str_replace('$row->tmpMapField,', $this->exportFieldRow, $newFile);
// break;
// case 'import':
// $newFile = str_replace("'tmpField' => \$row['tmpField'],", $this->importField, $newFile);
// break;
case 'model':
$newFile = str_replace('underLineCase', cc_camel_case_to_underscore($this->underLineCase), $newFile);
break;
case 'view-form':
$newFile = str_replace("// 生成的表单字段", "// 生成的表单字段 \r ". $this->viewForm, $newFile);
break;
case 'view-search':
$newFile = str_replace("// 生成的搜索字段", "// 生成的搜索字段 \r ". $this->viewSearch, $newFile);
break;
case 'view-table':
$newFile = str_replace("// 生成的列表字段", "// 生成的列表字段 \r ". $this->viewData, $newFile);
break;
}
$this->tmpFile[$type][$fileKey] = $newFile;
if (!file_exists($path) && in_array($fileKey, ['controller', 'service', 'model'])) {
$this->utils->createFile($path);
file_put_contents($path, $newFile);
} else {
Storage::put($path, $newFile);
}
}
/**
* 设置默认字段
*
* @param $item
* @return void
*/
private function checkSelectField($item): void
{
if (empty($item)) return;
// 设置列表字段
if (!isset($item['tableShow'])) {
ds($item);
}
if ($item['tableShow']) {
$this->selectField .= ', "' . $item['name'] . '"';
}
// 设置表单字段
if ($item['formShow']) {
$isNull = $item['not_null'] == 1 ? "'required'" : "''";
$this->insertField .= !empty($this->insertField) ? '","' . $item['name'] : $item['name'];
$this->updateField .= !empty($this->updateField) ? '","' . $item['name'] : 'id","' . $item['name'];
$this->viewForm .= "{\n fieldName: '" . $item['name'] . "',\n label: '" . $item['comment'] . "',\n component: '" . $item['formType'] . "',\n rules: " . $isNull . ",\n },\n";
}
// 设置搜索字段
if ($item['search']) {
$this->viewSearch .= "{\n fieldName: '" . $item['name'] . "',\n label: '" . $item['comment'] . "',\n component: '" . $item['formType'] . "',\n },\n ";
$this->searchField .= !empty($this->searchField) ? "','" . $item['name'] . '\' => \'' . $item['searchValue'] : $item['name'] . '\' => \'' . $item['searchValue'];
}
// // 设置导出字段
// $this->exportField .= !empty($this->exportField) ? ',"' . $item['comment'] . '"' : '"' . $item['comment'] . '"';
// $this->exportFieldRow .= '$row->' . $item['name'] . ",\n ";
// // 设置导入字段
// $this->importField .= "'" . $item['name'] . "' => \$row['" . $item['comment'] . "'],\n ";
// 设置视图数据字段
$this->viewData .= "{ field: '{$item['name']}', title: '{$item['comment']}' },\n ";
}
/**
* 执行sql
*
* @return void
* @throws Exception
*/
private function querySql(): void
{
try {
DB::statement($this->sql);
} catch (Exception $e) {
$this->utils->errorThrow($e->getMessage());
}
}
/**
* 获取数据库表列表
* @return array
*/
public function getTableList(): array
{
$database = config('database.connections.mysql.database');
$prefix = config('database.prefix', 'nl_');
$tables = DB::select("
SELECT
TABLE_NAME as `name`,
TABLE_COMMENT as `comment`,
TABLE_ROWS as `rows`,
CREATE_TIME as create_time
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME LIKE ?
ORDER BY TABLE_NAME
", [$database, $prefix . '%']);
$result = [];
foreach ($tables as $table) {
$result[] = [
'name' => $table->name,
'comment' => $table->comment ?: $table->name,
'rows' => $table->rows,
'create_time' => $table->create_time,
];
}
return $result;
}
/**
* 获取表字段信息
* @param string $tableName
* @return array
* @throws Exception
*/
public function getTableColumns(string $tableName): array
{
$database = config('database.connections.mysql.database');
$columns = DB::select("
SELECT
COLUMN_NAME as `name`,
DATA_TYPE as `type`,
CHARACTER_MAXIMUM_LENGTH as `length`,
COLUMN_DEFAULT as default_value,
COLUMN_COMMENT as `comment`,
IS_NULLABLE as nullable,
COLUMN_KEY as key_type,
EXTRA as extra
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
", [$database, $tableName]);
if (empty($columns)) {
$this->utils->errorThrow('表不存在或没有字段');
}
$result = [];
foreach ($columns as $column) {
// 跳过id字段代码生成会自动添加
if ($column->name === 'id') {
continue;
}
// 跳过系统字段
if (in_array($column->name, ['created_at', 'updated_at', 'deleted_at', 'status'])) {
continue;
}
// 转换数据类型
$type = strtoupper($column->type);
$typeLength = '';
if (in_array($type, ['VARCHAR', 'CHAR'])) {
$typeLength = $column->length ?: '255';
} elseif (in_array($type, ['INT', 'TINYINT', 'BIGINT', 'SMALLINT'])) {
$typeLength = $column->length ?: '11';
} elseif (in_array($type, ['DECIMAL', 'FLOAT', 'DOUBLE'])) {
$typeLength = $column->length ?: '10,2';
}
// 判断表单组件类型
$formType = 'VbenInput';
if (strpos($type, 'TEXT') !== false) {
$formType = 'VbenTextarea';
} elseif (in_array($type, ['TINYINT', 'INT']) && $column->length == 1) {
$formType = 'VbenSwitch';
} elseif (strpos($column->name, 'time') !== false || strpos($column->name, 'date') !== false) {
$formType = 'VbenDatePicker';
}
$result[] = [
'name' => $column->name,
'type' => $type,
'type_length' => $typeLength,
'default' => $column->default_value ?? '',
'comment' => $column->comment ?: $column->name,
'not_null' => $column->nullable === 'NO' ? 1 : 0,
'formShow' => 1,
'tableShow' => 1,
'formType' => $formType,
'search' => in_array($type, ['VARCHAR', 'CHAR', 'INT', 'TINYINT', 'BIGINT']) ? 1 : 0,
'searchValue' => in_array($type, ['VARCHAR', 'CHAR']) ? 'like' : '=',
];
}
return $result;
}
/**
* 将数据库表结构转换为代码生成数据格式
* @param string $tableName
* @param string $className
* @param string $classComment
* @param string $icon
* @param int $sort
* @param int $pid
* @return array
* @throws Exception
*/
public function convertTableToCodeGenData(
string $tableName,
string $className = '',
string $classComment = '',
string $icon = '',
int $sort = 9999,
int $pid = 0
): array {
// 如果没有提供类名,从表名生成
if (empty($className)) {
$prefix = config('database.prefix', 'nl_');
$tableNameWithoutPrefix = str_replace($prefix, '', $tableName);
// 下划线转驼峰
$className = str_replace('_', '', ucwords($tableNameWithoutPrefix, '_'));
}
// 如果没有提供中文名称,使用表注释或表名
if (empty($classComment)) {
$database = config('database.connections.mysql.database');
$tableInfo = DB::selectOne("
SELECT TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
", [$database, $tableName]);
$classComment = $tableInfo->TABLE_COMMENT ?: $tableName;
}
// 获取字段信息
$fields = $this->getTableColumns($tableName);
return [
'class_name' => $className,
'class_comment' => $classComment,
'icon' => $icon,
'sort' => $sort,
'pid' => $pid,
'field' => $fields,
];
}
}