fix: 端点问题调整

This commit is contained in:
old burden 2026-04-03 16:14:52 +08:00
parent eb8cab3f5a
commit c2c1ff8063
13 changed files with 491 additions and 32 deletions

View File

@ -5,7 +5,7 @@ VUE_APP_TITLE = 管理系统
ENV = 'production' ENV = 'production'
# 若依管理系统/生产环境 # 若依管理系统/生产环境
VUE_APP_BASE_API = 'http://111.230.37.169:10009' # VUE_APP_BASE_API = 'http://111.230.37.169:10009'
# VUE_APP_BASE_API = 'http://101.96.201.225:8011' # VUE_APP_BASE_API = 'http://101.96.201.225:8011'
# VUE_APP_BASE_API = 'http://47.86.170.114:8011' VUE_APP_BASE_API = 'http://47.86.170.114:8011'

View File

@ -96,7 +96,7 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body> <el-dialog :title="title" :visible.sync="open" width="760px" 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,6 +160,41 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row v-if="isSecondLevelCompanyForm">
<el-col :span="24">
<el-form-item label="视频模型">
<div class="model-parm-block">
<div
v-for="(row, idx) in modelParamRows"
:key="'mp-' + idx"
class="model-parm-row"
>
<el-input
v-model="row.label"
placeholder="显示名称(如 Seedance 2.0"
class="model-parm-input-label"
/>
<el-input
v-model="row.value"
placeholder="Endpoint / 模型 IDep-…)"
class="model-parm-input-value"
/>
<el-button
type="text"
icon="el-icon-delete"
:disabled="modelParamRows.length <= 1"
@click="removeModelParamRow(idx)"
/>
</div>
<el-button type="text" icon="el-icon-plus" @click="addModelParamRow">添加模型</el-button>
<p class="model-parm-hint">
保存为 JSON 写入库表 model_parm门户视频生成按用户所属二级部门读取
留空或未配置时使用配置文件 portal.video.models
</p>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button> <el-button type="primary" @click="submitForm"> </el-button>
@ -169,6 +204,31 @@
</div> </div>
</template> </template>
<style scoped>
.model-parm-block {
width: 100%;
}
.model-parm-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.model-parm-input-label {
width: 38%;
margin-right: 8px;
}
.model-parm-input-value {
flex: 1;
margin-right: 8px;
}
.model-parm-hint {
margin: 8px 0 0;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
</style>
<script> <script>
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/ai/dept" import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/ai/dept"
import Treeselect from "@riophae/vue-treeselect" import Treeselect from "@riophae/vue-treeselect"
@ -193,6 +253,7 @@ export default {
status: undefined status: undefined
}, },
form: {}, form: {},
modelParamRows: [{ label: '', value: '' }],
rules: { rules: {
parentId: [ parentId: [
{ required: true, message: "上级部门不能为空", trigger: "blur" } { required: true, message: "上级部门不能为空", trigger: "blur" }
@ -268,8 +329,10 @@ export default {
phone: undefined, phone: undefined,
email: undefined, email: undefined,
byteApiKey: undefined, byteApiKey: undefined,
modelParm: undefined,
status: "0" status: "0"
} }
this.modelParamRows = [{ label: '', value: '' }]
this.resetForm("form") this.resetForm("form")
}, },
handleQuery() { handleQuery() {
@ -297,10 +360,45 @@ export default {
this.refreshTable = true this.refreshTable = true
}) })
}, },
syncModelRowsFromForm() {
this.modelParamRows = [{ label: '', value: '' }]
const raw = this.form.modelParm
if (!raw || String(raw).trim() === '') {
return
}
try {
const arr = JSON.parse(raw)
if (Array.isArray(arr) && arr.length) {
this.modelParamRows = arr.map(x => ({
label: (x && x.label) ? String(x.label) : '',
value: (x && x.value) ? String(x.value) : ''
}))
}
} catch (e) {
this.modelParamRows = [{ label: '', value: '' }]
}
},
buildModelParmPayload() {
if (!this.isSecondLevelCompanyForm) {
return
}
const rows = (this.modelParamRows || []).filter(r => r.label && r.value)
this.form.modelParm = rows.length ? JSON.stringify(rows) : ''
},
addModelParamRow() {
this.modelParamRows.push({ label: '', value: '' })
},
removeModelParamRow(idx) {
if (this.modelParamRows.length <= 1) {
return
}
this.modelParamRows.splice(idx, 1)
},
handleUpdate(row) { handleUpdate(row) {
this.reset() this.reset()
getDept(row.deptId).then(response => { getDept(row.deptId).then(response => {
this.form = response.data this.form = response.data
this.syncModelRowsFromForm()
this.open = true this.open = true
this.title = "修改部门" this.title = "修改部门"
listDeptExcludeChild(row.deptId).then(response => { listDeptExcludeChild(row.deptId).then(response => {
@ -315,6 +413,7 @@ 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) { if (this.form.deptId != undefined) {
updateDept(this.form).then(response => { updateDept(this.form).then(response => {
this.$modal.msgSuccess("修改成功") this.$modal.msgSuccess("修改成功")

View File

@ -97,7 +97,7 @@
</el-table> </el-table>
<!-- 添加或修改部门对话框 --> <!-- 添加或修改部门对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body> <el-dialog :title="title" :visible.sync="open" width="760px" 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">
@ -174,6 +174,41 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row v-if="isSecondLevelCompanyForm">
<el-col :span="24">
<el-form-item label="视频模型">
<div class="model-parm-block">
<div
v-for="(row, idx) in modelParamRows"
:key="'mp-' + idx"
class="model-parm-row"
>
<el-input
v-model="row.label"
placeholder="显示名称(如 Seedance 2.0"
class="model-parm-input-label"
/>
<el-input
v-model="row.value"
placeholder="Endpoint / 模型 IDep-…)"
class="model-parm-input-value"
/>
<el-button
type="text"
icon="el-icon-delete"
:disabled="modelParamRows.length <= 1"
@click="removeModelParamRow(idx)"
/>
</div>
<el-button type="text" icon="el-icon-plus" @click="addModelParamRow">添加模型</el-button>
<p class="model-parm-hint">
保存为 JSON 写入 model_parm门户视频生成按用户所属二级部门读取
留空则使用 portal.video.models
</p>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button> <el-button type="primary" @click="submitForm"> </el-button>
@ -183,6 +218,31 @@
</div> </div>
</template> </template>
<style scoped>
.model-parm-block {
width: 100%;
}
.model-parm-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.model-parm-input-label {
width: 38%;
margin-right: 8px;
}
.model-parm-input-value {
flex: 1;
margin-right: 8px;
}
.model-parm-hint {
margin: 8px 0 0;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
</style>
<script> <script>
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept" import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
import Treeselect from "@riophae/vue-treeselect" import Treeselect from "@riophae/vue-treeselect"
@ -217,6 +277,7 @@ export default {
}, },
// //
form: {}, form: {},
modelParamRows: [{ label: '', value: '' }],
// //
rules: { rules: {
parentId: [ parentId: [
@ -262,6 +323,40 @@ export default {
} }
}, },
methods: { methods: {
syncModelRowsFromForm() {
this.modelParamRows = [{ label: '', value: '' }]
const raw = this.form.modelParm
if (!raw || String(raw).trim() === '') {
return
}
try {
const arr = JSON.parse(raw)
if (Array.isArray(arr) && arr.length) {
this.modelParamRows = arr.map(x => ({
label: (x && x.label) ? String(x.label) : '',
value: (x && x.value) ? String(x.value) : ''
}))
}
} catch (e) {
this.modelParamRows = [{ label: '', value: '' }]
}
},
buildModelParmPayload() {
if (!this.isSecondLevelCompanyForm) {
return
}
const rows = (this.modelParamRows || []).filter(r => r.label && r.value)
this.form.modelParm = rows.length ? JSON.stringify(rows) : ''
},
addModelParamRow() {
this.modelParamRows.push({ label: '', value: '' })
},
removeModelParamRow(idx) {
if (this.modelParamRows.length <= 1) {
return
}
this.modelParamRows.splice(idx, 1)
},
/** 查询部门列表 */ /** 查询部门列表 */
getList() { getList() {
this.loading = true this.loading = true
@ -298,8 +393,11 @@ export default {
phone: undefined, phone: undefined,
email: undefined, email: undefined,
byteApiKey: undefined, byteApiKey: undefined,
project: undefined,
modelParm: undefined,
status: "0" status: "0"
} }
this.modelParamRows = [{ label: '', value: '' }]
this.resetForm("form") this.resetForm("form")
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
@ -336,6 +434,7 @@ export default {
this.reset() this.reset()
getDept(row.deptId).then(response => { getDept(row.deptId).then(response => {
this.form = response.data this.form = response.data
this.syncModelRowsFromForm()
this.open = true this.open = true
this.title = "修改部门" this.title = "修改部门"
listDeptExcludeChild(row.deptId).then(response => { listDeptExcludeChild(row.deptId).then(response => {
@ -351,6 +450,7 @@ 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) { if (this.form.deptId != undefined) {
updateDept(this.form).then(response => { updateDept(this.form).then(response => {
this.$modal.msgSuccess("修改成功") this.$modal.msgSuccess("修改成功")

View File

@ -468,7 +468,7 @@ export default {
const res = await this.$axios({ const res = await this.$axios({
url: 'api/portal/video/tasks', url: 'api/portal/video/tasks',
method: 'GET', method: 'GET',
data: { pageNum, pageSize } params: { pageNum, pageSize }
}) })
const newRows = res.code === 200 ? res.rows || [] : [] const newRows = res.code === 200 ? res.rows || [] : []
@ -542,7 +542,7 @@ export default {
const res = await this.$axios({ const res = await this.$axios({
url: 'api/portal/video/tasks', url: 'api/portal/video/tasks',
method: 'GET', method: 'GET',
data: { pageNum: 1, pageSize: this.chatPageSize } params: { pageNum: 1, pageSize: this.chatPageSize }
}) })
const newRows = res.code === 200 ? res.rows || [] : [] const newRows = res.code === 200 ? res.rows || [] : []
const existMap = new Map((this.taskRows || []).map((x) => [x.id, x])) const existMap = new Map((this.taskRows || []).map((x) => [x.id, x]))
@ -581,18 +581,31 @@ export default {
}, },
taskRowResultTrim(row) { taskRowResultTrim(row) {
return String(row?.result ?? '').trim() let r = String(row?.result ?? '').trim()
if (r) return r
const raw = row?.videoParams
if (raw == null || raw === '') return ''
if (typeof raw !== 'string') return ''
try {
const o = JSON.parse(raw)
const id = o?.volcTaskId
if (id != null && String(id).trim() !== '') return String(id).trim()
} catch (_) {
/* ignore */
}
return ''
}, },
taskStatusText(row) { taskStatusText(row) {
if (row.status === 2) return '已失败/已取消' const st = row?.status
if (row.status === 0) return '执行中' if (st === 2 || st === '2') return '已失败/已取消'
if (row.status === 1) { if (st === 0 || st === '0') return '执行中'
if (st === 1 || st === '1') {
const r = this.taskRowResultTrim(row) const r = this.taskRowResultTrim(row)
if (!r) return '失败' if (!r) return '失败'
if (this.isHttpOrHttpsUrl(r)) return '已完成' if (this.isHttpOrHttpsUrl(r)) return '已完成'
if (/^cgt/i.test(r)) return '任务执行中' // id cgt URL
return '任务生成失败' return '任务执行中'
} }
return '执行中' return '执行中'
}, },
@ -600,17 +613,17 @@ export default {
/** 是否展示完整「结果区」(视频 / 链接);其余状态仅紧凑展示在用户行右侧 */ /** 是否展示完整「结果区」(视频 / 链接);其余状态仅紧凑展示在用户行右侧 */
isChatRowSuccessWithMedia(row) { isChatRowSuccessWithMedia(row) {
const r = this.taskRowResultTrim(row) const r = this.taskRowResultTrim(row)
return row.status === 1 && !!r && this.isHttpOrHttpsUrl(r) const st = row?.status
return (st === 1 || st === '1') && !!r && this.isHttpOrHttpsUrl(r)
}, },
chatRowInlineStatusClass(row) { chatRowInlineStatusClass(row) {
if (row.status === 2) return 'vg-chat-inline-status--failed' const st = row?.status
if (row.status === 1) { if (st === 2 || st === '2') return 'vg-chat-inline-status--failed'
if (st === 1 || st === '1') {
const r = this.taskRowResultTrim(row) const r = this.taskRowResultTrim(row)
if (!r) return 'vg-chat-inline-status--failed' if (!r) return 'vg-chat-inline-status--failed'
if (/^cgt/i.test(r)) return 'vg-chat-inline-status--running' return 'vg-chat-inline-status--running'
if (this.isHttpOrHttpsUrl(r)) return 'vg-chat-inline-status--running'
return 'vg-chat-inline-status--failed'
} }
return 'vg-chat-inline-status--running' return 'vg-chat-inline-status--running'
}, },

View File

@ -174,7 +174,8 @@ public class ByteApiController extends BaseController {
aiOrder.setImg1(firstUrl.toString()); aiOrder.setImg1(firstUrl.toString());
ByteBodyReq byteBodyReq = new ByteBodyReq(); ByteBodyReq byteBodyReq = new ByteBodyReq();
byteBodyReq.setModel("ep-20251104104536-2gpgz"); byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ?
request.getModel() : "ep-20260326165811-dlkth");
byteBodyReq.setPrompt(text); byteBodyReq.setPrompt(text);
byteBodyReq.setImage(firstUrl); byteBodyReq.setImage(firstUrl);
byteBodyReq.setSequential_image_generation("disabled"); byteBodyReq.setSequential_image_generation("disabled");

View File

@ -1,8 +1,8 @@
package com.ruoyi.api; package com.ruoyi.api;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.ruoyi.ai.domain.AiOrder; import com.ruoyi.ai.domain.AiOrder;
@ -72,8 +72,77 @@ public class PortalVideoController extends BaseController {
return "21"; return "21";
} }
private void applyOptionalParams(ByteBodyReq body, PortalVideoGenRequest req) { private List<PortalVideoProperties.ModelOption> loadModelOptionsForAiUser(Long aiUserId, Long secondDeptId) {
PortalVideoProperties.Defaults d = portalVideoProperties.getDefaults(); String json = byteDeptApiKeyService.resolveSecondLevelModelParm(aiUserId);
List<PortalVideoProperties.ModelOption> fromDb = parseModelParmJson(json);
if (fromDb != null && !fromDb.isEmpty()) {
return fromDb;
}
return portalVideoProperties.resolveModelsForSecondDept(secondDeptId);
}
private List<PortalVideoProperties.ModelOption> parseModelParmJson(String json) {
if (StringUtils.isEmpty(json)) {
return null;
}
try {
List<PortalVideoProperties.ModelOption> list = OM.readValue(json.trim(),
new TypeReference<List<PortalVideoProperties.ModelOption>>() { });
if (list == null || list.isEmpty()) {
return null;
}
List<PortalVideoProperties.ModelOption> out = new ArrayList<>();
for (PortalVideoProperties.ModelOption o : list) {
if (o != null && StringUtils.isNotEmpty(o.getLabel()) && StringUtils.isNotEmpty(o.getValue())) {
out.add(o);
}
}
return out.isEmpty() ? null : out;
} catch (Exception e) {
logger.warn("解析二级部门 model_parm 失败: {}", e.getMessage());
return null;
}
}
private static boolean isModelInOptions(String modelId, List<PortalVideoProperties.ModelOption> options) {
if (modelId == null || modelId.isEmpty() || options == null) {
return false;
}
for (PortalVideoProperties.ModelOption o : options) {
if (modelId.equals(o.getValue())) {
return true;
}
}
return false;
}
private static void alignDefaultsModelToOptions(PortalVideoProperties.Defaults defs, List<PortalVideoProperties.ModelOption> models) {
if (defs == null || models == null || models.isEmpty()) {
return;
}
String m = defs.getModel();
if (m == null || m.isEmpty()) {
defs.setModel(models.get(0).getValue());
return;
}
if (!isModelInOptions(m, models)) {
defs.setModel(models.get(0).getValue());
}
}
private String resolveDefaultModelForAiUser(Long aiUserId, Long secondDeptId, List<PortalVideoProperties.ModelOption> allowed) {
PortalVideoProperties.Defaults d = portalVideoProperties.resolveEffectiveDefaults(secondDeptId);
if (d.getModel() != null && !d.getModel().isEmpty() && isModelInOptions(d.getModel(), allowed)) {
return d.getModel();
}
if (!allowed.isEmpty() && StringUtils.isNotEmpty(allowed.get(0).getValue())) {
return allowed.get(0).getValue();
}
return null;
}
private void applyOptionalParams(ByteBodyReq body, PortalVideoGenRequest req, Long secondDeptId) {
PortalVideoProperties.Defaults d = portalVideoProperties.resolveEffectiveDefaults(secondDeptId);
body.setDuration(req.getDuration() != null ? req.getDuration() : d.getDuration()); body.setDuration(req.getDuration() != null ? req.getDuration() : d.getDuration());
body.setResolution(StringUtils.isNotEmpty(req.getResolution()) ? req.getResolution() : d.getResolution()); body.setResolution(StringUtils.isNotEmpty(req.getResolution()) ? req.getResolution() : d.getResolution());
body.setRatio(StringUtils.isNotEmpty(req.getRatio()) ? req.getRatio() : d.getRatio()); body.setRatio(StringUtils.isNotEmpty(req.getRatio()) ? req.getRatio() : d.getRatio());
@ -83,14 +152,20 @@ public class PortalVideoController extends BaseController {
} }
private ByteBodyReq newVideoBody(PortalVideoGenRequest req, List<ContentItem> content) { private ByteBodyReq newVideoBody(PortalVideoGenRequest req, List<ContentItem> content) {
String modelId = StringUtils.isNotEmpty(req.getModel()) ? req.getModel() : portalVideoProperties.resolveDefaultModelId(); Long uid = SecurityUtils.getAiUserId();
Long secondDeptId = byteDeptApiKeyService.resolveSecondLevelDeptId(uid);
List<PortalVideoProperties.ModelOption> allowed = loadModelOptionsForAiUser(uid, secondDeptId);
String modelId = StringUtils.isNotEmpty(req.getModel()) ? req.getModel() : resolveDefaultModelForAiUser(uid, secondDeptId, allowed);
if (StringUtils.isEmpty(modelId)) { if (StringUtils.isEmpty(modelId)) {
throw new ServiceException("未配置门户视频模型,请在 application.yml 的 portal.video 中配置 models 与 defaults.model"); throw new ServiceException("未配置门户视频模型:请在后台为该用户所属二级部门配置 model_parm或在 portal.video.models 中配置全局列表");
}
if (StringUtils.isNotEmpty(req.getModel()) && !isModelInOptions(modelId, allowed)) {
throw new ServiceException("所选模型对当前部门不可用");
} }
ByteBodyReq body = new ByteBodyReq(); ByteBodyReq body = new ByteBodyReq();
body.setModel(modelId); body.setModel(modelId);
body.setContent(content); body.setContent(content);
applyOptionalParams(body, req); applyOptionalParams(body, req, secondDeptId);
return body; return body;
} }
@ -405,9 +480,14 @@ public class PortalVideoController extends BaseController {
@GetMapping("/options") @GetMapping("/options")
@ApiOperation("门户视频生成可选参数(模型/比例/时长等,来自配置)") @ApiOperation("门户视频生成可选参数(模型/比例/时长等,来自配置)")
public AjaxResult videoParamOptions() { public AjaxResult videoParamOptions() {
Long uid = SecurityUtils.getAiUserId();
Long secondDeptId = byteDeptApiKeyService.resolveSecondLevelDeptId(uid);
List<PortalVideoProperties.ModelOption> models = loadModelOptionsForAiUser(uid, secondDeptId);
PortalVideoProperties.Defaults defs = portalVideoProperties.resolveEffectiveDefaults(secondDeptId);
alignDefaultsModelToOptions(defs, models);
Map<String, Object> data = new LinkedHashMap<>(); Map<String, Object> data = new LinkedHashMap<>();
data.put("defaults", portalVideoProperties.getDefaults()); data.put("defaults", defs);
data.put("models", portalVideoProperties.getModels()); data.put("models", models);
data.put("ratios", portalVideoProperties.getRatios()); data.put("ratios", portalVideoProperties.getRatios());
data.put("durations", portalVideoProperties.getDurations()); data.put("durations", portalVideoProperties.getDurations());
data.put("resolutions", portalVideoProperties.getResolutions()); data.put("resolutions", portalVideoProperties.getResolutions());

View File

@ -4,7 +4,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 门户视频生成参数模型比例时长分辨率等从配置读取不在代码中写死业务默认值 * 门户视频生成参数模型比例时长分辨率等从配置读取不在代码中写死业务默认值
@ -17,6 +19,16 @@ public class PortalVideoProperties {
private List<ModelOption> models = new ArrayList<>(); private List<ModelOption> models = new ArrayList<>();
/**
* 按二级部门 {@code dept_id}字符串 {@code "101"}配置专属模型列表未命中时使用全局 {@link #models}
*/
private Map<String, List<ModelOption>> modelsBySecondDept = new LinkedHashMap<>();
/**
* 按二级部门覆盖 {@link #defaults} 中的字段仅配置需要覆盖的项即可
*/
private Map<String, Defaults> defaultsBySecondDept = new LinkedHashMap<>();
private List<String> ratios = new ArrayList<>(); private List<String> ratios = new ArrayList<>();
private List<Integer> durations = new ArrayList<>(); private List<Integer> durations = new ArrayList<>();
@ -54,6 +66,22 @@ public class PortalVideoProperties {
this.models = models != null ? models : new ArrayList<>(); this.models = models != null ? models : new ArrayList<>();
} }
public Map<String, List<ModelOption>> getModelsBySecondDept() {
return modelsBySecondDept;
}
public void setModelsBySecondDept(Map<String, List<ModelOption>> modelsBySecondDept) {
this.modelsBySecondDept = modelsBySecondDept != null ? modelsBySecondDept : new LinkedHashMap<>();
}
public Map<String, Defaults> getDefaultsBySecondDept() {
return defaultsBySecondDept;
}
public void setDefaultsBySecondDept(Map<String, Defaults> defaultsBySecondDept) {
this.defaultsBySecondDept = defaultsBySecondDept != null ? defaultsBySecondDept : new LinkedHashMap<>();
}
public List<String> getRatios() { public List<String> getRatios() {
return ratios; return ratios;
} }
@ -91,6 +119,70 @@ public class PortalVideoProperties {
return null; return null;
} }
public List<ModelOption> resolveModelsForSecondDept(Long secondDeptId) {
if (secondDeptId != null) {
List<ModelOption> byDept = modelsBySecondDept.get(String.valueOf(secondDeptId));
if (byDept != null && !byDept.isEmpty()) {
return byDept;
}
}
return models;
}
public Defaults resolveEffectiveDefaults(Long secondDeptId) {
Defaults out = new Defaults();
Defaults g = this.defaults;
out.setModel(g.getModel());
out.setDuration(g.getDuration());
out.setResolution(g.getResolution());
out.setRatio(g.getRatio());
if (secondDeptId != null) {
Defaults o = defaultsBySecondDept.get(String.valueOf(secondDeptId));
if (o != null) {
if (o.getModel() != null && !o.getModel().isEmpty()) {
out.setModel(o.getModel());
}
if (o.getDuration() != null) {
out.setDuration(o.getDuration());
}
if (o.getResolution() != null && !o.getResolution().isEmpty()) {
out.setResolution(o.getResolution());
}
if (o.getRatio() != null && !o.getRatio().isEmpty()) {
out.setRatio(o.getRatio());
}
}
}
return out;
}
/**
* 在给定二级部门下解析默认模型先看合并后的 defaults.model否则取该部门 models 列表首项
*/
public String resolveDefaultModelIdForSecondDept(Long secondDeptId) {
Defaults d = resolveEffectiveDefaults(secondDeptId);
if (d.getModel() != null && !d.getModel().isEmpty()) {
return d.getModel();
}
List<ModelOption> m = resolveModelsForSecondDept(secondDeptId);
if (!m.isEmpty() && m.get(0).getValue() != null && !m.get(0).getValue().isEmpty()) {
return m.get(0).getValue();
}
return null;
}
public boolean isModelAllowedForSecondDept(String modelId, Long secondDeptId) {
if (modelId == null || modelId.isEmpty()) {
return true;
}
for (ModelOption opt : resolveModelsForSecondDept(secondDeptId)) {
if (modelId.equals(opt.getValue())) {
return true;
}
}
return false;
}
public static class Defaults { public static class Defaults {
private String model; private String model;
private Integer duration; private Integer duration;

View File

@ -261,6 +261,8 @@ portal:
value: ep-20260326165811-dlkth value: ep-20260326165811-dlkth
- label: Seedance 2.0 Fast - label: Seedance 2.0 Fast
value: ep-20260326170056-dkj9m value: ep-20260326170056-dkj9m
# 可选:按二级部门 dept_id 覆盖 YAML仅当库表 sys_dept.model_parm 为空时才会用到)
# models-by-second-dept / defaults-by-second-dept 见 PortalVideoProperties 说明
ratios: ratios:
- "16:9" - "16:9"
- "9:16" - "9:16"

View File

@ -55,6 +55,9 @@ public class SysDept extends BaseEntity
/** Byte API Key */ /** Byte API Key */
private String byteApiKey; private String byteApiKey;
/** 门户视频模型列表 JSON[{"label":"…","value":"ep-…"},…] */
private String modelParm;
/** Byte project */ /** Byte project */
private String project; private String project;
@ -187,6 +190,16 @@ public class SysDept extends BaseEntity
this.byteApiKey = byteApiKey; this.byteApiKey = byteApiKey;
} }
public String getModelParm()
{
return modelParm;
}
public void setModelParm(String modelParm)
{
this.modelParm = modelParm;
}
public String getProject() public String getProject()
{ {
return project; return project;
@ -219,6 +232,7 @@ public class SysDept extends BaseEntity
.append("phone", getPhone()) .append("phone", getPhone())
.append("email", getEmail()) .append("email", getEmail())
.append("byteApiKey", getByteApiKey()) .append("byteApiKey", getByteApiKey())
.append("modelParm", getModelParm())
.append("project", getProject()) .append("project", getProject())
.append("status", getStatus()) .append("status", getStatus())
.append("delFlag", getDelFlag()) .append("delFlag", getDelFlag())

View File

@ -9,4 +9,15 @@ public interface IByteDeptApiKeyService {
* Redis {userId}_byte_api_key优先读缓存再读库均无有效 key 则抛出业务异常 * Redis {userId}_byte_api_key优先读缓存再读库均无有效 key 则抛出业务异常
*/ */
String resolveVolcApiKey(Long aiUserId); String resolveVolcApiKey(Long aiUserId);
/**
* 当前 AI 用户所属二级部门 {@code sys_dept.dept_id} {@link #resolveVolcApiKey(Long)} 中使用的二级节点一致
* 用户未分配部门或部门数据缺失时返回 null
*/
Long resolveSecondLevelDeptId(Long aiUserId);
/**
* 当前用户所属二级部门 {@code sys_dept.model_parm}JSON 模型列表未配置或为空时返回 null
*/
String resolveSecondLevelModelParm(Long aiUserId);
} }

View File

@ -149,13 +149,23 @@ public class AiUserServiceImpl implements IAiUserService {
public int updateAiUser(AiUser aiUser) { public int updateAiUser(AiUser aiUser) {
aiUser.setUpdateTime(DateUtils.getNowDate()); aiUser.setUpdateTime(DateUtils.getNowDate());
if (StringUtils.isNotEmpty(aiUser.getPassword())) { if (StringUtils.isNotEmpty(aiUser.getPassword())) {
// 登录后 update 常带上库里的 BCrypt 密文勿对密文再 encrypt否则第二次登录永远密码错误
if (!isBcryptPasswordHash(aiUser.getPassword())) {
aiUser.setPassword(SecurityUtils.encryptPassword(aiUser.getPassword())); aiUser.setPassword(SecurityUtils.encryptPassword(aiUser.getPassword()));
}
} else { } else {
aiUser.setPassword(null); aiUser.setPassword(null);
} }
return aiUserMapper.updateById(aiUser); return aiUserMapper.updateById(aiUser);
} }
private static boolean isBcryptPasswordHash(String password) {
if (password == null || password.length() < 4) {
return false;
}
return password.startsWith("$2a$") || password.startsWith("$2b$") || password.startsWith("$2y$");
}
@Override @Override
public int countAiUserByDeptId(Long deptId) { public int countAiUserByDeptId(Long deptId) {
if (deptId == null) { if (deptId == null) {

View File

@ -58,7 +58,7 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
// 优先使用用户直接归属部门的 Key多数业务把用户挂在分公司 101并在该节点配 byte_api_key // 优先使用用户直接归属部门的 Key多数业务把用户挂在分公司 101并在该节点配 byte_api_key
String apiKey = trimKey(userDept.getByteApiKey()); String apiKey = trimKey(userDept.getByteApiKey());
if (StringUtils.isEmpty(apiKey)) { if (StringUtils.isEmpty(apiKey)) {
Long fallbackDeptId = resolveSecondLevelDeptId(userDept); Long fallbackDeptId = secondLevelDeptIdFrom(userDept);
if (!fallbackDeptId.equals(userDept.getDeptId())) { if (!fallbackDeptId.equals(userDept.getDeptId())) {
SysDept keyDept = sysDeptService.selectDeptById(fallbackDeptId); SysDept keyDept = sysDeptService.selectDeptById(fallbackDeptId);
if (keyDept != null) { if (keyDept != null) {
@ -75,6 +75,39 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
return apiKey; return apiKey;
} }
@Override
public Long resolveSecondLevelDeptId(Long aiUserId) {
if (aiUserId == null) {
return null;
}
AiUser aiUser = aiUserService.selectAiUserById(aiUserId);
if (aiUser == null || aiUser.getDeptId() == null) {
return null;
}
SysDept userDept = sysDeptService.selectDeptById(aiUser.getDeptId());
if (userDept == null) {
return null;
}
return secondLevelDeptIdFrom(userDept);
}
@Override
public String resolveSecondLevelModelParm(Long aiUserId) {
Long secondId = resolveSecondLevelDeptId(aiUserId);
if (secondId == null) {
return null;
}
SysDept second = sysDeptService.selectDeptById(secondId);
if (second == null) {
return null;
}
String raw = second.getModelParm();
if (StringUtils.isEmpty(raw)) {
return null;
}
return raw.trim();
}
private static String trimKey(String raw) { private static String trimKey(String raw) {
return raw == null ? null : raw.trim(); return raw == null ? null : raw.trim();
} }
@ -83,7 +116,7 @@ public class ByteDeptApiKeyServiceImpl implements IByteDeptApiKeyService {
* 二级部门祖级路径中紧接在一级ancestors 第二段之下的部门节点 * 二级部门祖级路径中紧接在一级ancestors 第二段之下的部门节点
* 深度不足时退回用户当前部门 * 深度不足时退回用户当前部门
*/ */
private Long resolveSecondLevelDeptId(SysDept userDept) { private Long secondLevelDeptIdFrom(SysDept userDept) {
String ancestors = userDept.getAncestors(); String ancestors = userDept.getAncestors();
if (StringUtils.isEmpty(ancestors)) { if (StringUtils.isEmpty(ancestors)) {
return userDept.getDeptId(); return userDept.getDeptId();

View File

@ -14,6 +14,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<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="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" />
@ -61,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.status, d.project, 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,
(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}
@ -104,6 +105,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<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="byteApiKey != null">byte_api_key,</if>
<if test="modelParm != null">model_parm,</if>
<if test="project != null">project,</if> <if test="project != null">project,</if>
<if test="status != null">status,</if> <if test="status != null">status,</if>
<if test="createBy != null and createBy != ''">create_by,</if> <if test="createBy != null and createBy != ''">create_by,</if>
@ -118,6 +120,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<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="byteApiKey != null">#{byteApiKey},</if>
<if test="modelParm != null">#{modelParm},</if>
<if test="project != null">#{project},</if> <if test="project != null">#{project},</if>
<if test="status != null">#{status},</if> <if test="status != null">#{status},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if> <if test="createBy != null and createBy != ''">#{createBy},</if>
@ -136,6 +139,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<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="byteApiKey != null">byte_api_key = #{byteApiKey},</if>
<if test="modelParm != null">model_parm = #{modelParm},</if>
<if test="project != null">project = #{project},</if> <if test="project != null">project = #{project},</if>
<if test="status != null and status != ''">status = #{status},</if> <if test="status != null and status != ''">status = #{status},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>