Files
ulthon_admin/extend/base/common/command/admin/VersionBase.php
2026-10-02 15:20:00 +00:00

392 lines
16 KiB
PHP
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
declare(strict_types=1);
namespace base\common\command\admin;
use app\common\console\Command;
use app\common\tools\PathTools;
use think\App as ThinkApp;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 版本管理与发布命令.
*
* 发布流程:
* 1. 修改 VERSION 常量为新版本号
* 2. php think admin:version --generate-comment
* → 从 git log 自动生成更新说明,输出到临时目录
* → 复制 commit_comment.php 中的数组内容到 COMMENT 常量
* 2.1 提交本次修改的 VersionBase.php 到本地仓库
* 3. php think admin:version --push-tag
* → 创建 annotated tag 并推送到远程
* 4. [可选] php think admin:version --generate-release [--release-from-tag= vX.X.X]
* → 生成发布说明 Markdown,手动发布到目标平台
*/
class VersionBase extends Command
{
public const VERSION = 'v2.7.0';
public const PRODUCT_VERSION = '';
public const LAYUI_VERSION = 'v2.10.1';
public const COMMENT = [
'版本更新说明:',
'【新功能】',
'- 配色专属标准皮肤,清爽全包观感与面板交互重构',
'- 商品列表默认形态改为商品模板',
'- 新增清爽皮肤(36px 紧凑头部)并设为默认',
'- 名片模板视觉升级,状态开关移至头部',
'- 商品卡模板视觉升级,状态开关移至图面右上',
'- 商品列表新增商品自定义卡片模板三形态示例',
'- 用户管理示例升级三形态并示范按端默认配置',
'- 默认形态支持按端配置,layoutDefault 演进对象式并兼容旧写法',
'- 列表双形态注入双端生效,PC 端默认可切卡片',
'- 受控数据流转为默认,controlled:false 作为兼容逃生口',
'- 用户管理名片卡片官方示例',
'- Vue 模板钩子支持',
'- laytpl 模板钩子支持',
'- 手机端默认卡片与销毁重建式视图切换',
'- 内置卡片形态与本地化样式',
'- 实现五级模板协议与行级渲染上下文',
'- 灰度第五批 app 层五页切换受控模式',
'- 灰度第四批 mall 三页切换受控模式',
'- 灰度第三批三页切换并收编业务直呼',
'- 灰度第二批三页切换受控模式',
'- 灰度第一批四页切换受控模式',
'- 支持 controlled 受控模式(灰度开关)',
'- 装载 reloadData shim 与 checkStatus 分发',
'- 新增受控数据层 store(dataProvider/pager/selection/reload)',
'- 移除寄生多模板实现,保留搜索与核心封装',
'【修复】',
'- 清爽皮肤内容容器padding归零',
'- 清爽皮肤弹框头部清新化与logo图片高度调整',
'- 清爽侧栏收窄至160并缩进三收紧(8/16/24)',
'- 清爽侧栏收窄至170并缩进二收紧(10/20/30)',
'- 头部与收起态细节修正(控制按钮收窄居中、收起图标居中、logo 高度贴线)',
'- 清爽侧栏图标与菜单标题间距收紧',
'- 清爽侧栏细节修正(导航树宽度对齐180、内边距收紧、缩进层级10/25/40)',
'- 清爽侧栏宽度再收窄至 180px',
'- 清爽侧栏减宽 190px 与三级菜单名完整显示',
'- 清爽皮肤侧栏菜单行高紧凑化(40px→32px)',
'- 修复刷新后 iframe 内容页皮肤类不生效(onInitElemStyle 早于 body 解析执行)',
'- 清爽皮肤按钮配色适配',
'- 配色面板矮视口显示滚动条,修复元素风格无法选择',
'- 清爽皮肤文字色交还配色系统,修复深色配色对比度',
'- 自定义操作按钮配置对象渲染中被就地污染,切换形态后退化为文本',
'- 手机端未配置页面自动启用内置卡片形态',
'- 失败通道摘除模板容器加载态并明示 where 白名单契约',
'【文档】',
'- 修正队列设计技能审核问题(phpredis Stream API 用法、redis store 环境前提、示例缺陷)',
'- 拆分表格与卡片双技能并同步最新代码',
'- 修正定时器技能数据消费竞态示例,补充分片索引失效警示',
'- 新增队列设计技能(DB 原子抢占/区间游标/Redis Stream 生产者消费者三模式)',
'- 新增自定义卡片模板定制指南技能',
'- 清空项目信息节留给下游,runtime 权限提示迁至 stack 文档',
'- 补充清爽皮肤说明与预览图',
'- 多形态章节改写为默认数据流与逃生口语义',
'- 修正对已退役 AdminInitServiceBase 与 AGENTS.md 旧章节名的失效引用',
'- 更新表格技能文档覆盖受控模式与模板协议',
'【样式】',
'- 清爽皮肤侧栏菜单项行高收紧为 32px',
'【重构】',
'- 清理十八页冗余的受控显式声明',
'- 跨窗口刷新显式走 ua.table.reloadData 分发入口',
'【测试】',
'- 模板形态行为集成验证与行选中委托统一',
'【其他】',
'- 删除未定义引用的死函数 refreshTable/reloadTable',
'- 删除第一代 uaTable 原型死代码',
];
protected const COMMIT_TYPES = [
'feat' => '新功能',
'fix' => '修复',
'docs' => '文档',
'style' => '样式',
'refactor' => '重构',
'perf' => '性能优化',
'test' => '测试',
'build' => '构建',
'ci' => '持续集成',
'chore' => '其他',
'revert' => '回退',
];
protected function configure()
{
parent::configure();
// 指令配置
$this->setName('admin:version')
->addOption('generate-comment', null, Option::VALUE_NONE, '使用git命令生成说明文件')
->addOption('generate-release', null, Option::VALUE_NONE, '使用git命令生成发布说明文件')
->addOption('release-from-tag', null, Option::VALUE_OPTIONAL, '从指定版本开始生成说明文件')
->addOption('push-tag', null, Option::VALUE_NONE, '使用git命令生成tag并推送')
->setDescription('查看当前版本号。可选 --generate-comment 生成更新说明、--push-tag 打tag推送、--generate-release 生成发布说明');
}
protected function execute(Input $input, Output $output)
{
// 文本模式输出
// 指令输出
if (!empty(static::PRODUCT_VERSION)) {
$output->info('当前版本号为:' . static::PRODUCT_VERSION);
}
$output->info('当前ulthon_admin版本号为:' . static::VERSION);
$output->info('当前Layui版本号为:' . static::LAYUI_VERSION);
$output->info('当前ThinkPHP版本号为:' . app()->version());
$output->writeln('当前的修改说明:');
$output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
foreach (static::COMMENT as $comment) {
$output->info($comment);
}
$output->writeln('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
$output->highlight('代码托管地址:https://gitee.com/ulthon/ulthon_admin');
$output->highlight('开发文档地址:http://doc.ulthon.com/home/read/ulthon_admin/home.html');
$is_push_tag = $input->hasOption('push-tag');
if ($is_push_tag) {
$this->pushTag();
}
$is_generate_comment = $input->hasOption('generate-comment');
if ($is_generate_comment) {
$this->generateComment();
}
$is_generate_release = $input->hasOption('generate-release');
if ($is_generate_release) {
$fromTag = $input->getOption('release-from-tag');
$this->generateRelease($fromTag);
}
}
protected function pushTag()
{
$input = $this->input;
$output = $this->output;
$output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
$version = static::VERSION;
if (!empty(static::PRODUCT_VERSION)) {
$version = static::PRODUCT_VERSION;
}
// 将提交信息写入临时文件
$comment = implode("\n", static::COMMENT);
$output->info('生成标签:' . $version);
$output->info('标签描述:' . $comment);
// 使用项目的临时文件路径生成方法
$tempFile = PathTools::tempBuildPath('git_tag_message_' . uniqid() . '.txt');
file_put_contents($tempFile, $comment);
// 使用 -F 参数从文件读取提交信息
exec("git tag -a $version -F \"$tempFile\"");
// 删除临时文件
if (file_exists($tempFile)) {
unlink($tempFile);
}
$output->info('推送到远程仓库');
exec('git push --tags');
}
/**
* 生成版本更新说明
* 读取自上次tag到现在所有的提交说明.
* @return string
*/
protected function generateComment()
{
$this->output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
// 获取最新的tag
$lastTag = shell_exec('git describe --tags --abbrev=0');
$lastTag = trim($lastTag);
// 获取从最新tag到现在的所有提交
$commits = shell_exec("git log {$lastTag}..HEAD --pretty=format:\"%s\"");
if (empty($commits)) {
return '暂无更新说明';
}
$commits = explode("\n", $commits);
$groupedCommits = $this->groupCommitsByType($commits, function ($message) {
return trim($message);
});
// 生成更新说明
$comment = "版本更新说明:\n";
foreach (static::COMMIT_TYPES as $type => $desc) {
if (!empty($groupedCommits[$desc])) {
$comment .= "\n【{$desc}】\n";
foreach ($groupedCommits[$desc] as $message) {
$comment .= "- {$message}\n";
}
}
}
$this->output->writeln($comment);
// 生成数组格式文件
$comment_arr = explode("\n", $comment);
$comment_arr = array_filter($comment_arr, function ($item) {
return !empty($item);
});
$comment_arr = array_map(function ($item) {
return "'" . addslashes(trim($item)) . "',";
}, $comment_arr);
$comment_arr_code = implode("\n", $comment_arr);
$tmp_comment_file = PathTools::tempBuildPath('commit_comment.php');
file_put_contents($tmp_comment_file, "<?php\nreturn [\n" . $comment_arr_code . "\n];");
$this->output->info('已生成版本更新代码:' . $tmp_comment_file);
$tmp_comment_markdown_file = PathTools::tempBuildPath('commit_comment.md');
file_put_contents($tmp_comment_markdown_file, $comment);
$this->output->info('已生成版本更新Markdown:' . $tmp_comment_markdown_file);
}
/**
* 生成发布说明文件
* 从指定版本开始生成发布说明.
* @param string|null $fromTag 起始版本号,如果为空则使用最近的tag
* @return void
*/
public function generateRelease($fromTag = null)
{
$output = $this->output;
// 获取所有tags,按时间倒序排列
$tags = explode("\n", trim(shell_exec('git tag --sort=-creatordate')));
if (empty($tags)) {
$output->error('没有找到任何tag');
return;
}
$toTag = $tags[0]; // 最新的tag
if (empty($fromTag)) {
if (count($tags) < 2) {
$output->error('没有足够的tag来生成发布说明');
return;
}
$fromTag = $tags[1]; // 倒数第二个tag
$output->info('未指定起始版本,使用倒数第二个tag: ' . $fromTag);
} else {
if (!in_array($fromTag, $tags)) {
$output->error('指定的起始版本 ' . $fromTag . ' 不存在');
return;
}
if (version_compare($fromTag, $toTag, '>=')) {
$output->error('指定的起始版本 ' . $fromTag . ' 不早于最新版本 ' . $toTag);
return;
}
}
$output->info('从版本 ' . $fromTag . ' 生成发布说明');
$output->info('到最新tag: ' . $toTag);
// 获取当前版本
$currentVersion = static::PRODUCT_VERSION ?: static::VERSION;
// 获取从指定tag到目标tag的所有提交,包含提交哈希和提交日期
$commits = shell_exec("git log {$fromTag}..{$toTag} --pretty=format:\"%h|%ad|%s\" --date=short");
if (empty($commits)) {
$output->warning('从 ' . $fromTag . ' 到 ' . $toTag . ' 没有任何提交');
return;
}
$commits = explode("\n", $commits);
$groupedCommits = $this->groupCommitsByType($commits, function ($message, $fullCommit) {
$parts = explode('|', $fullCommit);
if (count($parts) >= 3) {
return [
'message' => trim($message),
'hash' => $parts[0],
'date' => $parts[1],
];
}
return null;
});
// 生成发布说明
$releaseTitle = "## {$currentVersion} 发布说明\n\n";
$releaseDate = '发布日期: ' . date('Y-m-d') . "\n\n";
$releaseContent = "本次发布包含了从 {$fromTag} 到 {$currentVersion} 的所有更新。\n\n";
foreach (static::COMMIT_TYPES as $type => $desc) {
if (!empty($groupedCommits[$desc])) {
$releaseContent .= "### {$desc}\n\n";
foreach ($groupedCommits[$desc] as $item) {
$releaseContent .= "- [{$item['date']}] {$item['message']} (#{$item['hash']})\n";
}
$releaseContent .= "\n";
}
}
$release = $releaseTitle . $releaseDate . $releaseContent;
// 生成发布说明文件
$releaseFile = PathTools::tempBuildPath("release_{$currentVersion}.md");
file_put_contents($releaseFile, $release);
$output->info('已生成发布说明文件: ' . $releaseFile);
}
protected function groupCommitsByType($commits, $formatCallback)
{
$groupedCommits = [];
foreach (static::COMMIT_TYPES as $type => $desc) {
$groupedCommits[$desc] = [];
}
$groupedCommits['其他'] = []; // 添加"其他"类别
foreach ($commits as $commit) {
$parts = explode('|', $commit);
$message = $parts[2] ?? $commit;
$matched = false;
foreach (static::COMMIT_TYPES as $type => $desc) {
// 支持 type: message 和 type(scope): message 两种格式
if (preg_match("/^{$type}(\([^)]*\))?\s*:/i", $message)) {
$cleanMessage = preg_replace("/^{$type}(\([^)]*\))?\s*:\s*/i", '', $message);
$groupedCommits[$desc][] = $formatCallback($cleanMessage, $commit);
$matched = true;
break;
}
}
// 如果没有匹配到任何类型,归类为"其他"
if (!$matched) {
$groupedCommits['其他'][] = $formatCallback($message, $commit);
}
}
// 清空空分组
foreach (array_keys($groupedCommits) as $key) {
if (empty($groupedCommits[$key])) {
unset($groupedCommits[$key]);
}
}
return $groupedCommits;
}
}