发布 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 @RestController
@RequestMapping("/api/v1/sysApiKey") @RequestMapping("/api/v1/sysApiKey")
public class SysApiKeyController extends BaseCurdController<SysApiKeyService, 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) { public SysApiKeyController(SysApiKeyService service) {
super(service); super(service);
} }
@@ -56,13 +67,19 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
/** /**
* 添加(保存)数据 * 添加(保存)数据
* *
* @param name 访问令牌名称
* @return {@code Result.errorCode == 0} 添加成功,否则添加失败 * @return {@code Result.errorCode == 0} 添加成功,否则添加失败
*/ */
@PostMapping("/key/save") @PostMapping("/key/save")
@SaCheckPermission("/api/v1/sysApiKey/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 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(); SysApiKey entity = new SysApiKey();
entity.setName(normalizedName);
entity.setApiKey(apiKey); entity.setApiKey(apiKey);
entity.setCreated(new Date()); entity.setCreated(new Date());
entity.setStatus(1); entity.setStatus(1);
@@ -103,6 +120,10 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
if (entity == null || entity.getId() == null) { if (entity == null || entity.getId() == null) {
return Result.fail("访问令牌 ID 不能为空"); return Result.fail("访问令牌 ID 不能为空");
} }
Result<?> nameValidationResult = normalizeAndValidateUpdateName(entity);
if (nameValidationResult != null) {
return nameValidationResult;
}
if (!hasPersistentUpdateFields(entity) && !hasPermissionUpdateFields(entity)) { if (!hasPersistentUpdateFields(entity) && !hasPermissionUpdateFields(entity)) {
return Result.fail("没有可更新的访问令牌字段"); return Result.fail("没有可更新的访问令牌字段");
} }
@@ -230,7 +251,8 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
* @return 是否需要更新访问令牌主表 * @return 是否需要更新访问令牌主表
*/ */
private boolean hasPersistentUpdateFields(SysApiKey entity) { private boolean hasPersistentUpdateFields(SysApiKey entity) {
return entity.getApiKey() != null return entity.getName() != null
|| entity.getApiKey() != null
|| entity.getCreated() != null || entity.getCreated() != null
|| entity.getStatus() != null || entity.getStatus() != null
|| entity.getDeptId() != null || entity.getDeptId() != null
@@ -251,4 +273,42 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|| hasNewKnowledgePermissionFields(entity) || hasNewKnowledgePermissionFields(entity)
|| entity.getWorkflowApiEnabled() != null; || 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); 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") @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "id")
private BigInteger id; private BigInteger id;
/**
* 名称
*/
@Column(comment = "名称")
private String name;
/** /**
* apiKey * apiKey
*/ */
@@ -68,6 +74,24 @@ public class SysApiKeyBase implements Serializable {
this.id = id; this.id = id;
} }
/**
* 获取访问令牌名称。
*
* @return 访问令牌名称
*/
public String getName() {
return name;
}
/**
* 设置访问令牌名称。
*
* @param name 访问令牌名称
*/
public void setName(String name) {
this.name = name;
}
public String getApiKey() { public String getApiKey() {
return apiKey; 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", "id": "Id",
"name": "Name",
"apiKey": "ApiKey", "apiKey": "ApiKey",
"created": "Created", "created": "Created",
"status": "Status", "status": "Status",
@@ -18,5 +19,8 @@
"knowledgeImportPermission": "Knowledge Import", "knowledgeImportPermission": "Knowledge Import",
"knowledgeMaintenancePermission": "Knowledge Maintenance", "knowledgeMaintenancePermission": "Knowledge Maintenance",
"workflowApiPermission": "Workflow API", "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", "id": "id",
"name": "名称",
"apiKey": "apiKey", "apiKey": "apiKey",
"created": "创建时间", "created": "创建时间",
"status": "数据状态", "status": "数据状态",
@@ -18,5 +19,8 @@
"knowledgeImportPermission": "知识导入", "knowledgeImportPermission": "知识导入",
"knowledgeMaintenancePermission": "知识库维护", "knowledgeMaintenancePermission": "知识库维护",
"workflowApiPermission": "工作流 API 调用授权", "workflowApiPermission": "工作流 API 调用授权",
"addApiKeyNotice": "该操作会生成一个apiKey,请确认是否生成" "addApiKeyNotice": "请输入访问令牌名称",
"namePlaceholder": "请输入名称",
"nameRequired": "名称不能为空",
"nameTooLong": "名称不能超过100个字符"
} }

View File

@@ -92,22 +92,51 @@ function remove(row: any) {
}).catch(() => {}); }).catch(() => {});
} }
function addNewApiKey() { function addNewApiKey() {
ElMessageBox.confirm( ElMessageBox.prompt(
$t('sysApiKey.addApiKeyNotice'), $t('sysApiKey.addApiKeyNotice'),
$t('message.noticeTitle'), $t('message.noticeTitle'),
{ {
confirmButtonText: $t('message.ok'), confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'), 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;
},
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;
});
},
}, },
).then(() => { ).catch(() => {});
api.post('/api/v1/sysApiKey/key/save', {}).then((res) => {
if (res.errorCode === 0) {
ElMessage.success($t('message.saveOkMessage'));
pageDataRef.value.setQuery({});
}
});
});
} }
</script> </script>
@@ -129,6 +158,12 @@ function addNewApiKey() {
> >
<template #default="{ pageList }"> <template #default="{ pageList }">
<ElTable :data="pageList" border> <ElTable :data="pageList" border>
<ElTableColumn
prop="name"
:label="$t('sysApiKey.name')"
min-width="160"
show-overflow-tooltip
/>
<ElTableColumn <ElTableColumn
prop="apiKey" prop="apiKey"
:label="$t('sysApiKey.apiKey')" :label="$t('sysApiKey.apiKey')"

View File

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