fix: PMC 测试清单未完成项 + YYX#row22 未读角标

PMC#16 账户登录地址不正确:
- 写死的 https://airshelf.com/login(不存在的域名)改成 ${location.origin}/login,
  与邀请链接同源,成员拿到的就是当前真实访问地址。

PMC#17/#18 成员三档额度只有月度生效、日/总纯装饰 → 列表显示与配置对不上:
- TeamMember 新增 daily_credit_limit / total_credit_limit(0=不限,迁移 0006);
- reserve_credit 逐档真实管控(每日/每月/累计总额),口径=窗口内 CHARGE 流水 + 在途预留;
- 成员列表"每日额度"由写死"不限"改成真实日额度+今日已用,并新增"总额度"列;
- 创建/编辑弹窗三档额度真入库真生效;补 3 个计费单测(日/总拦截、0=不限)。

PMC#24 同批生成的图片文字/内容雷同:
- 根因=套图/自由创作同批 N 张用同一 prompt 且无变化(只有模特上身图按 index 变);
- 给平台套图/自由参考/纯文生图都加按 index 的版式变体,避开传 seed(防中转站 400)。

PMC#25 重跑应在原任务上重跑而非新开任务:
- 全批"重跑"改为原地复位同一批次卡;"重跑这张/再生成"改为补图进原卡(不覆盖好图、不新开任务)。

YYX#row22 图片生成未读角标(本仓库既有改动一并提交):
- AITask 新增 read_at(迁移 0020 + 索引一致性迁移 0021);导航栏"图片生成"未读胶囊、
  商品预览右下角未读分数;查看某商品即标记其生成任务已读、清零角标。

注:PMC#14/#15/#20/#22/#23 早前提交已修,需部署重测;PMC#13/#21 为 provider 侧
生成失败,代码侧重试/退费/僵尸回收/错误外露已就位,需部署后看真实 error_message 定位;
PMC#19 收件箱标记与设计稿逐字一致,待截图复现。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-27 18:27:15 +08:00
co-authored by Claude Opus 4.8
parent 2e1c28abea
commit 6e7425048c
19 changed files with 483 additions and 98 deletions
+25 -9
View File
@@ -8,7 +8,8 @@ import { ConfirmModal, TeamModal } from "../components/overlays";
import { Pager } from "../components/pager";
const MEMBERS_PER_PAGE = 10;
const LOGIN_URL = "https://airshelf.com/login";
// 登录地址必须是用户当前真实访问的域名(与邀请链接同源),写死的 airshelf.com 不存在 → PMC#16
const LOGIN_URL = `${location.origin}/login`;
const LIMIT_PRESETS = [1000, 3000, 5000, 10000, -1];
// 本地随机用户名:前缀 + 4 位数字(与 V1 设计稿一致)
@@ -82,8 +83,8 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
user: User;
billing: BillingSummary | null;
navigate: (page: Page) => void;
onCreateMember: (payload: { username: string; password: string; name?: string; role?: string; monthly_credit_limit?: number }) => void | Promise<unknown>;
onUpdateMember: (id: string, payload: { role?: string; monthly_credit_limit?: number }) => void | Promise<unknown>;
onCreateMember: (payload: { username: string; password: string; name?: string; role?: string; daily_credit_limit?: number; monthly_credit_limit?: number; total_credit_limit?: number }) => void | Promise<unknown>;
onUpdateMember: (id: string, payload: { role?: string; daily_credit_limit?: number; monthly_credit_limit?: number; total_credit_limit?: number }) => void | Promise<unknown>;
onRemoveMember: (id: string) => void | Promise<unknown>;
onResetPassword: (id: string, password: string) => void | Promise<unknown>;
onRecharge: (amount: number, bonus: number) => void | Promise<unknown>;
@@ -191,9 +192,10 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
function openEdit(member: TeamMember) {
setEditTarget(member);
setEdRole(member.role === "owner" ? "admin" : member.role || "member");
setEdDaily("");
// 回填该成员当前三档真实额度(0=不限 → 输入框留空)
setEdDaily(Number(member.daily_credit_limit || 0) > 0 ? String(Number(member.daily_credit_limit)) : "");
setEdMonthly(Number(member.monthly_credit_limit || 0) > 0 ? String(Number(member.monthly_credit_limit)) : "");
setEdTotal("-1");
setEdTotal(Number(member.total_credit_limit || 0) > 0 ? String(Number(member.total_credit_limit)) : "");
}
function openLimit() {
@@ -209,8 +211,10 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
password: cuPass,
name: cuName.trim() || undefined,
role: cuRole,
// 后端成员额度仅 monthly_credit_limit;每日/总额为展示档,不入参
monthly_credit_limit: Number(cuMonthly) || 0
// 三档额度均真实入库:-1/空 = 不限(后端归一成 0)。PMC#17/#18
daily_credit_limit: cuDaily.trim() === "" ? -1 : Number(cuDaily),
monthly_credit_limit: cuMonthly.trim() === "" ? -1 : Number(cuMonthly),
total_credit_limit: cuTotal.trim() === "" ? -1 : Number(cuTotal)
});
// 创建失败(action 报错返回 null)时保持弹窗与已填内容,只成功才关闭并清空
if (!created) return;
@@ -228,7 +232,13 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
async function submitEdit() {
if (!editTarget) return;
await onUpdateMember(editTarget.id, { role: edRole, monthly_credit_limit: Number(edMonthly) || 0 });
// 三档额度均下发:留空 = 不限(-1,后端归一成 0)。PMC#17/#18
await onUpdateMember(editTarget.id, {
role: edRole,
daily_credit_limit: edDaily.trim() === "" ? -1 : Number(edDaily),
monthly_credit_limit: edMonthly.trim() === "" ? -1 : Number(edMonthly),
total_credit_limit: edTotal.trim() === "" ? -1 : Number(edTotal)
});
setEditTarget(null);
}
@@ -375,6 +385,7 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
<th>角色</th>
<th>每日额度</th>
<th>月度额度</th>
<th>总额度</th>
<th style={{ width: "140px" }}>当月已用</th>
<th style={{ textAlign: "right", width: "88px" }}>操作</th>
</tr>
@@ -387,6 +398,10 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
const displayName = rawName && !rawName.includes("@") ? rawName : (email ? email.split("@")[0] : (rawName || "成员"));
const showEmail = !!email && email.toLowerCase() !== displayName.toLowerCase();
const role = roleUi(member.role);
const daily = Number(member.daily_credit_limit || 0);
const totalLimit = Number(member.total_credit_limit || 0);
const dayUsed = Number(member.day_charged || 0);
const totalUsed = Number(member.total_charged || 0);
const monthly = Number(member.monthly_credit_limit || 0);
const memberUsed = Number(member.month_charged || 0);
// 月度不限时按团队月限额/余额作分母给参考进度;有消费即显可见细条,避免「用了钱进度条却空白」
@@ -399,8 +414,9 @@ export function TeamPage({ team, user, billing, navigate, onCreateMember: onCrea
<tr key={member.id} data-id={member.id}>
<td><span className="member-cell"><span className="av">{displayName.slice(0, 1).toUpperCase()}</span><span className="member-meta"><span className="nm">{displayName}</span>{showEmail && <span className="em">{email}</span>}</span></span></td>
<td><span className={`role-pill role-${role.key}`}><span className="dot"></span>{role.label}</span></td>
<td><span className="quota-cell"><span className="v">不限</span></span></td>
<td><span className="quota-cell"><span className="v">{daily > 0 ? money(daily) : "不限"}</span>{daily > 0 && <span className="lbl">今日用 {money(dayUsed)}</span>}</span></td>
<td><span className="quota-cell"><span className="v">{monthly > 0 ? money(monthly) : "不限"}</span></span></td>
<td><span className="quota-cell"><span className="v">{totalLimit > 0 ? money(totalLimit) : "不限"}</span>{totalLimit > 0 && <span className="lbl">累计用 {money(totalUsed)}</span>}</span></td>
<td><div className="quota-cell"><span className="v">{money(memberUsed)}</span> <span className="lbl">/ {monthly > 0 ? `${memberPct.toFixed(0)}%` : "不限"}</span></div><div className="used-bar"><span className={barClass} style={{ width: `${barWidth}%` }}></span></div></td>
<td><div className="acts">{isOwner
? <span style={{ fontFamily: "var(--font-mono)", fontSize: "12px", color: "var(--black-alpha-32)", alignSelf: "center" }}>不可编辑</span>