Commit 1506e2fa by luoqi

feat(action-urls): 会话下发 actionUrls + 「新建预约」按钮消费(配置驱动,不写死)

- /auth/session 返回当前 host 的 actionUrls(AuthService.getActionUrls),所有角色可拿,
  不再依赖 admin 端点;SessionResponse 加 actionUrls 字段
- 前端 loadSession 把 actionUrls 存进会话 user;「新建预约」读 actionUrls.CREATE_APPOINTMENT
  → 替换 {patientId}(=external_id)/{doctorId} → window.open(_top);未配置则提示去配
- 宿主动作 URL 仍是宿主在 admin 配的数据,代码零硬编码

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 6182b2f5
Pipeline #3342 failed in 0 seconds
......@@ -87,7 +87,7 @@ export class AuthController {
summary:
'当前会话视图 — 把 JWT.orgScope 展开成 clinicIds/sourceUnits 返给前端(设计 B:JWT 只装通用 orgScope,前端展不开)',
})
session(
async session(
@CurrentUser() user: AccessTokenPayload,
@TenantScope() scope: TenantScopeContext,
) {
......@@ -101,6 +101,8 @@ export class AuthController {
clinicIds: scope.clinicIds, // 拦截器已按 host org 树展开
sourceUnits: scope.sourceUnits,
...(user.dictionary ? { dictionary: user.dictionary } : {}),
// 宿主动作跳转模板 —— 前端"新建预约"等按钮据此打开宿主页(缺失=不渲染/不跳)
actionUrls: await this.auth.getActionUrls(user.hostId),
};
}
}
......@@ -67,6 +67,15 @@ export class AuthService {
));
}
/** 当前 host 的 actionUrls(跳转模板)—— 会话视图带给前端;缺失返 {}。 */
async getActionUrls(hostId: string): Promise<Record<string, string | null>> {
const host = await this.prisma.host.findUnique({
where: { id: hostId },
select: { actionUrls: true },
});
return (host?.actionUrls as Record<string, string | null>) ?? {};
}
async exchangeToken(req: TokenExchangeRequest): Promise<TokenExchangeResponse> {
const host = await this.prisma.host.findUnique({ where: { appId: req.appId } });
if (!host || !host.active) {
......
......@@ -21,6 +21,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { plansApi } from '@/components/plans/plans-api';
import { usePlanSyncStore } from '@/stores/plan-sync-store';
import { useAuthStore } from '@/stores/auth-store';
import { IdentityCluster } from '@/components/identity-cluster';
import { isEmbedded } from '@/lib/embed';
import {
......@@ -484,10 +485,21 @@ export function PlanDetailApp({
plan={effectivePlan}
onSubmit={submitOutcome}
defaultChannel={effectivePlan.recommendedChannel}
onCreateAppointment={() =>
// 占位:真实接入时由宿主 actionUrls / env 下发 URL,代码不写死。
showToast('emerald', '打开宿主预约页', '带上患者 + 医生 + 治疗项预填')
onCreateAppointment={() => {
// 宿主 actionUrls.CREATE_APPOINTMENT(会话下发)→ 占位替换 → 打开宿主页。
// 未配置则提示去配,不写死任何 URL。
const tpl = useAuthStore.getState().user?.actionUrls?.CREATE_APPOINTMENT;
if (!tpl) {
showToast('amber', '未配置新建预约', '请在宿主管理页配置 actionUrls.CREATE_APPOINTMENT');
return;
}
const url = tpl
.replace(/\{patientId\}/g, encodeURIComponent(patient.externalId ?? ''))
.replace(/\{doctorId\}/g, '')
.replace(/\{[a-zA-Z]+\}/g, ''); // 其余未知占位清空
window.open(url, '_top');
showToast('emerald', '已打开宿主预约页', '带上患者 id');
}}
/>
</div>
</section>
......
......@@ -12,6 +12,8 @@ import type { AccessTokenPayload } from '@pac/types';
export type SessionUser = AccessTokenPayload & {
clinicIds?: string[];
sourceUnits?: string[];
/// 宿主动作跳转模板(actionUrls)—— 从 /auth/session 拉;"新建预约"等按钮据此打开宿主页。
actionUrls?: Record<string, string | null>;
};
/**
......@@ -102,7 +104,14 @@ export const useAuthStore = create<AuthState>()(
const s = await getSession();
set((st) =>
st.user
? { user: { ...st.user, clinicIds: s.clinicIds, sourceUnits: s.sourceUnits } }
? {
user: {
...st.user,
clinicIds: s.clinicIds,
sourceUnits: s.sourceUnits,
actionUrls: s.actionUrls,
},
}
: {},
);
} catch {
......
......@@ -194,5 +194,7 @@ export const SessionResponseSchema = z.object({
clinicIds: z.array(z.string()),
sourceUnits: z.array(z.string()),
dictionary: TokenDictionarySchema.optional(),
/// 宿主动作跳转模板(actionUrls)—— 前端"新建预约"等按钮据此打开宿主页;缺失=不渲染。
actionUrls: z.record(z.string(), z.string().nullable()).optional(),
});
export type SessionResponse = z.infer<typeof SessionResponseSchema>;
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