feat: 并发分组系统 + 品牌/Logo 上传 + 兑换码开关 + 文档分辨率表

并发分组(新功能):
- 新表 concurrency_groups(名称/上限/默认),用户加 concurrency_group_id
- 启动自动建「默认并发」组(上限10、默认),老用户回填、新注册自动绑定
- 并发计数改用 Redis(自愈 sorted-set + TTL + fail-open):
  · 用户并发(画图台 + API key 合计)受其分组上限限制,0=不限制 → 超返回 429
  · 账号级并发也从内存 gate 换成同一套 Redis(6 处调用点)
  · 移除旧的「已有正在生成的任务」单任务锁
- 后台「并发分组」新菜单:增删改、设默认、用户数;默认组不可删(删别的组成员转默认)
- 用户管理:并发列 + 新建/编辑可选分组
- 个人设置页:账户信息卡(用户名/邮箱/角色/余额/并发);/me 暴露 concurrency_group/limit

品牌 / Logo(上传到 RustFS):
- Logo 改成拖拽/点击上传,点保存才上传;替换自动删旧;branding/ 设为公开且被清理任务 pin 住(永不删)
- 有自定义就用:前台左侧 nav + 后台侧栏 + favicon(浏览器标签);没有则默认 V 图标
- 前台页头还原成文字;首页 Hero 子标题用 site.subtitle(默认那句宣传语,设置页预填)
- 邮件验证码标题用站点名;新增 POST/DELETE /settings/logo + POST /settings/asset(首页底图上传)

兑换码开关:
- 系统设置→积分 新增「开启兑换码」(默认开);关闭后后端拒绝兑换、前台隐藏兑换入口(/site 暴露 cdk_redeem_enabled)

文档 / 分辨率:
- 去掉 quality 参数:size(宽x高)同时决定比例 + 分辨率档(长边映射 1K/2K/4K)
- 文档加「分辨率对照表」(14 个比例 × 1K/2K/4K → size 该传的值);guessRatio 与自定义模型 RATIO_OPTS 对齐到 14 个

其它:
- 删模型时同步清掉各上游账号「支持模型」里的该 id
- 首页设置/兑换码弹窗去固定高度滚动条;展示位弹窗浅色主题适配

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 13:52:31 +08:00
co-authored by Claude Opus 4.8
parent 1137d0bd1d
commit 5cf6206ee9
37 changed files with 1257 additions and 161 deletions
+1
View File
@@ -13,6 +13,7 @@ const tabs = [
{ label: '模型管理', to: '/admin/models', icon: 'models' },
{ label: '账号管理', to: '/admin/accounts', icon: 'plug' },
{ label: '用户管理', to: '/admin/users', icon: 'accounts' },
{ label: '并发分组', to: '/admin/concurrency', icon: 'shield' },
{ label: '兑换码', to: '/admin/cdks', icon: 'spark' },
{ label: '邀请日志', to: '/admin/invites', icon: 'accounts' },
{ label: '图片管理', to: '/admin/images', icon: 'files' },
+7 -6
View File
@@ -16,8 +16,8 @@ const nav = computed(() => {
const items = [{ to: '/', label: '首页', icon: 'overview' }]
if (isAuthed()) {
items.push({ to: '/user', label: '画图', icon: 'spark' })
items.push({ to: '/logs', label: '记录', icon: 'log' })
items.push({ to: '/mylogs', label: '日志', icon: 'files' })
items.push({ to: '/logs', label: '图片', icon: 'files' })
items.push({ to: '/mylogs', label: '日志', icon: 'log' })
items.push({ to: '/invite', label: '邀请', icon: 'accounts' })
}
// 文档 + 关于 are public — visible to guests too.
@@ -53,7 +53,9 @@ const currentLabel = computed(() => {
<aside class="fixed inset-y-0 left-0 z-30 w-16 md:w-20 flex flex-col items-center py-5 border-r border-[color:var(--hairline)]">
<!-- Logo -->
<router-link to="/" class="mb-8 group transition-transform hover:scale-105">
<Logo :size="40" class="rounded-xl shadow-lg shadow-violet-500/20 ring-1 ring-white/10" />
<img v-if="site.logo" :src="site.logo" :alt="site.title"
class="w-10 h-10 rounded-xl object-contain shadow-lg shadow-violet-500/20 ring-1 ring-white/10" />
<Logo v-else :size="40" class="rounded-xl shadow-lg shadow-violet-500/20 ring-1 ring-white/10" />
</router-link>
<!-- Nav -->
@@ -107,11 +109,10 @@ const currentLabel = computed(() => {
stamp doesn't jump around. -->
<header class="relative z-10 px-8 md:px-14 pt-10 pb-4 flex items-center justify-between gap-4">
<div class="flex items-baseline gap-2">
<img v-if="site.logo" :src="site.logo" :alt="site.title" class="h-7 w-auto self-center object-contain" />
<span v-else class="text-[22px] font-bold tracking-tight bg-gradient-to-r from-fuchsia-300 via-violet-300 to-sky-300 bg-clip-text text-transparent">
<span class="text-[22px] font-bold tracking-tight bg-gradient-to-r from-fuchsia-300 via-violet-300 to-sky-300 bg-clip-text text-transparent">
{{ site.title }}
</span>
<span class="text-[10px] uppercase tracking-[0.3em] text-[color:var(--fg-faint)]">{{ route.path === '/' ? (site.subtitle || 'AI 生图 · 生视频') : currentLabel }}</span>
<span class="text-[10px] uppercase tracking-[0.3em] text-[color:var(--fg-faint)]">{{ route.path === '/' ? 'AI 生图 · 生视频' : currentLabel }}</span>
</div>
<router-link v-if="showBalance" to="/settings"
class="text-xs text-[color:var(--fg-2)] hover:text-[color:var(--fg)] tabular-nums transition-colors">