代码生成器前端模板升级 TS + Composition API 产出,增强实体租户与主键序列化判定

This commit is contained in:
smallchill 2026-08-07 16:24:46 +08:00
parent e518473599
commit 3d93b3d587
12 changed files with 141 additions and 77 deletions

View File

@ -60,7 +60,7 @@ public class CodeGenerator {
/**
* 基础业务字段
*/
public static String[] SUPER_ENTITY_COLUMNS = {"create_time", "create_user", "update_time", "update_user", "status", "is_deleted"};
public static String[] SUPER_ENTITY_COLUMNS = {"create_time", "create_user", "create_dept", "update_time", "update_user", "status", "is_deleted"};
/**
* 是否包含包装器
*/

View File

@ -15,11 +15,13 @@
*/
package org.springblade.develop.support;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.builder.CustomFile;
import com.baomidou.mybatisplus.generator.config.builder.Entity;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
@ -148,25 +150,33 @@ public class BladeCodeGenerator {
customFile.put("wrapper.java", "/templates/api/wrapper.java.vm");
}
if (Func.isNotBlank(packageWebDir)) {
customFile.put("api.js", "/templates/saber/api.js.vm");
customFile.put("api.ts", "/templates/saber/api.ts.vm");
customFile.put("crud.vue", "/templates/saber/crud.vue.vm");
}
// 无基础业务字段时实体不再继承 BaseEntity服务层须同步降级为 MyBatis-Plus 原生接口3.5.17 起位于 spring
String superServiceClass = hasSuperEntity ? "org.springblade.core.mp.base.BaseService" : "com.baomidou.mybatisplus.spring.service.IService";
String superServiceImplClass = hasSuperEntity ? "org.springblade.core.mp.base.BaseServiceImpl" : "com.baomidou.mybatisplus.spring.service.impl.ServiceImpl";
FastAutoGenerator.create(url, username, password)
.globalConfig(builder -> builder.author(props.getProperty("author")).dateType(DateType.TIME_PACK).enableSwagger().outputDir(getOutputDir()).disableOpenDir())
.packageConfig(builder -> builder.parent(packageName).controller("controller").entity("pojo.entity").service("service").serviceImpl("service.impl").mapper("mapper").xml("mapper"))
.strategyConfig(builder -> builder.addTablePrefix(tablePrefix).addInclude(includeTables).addExclude(excludeTables)
.entityBuilder().naming(NamingStrategy.underline_to_camel).columnNaming(NamingStrategy.underline_to_camel).enableLombok().superClass("org.springblade.core.mp.base.BaseEntity").addSuperEntityColumns(superEntityColumns).enableFileOverride()
.javaTemplate("/templates/api/entity.java.vm")
.serviceBuilder().superServiceClass("org.springblade.core.mp.base.BaseService").superServiceImplClass("org.springblade.core.mp.base.BaseServiceImpl").formatServiceFileName("I%sService").formatServiceImplFileName("%sServiceImpl").enableFileOverride()
.strategyConfig(builder -> {
Entity.Builder entityBuilder = builder.addTablePrefix(tablePrefix).addInclude(includeTables).addExclude(excludeTables)
.entityBuilder().naming(NamingStrategy.underline_to_camel).columnNaming(NamingStrategy.underline_to_camel).enableLombok().idType(IdType.ASSIGN_ID).enableFileOverride()
.javaTemplate("/templates/api/entity.java.vm");
if (hasSuperEntity) {
entityBuilder.superClass("org.springblade.core.mp.base.BaseEntity").addSuperEntityColumns(superEntityColumns);
}
entityBuilder.serviceBuilder().superServiceClass(superServiceClass).superServiceImplClass(superServiceImplClass).formatServiceFileName("I%sService").formatServiceImplFileName("%sServiceImpl").enableFileOverride()
.serviceTemplate("/templates/api/service.java.vm")
.serviceImplTemplate("/templates/api/serviceImpl.java.vm")
.mapperBuilder().mapperAnnotation(Mapper.class).enableBaseResultMap().enableBaseColumnList().formatMapperFileName("%sMapper").formatXmlFileName("%sMapper").enableFileOverride()
.mapperTemplate("/templates/api/mapper.java.vm")
.mapperXmlTemplate("/templates/api/mapper.xml.vm")
.controllerBuilder().superClass("org.springblade.core.boot.ctrl.BladeController").formatFileName("%sController").enableRestStyle().enableHyphenStyle().enableFileOverride()
.template("/templates/api/controller.java.vm")
)
.template("/templates/api/controller.java.vm");
})
.injectionConfig(builder -> builder.beforeOutputFile(
(tableInfo, objectMap) -> System.out.println("tableInfo: " + tableInfo.getEntityName() + " objectMap: " + objectMap.size())
).customMap(customMap).customFile(customFile)
@ -202,8 +212,8 @@ public class BladeCodeGenerator {
outputPath = getOutputDir() + StringPool.SLASH + packageName.replace(StringPool.DOT, StringPool.SLASH) + StringPool.SLASH + "wrapper" + StringPool.SLASH + entityName + "Wrapper" + StringPool.DOT_JAVA;
}
if (StringUtil.equals(key, "api.js")) {
outputPath = getOutputWebDir() + StringPool.SLASH + "api" + StringPool.SLASH + servicePackage.toLowerCase() + StringPool.SLASH + entityNameLower + ".js";
if (StringUtil.equals(key, "api.ts")) {
outputPath = getOutputWebDir() + StringPool.SLASH + "api" + StringPool.SLASH + servicePackage.toLowerCase() + StringPool.SLASH + entityNameLower + ".ts";
}
if (StringUtil.equals(key, "crud.vue")) {

View File

@ -1,11 +1,11 @@
/**
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -15,35 +15,30 @@
*/
package $!{package.Controller};
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.AllArgsConstructor;
import jakarta.validation.Valid;
#if($!{superControllerClassPackage})
import $!{superControllerClassPackage};
#end
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.swagger.annotation.ApiOrder;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springframework.web.bind.annotation.*;
#if($!{superEntityClass})
import org.springframework.web.bind.annotation.RequestParam;
#end
import com.baomidou.mybatisplus.core.metadata.IPage;
import $!{package.Entity}.$!{entity};
#set($voPackage=$package.Entity.replace("entity","vo"))
import $!{voPackage}.$!{entity}VO;
#set($wrapperPackage=$package.Entity.replace("pojo.entity","wrapper"))
#if($!{hasWrapper})
#set($wrapperPackage=$package.Entity.replace("pojo.entity","wrapper"))
import $!{wrapperPackage}.$!{entity}Wrapper;
#end
import $!{package.Service}.$!{table.serviceName};
#if($!{superControllerClassPackage})
import $!{superControllerClassPackage};
#end
#if(!$!{superEntityClass})
#end
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
/**
* $!{table.comment} 控制器
@ -84,7 +79,6 @@ public class $!{table.controllerName} {
IPage<$!{entity}> pages = $!{table.entityPath}Service.page(Condition.getPage(query), Condition.getQueryWrapper($!{table.entityPath}));
return R.data($!{entity}Wrapper.build().pageVO(pages));
}
#else
/**
* 详情
@ -144,8 +138,7 @@ public class $!{table.controllerName} {
return R.status($!{table.entityPath}Service.saveOrUpdate($!{table.entityPath}));
}
#if($!{superEntityClass})
#if($!{superEntityClass})
/**
* 删除 $!{table.comment}
*/
@ -155,8 +148,7 @@ public class $!{table.controllerName} {
return R.status($!{table.entityPath}Service.deleteLogic(Func.toLongList(ids)));
}
#else
#else
/**
* 删除 $!{table.comment}
*/
@ -166,6 +158,5 @@ public class $!{table.controllerName} {
return R.status($!{table.entityPath}Service.removeByIds(Func.toLongList(ids)));
}
#end
#end
}

View File

@ -1,3 +1,21 @@
#set($hasTenant = false)
#set($hasKeyField = false)
#set($hasLongKey = false)
#foreach($field in $!{table.fields})
#if($!{field.name} == $!{tenantColumn})
#set($hasTenant = true)
#end
#if($!{field.keyFlag})
#set($hasKeyField = true)
#end
#if($!{field.keyFlag} && $!{field.propertyType} == "Long")
#set($hasLongKey = true)
#end
#end
#set($useTenantEntity = false)
#if($hasTenant && "$!superEntityClass" == "BaseEntity")
#set($useTenantEntity = true)
#end
/**
* Copyright (c) 2018-2099, Chill Zhuang 庄骞 (bladejava@qq.com).
* <p>
@ -16,8 +34,20 @@
package $!{package.Entity};
#foreach($pkg in $!{table.importPackages})
#if($useTenantEntity && $!{pkg} == "org.springblade.core.mp.base.BaseEntity")
import org.springblade.core.mp.base.TenantEntity;
#elseif($!{pkg} == "java.io.Serializable" && $!{superEntityClass} && !$!{activeRecord})
## 继承父类时不直接实现 Serializable跳过强制注入的导入
#elseif(!$hasKeyField && ($!{pkg} == "com.baomidou.mybatisplus.annotation.IdType" || $!{pkg} == "com.baomidou.mybatisplus.annotation.TableId"))
## 主键列归入公共字段时实体内不产出主键注解,跳过全局 idType 注入的导入
#else
import $!{pkg};
#end
#end
#if($hasLongKey)
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
#end
#if($!{entityLombokModel})
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -46,7 +76,7 @@ import java.io.Serial;
@Schema(description = #if ("$!{table.comment}"=="")"$!{entity}对象"#else"$!{table.comment}"#end)
#end
#if($!{superEntityClass})
public class $!{entity} extends $!{superEntityClass}#if($!{activeRecord})<$!{entity}>#end {
public class $!{entity} extends #if($useTenantEntity)TenantEntity#{else}$!{superEntityClass}#end#if($!{activeRecord})<$!{entity}>#end {
#elseif($!{activeRecord})
@Accessors(chain = true)
public class $!{entity} extends Model<$!{entity}> {
@ -59,7 +89,7 @@ public class $!{entity} implements Serializable {
## ---------- BEGIN 字段循环遍历 ----------
#foreach($field in $!{table.fields})
#if($!{field.name}!=$!{tenantColumn})
#if($!{field.name} != $!{tenantColumn} || !$useTenantEntity)
#if($!{field.keyFlag})
#set($keyPropertyName=$!{field.propertyName})
#end
@ -80,6 +110,9 @@ public class $!{entity} implements Serializable {
#elseif($!{field.convert})
@TableId("$!{field.name}")
#end
#if($!{field.propertyType} == "Long")
@JsonSerialize(using = ToStringSerializer.class)
#end
## 普通字段
#elseif($!{field.fill})
## ----- 存在字段填充设置 -----

View File

@ -15,11 +15,12 @@
*/
package $!{package.Mapper};
import $!{superMapperClassPackage};
import com.baomidou.mybatisplus.core.metadata.IPage;
import $!{package.Entity}.$!{entity};
#set($voPackage=$package.Entity.replace("entity","vo"))
import $!{voPackage}.$!{entity}VO;
import $!{superMapperClassPackage};
import com.baomidou.mybatisplus.core.metadata.IPage;
import java.util.List;
/**

View File

@ -1,3 +1,13 @@
#set($hasTenant = false)
#foreach($field in $!{table.fields})
#if($!{field.name} == $!{tenantColumn})
#set($hasTenant = true)
#end
#end
#set($useTenantEntity = false)
#if($hasTenant && "$!superEntityClass" == "BaseEntity")
#set($useTenantEntity = true)
#end
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="$!{package.Mapper}.$!{table.mapperName}">
@ -19,7 +29,7 @@
<result column="$!{field.name}" property="$!{field.propertyName}"/>
#end
#foreach($field in $!{table.fields})
#if(!$!{field.keyFlag} && $!{field.name} != $!{tenantColumn})##生成普通字段
#if(!$!{field.keyFlag} && ($!{field.name} != $!{tenantColumn} || !$useTenantEntity))##生成普通字段
<result column="$!{field.name}" property="$!{field.propertyName}"/>
#end
#end
@ -28,7 +38,11 @@
#end
<select id="select$!{entity}Page" resultMap="$!{table.entityPath}ResultMap">
#if($!{superEntityClass})
select * from $!{table.name} where is_deleted = 0
#else
select * from $!{table.name}
#end
</select>
</mapper>

View File

@ -15,11 +15,11 @@
*/
package $!{package.Service};
import com.baomidou.mybatisplus.core.metadata.IPage;
import $!{superServiceClassPackage};
import $!{package.Entity}.$!{entity};
#set($voPackage=$package.Entity.replace("entity","vo"))
import $!{voPackage}.$!{entity}VO;
import $!{superServiceClassPackage};
import com.baomidou.mybatisplus.core.metadata.IPage;
/**
* $!{table.comment} 服务类

View File

@ -15,14 +15,14 @@
*/
package $!{package.ServiceImpl};
import com.baomidou.mybatisplus.core.metadata.IPage;
import $!{superServiceImplClassPackage};
import $!{package.Entity}.$!{entity};
#set($voPackage=$package.Entity.replace("entity","vo"))
import $!{voPackage}.$!{entity}VO;
import $!{package.Mapper}.$!{table.mapperName};
import $!{package.Service}.$!{table.serviceName};
import $!{superServiceImplClassPackage};
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.metadata.IPage;
/**
* $!{table.comment} 服务实现类

View File

@ -16,7 +16,6 @@
#set($wrapperPackage=$package.Entity.replace("pojo.entity","wrapper"))
package $!{wrapperPackage};
import lombok.AllArgsConstructor;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import $!{package.Entity}.$!{entity};

View File

@ -1,14 +1,23 @@
#set($hasTenant = false)
#foreach($field in $!{table.fields})
#if($!{field.name} == $!{tenantColumn})
#set($hasTenant = true)
#end
#end
#set($useTenantEntity = false)
#if($hasTenant && "$!superEntityClass" == "BaseEntity")
#set($useTenantEntity = true)
#end
<template>
<basic-container>
<avue-crud :option="option"
:table-loading="loading"
:data="data"
ref="crudRef"
v-model="form"
v-model:search="search"
v-model:page="page"
:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crudRef"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@ -17,6 +26,7 @@
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad">
<template #menu-left>
<el-button type="danger"
@ -41,8 +51,8 @@ import { validData } from '@/utils/util';
interface $!{entity}Entity {
id: string;
#foreach($field in $!{table.fields})
#if($!{field.name}!=$!{tenantColumn})
#if($!{field.propertyName} != "id")
#if($!{field.name} != $!{tenantColumn} || !$useTenantEntity)
#if(!$!{field.keyFlag})
#if($!{field.propertyType} == "Integer" || $!{field.propertyType} == "Long" || $!{field.propertyType} == "Short" || $!{field.propertyType} == "Byte" || $!{field.propertyType} == "Double" || $!{field.propertyType} == "Float" || $!{field.propertyType} == "BigDecimal")
$!{field.propertyName}?: number;
#elseif($!{field.propertyType} == "Boolean")
@ -65,14 +75,13 @@ const permission = computed(() => store.getters.permission);
// 表格实例与数据状态
const crudRef = ref();
const form = ref<$!{entity}Form>({});
const search = ref<Partial<$!{entity}Entity>>({});
const data = ref<$!{entity}Entity[]>([]);
const selectionList = ref<$!{entity}Entity[]>([]);
const query = ref<Partial<$!{entity}Entity>>({});
const loading = ref(true);
// 分页参数(整体替换,需用 ref
const page = ref({
// 分页参数
const page = reactive({
pageSize: 10,
currentPage: 1,
total: 0,
@ -85,16 +94,17 @@ const ids = computed(() => selectionList.value.map(ele => ele.id).join(','));
const option = reactive({
height: 'auto',
calcHeight: 210,
tip: false,
searchShow: true,
searchMenuSpan: 6,
tip: false,
border: true,
index: true,
viewBtn: true,
selection: true,
column: [
#foreach($field in $!{table.fields})
#if($!{field.name}!=$!{tenantColumn})
#if($!{field.name} != $!{tenantColumn} || !$useTenantEntity)
#if(!$!{field.keyFlag})
{
label: '$!{field.comment}',
prop: '$!{field.propertyName}',
@ -105,6 +115,7 @@ const option = reactive({
}],
},
#end
#end
#end
],
});
@ -121,9 +132,9 @@ const permissionList = computed(() => ({
const onLoad = (pageData: { currentPage: number; pageSize: number }, params: Partial<$!{entity}Entity> = {}) => {
loading.value = true;
getList(pageData.currentPage, pageData.pageSize, Object.assign(params, query.value)).then(res => {
const list = res.data.data;
page.value.total = list.total;
data.value = list.records;
const listData = res.data.data;
page.total = listData.total;
data.value = listData.records;
loading.value = false;
selectionClear();
});
@ -132,25 +143,30 @@ const onLoad = (pageData: { currentPage: number; pageSize: number }, params: Par
// 条件检索
const searchChange = (params: Partial<$!{entity}Entity>, done: () => void) => {
query.value = params;
page.value.currentPage = 1;
onLoad(page.value, params);
page.currentPage = 1;
onLoad(page, params);
done();
};
// 重置检索条件
const searchReset = () => {
query.value = {};
onLoad(page.value);
onLoad(page);
};
// 切换页码
const currentChange = (currentPage: number) => {
page.value.currentPage = currentPage;
page.currentPage = currentPage;
};
// 调整每页条数
const sizeChange = (pageSize: number) => {
page.value.pageSize = pageSize;
page.pageSize = pageSize;
};
// 刷新当前列表
const refreshChange = () => {
onLoad(page, query.value);
};
// 记录当前选中行
@ -168,7 +184,7 @@ const selectionClear = () => {
const rowSave = (row: $!{entity}Form, done: () => void, loading: () => void) => {
add(row).then(() => {
done();
onLoad(page.value);
onLoad(page);
ElMessage({
type: 'success',
message: '操作成功!',
@ -183,7 +199,7 @@ const rowSave = (row: $!{entity}Form, done: () => void, loading: () => void) =>
const rowUpdate = (row: $!{entity}Form, index: number, done: () => void, loading: () => void) => {
update(row).then(() => {
done();
onLoad(page.value);
onLoad(page);
ElMessage({
type: 'success',
message: '操作成功!',
@ -205,7 +221,7 @@ const rowDel = (row: $!{entity}Entity) => {
return remove(row.id);
})
.then(() => {
onLoad(page.value);
onLoad(page);
ElMessage({
type: 'success',
message: '操作成功!',
@ -228,7 +244,7 @@ const handleDelete = () => {
return remove(ids.value);
})
.then(() => {
onLoad(page.value);
onLoad(page);
ElMessage({
type: 'success',
message: '操作成功!',
@ -237,7 +253,7 @@ const handleDelete = () => {
});
};
// 打开弹窗前置处理:编辑与查看回填详情
// 打开弹窗前加载编辑/查看详情
const beforeOpen = (done: () => void, type: string) => {
if (['edit', 'view'].includes(type)) {
getDetail(form.value.id).then(res => {

View File

@ -652,7 +652,7 @@ import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
**⑥ 安全放行(springdoc 路径)**:springdoc UI 与规格端点需匿名可达。框架默认放行清单 `blade-core-secure``SecureRegistry.defaultExcludePatterns` 已含 `/v3/api-docs/**`、`/swagger-ui/**`、`/swagger-ui.html`(`/swagger-ui/**` 为 UI 静态资源,`/swagger-ui.html` 为跳转入口),下游 servlet 应用继承即可。若应用另有自建放行清单(Boot 单体的 `BladeConfiguration`、Cloud 网关的 `AuthProvider`),须同步补齐这三项,并去掉 `/doc.html`、`/swagger-resources/**` 等 knife4j / springfox 旧路径。
**⑦ 代码生成器模板**:`blade-starter-develop/src/main/resources/templates/controller.java.vm` 里的 `@ApiOperationSupport` import 与注解一并删除——它是 `.vm` 资源,下游按 `*.java` 批量清理的脚本覆盖不到,须单独处理,否则该生成器产出的 Controller 引用已移除的 knife4j 包、编译不过(排序改用 §9⑧ 的 `@ApiOrder`)。删干净后**还须在模板 `@Tag` 上方补一行无参 `@ApiOrder` + 其 import**(`org.springblade.core.swagger.annotation.ApiOrder`),让生成的 Controller 与手写控制器一致带排序(见下「落地约定」);下游 Boot(`src/{main,test}/resources/templates/`)、Cloud(`blade-ops/blade-develop/src/{main,test}/resources/templates/`)的同名模板副本一并补。
**⑦ 代码生成器模板**:`blade-starter-develop/src/main/resources/templates/controller.java.vm` 里的 `@ApiOperationSupport` import 与注解一并删除——它是 `.vm` 资源,下游按 `*.java` 批量清理的脚本覆盖不到,须单独处理,否则该生成器产出的 Controller 引用已移除的 knife4j 包、编译不过(排序改用 §9⑧ 的 `@ApiOrder`)。删干净后**还须在模板 `@Tag` 上方补一行无参 `@ApiOrder` + 其 import**(`org.springblade.core.swagger.annotation.ApiOrder`),让生成的 Controller 与手写控制器一致带排序(见下「落地约定」);下游 Boot(`src/main/resources/templates/`)、Cloud(`blade-ops/blade-develop/src/main/resources/templates/`)的同名模板副本一并补。`src/test/resources/templates/` 下的旧扁平路径 `.vm` 副本自模板重组为 `api/` 子目录后不再被加载,应直接删除,test 侧仅保留 `code.properties` 作为数据源配置覆盖点。
**⑧ 接口排序:`@ApiOrder`(替代 knife4j 的 `@ApiOperationSupport(order = n)`)**
@ -664,7 +664,7 @@ springdoc / OpenAPI 3 无接口排序原生注解,blade-starter-swagger 新增 `
实现在 `SwaggerAutoConfiguration`:`GlobalOperationCustomizer` 读注解写入 `x-order` 扩展(类级用字节码行号取声明序,反射不保证方法顺序)并记录「tag → 类级序号」;`GlobalOpenApiCustomizer` 按 `x-order` 重排 `paths`、按类级序号重排顶层 `tags`。swagger-ui 仅在两个 sorter 未设时保留 spec 顺序(§9⑤),并须保持 `springdoc.writer-with-order-by-keys` 默认 `false`
**落地约定(本次统一)**:三工程**每个带 `@Tag` 的控制器**均在 `@Tag` 上方补一行**无参 `@ApiOrder`**(值取默认 `Integer.MAX_VALUE`),方法体只保留 `@Operation`——效果是分组内接口按**源码声明顺序**展示、各 `@Tag` 分组之间因类级序号相等而回落 tag 名字母序;仅当个别接口要打破源码顺序时,才在该方法上补 `@ApiOrder(n)` 精确覆盖。删 knife4j 排序注解(§3.1 / §4.2)后**只删不补**会让接口在 springdoc UI 下按字母序、丢失原有顺序,故「删旧 + 补 `@ApiOrder`」是同一件事的两半。代码生成器模板 `controller.java.vm`(blade-tool `blade-starter-develop` 源模板 + 下游 Boot/Cloud `blade-develop` 的 main/test 副本,共 5 份)同步在 `@Tag` 上方生成 `@ApiOrder`,新生成的 Controller 开箱即带排序。
**落地约定(本次统一)**:三工程**每个带 `@Tag` 的控制器**均在 `@Tag` 上方补一行**无参 `@ApiOrder`**(值取默认 `Integer.MAX_VALUE`),方法体只保留 `@Operation`——效果是分组内接口按**源码声明顺序**展示、各 `@Tag` 分组之间因类级序号相等而回落 tag 名字母序;仅当个别接口要打破源码顺序时,才在该方法上补 `@ApiOrder(n)` 精确覆盖。删 knife4j 排序注解(§3.1 / §4.2)后**只删不补**会让接口在 springdoc UI 下按字母序、丢失原有顺序,故「删旧 + 补 `@ApiOrder`」是同一件事的两半。代码生成器模板 `controller.java.vm`(blade-tool `blade-starter-develop` 源模板 + 下游 Boot/Cloud `blade-develop` 的 main 副本,共 3 份)同步在 `@Tag` 上方生成 `@ApiOrder`,新生成的 Controller 开箱即带排序。
> 提示:类级 `@ApiOrder(n)``value` 用作 `@Tag` 分组之间的排序序号(`SwaggerAutoConfiguration#recordTagOrder` 写入、`GlobalOpenApiCustomizer` 据此重排顶层 `tags`);分组内各接口不受此值影响,按源码声明顺序排列。本次全量采用**无参**形式,`value` 恒为默认值、各分组落 tag 名字母序;如需固定分组顺序,给对应控制器标 `@ApiOrder(n)` 即可。