Compare commits

..

3 Commits

Author SHA1 Message Date
yys e516105893 Merge remote-tracking branch 'origin/seedance_balance' into seedance_balance
# Conflicts:
#	web-api/ruoyi-system/src/main/java/com/ruoyi/ai/mapper/AiUserMapper.java
#	web-api/ruoyi-system/src/main/resources/mapper/system/AiUserMapper.xml
2026-04-21 11:00:27 +08:00
yys 8d7cd70cfc feat: 火山配置从部门表独立出来,相关取值、缓存、保存重写逻辑 2026-04-21 10:59:03 +08:00
yys cb72d4edae feat: 调整AI用户、部门的界面显示 2026-04-21 10:17:25 +08:00
19 changed files with 557 additions and 294 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) { export function addDept(data) {
return request({ return request({

View File

@ -112,6 +112,7 @@
--> -->
<el-table-column label="用户昵称" align="center" prop="nickname" /> <el-table-column label="用户昵称" align="center" prop="nickname" />
<el-table-column label="归属部门" align="center" prop="deptName" width="120" show-overflow-tooltip /> <el-table-column label="归属部门" align="center" prop="deptName" width="120" show-overflow-tooltip />
<!--
<el-table-column label="邮箱" align="center" prop="email" /> <el-table-column label="邮箱" align="center" prop="email" />
<el-table-column label="性别" align="center" prop="gender"> <el-table-column label="性别" align="center" prop="gender">
<template slot-scope="scope"> <template slot-scope="scope">
@ -119,6 +120,7 @@
<el-tag v-else type="danger"></el-tag> <el-tag v-else type="danger"></el-tag>
</template> </template>
</el-table-column> </el-table-column>
-->
<el-table-column label="状态" align="center" key="status"> <el-table-column label="状态" align="center" key="status">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch <el-switch
@ -129,7 +131,9 @@
></el-switch> ></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<!--
<el-table-column label="邀请码" align="center" prop="invitationCode" /> <el-table-column label="邀请码" align="center" prop="invitationCode" />
-->
<el-table-column label="登录时间" align="center" prop="loginTime" width="150"> <el-table-column label="登录时间" align="center" prop="loginTime" width="150">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ parseTime(scope.row.loginTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.loginTime, '{y}-{m}-{d}') }}</span>

View File

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

View File

@ -42,7 +42,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 门户视频生成用户二级部门 byte_api_key 调用火山任务列表含库表与火山过滤列表 * 门户视频生成 {@code ai_user.dept_id} 对应 {@code ai_dept_ark_config} API Key 调用火山任务列表含库表与火山过滤列表
*/ */
@Api(tags = "门户-视频生成") @Api(tags = "门户-视频生成")
@RestController @RestController
@ -104,7 +104,7 @@ public class PortalVideoController extends BaseController {
} }
return out.isEmpty() ? null : out; return out.isEmpty() ? null : out;
} catch (Exception e) { } catch (Exception e) {
logger.warn("解析二级部门 model_parm 失败: {}", e.getMessage()); logger.warn("解析部门火山配置 model_parm 失败: {}", e.getMessage());
return null; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; 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.ai.service.IDeptChargeRefundService;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants; import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept; 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.DeptChargeRefundRequest;
import com.ruoyi.common.core.request.system.DeptPointsCorrectionRequest; import com.ruoyi.common.core.request.system.DeptPointsCorrectionRequest;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
@ -40,6 +43,9 @@ public class SysDeptController extends BaseController
@Autowired @Autowired
private IDeptChargeRefundService deptChargeRefundService; private IDeptChargeRefundService deptChargeRefundService;
@Autowired
private IAiDeptArkConfigService aiDeptArkConfigService;
/** /**
* 获取部门列表 * 获取部门列表
*/ */
@ -74,6 +80,46 @@ public class SysDeptController extends BaseController
return success(deptService.selectDeptById(deptId)); 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; private String parentName;
/** Byte API Key */
private String byteApiKey;
/** 门户视频模型列表 JSON[{"label":"…","value":"ep-…"},…] */
private String modelParm;
/** Byte project */
private String project;
/** 部门余额 */ /** 部门余额 */
private BigDecimal balance; private BigDecimal balance;
@ -187,36 +178,6 @@ public class SysDept extends BaseEntity
this.parentName = parentName; 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() public BigDecimal getBalance()
{ {
return balance; return balance;
@ -258,9 +219,6 @@ public class SysDept extends BaseEntity
.append("leader", getLeader()) .append("leader", getLeader())
.append("phone", getPhone()) .append("phone", getPhone())
.append("email", getEmail()) .append("email", getEmail())
.append("byteApiKey", getByteApiKey())
.append("modelParm", getModelParm())
.append("project", getProject())
.append("balance", getBalance()) .append("balance", getBalance())
.append("maxUserCount", getMaxUserCount()) .append("maxUserCount", getMaxUserCount())
.append("status", getStatus()) .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 */ /** $column.columnComment */
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
private String id; private Long id;
/** 部门ID */ /** 部门ID */
@Excel(name = "部门ID") @Excel(name = "部门ID")

View File

@ -26,6 +26,9 @@ public interface AiUserMapper extends BaseMapper<AiUser> {
int countAiUserByDeptId(@Param("deptId") Long deptId); int countAiUserByDeptId(@Param("deptId") Long deptId);
/** 归属指定部门的门户用户主键(用于火山 API Key 缓存失效) */
List<Long> selectAiUserIdsByDeptId(@Param("deptId") Long deptId);
/** /**
* 部门下启用中的 AI 用户未删除且 status=0 * 部门下启用中的 AI 用户未删除且 status=0
*/ */

View File

@ -21,6 +21,21 @@ public interface IAiDeptArkConfigService {
*/ */
AiDeptArkConfig selectAiDeptArkConfigById(String id); 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; 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 { public interface IByteDeptApiKeyService {
@ -11,13 +11,18 @@ public interface IByteDeptApiKeyService {
String resolveVolcApiKey(Long aiUserId); String resolveVolcApiKey(Long aiUserId);
/** /**
* 当前 AI 用户所属二级部门 {@code sys_dept.dept_id} {@link #resolveVolcApiKey(Long)} 中使用的二级节点一致 * 当前 AI 用户归属部门 {@code ai_user.dept_id}在仅一级二级部门树前提下与历史二级部门语义对齐对外方法名保留
* 用户未分配部门或部门数据缺失时返回 null * 用户未分配部门或部门数据缺失时返回 null
*/ */
Long resolveSecondLevelDeptId(Long aiUserId); Long resolveSecondLevelDeptId(Long aiUserId);
/** /**
* 当前用户所属二级部门 {@code sys_dept.model_parm}JSON 模型列表未配置或为空时返回 null * 当前用户归属部门在 {@code ai_dept_ark_config.model_parm} JSON 模型列表未配置或为空时返回 null
*/ */
String resolveSecondLevelModelParm(Long aiUserId); 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; package com.ruoyi.ai.service.impl;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.DateUtils;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.ai.mapper.AiDeptArkConfigMapper;
import com.ruoyi.ai.domain.AiDeptArkConfig;
import com.ruoyi.ai.service.IAiDeptArkConfigService;
/** /**
* 团队部门对应火山引擎配置Service业务层处理 * 团队部门对应火山引擎配置Service业务层处理
@ -22,92 +29,208 @@ import com.ruoyi.ai.service.IAiDeptArkConfigService;
@Service @Service
public class AiDeptArkConfigServiceImpl implements IAiDeptArkConfigService { public class AiDeptArkConfigServiceImpl implements IAiDeptArkConfigService {
private static final String BYTE_API_KEY_CACHE_SUFFIX = "_byte_api_key";
@Autowired @Autowired
private AiDeptArkConfigMapper aiDeptArkConfigMapper; private AiDeptArkConfigMapper aiDeptArkConfigMapper;
/** @Autowired
* 查询团队部门对应火山引擎配置 private EncryptionService encryptionService;
*
* @param id 团队部门对应火山引擎配置主键 @Autowired
* @return 团队部门对应火山引擎配置 private RedisCache redisCache;
*/
@Autowired
private AiUserMapper aiUserMapper;
@Override @Override
public AiDeptArkConfig selectAiDeptArkConfigById(String id) { 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 @Override
public List<AiDeptArkConfig> selectAiDeptArkConfigList(AiDeptArkConfig aiDeptArkConfig) { public List<AiDeptArkConfig> selectAiDeptArkConfigList(AiDeptArkConfig aiDeptArkConfig) {
LambdaQueryWrapper<AiDeptArkConfig> query = Wrappers.lambdaQuery(aiDeptArkConfig); LambdaQueryWrapper<AiDeptArkConfig> query = Wrappers.lambdaQuery(aiDeptArkConfig);
query.orderByDesc(AiDeptArkConfig::getId); 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 @Override
public IPage<AiDeptArkConfig> selectAiDeptArkConfigPage(Page page, AiDeptArkConfig aiDeptArkConfig) { public IPage<AiDeptArkConfig> selectAiDeptArkConfigPage(Page page, AiDeptArkConfig aiDeptArkConfig) {
LambdaQueryWrapper<AiDeptArkConfig> query = Wrappers.lambdaQuery(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 @Override
public int insertAiDeptArkConfig(AiDeptArkConfig aiDeptArkConfig) { public int insertAiDeptArkConfig(AiDeptArkConfig aiDeptArkConfig) {
encryptSensitiveForStore(aiDeptArkConfig);
aiDeptArkConfig.setCreateBy(SecurityUtils.getUsername()); aiDeptArkConfig.setCreateBy(SecurityUtils.getUsername());
aiDeptArkConfig.setCreateTime(DateUtils.getNowDate()); 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 @Override
public int updateAiDeptArkConfig(AiDeptArkConfig aiDeptArkConfig) { public int updateAiDeptArkConfig(AiDeptArkConfig aiDeptArkConfig) {
encryptSensitiveForStore(aiDeptArkConfig);
aiDeptArkConfig.setUpdateBy(SecurityUtils.getUsername()); aiDeptArkConfig.setUpdateBy(SecurityUtils.getUsername());
aiDeptArkConfig.setUpdateTime(DateUtils.getNowDate()); 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 @Override
public int deleteAiDeptArkConfigByIds(String[] ids) public int deleteAiDeptArkConfigByIds(String[] ids) {
{ if (ids == null || ids.length == 0) {
return aiDeptArkConfigMapper.deleteBatchIds(java.util.Arrays.asList(ids)); 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 @Override
public int deleteAiDeptArkConfigById(String id) public int deleteAiDeptArkConfigById(String id) {
{ if (StringUtils.isEmpty(id)) {
return aiDeptArkConfigMapper.deleteById(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.annotation.JsonInclude;
import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.*;
import com.ruoyi.ai.domain.AiDeptArkConfig;
import com.ruoyi.ai.mapper.AiUserMapper; 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.AiUser;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.OkHttpUtils; import com.ruoyi.common.utils.http.OkHttpUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -34,10 +34,7 @@ public class BaseByteApiService {
@Resource @Resource
private AiUserMapper userMapper; private AiUserMapper userMapper;
@Resource @Resource
private SysDeptMapper deptMapper; private IAiDeptArkConfigService aiDeptArkConfigService;
@Resource
private EncryptionService encryptionService;
protected String DEPT_ANCESTORS_SPLIT = ",";
@Value("${volcengine.ak}") @Value("${volcengine.ak}")
protected String accessKeyId; protected String accessKeyId;
@Value("${volcengine.sk}") @Value("${volcengine.sk}")
@ -70,7 +67,7 @@ public class BaseByteApiService {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
/** /**
* 根据用户找到对应的project * 根据门户用户 {@code ai_user.dept_id} 对应 {@link AiDeptArkConfig#getProject()}解密后解析 project
*/ */
protected String getUserProject() { protected String getUserProject() {
Long userId = SecurityUtils.getAiUserId(); Long userId = SecurityUtils.getAiUserId();
@ -78,39 +75,14 @@ public class BaseByteApiService {
return null; return null;
} }
AiUser user = userMapper.selectAiUserById(userId); AiUser user = userMapper.selectAiUserById(userId);
if (user == null) { if (user == null || user.getDeptId() == null) {
return null; return null;
} }
// 第二层部门IDAPI_KEY放在这里 AiDeptArkConfig ark = aiDeptArkConfigService.selectDecryptedByDeptId(user.getDeptId());
Long secondLvDeptId = getSecondLevelDept(user.getDeptId()); if (ark == null || StringUtils.isEmpty(ark.getProject())) {
if (secondLvDeptId == null) {
return null; return null;
} }
SysDept secondDept = deptMapper.selectDeptById(secondLvDeptId); return ark.getProject().trim();
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]);
} }
public <R> R callApi(String action, Object request, Class<R> responseClass) throws IOException { 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; 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.IAiUserService;
import com.ruoyi.ai.service.IByteDeptApiKeyService; import com.ruoyi.ai.service.IByteDeptApiKeyService;
import com.ruoyi.common.EncryptionService; import com.ruoyi.common.EncryptionService;
@ -14,19 +17,22 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Service @Service
public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService { public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
@Resource private static final String BYTE_API_KEY_CACHE_SUFFIX = "_byte_api_key";
private EncryptionService encryptionService;
private static final String NO_DEPT_MSG = "用户未分配部门:请在后台为门户用户设置 ai_user.dept_id关联 sys_dept.dept_id"; 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_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; private static final int CACHE_HOURS = 1;
@Resource
private EncryptionService encryptionService;
@Autowired @Autowired
private RedisCache redisCache; private RedisCache redisCache;
@ -36,15 +42,20 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
@Autowired @Autowired
private ISysDeptService sysDeptService; private ISysDeptService sysDeptService;
@Autowired
private IAiDeptArkConfigService aiDeptArkConfigService;
@Autowired
private AiUserMapper aiUserMapper;
@Override @Override
public String resolveVolcApiKey(Long aiUserId) { public String resolveVolcApiKey(Long aiUserId) {
if (aiUserId == null) { if (aiUserId == null) {
throw new ServiceException(NO_DEPT_MSG); 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); String cached = redisCache.getCacheObject(cacheKey);
if (StringUtils.isNotEmpty(cached)) { if (StringUtils.isNotEmpty(cached)) {
// 解密缓存保存加密后的字符串比较安全
return encryptionService.decode(cached); return encryptionService.decode(cached);
} }
AiUser aiUser = aiUserService.selectAiUserById(aiUserId); AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
@ -55,21 +66,11 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
if (userDept == null) { if (userDept == null) {
throw new ServiceException(NO_DEPT_ROW_MSG); throw new ServiceException(NO_DEPT_ROW_MSG);
} }
// 优先使用用户直接归属部门的 Key多数业务把用户挂在分公司 101并在该节点配 byte_api_key AiDeptArkConfig ark = aiDeptArkConfigService.selectDecryptedByDeptId(aiUser.getDeptId());
String apiKey = trimKey(userDept.getByteApiKey()); String apiKey = ark != null ? trimKey(ark.getByteApiKey()) : null;
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());
}
}
}
if (StringUtils.isEmpty(apiKey)) { if (StringUtils.isEmpty(apiKey)) {
throw new ServiceException(NO_API_KEY_MSG); throw new ServiceException(NO_API_KEY_MSG);
} }
// 缓存中保存加密值
String encodeApiKey = encryptionService.encode(apiKey); String encodeApiKey = encryptionService.encode(apiKey);
redisCache.setCacheObject(cacheKey, encodeApiKey, CACHE_HOURS, TimeUnit.HOURS); redisCache.setCacheObject(cacheKey, encodeApiKey, CACHE_HOURS, TimeUnit.HOURS);
return apiKey; return apiKey;
@ -84,59 +85,46 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
if (aiUser == null || aiUser.getDeptId() == null) { if (aiUser == null || aiUser.getDeptId() == null) {
return null; return null;
} }
SysDept userDept = sysDeptService.selectDeptById(aiUser.getDeptId()); return aiUser.getDeptId();
if (userDept == null) {
return null;
}
return secondLevelDeptIdFrom(userDept);
} }
@Override @Override
public String resolveSecondLevelModelParm(Long aiUserId) { public String resolveSecondLevelModelParm(Long aiUserId) {
Long secondId = resolveSecondLevelDeptId(aiUserId); if (aiUserId == null) {
if (secondId == null) {
return null; return null;
} }
SysDept second = sysDeptService.selectDeptById(secondId); AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
if (second == null) { if (aiUser == null || aiUser.getDeptId() == null) {
return 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)) { if (StringUtils.isEmpty(raw)) {
return null; return null;
} }
return raw.trim(); 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) { private static String trimKey(String raw) {
return raw == null ? null : raw.trim(); 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); void editScore(DeptPointsCorrectionRequest request);
/**
* 是否允许配置火山引擎一级或二级部门与仅两级部门树一致
*/
boolean isArkConfigurableDept(Long deptId);
} }

View File

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

View File

@ -87,12 +87,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select count(1) from ai_user where del_flag = '0' and dept_id = #{deptId} select count(1) from ai_user where del_flag = '0' and dept_id = #{deptId}
</select> </select>
<<<<<<< HEAD
<select id="selectAiUserIdsByDeptId" resultType="java.lang.Long">
select id from ai_user where del_flag = '0' and dept_id = #{deptId}
=======
<select id="countNormalAiUsersByDeptId" resultType="int"> <select id="countNormalAiUsersByDeptId" resultType="int">
select count(1) from ai_user select count(1) from ai_user
where del_flag = '0' and status = 0 and dept_id = #{deptId} where del_flag = '0' and status = 0 and dept_id = #{deptId}
<if test="excludeUserId != null"> <if test="excludeUserId != null">
and id != #{excludeUserId} and id != #{excludeUserId}
</if> </if>
>>>>>>> origin/seedance_balance
</select> </select>
<select id="selectPasswordById" resultType="java.lang.String"> <select id="selectPasswordById" resultType="java.lang.String">
select password from ai_user where id = #{id} select password from ai_user where id = #{id}

View File

@ -13,8 +13,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="leader" column="leader" /> <result property="leader" column="leader" />
<result property="phone" column="phone" /> <result property="phone" column="phone" />
<result property="email" column="email" /> <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="status" column="status" />
<result property="delFlag" column="del_flag" /> <result property="delFlag" column="del_flag" />
<result property="parentName" column="parent_name" /> <result property="parentName" column="parent_name" />
@ -28,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap> </resultMap>
<sql id="selectDeptVo"> <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 from sys_dept d
</sql> </sql>
@ -64,7 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult"> <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 (select dept_name from sys_dept where dept_id = d.parent_id) parent_name
from sys_dept d from sys_dept d
where d.dept_id = #{deptId} where d.dept_id = #{deptId}
@ -80,7 +78,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectChildrenDeptById" parameterType="Long" resultMap="SysDeptResult"> <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>
<select id="selectNormalChildrenDeptById" parameterType="Long" resultType="int"> <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="leader != null and leader != ''">leader,</if>
<if test="phone != null and phone != ''">phone,</if> <if test="phone != null and phone != ''">phone,</if>
<if test="email != null and email != ''">email,</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="balance != null">balance,</if>
<if test="maxUserCount != null">max_user_count,</if> <if test="maxUserCount != null">max_user_count,</if>
<if test="status != null">status,</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="leader != null and leader != ''">#{leader},</if>
<if test="phone != null and phone != ''">#{phone},</if> <if test="phone != null and phone != ''">#{phone},</if>
<if test="email != null and email != ''">#{email},</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="balance != null">#{balance},</if>
<if test="maxUserCount != null">#{maxUserCount},</if> <if test="maxUserCount != null">#{maxUserCount},</if>
<if test="status != null">#{status},</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="leader != null">leader = #{leader},</if>
<if test="phone != null">phone = #{phone},</if> <if test="phone != null">phone = #{phone},</if>
<if test="email != null">email = #{email},</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="balance != null">balance = #{balance},</if>
<if test="maxUserCount != null">max_user_count = #{maxUserCount},</if> <if test="maxUserCount != null">max_user_count = #{maxUserCount},</if>
<if test="status != null and status != ''">status = #{status},</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`;