feat: 为访问令牌增加名称
- 支持创建和编辑访问令牌名称 - 在列表首列展示名称并兼容历史数据
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证新版知识库权限缺少字段时拒绝更新,避免遗漏字段被隐式关闭。
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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` = '未命名访问令牌';
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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个字符"
|
||||
}
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user