新增数字人版本管理

This commit is contained in:
super
2026-06-14 19:00:55 +08:00
parent ac19637ad6
commit f44110fae5
15 changed files with 971 additions and 32 deletions

View File

@@ -21,6 +21,8 @@ app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR)
CORS(app)
app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32))
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
# 文件上传大小限制2GB数字人 ZIP 包等大文件)
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 * 1024
# 注册蓝图
app.register_blueprint(auth)

View File

@@ -1430,6 +1430,134 @@ def list_versions():
return jsonify({'success': False, 'error': str(e)})
# ========== 数字人版本管理(代理到 Java 后端)==========
@admin_api.route('/digital-human-versions')
@admin_required
def list_digital_human_versions():
"""代理:查询数字人版本列表"""
_, _, denied = _ensure_admin_menu_access('version')
if denied:
return denied
page = request.args.get('page', '1')
page_size = request.args.get('pageSize', '20')
status = request.args.get('status', '')
params = {'page': page, 'pageSize': page_size}
if status:
params['status'] = status
result, error_response, status_code = _proxy_backend_java(
'GET',
'/api/digital-human/versions',
params=params,
timeout=30
)
if error_response:
return error_response, status_code
return jsonify(result)
@admin_api.route('/digital-human-versions/latest')
@admin_required
def get_latest_digital_human_version():
"""代理:获取最新数字人版本"""
result, error_response, status_code = _proxy_backend_java(
'GET',
'/api/digital-human/versions/latest',
timeout=10
)
if error_response:
return error_response, status_code
return jsonify(result)
@admin_api.route('/digital-human-versions/upload', methods=['POST'])
@admin_required
def upload_digital_human_version():
"""代理:上传数字人版本"""
_, _, denied = _ensure_admin_menu_access('version')
if denied:
return denied
version = request.form.get('version', '').strip()
changelog = request.form.get('changelog', '').strip()
min_client_version = request.form.get('minClientVersion', '').strip()
file_storage = request.files.get('file')
if not version:
return jsonify({'success': False, 'message': '请填写版本号'}), 400
if not file_storage:
return jsonify({'success': False, 'message': '请选择文件'}), 400
files = {'file': (file_storage.filename, file_storage.stream, file_storage.content_type)}
data = {'version': version}
if changelog:
data['changelog'] = changelog
if min_client_version:
data['minClientVersion'] = min_client_version
result, error_response, status_code = _proxy_backend_java(
'POST',
'/api/digital-human/versions/upload',
files=files,
data=data,
timeout=300
)
if error_response:
return error_response, status_code
return jsonify(result)
@admin_api.route('/digital-human-versions/<version>/release', methods=['POST'])
@admin_required
def release_digital_human_version(version):
"""代理:发布数字人版本"""
_, _, denied = _ensure_admin_menu_access('version')
if denied:
return denied
result, error_response, status_code = _proxy_backend_java(
'POST',
f'/api/digital-human/versions/{version}/release',
timeout=10
)
if error_response:
return error_response, status_code
return jsonify(result)
@admin_api.route('/digital-human-versions/<version>/set-latest', methods=['POST'])
@admin_required
def set_latest_digital_human_version(version):
"""代理:设为最新数字人版本"""
_, _, denied = _ensure_admin_menu_access('version')
if denied:
return denied
result, error_response, status_code = _proxy_backend_java(
'POST',
f'/api/digital-human/versions/{version}/set-latest',
timeout=10
)
if error_response:
return error_response, status_code
return jsonify(result)
@admin_api.route('/digital-human-versions/<version>', methods=['DELETE'])
@admin_required
def delete_digital_human_version(version):
"""代理:删除数字人版本"""
_, _, denied = _ensure_admin_menu_access('version')
if denied:
return denied
result, error_response, status_code = _proxy_backend_java(
'DELETE',
f'/api/digital-human/versions/{version}',
timeout=30
)
if error_response:
return error_response, status_code
return jsonify(result)
@admin_api.route('/version', methods=['POST'])
@admin_required
def upload_version():

View File

@@ -59,8 +59,8 @@ file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
backend_java_base_url, backend_java_base_url_source = _get_env(
"java_api_base",
"BACKEND_JAVA_BASE_URL",
default="http://api.aishufu.top:18080/",
# default="http://127.0.0.1:18080/",
# default="http://api.aishufu.top:18080/",
default="http://127.0.0.1:18080/",
)
backend_java_base_url = backend_java_base_url.rstrip("/")
os.environ["OSS_ACCESS_KEY_ID"] = accessKeyId

View File

@@ -3519,35 +3519,98 @@
});
};
function loadVersions() {
fetch('/api/admin/versions')
function loadVersions(page) {
page = page || 1;
fetch('/api/admin/digital-human-versions?page=' + page + '&pageSize=20')
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('versionListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.message || res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
// 兼容两种返回格式:直接数组 或 分页对象
var data = res.data;
var items = [];
var pages = 1;
var current = 1;
if (Array.isArray(data)) {
items = data;
} else if (data && Array.isArray(data.records)) {
items = data.records;
pages = data.pages || 1;
current = data.current || 1;
} else if (data && Array.isArray(data.items)) {
items = data.items;
}
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">暂无版本记录</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无版本记录</td></tr>';
} else {
tbody.innerHTML = items.map(function (v) {
var url = (v.file_url || '').replace(/"/g, '&quot;');
return '<tr><td>' + (v.version || '') + '</td><td><a href="' + url + '" target="_blank" rel="noopener">' + url + '</a></td><td>' + (v.created_at || '') + '</td><td><a href="' + url + '" download class="btn btn-sm">下载</a></td></tr>';
var statusText = v.status === 'DRAFT' ? '草稿' : (v.status === 'RELEASED' ? '已发布' : '已废弃');
var statusColor = v.status === 'DRAFT' ? '#999' : (v.status === 'RELEASED' ? '#52c41a' : '#ff4d4f');
var isLatestBadge = v.isLatest ? '<span style="color:#ff4d4f;font-weight:600;">★</span>' : '';
var fileSize = v.fileSize ? (v.fileSize / 1024 / 1024).toFixed(2) + ' MB' : '-';
var md5Short = (v.md5 || 'unknown').substring(0, 12) + '...';
var changelog = (v.changelog || '-').substring(0, 50);
if ((v.changelog || '').length > 50) changelog += '...';
var releasedAt = v.releasedAt || '-';
var downloadUrl = v.downloadUrl || '';
var actions = '';
if (v.status === 'DRAFT') {
actions += '<button class="btn btn-sm" onclick="releaseVersion(\'' + v.version + '\')">发布</button> ';
}
if (v.status === 'RELEASED' && !v.isLatest) {
actions += '<button class="btn btn-sm" onclick="setLatestVersion(\'' + v.version + '\')">设为最新</button> ';
}
if (downloadUrl) {
actions += '<a href="' + downloadUrl + '" class="btn btn-sm" download>下载</a> ';
}
if (!v.isLatest) {
actions += '<button class="btn btn-sm btn-danger" onclick="deleteVersion(\'' + v.version + '\')">删除</button>';
}
return '<tr>' +
'<td>' + (v.version || '') + '</td>' +
'<td style="color:' + statusColor + ';">' + statusText + '</td>' +
'<td>' + isLatestBadge + '</td>' +
'<td>' + fileSize + '</td>' +
'<td title="' + (v.md5 || '') + '">' + md5Short + '</td>' +
'<td>' + changelog + '</td>' +
'<td>' + releasedAt + '</td>' +
'<td>' + actions + '</td>' +
'</tr>';
}).join('');
}
// 分页
var pagination = document.getElementById('versionPagination');
if (pages > 1) {
var html = '';
for (var i = 1; i <= pages; i++) {
var active = i === current ? ' active' : '';
html += '<span class="page-item' + active + '" onclick="loadVersions(' + i + ')">' + i + '</span>';
}
pagination.innerHTML = html;
} else {
pagination.innerHTML = '';
}
})
.catch(function () {
document.getElementById('versionListBody').innerHTML = '<tr><td colspan="4" class="empty-tip">请求失败</td></tr>';
.catch(function (err) {
document.getElementById('versionListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败: ' + (err.message || '') + '</td></tr>';
});
}
document.getElementById('btnUploadVersion').onclick = function () {
var version = (document.getElementById('versionNumber').value || '').trim();
var minClientVersion = (document.getElementById('versionMinClientVersion').value || '').trim();
var changelog = (document.getElementById('versionChangelog').value || '').trim();
var fileInput = document.getElementById('versionZip');
var msgEl = document.getElementById('msgVersion');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!version) {
msgEl.textContent = '请填写版本号';
msgEl.classList.add('err');
@@ -3564,34 +3627,99 @@
msgEl.classList.add('err');
return;
}
var formData = new FormData();
formData.append('version', version);
formData.append('file', file);
msgEl.textContent = '上传中...';
formData.append('version', version);
if (minClientVersion) formData.append('minClientVersion', minClientVersion);
if (changelog) formData.append('changelog', changelog);
msgEl.textContent = '上传中,请稍候...';
msgEl.classList.remove('err', 'ok');
fetch('/api/admin/version', {
fetch('/api/admin/digital-human-versions/upload', {
method: 'POST',
body: formData
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
msgEl.textContent = '发布成功版本:' + res.version + ',链接:' + (res.file_url || '');
if (res.success && res.data) {
msgEl.textContent = '上传成功版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)';
msgEl.classList.add('ok');
document.getElementById('versionNumber').value = '';
document.getElementById('versionMinClientVersion').value = '';
document.getElementById('versionChangelog').value = '';
fileInput.value = '';
loadVersions();
} else {
msgEl.textContent = res.error || '上传失败';
msgEl.textContent = res.message || res.error || '上传失败';
msgEl.classList.add('err');
}
})
.catch(function () {
msgEl.textContent = '请求失败';
.catch(function (err) {
msgEl.textContent = '请求失败: ' + (err.message || '');
msgEl.classList.add('err');
});
};
window.releaseVersion = function(version) {
if (!confirm('确认发布版本 ' + version + ' 吗?')) return;
fetch('/api/admin/digital-human-versions/' + version + '/release', {
method: 'POST'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
alert('发布成功!');
loadVersions();
} else {
alert('发布失败:' + (res.message || res.error || ''));
}
})
.catch(function (err) {
alert('请求失败:' + (err.message || ''));
});
};
window.setLatestVersion = function(version) {
if (!confirm('确认将版本 ' + version + ' 设为最新吗?客户端将自动检测并更新到此版本。')) return;
fetch('/api/admin/digital-human-versions/' + version + '/set-latest', {
method: 'POST'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
alert('设置成功!版本 ' + version + ' 已标记为最新。');
loadVersions();
} else {
alert('设置失败:' + (res.message || res.error || ''));
}
})
.catch(function (err) {
alert('请求失败:' + (err.message || ''));
});
};
window.deleteVersion = function(version) {
if (!confirm('确认删除版本 ' + version + ' 吗?此操作将删除 OSS 文件,不可恢复!')) return;
fetch('/api/admin/digital-human-versions/' + version, {
method: 'DELETE'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
alert('删除成功!');
loadVersions();
} else {
alert('删除失败:' + (res.message || res.error || ''));
}
})
.catch(function (err) {
alert('请求失败:' + (err.message || ''));
});
};
// ========== 栏目权限配置 ==========
function loadColumns() {
fetch('/api/admin/columns')

View File

@@ -916,19 +916,33 @@
</div>
</div>
<!-- 版本管理 -->
<!-- 数字人版本管理 -->
<div id="panel-version" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;font-size:15px;">发布新版本</h3>
<div class="form-group">
<label>版本号</label>
<input type="text" id="versionNumber" placeholder="例如1.0.0">
<h3 style="margin-bottom:16px;font-size:15px;">上传新版本</h3>
<p style="margin-bottom:16px;font-size:13px;color:#666;">
上传数字人程序 ZIP 包,系统会自动计算 MD5 并存储到 OSS。上传后状态为草稿需要手动发布。
</p>
<div class="form-row">
<div class="form-group" style="min-width:200px;">
<label>版本号 *</label>
<input type="text" id="versionNumber" placeholder="例如1.0.0">
</div>
<div class="form-group" style="min-width:200px;">
<label>最低客户端版本</label>
<input type="text" id="versionMinClientVersion" placeholder="例如2.0.0(可选)">
</div>
</div>
<div class="form-group">
<label>ZIP 压缩包</label>
<label>更新日志</label>
<textarea id="versionChangelog" rows="3" placeholder="例如:修复数字人口型同步问题,优化启动速度" style="width:100%;padding:10px;border:1px solid #ddd;border-radius:6px;resize:vertical;"></textarea>
</div>
<div class="form-group">
<label>ZIP 压缩包 *</label>
<input type="file" id="versionZip" accept=".zip">
<p style="font-size:12px;color:#999;margin-top:4px;">仅支持 .zip 格式文件名ShuFuDigitalHuman.zip</p>
</div>
<button class="btn" id="btnUploadVersion">上传并发布</button>
<button class="btn" id="btnUploadVersion">上传(草稿状态)</button>
<p class="msg" id="msgVersion"></p>
</div>
<div class="panel-box">
@@ -936,14 +950,19 @@
<table>
<thead>
<tr>
<th>版本号</th>
<th>下载链接</th>
<th>发布时间</th>
<th>操作</th>
<th style="width:80px;">版本号</th>
<th style="width:60px;">状态</th>
<th style="width:60px;">最新</th>
<th style="width:80px;">文件大小</th>
<th style="width:120px;">MD5</th>
<th>更新日志</th>
<th style="width:140px;">发布时间</th>
<th style="width:240px;">操作</th>
</tr>
</thead>
<tbody id="versionListBody"></tbody>
</table>
<div class="pagination" id="versionPagination"></div>
</div>
</div>