add complete parent-bff implementation including: - GraphQL endpoint with depth/cost validation - ChildGuard越权校验 with redis cache and singleflight - parallel orchestration with partial failure fallback - three-level cache fallback strategy (Redis + LRU + downstream) - Kafka consumer for cache invalidation and notification push - opossum circuit breaker for downstream services - Prometheus metrics and SLO alerts - Helm chart for k8s deployment with multi-environment configs - Grafana dashboard for observability - complete unit and integration tests
135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
import type { MsgClient } from "../../clients/msg.client.js";
|
||
import type { NotificationPreferencesDto } from "../../clients/dtos.js";
|
||
import type {
|
||
NotificationPreferencesType,
|
||
NotificationChannel,
|
||
} from "../types.js";
|
||
import type { GraphqlContext } from "../context.js";
|
||
import { CacheKeys } from "../../shared/cache/cache-key.builder.js";
|
||
import {
|
||
withCacheFallback,
|
||
invalidateCache,
|
||
} from "../../aggregation/fallback-strategy.js";
|
||
import { env } from "../../config/env.js";
|
||
import { logger } from "../../shared/observability/logger.js";
|
||
import {
|
||
UpdateNotificationPreferencesSchema,
|
||
type UpdateNotificationPreferencesInput,
|
||
} from "../../parent/dto/parent-inputs.dto.js";
|
||
|
||
const CHANNEL_MAP: Record<string, NotificationChannel> = {
|
||
APP: "APP",
|
||
SMS: "SMS",
|
||
EMAIL: "EMAIL",
|
||
WECHAT: "WECHAT",
|
||
};
|
||
|
||
/**
|
||
* DTO → GraphQL Type 映射。
|
||
*/
|
||
function mapPreferences(
|
||
dto: NotificationPreferencesDto,
|
||
): NotificationPreferencesType {
|
||
return {
|
||
channels: dto.channels.map((c) => CHANNEL_MAP[c] ?? "APP"),
|
||
eventTypes: { ...dto.eventTypes },
|
||
};
|
||
}
|
||
|
||
/**
|
||
* notificationPreferences Query resolver。
|
||
*
|
||
* Redis 缓存 300s(PERMISSIONS_CACHE_TTL_SECONDS)。
|
||
*/
|
||
export function buildNotificationPreferencesQueryResolver(deps: {
|
||
msgClient: MsgClient;
|
||
}) {
|
||
return async (
|
||
_parent: unknown,
|
||
_args: unknown,
|
||
ctx: GraphqlContext,
|
||
): Promise<NotificationPreferencesType> => {
|
||
const parentId = ctx.session.parentId;
|
||
const cacheKey = CacheKeys.notificationPrefs(parentId);
|
||
|
||
const result = await withCacheFallback(
|
||
cacheKey,
|
||
() => deps.msgClient.getNotificationPreferences(parentId),
|
||
env.PERMISSIONS_CACHE_TTL_SECONDS,
|
||
"notification-prefs",
|
||
);
|
||
|
||
const dto = result.data;
|
||
if (!dto) {
|
||
// 下游失败且无缓存,返回默认偏好
|
||
return {
|
||
channels: ["APP"],
|
||
eventTypes: {
|
||
gradeReleased: true,
|
||
homeworkGraded: true,
|
||
examPublished: true,
|
||
attendanceAlert: true,
|
||
schoolAnnouncement: true,
|
||
},
|
||
};
|
||
}
|
||
return mapPreferences(dto);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* updateNotificationPreferences Mutation resolver。
|
||
*
|
||
* Zod 校验输入 → 调 msg.UpdateNotificationPreferences → 失效缓存 → 返回新偏好。
|
||
*/
|
||
export function buildUpdateNotificationPreferencesMutationResolver(deps: {
|
||
msgClient: MsgClient;
|
||
}) {
|
||
return async (
|
||
_parent: unknown,
|
||
args: { input: UpdateNotificationPreferencesInput },
|
||
ctx: GraphqlContext,
|
||
): Promise<NotificationPreferencesType> => {
|
||
const parentId = ctx.session.parentId;
|
||
|
||
// Zod 校验
|
||
const parsed = UpdateNotificationPreferencesSchema.safeParse(args.input);
|
||
if (!parsed.success) {
|
||
throw new Error(
|
||
`Invalid notification preferences input: ${JSON.stringify(
|
||
parsed.error.flatten(),
|
||
)}`,
|
||
);
|
||
}
|
||
|
||
// 获取当前偏好(合并未传的 eventTypes)
|
||
const current = await deps.msgClient.getNotificationPreferences(parentId);
|
||
const merged: NotificationPreferencesDto = {
|
||
parentId,
|
||
channels: parsed.data.channels,
|
||
eventTypes: {
|
||
gradeReleased: parsed.data.eventTypes.gradeReleased ?? current.eventTypes.gradeReleased,
|
||
homeworkGraded: parsed.data.eventTypes.homeworkGraded ?? current.eventTypes.homeworkGraded,
|
||
examPublished: parsed.data.eventTypes.examPublished ?? current.eventTypes.examPublished,
|
||
attendanceAlert: parsed.data.eventTypes.attendanceAlert ?? current.eventTypes.attendanceAlert,
|
||
schoolAnnouncement: parsed.data.eventTypes.schoolAnnouncement ?? current.eventTypes.schoolAnnouncement,
|
||
},
|
||
};
|
||
|
||
const updated = await deps.msgClient.updateNotificationPreferences(
|
||
parentId,
|
||
merged,
|
||
);
|
||
|
||
// 失效偏好缓存
|
||
await invalidateCache(CacheKeys.notificationPrefs(parentId));
|
||
|
||
logger.info(
|
||
{ parentId, channels: updated.channels },
|
||
"Notification preferences updated",
|
||
);
|
||
|
||
return mapPreferences(updated);
|
||
};
|
||
}
|