fix(model):模型导入功能
This commit is contained in:
1 parent
cdd314675b
commit
d0f6f14371
6 files changed
+95
-54
No files matched your search
@@ -228,7 +228,7 @@ type ModelFetchOfficialInfoRes struct {
|
||||
// ModelExportJsonReq 导出模型配置(JSON,用于跨环境迁移)
|
||||
type ModelExportJsonReq struct {
|
||||
g.Meta `path:"/models/export-json" method:"post" mime:"json" tags:"管理后台-模型" summary:"导出模型配置JSON"`
|
||||
ModelIds []string `json:"model_ids" v:"required|min:1#请选择模型|至少选择一个模型" dc:"要导出的model_id列表"`
|
||||
ModelIds []string `json:"model_ids" v:"required#请至少选择一个模型" dc:"要导出的model_id列表"`
|
||||
}
|
||||
|
||||
// ModelExportJsonRes 导出模型配置JSON响应(直接写入响应流)
|
||||
@@ -236,7 +236,7 @@ type ModelExportJsonRes struct{}
|
||||
|
||||
// ModelImportPreviewReq 导入模型预览请求
|
||||
type ModelImportPreviewReq struct {
|
||||
g.Meta `path:"/models/import-preview" method:"post" mime:"json" tags:"管理后台-模型" summary:"导入模型预览"`
|
||||
g.Meta `path:"/models/import-preview" method:"post" tags:"管理后台-模型" summary:"导入模型预览"`
|
||||
}
|
||||
|
||||
// ModelImportPreviewRes 导入模型预览响应
|
||||
@@ -264,7 +264,7 @@ type ModelImportPreviewItem struct {
|
||||
// ModelImportReq 确认导入模型请求
|
||||
type ModelImportReq struct {
|
||||
g.Meta `path:"/models/import" method:"post" mime:"json" tags:"管理后台-模型" summary:"确认导入模型"`
|
||||
Models []ModelImportItem `json:"models" v:"required|min:1#无数据|至少导入一个模型"`
|
||||
Models []ModelImportItem `json:"models" v:"required#请至少导入一个模型"`
|
||||
}
|
||||
|
||||
// ModelImportItem 导入模型项
|
||||
|
||||
@@ -147,19 +147,37 @@ func (s *sAdmin) ExportModelsJson(ctx context.Context, req *v1.ModelExportJsonRe
|
||||
|
||||
// ImportModelsPreview 导入模型预览(解析上传文件,检测冲突)
|
||||
func (s *sAdmin) ImportModelsPreview(ctx context.Context, req *v1.ModelImportPreviewReq) (*v1.ModelImportPreviewRes, error) {
|
||||
var data []byte
|
||||
|
||||
// 优先通过标准文件上传读取
|
||||
file := g.RequestFromCtx(ctx).GetUploadFile("file")
|
||||
if file == nil {
|
||||
return nil, gerror.NewCode(gcode.New(consts.CodeModelImportInvalidFile, consts.MsgModelImportInvalidFile, nil), consts.MsgModelImportInvalidFile)
|
||||
if file != nil {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, gerror.NewCode(gcode.New(consts.CodeModelImportInvalidFile, consts.MsgModelImportInvalidFile, nil), consts.MsgModelImportInvalidFile)
|
||||
}
|
||||
defer f.Close()
|
||||
data, err = io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, gerror.NewCode(gcode.New(consts.CodeModelImportInvalidFile, consts.MsgModelImportInvalidFile, nil), consts.MsgModelImportInvalidFile)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, gerror.NewCode(gcode.New(consts.CodeModelImportInvalidFile, consts.MsgModelImportInvalidFile, nil), consts.MsgModelImportInvalidFile)
|
||||
// 兜底:从已解析的 MultipartForm.File 中读取(文件在 File 而非 Value 中)
|
||||
if len(data) == 0 {
|
||||
mf := g.RequestFromCtx(ctx).GetMultipartForm()
|
||||
if mf != nil {
|
||||
if files := mf.File["file"]; len(files) > 0 {
|
||||
f, err := files[0].Open()
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
data, _ = io.ReadAll(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
if len(data) == 0 {
|
||||
return nil, gerror.NewCode(gcode.New(consts.CodeModelImportInvalidFile, consts.MsgModelImportInvalidFile, nil), consts.MsgModelImportInvalidFile)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,12 @@ func OperationLog(r *ghttp.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Capture request body (before Next() reads it)
|
||||
body := r.GetBodyString()
|
||||
// Skip body capture for multipart/form-data (file uploads)
|
||||
// r.GetBodyString() consumes the body stream and breaks multipart parsing
|
||||
var body string
|
||||
if !strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/") {
|
||||
body = r.GetBodyString()
|
||||
}
|
||||
|
||||
r.Middleware.Next()
|
||||
|
||||
|
||||
@@ -121,7 +121,60 @@ CREATE INDEX idx_pln_tenant_plans_plan ON pln_tenant_plans (plan_id);
|
||||
CREATE INDEX idx_spt_feedbacks_user ON spt_feedbacks (tenant_id, user_id);
|
||||
CREATE INDEX idx_spt_replies_user ON spt_replies (user_id);
|
||||
|
||||
-- ============================================================
|
||||
-- 四、租户 max_members 和 max_concurrency 改为可 NULL
|
||||
-- NULL 表示跟随等级配置(tnt_tenant_level_configs),非 NULL 表示自定义覆盖
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members DROP NOT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members SET DEFAULT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency DROP NOT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency SET DEFAULT NULL;
|
||||
|
||||
COMMENT ON COLUMN tnt_tenants.max_members IS '最大成员数上限(NULL表示跟随等级配置)';
|
||||
COMMENT ON COLUMN tnt_tenants.max_concurrency IS '租户总并发上限(NULL表示跟随等级配置,0表示不限制)';
|
||||
|
||||
-- ============================================================
|
||||
-- 五、sys_sessions 添加 jti 列(JWT ID,用于会话吊销)
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE sys_sessions ADD COLUMN jti VARCHAR(36) NOT NULL DEFAULT '';
|
||||
|
||||
UPDATE sys_sessions SET jti = 'legacy-' || id WHERE jti = '';
|
||||
|
||||
ALTER TABLE sys_sessions ADD CONSTRAINT uk_sys_sessions_jti UNIQUE (jti);
|
||||
|
||||
COMMENT ON COLUMN sys_sessions.jti IS 'JWT ID (jti),会话唯一标识符(UUID),用于 Redis 吊销缓存';
|
||||
|
||||
-- +goose Down
|
||||
-- 五、撤销 jti
|
||||
ALTER TABLE sys_sessions DROP CONSTRAINT IF EXISTS uk_sys_sessions_jti;
|
||||
ALTER TABLE sys_sessions DROP COLUMN IF EXISTS jti;
|
||||
|
||||
-- 四、恢复 max_members / max_concurrency 为 NOT NULL
|
||||
UPDATE tnt_tenants t
|
||||
SET max_members = COALESCE(
|
||||
(SELECT lc.max_members FROM tnt_tenant_level_configs lc WHERE lc.level = t.level),
|
||||
10
|
||||
)
|
||||
WHERE t.max_members IS NULL;
|
||||
|
||||
UPDATE tnt_tenants t
|
||||
SET max_concurrency = COALESCE(
|
||||
(SELECT lc.max_concurrency FROM tnt_tenant_level_configs lc WHERE lc.level = t.level),
|
||||
0
|
||||
)
|
||||
WHERE t.max_concurrency IS NULL;
|
||||
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members SET DEFAULT 10;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members SET NOT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency SET DEFAULT 0;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency SET NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN tnt_tenants.max_members IS '最大成员数上限';
|
||||
COMMENT ON COLUMN tnt_tenants.max_concurrency IS '租户总并发上限(0表示不限制)';
|
||||
|
||||
-- 三、删除索引
|
||||
DROP INDEX IF EXISTS idx_spt_replies_user;
|
||||
DROP INDEX IF EXISTS idx_spt_feedbacks_user;
|
||||
DROP INDEX IF EXISTS idx_pln_tenant_plans_plan;
|
||||
@@ -131,10 +184,12 @@ DROP INDEX IF EXISTS idx_fil_files_user;
|
||||
DROP INDEX IF EXISTS idx_bil_records_channel;
|
||||
DROP INDEX IF EXISTS idx_api_keys_user;
|
||||
|
||||
-- 二、删除模型分组
|
||||
DROP TABLE IF EXISTS mdl_tenant_groups;
|
||||
DROP TABLE IF EXISTS mdl_group_models;
|
||||
DROP TABLE IF EXISTS mdl_model_groups;
|
||||
|
||||
-- 一、删除租户等级
|
||||
ALTER TABLE tnt_tenants DROP COLUMN IF EXISTS level;
|
||||
ALTER TABLE bil_wallets DROP COLUMN IF EXISTS cumulative_recharge;
|
||||
DROP TABLE IF EXISTS tnt_tenant_level_configs;
|
||||
@@ -1,38 +0,0 @@
|
||||
-- +goose Up
|
||||
-- 租户 max_members 和 max_concurrency 改为可 NULL
|
||||
-- NULL 表示跟随等级配置(tnt_tenant_level_configs),非 NULL 表示自定义覆盖
|
||||
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members DROP NOT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members SET DEFAULT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency DROP NOT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency SET DEFAULT NULL;
|
||||
|
||||
COMMENT ON COLUMN tnt_tenants.max_members IS '最大成员数上限(NULL表示跟随等级配置)';
|
||||
COMMENT ON COLUMN tnt_tenants.max_concurrency IS '租户总并发上限(NULL表示跟随等级配置,0表示不限制)';
|
||||
|
||||
-- 将现有记录的值保留(已有具体数值的不做变更,保持向后兼容)
|
||||
|
||||
-- +goose Down
|
||||
-- 恢复为 NOT NULL,将 NULL 值设为等级对应配置值,无等级配置的回退到默认值
|
||||
|
||||
UPDATE tnt_tenants t
|
||||
SET max_members = COALESCE(
|
||||
(SELECT lc.max_members FROM tnt_tenant_level_configs lc WHERE lc.level = t.level),
|
||||
10
|
||||
)
|
||||
WHERE t.max_members IS NULL;
|
||||
|
||||
UPDATE tnt_tenants t
|
||||
SET max_concurrency = COALESCE(
|
||||
(SELECT lc.max_concurrency FROM tnt_tenant_level_configs lc WHERE lc.level = t.level),
|
||||
0
|
||||
)
|
||||
WHERE t.max_concurrency IS NULL;
|
||||
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members SET DEFAULT 10;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_members SET NOT NULL;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency SET DEFAULT 0;
|
||||
ALTER TABLE tnt_tenants ALTER COLUMN max_concurrency SET NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN tnt_tenants.max_members IS '最大成员数上限';
|
||||
COMMENT ON COLUMN tnt_tenants.max_concurrency IS '租户总并发上限(0表示不限制)';
|
||||
@@ -433,8 +433,10 @@ function openImportModal() {
|
||||
showImportModal.value = true
|
||||
}
|
||||
|
||||
async function handleImportUpload(fileItem: any) {
|
||||
const file = fileItem.fileItem?.file || fileItem
|
||||
async function handleImportUpload(data: { file: any; fileList: any[] }) {
|
||||
// Naive UI @change 回调参数: { file: UploadFileInfo, fileList: UploadFileInfo[] }
|
||||
// UploadFileInfo.file 才是浏览器原生 File 对象
|
||||
const file = data.file?.file
|
||||
if (!file) return
|
||||
|
||||
importPreviewing.value = true
|
||||
@@ -667,7 +669,7 @@ function resetImport() {
|
||||
draggable
|
||||
accept=".json"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:show-file-list="false"
|
||||
@change="handleImportUpload"
|
||||
>
|
||||
<template #upload-button>
|
||||
|
||||
Reference in new issue
Block a user