Files
Edu/services/parent-bff/src/graphql/resolvers/notification-preference.resolver.ts
SpecialX 2229309a1e feat: initialize parent-bff service with full core features
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
2026-07-10 18:49:06 +08:00

135 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 缓存 300sPERMISSIONS_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);
};
}