feat: 火山配置从部门表独立出来,相关取值、缓存、保存重写逻辑

This commit is contained in:
yys 2026-04-21 10:59:03 +08:00
parent cb72d4edae
commit 8d7cd70cfc
18 changed files with 550 additions and 292 deletions

View File

@ -25,6 +25,23 @@ export function getDept(deptId) {
})
}
// 部门火山引擎配置(解密后明文)
export function getDeptArk(deptId) {
return request({
url: '/system/dept/ark/' + deptId,
method: 'get'
})
}
// 保存部门火山引擎配置
export function updateDeptArk(data) {
return request({
url: '/system/dept/ark',
method: 'put',
data
})
}
// 新增部门
export function addDept(data) {
return request({

View File

@ -65,6 +65,14 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
v-if="isFirstLevelRow(scope.row) || isSecondLevelRow(scope.row)"
size="mini"
type="text"
icon="el-icon-setting"
@click="handleArkConfig(scope.row)"
v-hasPermi="['system:dept:query', 'system:dept:edit']"
>火山配置</el-button>
<el-button
size="mini"
type="text"
@ -163,11 +171,21 @@
<p class="model-parm-hint" style="margin: 0; padding-top: 8px">&nbsp;&nbsp;限制本部门下启用状态账号数量0 或不填表示不限制</p>
</el-col>
</el-row>
<el-row v-if="isSecondLevelCompanyForm">
</el-form>
<div slot="footer" class="dialog-footer">
<span v-if="isFirstLevelEditForm" class="form-tip">一级部门仅允许修改名称</span>
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<el-dialog title="火山配置" :visible.sync="arkOpen" width="800px" append-to-body @close="resetArkForm">
<el-form ref="arkFormRef" :model="arkForm" label-width="120px">
<el-row>
<el-col :span="24">
<el-form-item label="Byte API Key">
<el-input
v-model="form.byteApiKey"
v-model="arkForm.byteApiKey"
type="password"
show-password
placeholder="选填"
@ -176,11 +194,11 @@
</el-form-item>
</el-col>
</el-row>
<el-row v-if="isSecondLevelCompanyForm">
<el-row>
<el-col :span="24">
<el-form-item label="火山配置项目">
<el-input
v-model="form.project"
v-model="arkForm.project"
type="password"
show-password
placeholder="选填"
@ -189,13 +207,13 @@
</el-form-item>
</el-col>
</el-row>
<el-row v-if="isSecondLevelCompanyForm">
<el-row>
<el-col :span="24">
<el-form-item label="视频模型">
<div class="model-parm-block">
<div
v-for="(row, idx) in modelParamRows"
:key="'mp-' + idx"
v-for="(row, idx) in arkModelParamRows"
:key="'ark-mp-' + idx"
class="model-parm-row"
>
<el-input
@ -211,13 +229,13 @@
<el-button
type="text"
icon="el-icon-delete"
:disabled="modelParamRows.length <= 1"
@click="removeModelParamRow(idx)"
:disabled="arkModelParamRows.length <= 1"
@click="removeArkModelParamRow(idx)"
/>
</div>
<el-button type="text" icon="el-icon-plus" @click="addModelParamRow">添加模型</el-button>
<el-button type="text" icon="el-icon-plus" @click="addArkModelParamRow">添加模型</el-button>
<p class="model-parm-hint">
保存为 JSON 写入 model_parm门户视频生成用户所属二级部门读取
保存为 JSON 写入 ai_dept_ark_config.model_parm门户视频生成 ai_user.dept_id 与本部门配置读取
留空则使用 portal.video.models
</p>
</div>
@ -226,9 +244,8 @@
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<span v-if="isFirstLevelEditForm" class="form-tip">一级部门仅允许修改名称</span>
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="submitArkForm" v-hasPermi="['system:dept:edit']"> </el-button>
<el-button @click="arkOpen = false"> </el-button>
</div>
</el-dialog>
</div>
@ -265,7 +282,7 @@
</style>
<script>
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild, getDeptArk, updateDeptArk } from "@/api/system/dept"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
@ -287,6 +304,14 @@ export default {
title: "",
//
open: false,
arkOpen: false,
arkDeptId: null,
arkForm: {
byteApiKey: undefined,
project: undefined,
modelParm: undefined
},
arkModelParamRows: [{ label: '', value: '' }],
//
isExpandAll: true,
//
@ -299,7 +324,6 @@ export default {
//
form: {},
originalForm: {},
modelParamRows: [{ label: '', value: '' }],
//
rules: {
parentId: [
@ -332,17 +356,6 @@ export default {
this.getList()
},
computed: {
/** 二级公司ancestors 为 0,100即上级为根公司 dept_id=100 */
isSecondLevelCompanyForm() {
if (this.form.ancestors === "0,100") {
return true
}
const pid = this.form.parentId
if (pid !== undefined && pid !== null && Number(pid) === 100) {
return true
}
return false
},
isFirstLevelEditForm() {
return this.form.deptId !== undefined && this.isFirstLevelRow(this.form)
}
@ -371,39 +384,78 @@ export default {
return current
})
},
syncModelRowsFromForm() {
this.modelParamRows = [{ label: '', value: '' }]
const raw = this.form.modelParm
syncArkModelRowsFromForm() {
this.arkModelParamRows = [{ label: '', value: '' }]
const raw = this.arkForm.modelParm
if (!raw || String(raw).trim() === '') {
return
}
try {
const arr = JSON.parse(raw)
if (Array.isArray(arr) && arr.length) {
this.modelParamRows = arr.map(x => ({
this.arkModelParamRows = arr.map(x => ({
label: (x && x.label) ? String(x.label) : '',
value: (x && x.value) ? String(x.value) : ''
}))
}
} catch (e) {
this.modelParamRows = [{ label: '', value: '' }]
this.arkModelParamRows = [{ label: '', value: '' }]
}
},
buildModelParmPayload() {
if (!this.isSecondLevelCompanyForm) {
buildArkModelParmPayload() {
const rows = (this.arkModelParamRows || []).filter(r => r.label && r.value)
this.arkForm.modelParm = rows.length ? JSON.stringify(rows) : ''
},
addArkModelParamRow() {
this.arkModelParamRows.push({ label: '', value: '' })
},
removeArkModelParamRow(idx) {
if (this.arkModelParamRows.length <= 1) {
return
}
const rows = (this.modelParamRows || []).filter(r => r.label && r.value)
this.form.modelParm = rows.length ? JSON.stringify(rows) : ''
this.arkModelParamRows.splice(idx, 1)
},
addModelParamRow() {
this.modelParamRows.push({ label: '', value: '' })
resetArkForm() {
this.arkDeptId = null
this.arkForm = {
byteApiKey: undefined,
project: undefined,
modelParm: undefined
}
this.arkModelParamRows = [{ label: '', value: '' }]
if (this.$refs.arkFormRef) {
this.$refs.arkFormRef.resetFields()
}
},
removeModelParamRow(idx) {
if (this.modelParamRows.length <= 1) {
handleArkConfig(row) {
if (!this.isFirstLevelRow(row) && !this.isSecondLevelRow(row)) {
return
}
this.modelParamRows.splice(idx, 1)
this.arkDeptId = row.deptId
getDeptArk(row.deptId).then(response => {
const d = response.data || {}
this.arkForm = {
byteApiKey: d.byteApiKey,
project: d.project,
modelParm: d.modelParm
}
this.syncArkModelRowsFromForm()
this.arkOpen = true
})
},
submitArkForm() {
this.buildArkModelParmPayload()
const payload = {
deptId: this.arkDeptId,
byteApiKey: this.arkForm.byteApiKey !== undefined && this.arkForm.byteApiKey !== null ? this.arkForm.byteApiKey : '',
project: this.arkForm.project !== undefined && this.arkForm.project !== null ? this.arkForm.project : '',
modelParm: this.arkForm.modelParm !== undefined && this.arkForm.modelParm !== null ? this.arkForm.modelParm : ''
}
updateDeptArk(payload).then(() => {
this.$modal.msgSuccess('保存成功')
this.arkOpen = false
this.resetArkForm()
})
},
/** 查询部门列表 */
getList() {
@ -441,13 +493,9 @@ export default {
phone: undefined,
email: undefined,
maxUserCount: undefined,
byteApiKey: undefined,
project: undefined,
modelParm: undefined,
status: "0"
}
this.originalForm = {}
this.modelParamRows = [{ label: '', value: '' }]
this.resetForm("form")
},
/** 搜索按钮操作 */
@ -489,7 +537,6 @@ export default {
getDept(row.deptId).then(response => {
this.form = response.data
this.originalForm = { ...response.data }
this.syncModelRowsFromForm()
this.open = true
this.title = "修改部门"
listDeptExcludeChild(row.deptId).then(response => {
@ -506,7 +553,6 @@ export default {
submitForm: function() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buildModelParmPayload()
if (this.form.deptId == undefined && !this.deptOptions.some(item => Number(item.deptId) === Number(this.form.parentId))) {
this.$modal.msgError("仅允许在一级部门下创建二级部门")
return
@ -521,9 +567,6 @@ export default {
this.form.email = oldDept.email
this.form.status = oldDept.status
this.form.maxUserCount = oldDept.maxUserCount
this.form.byteApiKey = oldDept.byteApiKey
this.form.project = oldDept.project
this.form.modelParm = oldDept.modelParm
}
updateDept(this.form).then(response => {
this.$modal.msgSuccess("修改成功")

View File

@ -42,7 +42,7 @@ import java.util.Set;
import java.util.stream.Collectors;
/**
* 门户视频生成用户二级部门 byte_api_key 调用火山任务列表含库表与火山过滤列表
* 门户视频生成 {@code ai_user.dept_id} 对应 {@code ai_dept_ark_config} API Key 调用火山任务列表含库表与火山过滤列表
*/
@Api(tags = "门户-视频生成")
@RestController
@ -104,7 +104,7 @@ public class PortalVideoController extends BaseController {
}
return out.isEmpty() ? null : out;
} catch (Exception e) {
logger.warn("解析二级部门 model_parm 失败: {}", e.getMessage());
logger.warn("解析部门火山配置 model_parm 失败: {}", e.getMessage());
return null;
}
}

View File

@ -13,12 +13,15 @@ import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.ai.domain.AiDeptArkConfig;
import com.ruoyi.ai.service.IAiDeptArkConfigService;
import com.ruoyi.ai.service.IDeptChargeRefundService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.request.system.DeptArkConfigRequest;
import com.ruoyi.common.core.request.system.DeptChargeRefundRequest;
import com.ruoyi.common.core.request.system.DeptPointsCorrectionRequest;
import com.ruoyi.common.enums.BusinessType;
@ -40,6 +43,9 @@ public class SysDeptController extends BaseController
@Autowired
private IDeptChargeRefundService deptChargeRefundService;
@Autowired
private IAiDeptArkConfigService aiDeptArkConfigService;
/**
* 获取部门列表
*/
@ -74,6 +80,46 @@ public class SysDeptController extends BaseController
return success(deptService.selectDeptById(deptId));
}
/**
* 部门火山引擎配置解密后明文无配置时返回仅含 deptId 的空对象
*/
@PreAuthorize("@ss.hasPermi('system:dept:query')")
@GetMapping("/ark/{deptId}")
public AjaxResult getArk(@PathVariable Long deptId)
{
deptService.checkDeptDataScope(deptId);
if (!deptService.isArkConfigurableDept(deptId))
{
return error("仅一级或二级部门可配置火山引擎");
}
AiDeptArkConfig ark = aiDeptArkConfigService.selectDecryptedByDeptId(deptId);
if (ark == null)
{
AiDeptArkConfig empty = new AiDeptArkConfig();
empty.setDeptId(deptId);
return success(empty);
}
return success(ark);
}
/**
* 保存部门火山引擎配置写入 ai_dept_ark_config
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "部门火山配置", businessType = BusinessType.UPDATE)
@PutMapping("/ark")
public AjaxResult saveArk(@Validated @RequestBody DeptArkConfigRequest request)
{
Long deptId = request.getDeptId();
deptService.checkDeptDataScope(deptId);
if (!deptService.isArkConfigurableDept(deptId))
{
return error("仅一级或二级部门可配置火山引擎");
}
aiDeptArkConfigService.saveOrUpdateForDept(deptId, request.getByteApiKey(), request.getProject(), request.getModelParm());
return success();
}
/**
* 新增部门
*/

View File

@ -53,15 +53,6 @@ public class SysDept extends BaseEntity
/** 父部门名称 */
private String parentName;
/** Byte API Key */
private String byteApiKey;
/** 门户视频模型列表 JSON[{"label":"…","value":"ep-…"},…] */
private String modelParm;
/** Byte project */
private String project;
/** 部门余额 */
private BigDecimal balance;
@ -187,36 +178,6 @@ public class SysDept extends BaseEntity
this.parentName = parentName;
}
public String getByteApiKey()
{
return byteApiKey;
}
public void setByteApiKey(String byteApiKey)
{
this.byteApiKey = byteApiKey;
}
public String getModelParm()
{
return modelParm;
}
public void setModelParm(String modelParm)
{
this.modelParm = modelParm;
}
public String getProject()
{
return project;
}
public void setProject(String project)
{
this.project = project;
}
public BigDecimal getBalance()
{
return balance;
@ -258,9 +219,6 @@ public class SysDept extends BaseEntity
.append("leader", getLeader())
.append("phone", getPhone())
.append("email", getEmail())
.append("byteApiKey", getByteApiKey())
.append("modelParm", getModelParm())
.append("project", getProject())
.append("balance", getBalance())
.append("maxUserCount", getMaxUserCount())
.append("status", getStatus())

View File

@ -0,0 +1,53 @@
package com.ruoyi.common.core.request.system;
import javax.validation.constraints.NotNull;
/**
* 部门火山引擎配置保存至 ai_dept_ark_config
*/
public class DeptArkConfigRequest {
@NotNull(message = "部门ID不能为空")
private Long deptId;
/** 明文;空字符串表示清空 */
private String byteApiKey;
/** 明文;空字符串表示清空 */
private String project;
/** 明文 JSON空字符串表示清空 */
private String modelParm;
public Long getDeptId() {
return deptId;
}
public void setDeptId(Long deptId) {
this.deptId = deptId;
}
public String getByteApiKey() {
return byteApiKey;
}
public void setByteApiKey(String byteApiKey) {
this.byteApiKey = byteApiKey;
}
public String getProject() {
return project;
}
public void setProject(String project) {
this.project = project;
}
public String getModelParm() {
return modelParm;
}
public void setModelParm(String modelParm) {
this.modelParm = modelParm;
}
}

View File

@ -23,7 +23,7 @@ public class AiDeptArkConfig extends BaseEntity {
/** $column.columnComment */
@TableId(type = IdType.AUTO)
private String id;
private Long id;
/** 部门ID */
@Excel(name = "部门ID")

View File

@ -26,6 +26,9 @@ public interface AiUserMapper extends BaseMapper<AiUser> {
int countAiUserByDeptId(@Param("deptId") Long deptId);
/** 归属指定部门的门户用户主键(用于火山 API Key 缓存失效) */
List<Long> selectAiUserIdsByDeptId(@Param("deptId") Long deptId);
@Update("update ai_user set dept_id = #{deptId}, update_time = sysdate() where id = #{userId}")
int updateAiUserDeptId(@Param("userId") Long userId, @Param("deptId") Long deptId);
}

View File

@ -21,6 +21,21 @@ public interface IAiDeptArkConfigService {
*/
AiDeptArkConfig selectAiDeptArkConfigById(String id);
/**
* 按部门查询火山配置byte_api_keyproject 为解密后的明文无行返回 null
*/
AiDeptArkConfig selectDecryptedByDeptId(Long deptId);
/**
* 按部门物理删除配置行无行时返回 0
*/
int deleteByDeptId(Long deptId);
/**
* 保存或更新指定部门的火山配置密文字段按与历史 sys_dept 相同规则加密落库后使该部门下门户用户的 API Key 缓存失效
*/
void saveOrUpdateForDept(Long deptId, String byteApiKey, String project, String modelParm);
/**
* 查询团队部门对应火山引擎配置列表
*

View File

@ -1,7 +1,7 @@
package com.ruoyi.ai.service;
/**
* 门户用户火山方舟 API Key取自所属二级部门 {@code sys_dept.byte_api_key} Redis 缓存
* 门户用户火山方舟 API Key取自 {@code ai_dept_ark_config} {@code ai_user.dept_id} 对应 Redis 缓存
*/
public interface IByteDeptApiKeyService {
@ -11,13 +11,18 @@ public interface IByteDeptApiKeyService {
String resolveVolcApiKey(Long aiUserId);
/**
* 当前 AI 用户所属二级部门 {@code sys_dept.dept_id} {@link #resolveVolcApiKey(Long)} 中使用的二级节点一致
* 当前 AI 用户归属部门 {@code ai_user.dept_id}在仅一级二级部门树前提下与历史二级部门语义对齐对外方法名保留
* 用户未分配部门或部门数据缺失时返回 null
*/
Long resolveSecondLevelDeptId(Long aiUserId);
/**
* 当前用户所属二级部门 {@code sys_dept.model_parm}JSON 模型列表未配置或为空时返回 null
* 当前用户归属部门在 {@code ai_dept_ark_config.model_parm} JSON 模型列表未配置或为空时返回 null
*/
String resolveSecondLevelModelParm(Long aiUserId);
/**
* 使 {@code ai_user.dept_id = deptId} 的门户用户的火山 API Key 缓存失效配置变更或部门删除后调用
*/
void evictVolcApiKeyCacheForDept(Long deptId);
}

View File

@ -1,17 +1,24 @@
package com.ruoyi.ai.service.impl;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ai.domain.AiDeptArkConfig;
import com.ruoyi.ai.mapper.AiDeptArkConfigMapper;
import com.ruoyi.ai.mapper.AiUserMapper;
import com.ruoyi.ai.service.IAiDeptArkConfigService;
import com.ruoyi.common.EncryptionService;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.ai.mapper.AiDeptArkConfigMapper;
import com.ruoyi.ai.domain.AiDeptArkConfig;
import com.ruoyi.ai.service.IAiDeptArkConfigService;
/**
* 团队部门对应火山引擎配置Service业务层处理
@ -22,92 +29,208 @@ import com.ruoyi.ai.service.IAiDeptArkConfigService;
@Service
public class AiDeptArkConfigServiceImpl implements IAiDeptArkConfigService {
private static final String BYTE_API_KEY_CACHE_SUFFIX = "_byte_api_key";
@Autowired
private AiDeptArkConfigMapper aiDeptArkConfigMapper;
/**
* 查询团队部门对应火山引擎配置
*
* @param id 团队部门对应火山引擎配置主键
* @return 团队部门对应火山引擎配置
*/
@Autowired
private EncryptionService encryptionService;
@Autowired
private RedisCache redisCache;
@Autowired
private AiUserMapper aiUserMapper;
@Override
public AiDeptArkConfig selectAiDeptArkConfigById(String id) {
return aiDeptArkConfigMapper.selectById(id);
if (StringUtils.isEmpty(id)) {
return null;
}
AiDeptArkConfig row = aiDeptArkConfigMapper.selectById(Long.parseLong(id.trim()));
decryptSensitive(row);
return row;
}
@Override
public AiDeptArkConfig selectDecryptedByDeptId(Long deptId) {
if (deptId == null) {
return null;
}
AiDeptArkConfig row = aiDeptArkConfigMapper.selectOne(
Wrappers.<AiDeptArkConfig>lambdaQuery().eq(AiDeptArkConfig::getDeptId, deptId));
decryptSensitive(row);
return row;
}
@Override
public int deleteByDeptId(Long deptId) {
if (deptId == null) {
return 0;
}
int n = aiDeptArkConfigMapper.delete(
Wrappers.<AiDeptArkConfig>lambdaQuery().eq(AiDeptArkConfig::getDeptId, deptId));
evictVolcApiKeyCacheForDept(deptId);
return n;
}
@Override
public void saveOrUpdateForDept(Long deptId, String byteApiKey, String project, String modelParm) {
if (deptId == null) {
return;
}
String keyPlain = blankToNull(byteApiKey);
String projectPlain = blankToNull(project);
String modelPlain = blankToNull(modelParm);
AiDeptArkConfig existing = aiDeptArkConfigMapper.selectOne(
Wrappers.<AiDeptArkConfig>lambdaQuery().eq(AiDeptArkConfig::getDeptId, deptId));
if (existing == null) {
AiDeptArkConfig row = new AiDeptArkConfig();
row.setDeptId(deptId);
row.setByteApiKey(encodeNullable(keyPlain));
row.setProject(encodeNullable(projectPlain));
row.setModelParm(modelPlain);
row.setCreateBy(SecurityUtils.getUsername());
row.setCreateTime(DateUtils.getNowDate());
aiDeptArkConfigMapper.insert(row);
} else {
existing.setByteApiKey(encodeNullable(keyPlain));
existing.setProject(encodeNullable(projectPlain));
existing.setModelParm(modelPlain);
existing.setUpdateBy(SecurityUtils.getUsername());
existing.setUpdateTime(DateUtils.getNowDate());
aiDeptArkConfigMapper.updateById(existing);
}
evictVolcApiKeyCacheForDept(deptId);
}
private static String blankToNull(String s) {
if (s == null) {
return null;
}
String t = s.trim();
return t.isEmpty() ? null : t;
}
private String encodeNullable(String plain) {
if (plain == null) {
return null;
}
return encryptionService.encode(plain);
}
private void decryptSensitive(AiDeptArkConfig row) {
if (row == null) {
return;
}
if (StringUtils.isNotEmpty(row.getByteApiKey())) {
row.setByteApiKey(encryptionService.decode(row.getByteApiKey()));
}
if (StringUtils.isNotEmpty(row.getProject())) {
row.setProject(encryptionService.decode(row.getProject()));
}
}
private void encryptSensitiveForStore(AiDeptArkConfig row) {
if (row == null) {
return;
}
if (StringUtils.isNotEmpty(row.getByteApiKey())) {
row.setByteApiKey(encryptionService.encode(row.getByteApiKey()));
}
if (StringUtils.isNotEmpty(row.getProject())) {
row.setProject(encryptionService.encode(row.getProject()));
}
}
private void evictVolcApiKeyCacheForDept(Long deptId) {
List<Long> userIds = aiUserMapper.selectAiUserIdsByDeptId(deptId);
if (userIds == null || userIds.isEmpty()) {
return;
}
for (Long uid : userIds) {
if (uid != null) {
redisCache.deleteObject(uid + BYTE_API_KEY_CACHE_SUFFIX);
}
}
}
/**
* 查询团队部门对应火山引擎配置列表
*
* @param aiDeptArkConfig 团队部门对应火山引擎配置
* @return 团队部门对应火山引擎配置
*/
@Override
public List<AiDeptArkConfig> selectAiDeptArkConfigList(AiDeptArkConfig aiDeptArkConfig) {
LambdaQueryWrapper<AiDeptArkConfig> query = Wrappers.lambdaQuery(aiDeptArkConfig);
query.orderByDesc(AiDeptArkConfig::getId);
return aiDeptArkConfigMapper.selectList(query);
List<AiDeptArkConfig> list = aiDeptArkConfigMapper.selectList(query);
for (AiDeptArkConfig row : list) {
decryptSensitive(row);
}
return list;
}
/**
* 分页查询团队部门对应火山引擎配置列表
*
* @param aiDeptArkConfig 团队部门对应火山引擎配置
* @return 团队部门对应火山引擎配置
*/
@Override
public IPage<AiDeptArkConfig> selectAiDeptArkConfigPage(Page page, AiDeptArkConfig aiDeptArkConfig) {
LambdaQueryWrapper<AiDeptArkConfig> query = Wrappers.lambdaQuery(aiDeptArkConfig);
return aiDeptArkConfigMapper.selectPage(page, query);
IPage<AiDeptArkConfig> result = aiDeptArkConfigMapper.selectPage(page, query);
for (AiDeptArkConfig row : result.getRecords()) {
decryptSensitive(row);
}
return result;
}
/**
* 新增团队部门对应火山引擎配置
*
* @param aiDeptArkConfig 团队部门对应火山引擎配置
* @return 结果
*/
@Override
public int insertAiDeptArkConfig(AiDeptArkConfig aiDeptArkConfig) {
encryptSensitiveForStore(aiDeptArkConfig);
aiDeptArkConfig.setCreateBy(SecurityUtils.getUsername());
aiDeptArkConfig.setCreateTime(DateUtils.getNowDate());
return aiDeptArkConfigMapper.insert(aiDeptArkConfig);
int r = aiDeptArkConfigMapper.insert(aiDeptArkConfig);
if (r > 0 && aiDeptArkConfig.getDeptId() != null) {
evictVolcApiKeyCacheForDept(aiDeptArkConfig.getDeptId());
}
return r;
}
/**
* 修改团队部门对应火山引擎配置
*
* @param aiDeptArkConfig 团队部门对应火山引擎配置
* @return 结果
*/
@Override
public int updateAiDeptArkConfig(AiDeptArkConfig aiDeptArkConfig) {
encryptSensitiveForStore(aiDeptArkConfig);
aiDeptArkConfig.setUpdateBy(SecurityUtils.getUsername());
aiDeptArkConfig.setUpdateTime(DateUtils.getNowDate());
return aiDeptArkConfigMapper.updateById(aiDeptArkConfig);
int r = aiDeptArkConfigMapper.updateById(aiDeptArkConfig);
if (r > 0 && aiDeptArkConfig.getDeptId() != null) {
evictVolcApiKeyCacheForDept(aiDeptArkConfig.getDeptId());
}
return r;
}
/**
* 批量删除团队部门对应火山引擎配置
*
* @param ids 需要删除的团队部门对应火山引擎配置主键
* @return 结果
*/
@Override
public int deleteAiDeptArkConfigByIds(String[] ids)
{
return aiDeptArkConfigMapper.deleteBatchIds(java.util.Arrays.asList(ids));
public int deleteAiDeptArkConfigByIds(String[] ids) {
if (ids == null || ids.length == 0) {
return 0;
}
List<Long> longIds = Arrays.stream(ids).map(String::trim).filter(StringUtils::isNotEmpty)
.map(Long::parseLong).collect(Collectors.toList());
List<AiDeptArkConfig> rows = aiDeptArkConfigMapper.selectBatchIds(longIds);
int n = aiDeptArkConfigMapper.deleteBatchIds(longIds);
if (n > 0) {
for (AiDeptArkConfig row : rows) {
if (row != null && row.getDeptId() != null) {
evictVolcApiKeyCacheForDept(row.getDeptId());
}
}
}
return n;
}
/**
* 删除团队部门对应火山引擎配置信息
*
* @param id 团队部门对应火山引擎配置主键
* @return 结果
*/
@Override
public int deleteAiDeptArkConfigById(String id)
{
return aiDeptArkConfigMapper.deleteById(id);
public int deleteAiDeptArkConfigById(String id) {
if (StringUtils.isEmpty(id)) {
return 0;
}
AiDeptArkConfig row = aiDeptArkConfigMapper.selectById(Long.parseLong(id.trim()));
int n = aiDeptArkConfigMapper.deleteById(Long.parseLong(id.trim()));
if (n > 0 && row != null && row.getDeptId() != null) {
evictVolcApiKeyCacheForDept(row.getDeptId());
}
return n;
}
}

View File

@ -2,13 +2,13 @@ package com.ruoyi.ai.service.impl;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.*;
import com.ruoyi.ai.domain.AiDeptArkConfig;
import com.ruoyi.ai.mapper.AiUserMapper;
import com.ruoyi.common.EncryptionService;
import com.ruoyi.ai.service.IAiDeptArkConfigService;
import com.ruoyi.common.core.domain.entity.AiUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.OkHttpUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value;
@ -34,10 +34,7 @@ public class BaseByteApiService {
@Resource
private AiUserMapper userMapper;
@Resource
private SysDeptMapper deptMapper;
@Resource
private EncryptionService encryptionService;
protected String DEPT_ANCESTORS_SPLIT = ",";
private IAiDeptArkConfigService aiDeptArkConfigService;
@Value("${volcengine.ak}")
protected String accessKeyId;
@Value("${volcengine.sk}")
@ -70,7 +67,7 @@ public class BaseByteApiService {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
/**
* 根据用户找到对应的project
* 根据门户用户 {@code ai_user.dept_id} 对应 {@link AiDeptArkConfig#getProject()}解密后解析 project
*/
protected String getUserProject() {
Long userId = SecurityUtils.getAiUserId();
@ -78,39 +75,14 @@ public class BaseByteApiService {
return null;
}
AiUser user = userMapper.selectAiUserById(userId);
if (user == null) {
if (user == null || user.getDeptId() == null) {
return null;
}
// 第二层部门IDAPI_KEY放在这里
Long secondLvDeptId = getSecondLevelDept(user.getDeptId());
if (secondLvDeptId == null) {
AiDeptArkConfig ark = aiDeptArkConfigService.selectDecryptedByDeptId(user.getDeptId());
if (ark == null || StringUtils.isEmpty(ark.getProject())) {
return null;
}
SysDept secondDept = deptMapper.selectDeptById(secondLvDeptId);
return encryptionService.decode(secondDept.getProject());
}
/**
* 找到当前部门所属第二部门ID
*
* @param deptId 当前部门
*/
protected Long getSecondLevelDept(long deptId) {
SysDept dept = deptMapper.selectDeptById(deptId);
String ancestors = dept.getAncestors();
// 判断是第几层
if (ancestors == null || ancestors.isEmpty() || "0".equals(ancestors)) {
// 第一层
return null;
}
String[] parentDeptArray = ancestors.split(DEPT_ANCESTORS_SPLIT);
int length = parentDeptArray.length;
if (length == 2) {
// 只有一个上级所以当前节点是第二层直接返回
return deptId;
}
// 大于二级
return Long.parseLong(parentDeptArray[2]);
return ark.getProject().trim();
}
public <R> R callApi(String action, Object request, Class<R> responseClass) throws IOException {

View File

@ -1,5 +1,8 @@
package com.ruoyi.ai.service.impl;
import com.ruoyi.ai.domain.AiDeptArkConfig;
import com.ruoyi.ai.mapper.AiUserMapper;
import com.ruoyi.ai.service.IAiDeptArkConfigService;
import com.ruoyi.ai.service.IAiUserService;
import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.common.EncryptionService;
@ -14,19 +17,22 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
@Resource
private EncryptionService encryptionService;
private static final String BYTE_API_KEY_CACHE_SUFFIX = "_byte_api_key";
private static final String NO_DEPT_MSG = "用户未分配部门:请在后台为门户用户设置 ai_user.dept_id关联 sys_dept.dept_id";
private static final String NO_DEPT_ROW_MSG = "用户所属部门不存在或已删除,请核对 ai_user.dept_id";
private static final String NO_API_KEY_MSG = "部门未配置火山 API Key请在 sys_dept 为用户所在部门(或其上级二级部门)配置 byte_api_key";
private static final String NO_API_KEY_MSG = "部门未配置火山 API Key请在后台「部门管理」中为该部门配置火山 API Keyai_dept_ark_config";
private static final int CACHE_HOURS = 1;
@Resource
private EncryptionService encryptionService;
@Autowired
private RedisCache redisCache;
@ -36,15 +42,20 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
@Autowired
private ISysDeptService sysDeptService;
@Autowired
private IAiDeptArkConfigService aiDeptArkConfigService;
@Autowired
private AiUserMapper aiUserMapper;
@Override
public String resolveVolcApiKey(Long aiUserId) {
if (aiUserId == null) {
throw new ServiceException(NO_DEPT_MSG);
}
String cacheKey = aiUserId + "_byte_api_key";
String cacheKey = aiUserId + BYTE_API_KEY_CACHE_SUFFIX;
String cached = redisCache.getCacheObject(cacheKey);
if (StringUtils.isNotEmpty(cached)) {
// 解密缓存保存加密后的字符串比较安全
return encryptionService.decode(cached);
}
AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
@ -55,21 +66,11 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
if (userDept == null) {
throw new ServiceException(NO_DEPT_ROW_MSG);
}
// 优先使用用户直接归属部门的 Key多数业务把用户挂在分公司 101并在该节点配 byte_api_key
String apiKey = trimKey(userDept.getByteApiKey());
if (StringUtils.isEmpty(apiKey)) {
Long fallbackDeptId = secondLevelDeptIdFrom(userDept);
if (!fallbackDeptId.equals(userDept.getDeptId())) {
SysDept keyDept = sysDeptService.selectDeptById(fallbackDeptId);
if (keyDept != null) {
apiKey = trimKey(keyDept.getByteApiKey());
}
}
}
AiDeptArkConfig ark = aiDeptArkConfigService.selectDecryptedByDeptId(aiUser.getDeptId());
String apiKey = ark != null ? trimKey(ark.getByteApiKey()) : null;
if (StringUtils.isEmpty(apiKey)) {
throw new ServiceException(NO_API_KEY_MSG);
}
// 缓存中保存加密值
String encodeApiKey = encryptionService.encode(apiKey);
redisCache.setCacheObject(cacheKey, encodeApiKey, CACHE_HOURS, TimeUnit.HOURS);
return apiKey;
@ -84,59 +85,46 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
if (aiUser == null || aiUser.getDeptId() == null) {
return null;
}
SysDept userDept = sysDeptService.selectDeptById(aiUser.getDeptId());
if (userDept == null) {
return null;
}
return secondLevelDeptIdFrom(userDept);
return aiUser.getDeptId();
}
@Override
public String resolveSecondLevelModelParm(Long aiUserId) {
Long secondId = resolveSecondLevelDeptId(aiUserId);
if (secondId == null) {
if (aiUserId == null) {
return null;
}
SysDept second = sysDeptService.selectDeptById(secondId);
if (second == null) {
AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
if (aiUser == null || aiUser.getDeptId() == null) {
return null;
}
String raw = second.getModelParm();
AiDeptArkConfig ark = aiDeptArkConfigService.selectDecryptedByDeptId(aiUser.getDeptId());
if (ark == null) {
return null;
}
String raw = ark.getModelParm();
if (StringUtils.isEmpty(raw)) {
return null;
}
return raw.trim();
}
@Override
public void evictVolcApiKeyCacheForDept(Long deptId) {
if (deptId == null) {
return;
}
List<Long> userIds = aiUserMapper.selectAiUserIdsByDeptId(deptId);
if (userIds == null || userIds.isEmpty()) {
return;
}
for (Long uid : userIds) {
if (uid != null) {
redisCache.deleteObject(uid + BYTE_API_KEY_CACHE_SUFFIX);
}
}
}
private static String trimKey(String raw) {
return raw == null ? null : raw.trim();
}
/**
* 二级部门祖级路径中紧接在一级ancestors 第二段之下的部门节点
* 深度不足时退回用户当前部门
*/
private Long secondLevelDeptIdFrom(SysDept userDept) {
String ancestors = userDept.getAncestors();
if (StringUtils.isEmpty(ancestors)) {
return userDept.getDeptId();
}
String[] parts = ancestors.split(",");
if (parts.length >= 3) {
try {
return Long.parseLong(parts[2].trim());
} catch (NumberFormatException ignored) {
return userDept.getDeptId();
}
}
if (parts.length == 2) {
try {
// return Long.parseLong(parts[1].trim());
return userDept.getDeptId();
} catch (NumberFormatException ignored) {
return userDept.getDeptId();
}
}
return userDept.getDeptId();
}
}

View File

@ -144,4 +144,9 @@ public interface ISysDeptService
* 积分更正更新部门余额并写入集团流水手动修改类型不产生充值/退款订单
*/
void editScore(DeptPointsCorrectionRequest request);
/**
* 是否允许配置火山引擎一级或二级部门与仅两级部门树一致
*/
boolean isArkConfigurableDept(Long deptId);
}

View File

@ -9,8 +9,8 @@ import java.util.Objects;
import java.util.stream.Collectors;
import com.ruoyi.ai.domain.AiGroupBalanceChangeRecord;
import com.ruoyi.ai.service.IAiDeptArkConfigService;
import com.ruoyi.ai.service.IAiGroupBalanceChangeRecordService;
import com.ruoyi.common.EncryptionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -33,8 +33,6 @@ import com.ruoyi.system.mapper.SysRoleMapper;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.ISysDeptService;
import javax.annotation.Resource;
/**
* 部门管理 服务实现
*
@ -52,12 +50,12 @@ public class SysDeptServiceImpl implements ISysDeptService
@Autowired
private SysUserMapper userMapper;
@Resource
private EncryptionService encryptionService;
@Autowired
private IAiGroupBalanceChangeRecordService aiGroupBalanceChangeRecordService;
@Autowired
private IAiDeptArkConfigService aiDeptArkConfigService;
/**
* 查询部门管理数据
*
@ -146,14 +144,7 @@ public class SysDeptServiceImpl implements ISysDeptService
@Override
public SysDept selectDeptById(Long deptId)
{
SysDept sysDept = deptMapper.selectDeptById(deptId);
if (sysDept.getByteApiKey() != null && !sysDept.getByteApiKey().isEmpty()) {
sysDept.setByteApiKey(encryptionService.decode(sysDept.getByteApiKey()));
}
if (sysDept.getProject() != null && !sysDept.getProject().isEmpty()) {
sysDept.setProject(encryptionService.decode(sysDept.getProject()));
}
return sysDept;
return deptMapper.selectDeptById(deptId);
}
/**
@ -252,12 +243,6 @@ public class SysDeptServiceImpl implements ISysDeptService
throw new ServiceException("部门停用,不允许新增");
}
dept.setAncestors(info.getAncestors() + "," + dept.getParentId());
if (StringUtils.isNotEmpty(dept.getByteApiKey())) {
dept.setByteApiKey(encryptionService.encode(dept.getByteApiKey()));
}
if (StringUtils.isNotEmpty(dept.getProject())) {
dept.setProject(encryptionService.encode(dept.getProject()));
}
return deptMapper.insertDept(dept);
}
@ -285,12 +270,6 @@ public class SysDeptServiceImpl implements ISysDeptService
dept.setAncestors(newAncestors);
updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors);
}
if (StringUtils.isNotEmpty(dept.getByteApiKey())) {
dept.setByteApiKey(encryptionService.encode(dept.getByteApiKey()));
}
if (StringUtils.isNotEmpty(dept.getProject())) {
dept.setProject(encryptionService.encode(dept.getProject()));
}
int result = deptMapper.updateDept(dept);
if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors())
&& !StringUtils.equals("0", dept.getAncestors()))
@ -357,9 +336,30 @@ public class SysDeptServiceImpl implements ISysDeptService
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteDeptById(Long deptId)
{
return deptMapper.deleteDeptById(deptId);
int rows = deptMapper.deleteDeptById(deptId);
if (rows > 0)
{
aiDeptArkConfigService.deleteByDeptId(deptId);
}
return rows;
}
@Override
public boolean isArkConfigurableDept(Long deptId)
{
if (deptId == null)
{
return false;
}
SysDept d = deptMapper.selectDeptById(deptId);
if (d == null)
{
return false;
}
return isFirstLevelDept(d) || isSecondLevelDept(d);
}
@Override
@ -518,9 +518,6 @@ public class SysDeptServiceImpl implements ISysDeptService
throw new ServiceException("一级部门仅允许修改名称");
}
newDept.setAncestors(oldDept.getAncestors());
newDept.setByteApiKey(oldDept.getByteApiKey());
newDept.setProject(oldDept.getProject());
newDept.setModelParm(oldDept.getModelParm());
}
/**

View File

@ -86,6 +86,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="countAiUserByDeptId" resultType="int">
select count(1) from ai_user where del_flag = '0' and dept_id = #{deptId}
</select>
<select id="selectAiUserIdsByDeptId" resultType="java.lang.Long">
select id from ai_user where del_flag = '0' and dept_id = #{deptId}
</select>
<select id="selectPasswordById" resultType="java.lang.String">
select password from ai_user where id = #{id}
</select>

View File

@ -13,8 +13,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="leader" column="leader" />
<result property="phone" column="phone" />
<result property="email" column="email" />
<result property="byteApiKey" column="byte_api_key" />
<result property="modelParm" column="model_parm" />
<result property="status" column="status" />
<result property="delFlag" column="del_flag" />
<result property="parentName" column="parent_name" />
@ -28,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectDeptVo">
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.byte_api_key, d.status, d.del_flag, d.create_by, d.create_time, d.project, d.balance, d.max_user_count
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time, d.balance, d.max_user_count
from sys_dept d
</sql>
@ -64,7 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult">
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.byte_api_key, d.model_parm, d.status, d.project, d.balance, d.max_user_count,
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.balance, d.max_user_count,
(select dept_name from sys_dept where dept_id = d.parent_id) parent_name
from sys_dept d
where d.dept_id = #{deptId}
@ -80,7 +78,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectChildrenDeptById" parameterType="Long" resultMap="SysDeptResult">
select * from sys_dept where find_in_set(#{deptId}, ancestors)
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time, d.update_by, d.update_time, d.balance, d.max_user_count
from sys_dept d where find_in_set(#{deptId}, d.ancestors)
</select>
<select id="selectNormalChildrenDeptById" parameterType="Long" resultType="int">
@ -106,9 +105,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="leader != null and leader != ''">leader,</if>
<if test="phone != null and phone != ''">phone,</if>
<if test="email != null and email != ''">email,</if>
<if test="byteApiKey != null">byte_api_key,</if>
<if test="modelParm != null">model_parm,</if>
<if test="project != null">project,</if>
<if test="balance != null">balance,</if>
<if test="maxUserCount != null">max_user_count,</if>
<if test="status != null">status,</if>
@ -123,9 +119,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="leader != null and leader != ''">#{leader},</if>
<if test="phone != null and phone != ''">#{phone},</if>
<if test="email != null and email != ''">#{email},</if>
<if test="byteApiKey != null">#{byteApiKey},</if>
<if test="modelParm != null">#{modelParm},</if>
<if test="project != null">#{project},</if>
<if test="balance != null">#{balance},</if>
<if test="maxUserCount != null">#{maxUserCount},</if>
<if test="status != null">#{status},</if>
@ -144,9 +137,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="leader != null">leader = #{leader},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="email != null">email = #{email},</if>
<if test="byteApiKey != null">byte_api_key = #{byteApiKey},</if>
<if test="modelParm != null">model_parm = #{modelParm},</if>
<if test="project != null">project = #{project},</if>
<if test="balance != null">balance = #{balance},</if>
<if test="maxUserCount != null">max_user_count = #{maxUserCount},</if>
<if test="status != null and status != ''">status = #{status},</if>

View File

@ -0,0 +1,39 @@
-- =============================================================================
-- 部门火山配置迁表ai_dept_ark_config 唯一约束、数据搬迁、sys_dept 删除三列
-- 部署顺序:在已有 ai_dept_ark_config 与 sys_dept 火山列的前提下执行;
-- 可重复执行 INSERT 段NOT EXISTS 跳过已搬迁行)。
-- =============================================================================
-- 1) 一部门一行(若已存在同名索引请跳过本步)
ALTER TABLE `ai_dept_ark_config`
ADD UNIQUE INDEX `uk_dept_id` (`dept_id`);
-- 2) 从 sys_dept 搬迁至 ai_dept_ark_config仅一级、二级部门且三字段至少其一非空
INSERT INTO `ai_dept_ark_config` (`dept_id`, `model_parm`, `project`, `byte_api_key`, `create_time`)
SELECT
d.`dept_id`,
d.`model_parm`,
d.`project`,
d.`byte_api_key`,
NOW()
FROM `sys_dept` d
WHERE d.`del_flag` = '0'
AND d.`ancestors` IS NOT NULL
AND (
(d.`parent_id` = 0 AND d.`ancestors` = '0')
OR (CHAR_LENGTH(d.`ancestors`) - CHAR_LENGTH(REPLACE(d.`ancestors`, ',', ''))) = 1
)
AND (
NULLIF(TRIM(d.`byte_api_key`), '') IS NOT NULL
OR NULLIF(TRIM(d.`project`), '') IS NOT NULL
OR NULLIF(TRIM(d.`model_parm`), '') IS NOT NULL
)
AND NOT EXISTS (
SELECT 1 FROM `ai_dept_ark_config` a WHERE a.`dept_id` = d.`dept_id`
);
-- 3) 删除 sys_dept 上已迁移的火山列(搬迁完成后再执行)
ALTER TABLE `sys_dept`
DROP COLUMN `byte_api_key`,
DROP COLUMN `project`,
DROP COLUMN `model_parm`;