Commit 17ae97bd by luoqi

feat(auth): 模拟登录支持 FRIDAY 市场(第二宿主,集团级)

工作台此前只能登 jvs-dw 租户,FRIDAY 摄入的 10.7 万患者 / 1.17 万召回计划无入口查看。
- MockLoginRequestSchema.tenant 加 'friday'
- auth.service 独立 early 分支:host=friday / tenantId=friday-market /
  orgScope=['friday-market'](集团根 → 展开后 sourceUnits/clinicIds 均不限)。
  多品牌 SaaS 无在岗花名册,mock 只做集团级;诊所名未摄入 → dictionary.clinics 空,
  UI 回退显示 id。jvs-dw 路径(MOCK_PRESETS)完全不变
- 弹窗集团级横条下加青色 FRIDAY 入口,角色沿用①所选

验证:mock-login 签发 token(hostId=friday/tenantId=friday-market)、瑞尔对照未受影响;
浏览器登入实测召回池 11,723 人,详情页画像 9 项 / 治疗历史 1,621 项 / 累计消费 ¥3,320
/ AI 话术均正常。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parent 4f176611
......@@ -571,6 +571,30 @@ export class AuthService {
if (!this.isMockLoginEnabled()) {
throw new BizError(ApiCode.AUTH_PERMISSION_DENIED, '模拟登录已在当前环境禁用');
}
// ── FRIDAY 市场(第二宿主)— 集团级专用分支 ──
// 多品牌 SaaS(30+ 品牌 = source_unit),mock 只做集团级(orgScope=[friday-market] →
// 全部品牌全部诊所);品牌级/客服级没有在岗花名册数据,不做。
// 诊所名字典未摄入(manifest 无 clinic_directory)→ dictionary.clinics 空,UI 回退显示 id。
if (req.tenant === 'friday') {
const fridayHost = await this.prisma.host.findFirst({ where: { name: 'friday' } });
if (!fridayHost) {
throw new BizError(ApiCode.HOST_NOT_FOUND, '请先建 friday host(cold-import 会自动创建)');
}
const fSub = `mock-friday-${req.role}`;
const fName = `FRIDAY ${roleNameZh(req.role)}`;
const { accessToken, refreshToken } = await this.issueTokens(fridayHost, {
userId: fSub,
tenantId: 'friday-market',
role: req.role,
orgScope: ['friday-market'], // 集团根 → 展开后 sourceUnits=[]/clinicIds=[] 不限
dictionary: { clinics: {}, users: { [fSub]: fName } },
});
this.logger.log(
`mock-login: tenant=friday(FRIDAY 市场) role=${req.role} sub=${fSub} orgScope=[friday-market]`,
);
return { accessToken, refreshToken, expiresIn: this.accessExpiresInSeconds };
}
const preset = this.MOCK_PRESETS[req.tenant];
if (!preset) {
throw new BizError(ApiCode.CLIENT_VALIDATION_FAILED, `unknown tenant: ${req.tenant}`);
......
......@@ -118,6 +118,21 @@ export function MockLoginDialog({ open }: { open: boolean }) {
}
};
// 第二宿主 FRIDAY 市场:集团级登录(orgScope=[friday-market] → 全部品牌全部诊所)
const pickFriday = async (roleKey: UserRole) => {
const key = `friday:${roleKey}`;
if (busy) return;
setBusy(key);
try {
const r = await mockLogin({ tenant: 'friday', role: roleKey });
applyTokens(r);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
toast.error('登录失败', { description: msg.slice(0, 120) });
setBusy(null);
}
};
const q = query.trim();
const filteredCs = csUsers
.filter((u) => (tenantFilter === 'all' ? true : u.tenant === tenantFilter))
......@@ -185,6 +200,15 @@ export function MockLoginDialog({ open }: { open: boolean }) {
>
数据范围 · <strong>集团级</strong> —— 看<strong>全部品牌全部诊所</strong>(跨品牌,最上一层)。角色沿用上方「{roleNameZh(role)}」。
</button>
{/* 第二宿主 FRIDAY 市场:集团级专用(多品牌 SaaS,mock 不做品牌/客服级);诊所名未摄入显示 id */}
<button
type="button"
onClick={() => pickFriday(role)}
disabled={!!busy}
className="mt-2 w-full rounded-md border border-dashed border-teal-300 bg-teal-50/40 px-2.5 py-1.5 text-left text-[11.5px] text-teal-800 transition-colors hover:border-teal-400 disabled:opacity-50"
>
宿主 · <strong>FRIDAY 市场</strong>(第二宿主,集团级)—— 看全部品牌全部诊所。角色沿用上方「{roleNameZh(role)}」。
</button>
</div>
{/* ② 选客服 */}
......
......@@ -114,8 +114,8 @@ export type RefreshTokenResponse = z.infer<typeof RefreshTokenResponseSchema>;
// 安全:env 门控 — 只在 NODE_ENV != 'production' 或 PAC_ENABLE_MOCK_LOGIN=true 时启用,
// 生产 host 真接入后置 false / 删该 endpoint
export const MockLoginRequestSchema = z.strictObject({
/// 租户 slug — 'ruier'(瑞尔) / 'ruitai'(瑞泰),后端映射到 tenant UUID + clinics
tenant: z.enum(['ruier', 'ruitai']),
/// 租户 slug — 'ruier'(瑞尔) / 'ruitai'(瑞泰) → jvs-dw;'friday' → FRIDAY 市场(集团级)
tenant: z.enum(['ruier', 'ruitai', 'friday']),
/// PAC 角色 — staff / leader / admin
role: UserRoleSchema,
/// 可选:登录为具体客服(externalId,来自 GET /auth/mock-users)。
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment