发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
8 changed files with 220 additions and 14 deletions
Showing only changes of commit 13dec6c216 - Show all commits

View File

@@ -43,6 +43,17 @@ import java.util.Set;
@RestController
@RequestMapping("/api/v1/sysApiKey")
public class SysApiKeyController extends BaseCurdController<SysApiKeyService, SysApiKey> {
/**
* 访问令牌名称最大长度。
*/
private static final int API_KEY_NAME_MAX_LENGTH = 100;
/**
* 兼容旧客户端时使用的默认名称前缀。
*/
private static final String DEFAULT_API_KEY_NAME_PREFIX = "访问令牌-";
public SysApiKeyController(SysApiKeyService service) {
super(service);
}
@@ -56,13 +67,19 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
/**
* 添加(保存)数据
*
* @param name 访问令牌名称
* @return {@code Result.errorCode == 0} 添加成功,否则添加失败
*/
@PostMapping("/key/save")
@SaCheckPermission("/api/v1/sysApiKey/save")
public Result<PkVo> save() {
public Result<PkVo> save(@JsonBody(value = "name", required = false) String name) {
String apiKey = IdUtil.generateUUID();
String normalizedName = normalizeCreateName(name, apiKey);
if (normalizedName.length() > API_KEY_NAME_MAX_LENGTH) {
return Result.fail("访问令牌名称不能超过100个字符", null);
}
SysApiKey entity = new SysApiKey();
entity.setName(normalizedName);
entity.setApiKey(apiKey);
entity.setCreated(new Date());
entity.setStatus(1);
@@ -103,6 +120,10 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
if (entity == null || entity.getId() == null) {
return Result.fail("访问令牌 ID 不能为空");
}
Result<?> nameValidationResult = normalizeAndValidateUpdateName(entity);
if (nameValidationResult != null) {
return nameValidationResult;
}
if (!hasPersistentUpdateFields(entity) && !hasPermissionUpdateFields(entity)) {
return Result.fail("没有可更新的访问令牌字段");
}
@@ -230,7 +251,8 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
* @return 是否需要更新访问令牌主表
*/
private boolean hasPersistentUpdateFields(SysApiKey entity) {
return entity.getApiKey() != null
return entity.getName() != null
|| entity.getApiKey() != null
|| entity.getCreated() != null
|| entity.getStatus() != null
|| entity.getDeptId() != null
@@ -251,4 +273,42 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|| hasNewKnowledgePermissionFields(entity)
|| entity.getWorkflowApiEnabled() != null;
}
/**
* 标准化新建访问令牌的名称。
*
* <p>未传名称时生成可识别的兼容名称,避免旧客户端在升级期间创建空名称记录。</p>
*
* @param name 客户端提交的名称
* @param apiKey 新生成的访问令牌
* @return 标准化后的名称
*/
private String normalizeCreateName(String name, String apiKey) {
if (name != null && !name.trim().isEmpty()) {
return name.trim();
}
int suffixStart = Math.max(0, apiKey.length() - 6);
return DEFAULT_API_KEY_NAME_PREFIX + apiKey.substring(suffixStart);
}
/**
* 标准化并校验更新请求中的访问令牌名称。
*
* @param entity 待更新的访问令牌
* @return 校验失败结果;无需校验或校验成功时返回 {@code null}
*/
private Result<?> normalizeAndValidateUpdateName(SysApiKey entity) {
if (entity.getName() == null) {
return null;
}
String normalizedName = entity.getName().trim();
if (normalizedName.isEmpty()) {
return Result.fail("访问令牌名称不能为空");
}
if (normalizedName.length() > API_KEY_NAME_MAX_LENGTH) {
return Result.fail("访问令牌名称不能超过100个字符");
}
entity.setName(normalizedName);
return null;
}
}

View File

@@ -79,6 +79,50 @@ public class SysApiKeyControllerTest {
verify(knowledgePermissionService).replaceApiPermissions(apiKeyId, true, true, true);
}
/**
* 验证名称可以单独更新,并在持久化前移除首尾空格。
*/
@Test
public void updateShouldPersistTrimmedNameOnly() {
BigInteger apiKeyId = BigInteger.valueOf(103);
SysApiKeyService apiKeyService = mock(SysApiKeyService.class);
KnowledgeSharePermissionService knowledgePermissionService =
mock(KnowledgeSharePermissionService.class);
SysApiKeyController controller = controller(apiKeyService, knowledgePermissionService);
SysApiKey existing = new SysApiKey();
existing.setId(apiKeyId);
when(apiKeyService.getById(apiKeyId)).thenReturn(existing);
SysApiKey request = new SysApiKey();
request.setId(apiKeyId);
request.setName(" 生产环境调用 ");
Result<?> result = controller.update(request);
assertEquals(result.getErrorCode(), 0);
assertEquals(request.getName(), "生产环境调用");
verify(apiKeyService).updateById(request);
}
/**
* 验证空白名称会被拒绝,避免覆盖为无意义内容。
*/
@Test
public void updateShouldRejectBlankName() {
SysApiKeyService apiKeyService = mock(SysApiKeyService.class);
KnowledgeSharePermissionService knowledgePermissionService =
mock(KnowledgeSharePermissionService.class);
SysApiKeyController controller = controller(apiKeyService, knowledgePermissionService);
SysApiKey request = new SysApiKey();
request.setId(BigInteger.valueOf(104));
request.setName(" ");
Result<?> result = controller.update(request);
assertNotEquals(result.getErrorCode(), 0);
verifyNoInteractions(apiKeyService, knowledgePermissionService);
}
/**
* 验证新版知识库权限缺少字段时拒绝更新,避免遗漏字段被隐式关闭。
*/

View File

@@ -18,6 +18,12 @@ public class SysApiKeyBase implements Serializable {
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "id")
private BigInteger id;
/**
* 名称
*/
@Column(comment = "名称")
private String name;
/**
* apiKey
*/
@@ -68,6 +74,24 @@ public class SysApiKeyBase implements Serializable {
this.id = id;
}
/**
* 获取访问令牌名称。
*
* @return 访问令牌名称
*/
public String getName() {
return name;
}
/**
* 设置访问令牌名称。
*
* @param name 访问令牌名称
*/
public void setName(String name) {
this.name = name;
}
public String getApiKey() {
return apiKey;
}

View File

@@ -0,0 +1,6 @@
ALTER TABLE `tb_sys_api_key`
ADD COLUMN `name` varchar(100) NOT NULL DEFAULT '未命名访问令牌' COMMENT '名称';
UPDATE `tb_sys_api_key`
SET `name` = CONCAT('访问令牌-', RIGHT(COALESCE(`api_key`, CAST(`id` AS CHAR)), 6))
WHERE `name` = '未命名访问令牌';

View File

@@ -1,5 +1,6 @@
{
"id": "Id",
"name": "Name",
"apiKey": "ApiKey",
"created": "Created",
"status": "Status",
@@ -18,5 +19,8 @@
"knowledgeImportPermission": "Knowledge Import",
"knowledgeMaintenancePermission": "Knowledge Maintenance",
"workflowApiPermission": "Workflow API",
"addApiKeyNotice": "This operation will generate an API key. Please confirm whether to proceed"
"addApiKeyNotice": "Enter an access token name",
"namePlaceholder": "Enter a name",
"nameRequired": "Name is required",
"nameTooLong": "Name cannot exceed 100 characters"
}

View File

@@ -1,5 +1,6 @@
{
"id": "id",
"name": "名称",
"apiKey": "apiKey",
"created": "创建时间",
"status": "数据状态",
@@ -18,5 +19,8 @@
"knowledgeImportPermission": "知识导入",
"knowledgeMaintenancePermission": "知识库维护",
"workflowApiPermission": "工作流 API 调用授权",
"addApiKeyNotice": "该操作会生成一个apiKey,请确认是否生成"
"addApiKeyNotice": "请输入访问令牌名称",
"namePlaceholder": "请输入名称",
"nameRequired": "名称不能为空",
"nameTooLong": "名称不能超过100个字符"
}

View File

@@ -92,22 +92,51 @@ function remove(row: any) {
}).catch(() => {});
}
function addNewApiKey() {
ElMessageBox.confirm(
ElMessageBox.prompt(
$t('sysApiKey.addApiKeyNotice'),
$t('message.noticeTitle'),
{
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
inputPlaceholder: $t('sysApiKey.namePlaceholder'),
inputValidator: (value) => {
const normalizedValue = value.trim();
if (!normalizedValue) {
return $t('sysApiKey.nameRequired');
}
if (normalizedValue.length > 100) {
return $t('sysApiKey.nameTooLong');
}
return true;
},
).then(() => {
api.post('/api/v1/sysApiKey/key/save', {}).then((res) => {
beforeClose: (action, instance, done) => {
if (action !== 'confirm') {
done();
return;
}
instance.confirmButtonLoading = true;
api
.post('/api/v1/sysApiKey/key/save', {
name: instance.inputValue.trim(),
})
.then((res) => {
if (res.errorCode === 0) {
ElMessage.success($t('message.saveOkMessage'));
pageDataRef.value.setQuery({});
done();
} else {
ElMessage.error(res.message || $t('message.saveFailMessage'));
}
})
.catch(() => {
ElMessage.error($t('message.saveFailMessage'));
})
.finally(() => {
instance.confirmButtonLoading = false;
});
});
},
},
).catch(() => {});
}
</script>
@@ -129,6 +158,12 @@ function addNewApiKey() {
>
<template #default="{ pageList }">
<ElTable :data="pageList" border>
<ElTableColumn
prop="name"
:label="$t('sysApiKey.name')"
min-width="160"
show-overflow-tooltip
/>
<ElTableColumn
prop="apiKey"
:label="$t('sysApiKey.apiKey')"

View File

@@ -11,6 +11,7 @@ import {
ElDatePicker,
ElForm,
ElFormItem,
ElInput,
ElMessage,
} from 'element-plus';
@@ -27,6 +28,7 @@ interface ResourcePermission {
// 定义表单数据接口
interface Entity {
name: string;
apiKey: string;
status: number | string;
deptId: number | string;
@@ -49,6 +51,7 @@ const dialogVisible = ref(false);
const isAdd = ref(true);
// 表单数据(初始化默认值)
const entity = ref<Entity>({
name: '',
apiKey: '',
status: '',
deptId: '',
@@ -66,6 +69,19 @@ const resourcePermissionList = ref<ResourcePermission[]>([]);
// 表单校验规则(必填项校验)
const rules = ref({
name: [
{
required: true,
whitespace: true,
message: $t('sysApiKey.nameRequired'),
trigger: 'blur',
},
{
max: 100,
message: $t('sysApiKey.nameTooLong'),
trigger: 'change',
},
],
status: [
{
required: true,
@@ -136,6 +152,7 @@ function createDefaultEntity(row: Partial<Entity> = {}): Entity {
const knowledgeMaintenanceEnabled = Boolean(row.knowledgeMaintenanceEnabled);
const workflowApiEnabled = Boolean(row.workflowApiEnabled);
return {
name: '',
apiKey: '',
status: '',
deptId: '',
@@ -170,6 +187,7 @@ function save() {
saveForm.value?.validate((valid) => {
if (valid) {
btnLoading.value = true;
entity.value.name = entity.value.name.trim();
const url = isAdd.value
? 'api/v1/sysApiKey/save'
: 'api/v1/sysApiKey/update';
@@ -198,6 +216,7 @@ function closeDialog() {
saveForm.value?.resetFields();
// 重置表单数据
entity.value = {
name: '',
apiKey: '',
status: '',
deptId: '',
@@ -240,6 +259,16 @@ defineExpose({
label-position="top"
class="easyflow-modal-form easyflow-modal-form--compact form-container"
>
<ElFormItem prop="name" :label="$t('sysApiKey.name')">
<ElInput
v-model="entity.name"
clearable
:maxlength="100"
show-word-limit
:placeholder="$t('sysApiKey.namePlaceholder')"
/>
</ElFormItem>
<!-- 状态选择 -->
<ElFormItem prop="status" :label="$t('sysApiKey.status')">
<DictSelect