fix: code compliance audit and fix across all services
Some checks failed
CI / quality-ts (push) Failing after 48s
CI / quality-go (push) Failing after 4s
CI / quality-proto (push) Failing after 2s
CI / deploy (push) Has been skipped

NestJS (6 services): implement @RequirePermission decorator with
SetMetadata+Reflector, register APP_GUARD globally, fix as assertions
to type guards, add explicit return types, fix import type for express,
fix /metrics implicit any, replace native Error with ApplicationError,
remove typeorm remnants, register LifecycleService.

teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward
real userId to downstream, log downstream failures, migrate health
controller to shared/health.

Go (2 services): interface to any, doc comments, CORS dev whitelist,
JWT secret fail-fast, push-gateway internal API auth, metrics and
readyz endpoints, remove dead code.

Python (2 services): lifespan return type, dev_mode to bool, data-ana
APIRouter, ai POST body model, ClickHouse async wrapping.
This commit is contained in:
SpecialX
2026-07-09 17:28:27 +08:00
parent b53a486c6e
commit 0a71b02e04
93 changed files with 5775 additions and 608 deletions

View File

@@ -2,8 +2,6 @@ import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Param,
Post,
Put,
@@ -11,42 +9,42 @@ import {
Req,
} from "@nestjs/common";
import { NotificationsService } from "./notifications.service.js";
import type { SendNotificationDto } from "./notifications.service.js";
interface AuthedRequest {
headers: Record<string, string | string[] | undefined>;
}
/**
* 从请求头读取用户身份。
* Gateway 在通过鉴权后会注入 x-user-id如未注入开发模式或匿名请求返回 null。
*/
function getUserIdFromRequest(req: AuthedRequest): string | null {
const raw = req.headers["x-user-id"];
if (typeof raw === "string" && raw.length > 0) return raw;
return null;
}
import {
sendNotificationSchema,
sendNotificationBatchSchema,
} from "./notifications.dto.js";
import type { SendNotificationDto } from "./notifications.dto.js";
import {
Permissions,
RequirePermission,
} from "../middleware/permission.guard.js";
import type { AuthenticatedRequest } from "../middleware/auth.middleware.js";
import { PermissionDeniedError } from "../shared/errors/application-error.js";
@Controller("notifications")
export class NotificationsController {
constructor(private readonly service: NotificationsService) {}
@Post()
@RequirePermission(Permissions.MSG_NOTIFICATION_SEND)
async send(@Body() body: unknown): Promise<{ success: true; data: unknown }> {
const result = await this.service.send(body as SendNotificationDto);
const dto: SendNotificationDto = sendNotificationSchema.parse(body);
const result = await this.service.send(dto);
return { success: true, data: result };
}
@Post("batch")
@RequirePermission(Permissions.MSG_NOTIFICATION_SEND)
async createBatch(
@Body() body: unknown,
): Promise<{ success: true; data: unknown }> {
const dtos = (body as SendNotificationDto[]) ?? [];
const dtos: SendNotificationDto[] = sendNotificationBatchSchema.parse(body);
const result = await this.service.createBatch(dtos);
return { success: true, data: result };
}
@Get("user/:userId")
@RequirePermission(Permissions.MSG_NOTIFICATION_READ)
async listByUser(
@Param("userId") userId: string,
@Query("unread") unread: string,
@@ -56,6 +54,7 @@ export class NotificationsController {
}
@Get("user/:userId/page")
@RequirePermission(Permissions.MSG_NOTIFICATION_READ)
async listByUserPaginated(
@Param("userId") userId: string,
@Query("page") page: string,
@@ -72,29 +71,22 @@ export class NotificationsController {
}
@Put(":id/read")
@RequirePermission(Permissions.MSG_NOTIFICATION_MANAGE)
async markAsRead(@Param("id") id: string): Promise<{ success: true }> {
await this.service.markAsRead(id);
return { success: true };
}
@Get("search")
@RequirePermission(Permissions.MSG_NOTIFICATION_READ)
async search(
@Req() req: AuthedRequest,
@Req() req: AuthenticatedRequest,
@Query("q") q: string,
@Query("userId") userIdParam: string,
): Promise<{ success: true; data: unknown }> {
const userId = getUserIdFromRequest(req) ?? userIdParam;
const userId = req.userId ?? userIdParam;
if (!userId) {
throw new HttpException(
{
success: false,
error: {
code: "MSG_PERMISSION_DENIED",
message: "Missing user identity",
},
},
HttpStatus.FORBIDDEN,
);
throw new PermissionDeniedError("MSG_NOTIFICATION_READ");
}
const result = await this.service.search(userId, q);
return { success: true, data: result };

View File

@@ -0,0 +1,14 @@
import { z } from "zod";
export const sendNotificationSchema = z.object({
userId: z.string().min(1),
type: z.string().min(1).max(50),
title: z.string().min(1).max(200),
content: z.string().min(1),
channel: z.string().max(20).optional(),
metadata: z.record(z.unknown()).optional(),
});
export const sendNotificationBatchSchema = z.array(sendNotificationSchema);
export type SendNotificationDto = z.infer<typeof sendNotificationSchema>;

View File

@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { randomUUID } from "node:crypto";
import { eq, and, desc, sql } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
import { db } from "../config/database.js";
import {
notifications,
@@ -10,19 +10,11 @@ import type {
Notification,
NotificationPreference,
} from "./notifications.schema.js";
import type { SendNotificationDto } from "./notifications.dto.js";
import { safeIndex, safeSearch } from "../config/elasticsearch.js";
import { env } from "../config/env.js";
import { logger } from "../shared/observability/logger.js";
export interface SendNotificationDto {
userId: string;
type: string;
title: string;
content: string;
channel?: string;
metadata?: Record<string, unknown>;
}
export interface SendResult {
id: string;
skipped: boolean;
@@ -39,7 +31,7 @@ export interface PaginatedResult {
@Injectable()
export class NotificationsService {
async send(dto: SendNotificationDto): Promise<SendResult> {
const id = uuidv4();
const id = randomUUID();
const channel = dto.channel ?? "in_app";
const [pref] = await db