This commit is contained in:
ditiqi 2025-01-27 22:43:15 +08:00
parent 8c87e39c6a
commit 88b66c50bf
3 changed files with 354 additions and 262 deletions

View File

@ -1,4 +1,19 @@
import { Controller, Headers, Post, Body, UseGuards, Get, Req, HttpException, HttpStatus, BadRequestException, InternalServerErrorException, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common'; import {
Controller,
Headers,
Post,
Body,
UseGuards,
Get,
Req,
HttpException,
HttpStatus,
BadRequestException,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
Logger,
} from '@nestjs/common';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthSchema, JwtPayload } from '@nice/common'; import { AuthSchema, JwtPayload } from '@nice/common';
import { AuthGuard } from './auth.guard'; import { AuthGuard } from './auth.guard';
@ -7,8 +22,8 @@ import { z } from 'zod';
import { FileValidationErrorType } from './types'; import { FileValidationErrorType } from './types';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
private logger = new Logger(AuthController.name) private logger = new Logger(AuthController.name);
constructor(private readonly authService: AuthService) { } constructor(private readonly authService: AuthService) {}
@Get('file') @Get('file')
async authFileRequset( async authFileRequset(
@Headers('x-original-uri') originalUri: string, @Headers('x-original-uri') originalUri: string,
@ -18,7 +33,6 @@ export class AuthController {
@Headers('host') host: string, @Headers('host') host: string,
@Headers('authorization') authorization: string, @Headers('authorization') authorization: string,
) { ) {
try { try {
const fileRequest = { const fileRequest = {
originalUri, originalUri,
@ -26,10 +40,11 @@ export class AuthController {
method, method,
queryParams, queryParams,
host, host,
authorization authorization,
}; };
const authResult = await this.authService.validateFileRequest(fileRequest); const authResult =
await this.authService.validateFileRequest(fileRequest);
if (!authResult.isValid) { if (!authResult.isValid) {
// 使用枚举类型进行错误处理 // 使用枚举类型进行错误处理
switch (authResult.error) { switch (authResult.error) {
@ -41,7 +56,9 @@ export class AuthController {
case FileValidationErrorType.INVALID_TOKEN: case FileValidationErrorType.INVALID_TOKEN:
throw new UnauthorizedException(authResult.error); throw new UnauthorizedException(authResult.error);
default: default:
throw new InternalServerErrorException(authResult.error || FileValidationErrorType.UNKNOWN_ERROR); throw new InternalServerErrorException(
authResult.error || FileValidationErrorType.UNKNOWN_ERROR,
);
} }
} }
return { return {
@ -51,17 +68,20 @@ export class AuthController {
}, },
}; };
} catch (error: any) { } catch (error: any) {
this.logger.verbose(`File request auth failed from ${realIp} reason:${error.message}`) this.logger.verbose(
`File request auth failed from ${realIp} reason:${error.message}`,
);
throw error; throw error;
} }
} }
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@Get('user-profile') @Get('user-profile')
async getUserProfile(@Req() request: Request) { async getUserProfile(@Req() request: Request) {
const payload: JwtPayload = (request as any).user; const payload: JwtPayload = (request as any).user;
const { staff } = await UserProfileService.instance.getUserProfileById(payload.sub); const { staff } = await UserProfileService.instance.getUserProfileById(
return staff payload.sub,
);
return staff;
} }
@Post('login') @Post('login')
async login(@Body() body: z.infer<typeof AuthSchema.signInRequset>) { async login(@Body() body: z.infer<typeof AuthSchema.signInRequset>) {

View File

@ -1,5 +1,5 @@
import { Logger } from "@nestjs/common"; import { Logger } from '@nestjs/common';
import { UserProfile, db, RowModelRequest } from "@nice/common"; import { UserProfile, db, RowModelRequest } from '@nice/common';
import { LogicalCondition, OperatorType, SQLBuilder } from './sql-builder'; import { LogicalCondition, OperatorType, SQLBuilder } from './sql-builder';
export interface GetRowOptions { export interface GetRowOptions {
id?: string; id?: string;
@ -9,26 +9,39 @@ export interface GetRowOptions {
} }
export abstract class RowModelService { export abstract class RowModelService {
private keywords: Set<string> = new Set([ private keywords: Set<string> = new Set([
'SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'GROUP', 'JOIN', 'AND', 'OR' 'SELECT',
'FROM',
'WHERE',
'ORDER',
'BY',
'GROUP',
'JOIN',
'AND',
'OR',
// 添加更多需要引号的关键词 // 添加更多需要引号的关键词
]); ]);
protected logger = new Logger(this.tableName); protected logger = new Logger(this.tableName);
protected constructor(protected tableName: string) { } protected constructor(protected tableName: string) {}
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> { protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
return row; return row;
} }
protected async getRowsSqlWrapper(sql: string, request?: RowModelRequest, staff?: UserProfile) { protected async getRowsSqlWrapper(
if (request) sql: string,
return SQLBuilder.join([sql, this.getLimitSql(request)]) request?: RowModelRequest,
return sql staff?: UserProfile,
) {
if (request) return SQLBuilder.join([sql, this.getLimitSql(request)]);
return sql;
} }
protected getLimitSql(request: RowModelRequest) { protected getLimitSql(request: RowModelRequest) {
return SQLBuilder.limit(request.endRow - request.startRow, request.startRow) return SQLBuilder.limit(
request.endRow - request.startRow,
request.startRow,
);
} }
abstract createJoinSql(request?: RowModelRequest): string[]; abstract createJoinSql(request?: RowModelRequest): string[];
async getRows(request: RowModelRequest, staff?: UserProfile) { async getRows(request: RowModelRequest, staff?: UserProfile) {
try { try {
let SQL = SQLBuilder.join([ let SQL = SQLBuilder.join([
SQLBuilder.select(this.getRowSelectCols(request)), SQLBuilder.select(this.getRowSelectCols(request)),
SQLBuilder.from(this.tableName), SQLBuilder.from(this.tableName),
@ -37,17 +50,21 @@ export abstract class RowModelService {
SQLBuilder.groupBy(this.getGroupByColumns(request)), SQLBuilder.groupBy(this.getGroupByColumns(request)),
SQLBuilder.orderBy(this.getOrderByColumns(request)), SQLBuilder.orderBy(this.getOrderByColumns(request)),
]); ]);
SQL = await this.getRowsSqlWrapper(SQL, request, staff) SQL = await this.getRowsSqlWrapper(SQL, request, staff);
this.logger.debug('getrows', SQL) this.logger.debug('getrows', SQL);
const results: any[] = await db?.$queryRawUnsafe(SQL) || []; const results: any[] = (await db?.$queryRawUnsafe(SQL)) || [];
let rowDataDto = await Promise.all(results.map(row => this.getRowDto(row, staff))) const rowDataDto = await Promise.all(
return { rowCount: this.getRowCount(request, rowDataDto) || 0, rowData: rowDataDto }; results.map((row) => this.getRowDto(row, staff)),
);
return {
rowCount: this.getRowCount(request, rowDataDto) || 0,
rowData: rowDataDto,
};
} catch (error: any) { } catch (error: any) {
this.logger.error('Error executing getRows:', error); this.logger.error('Error executing getRows:', error);
} }
} }
getRowCount(request: RowModelRequest, results: any[]) { getRowCount(request: RowModelRequest, results: any[]) {
@ -59,50 +76,75 @@ export abstract class RowModelService {
} }
async getRowById(options: GetRowOptions): Promise<any> { async getRowById(options: GetRowOptions): Promise<any> {
const { id, extraCondition = { const {
id,
extraCondition = {
field: `${this.tableName}.deleted_at`, field: `${this.tableName}.deleted_at`,
op: "blank", op: 'blank',
type: "date" type: 'date',
}, staff } = options; },
return this.getSingleRow({ AND: [this.createGetByIdFilter(id!), extraCondition] }, staff); staff,
} = options;
return this.getSingleRow(
{ AND: [this.createGetByIdFilter(id!), extraCondition] },
staff,
);
} }
async getRowByIds(options: GetRowOptions): Promise<any[]> { async getRowByIds(options: GetRowOptions): Promise<any[]> {
const { ids, extraCondition = { const {
ids,
extraCondition = {
field: `${this.tableName}.deleted_at`, field: `${this.tableName}.deleted_at`,
op: "blank", op: 'blank',
type: "date" type: 'date',
}, staff } = options; },
return this.getMultipleRows({ AND: [this.createGetByIdsFilter(ids!), extraCondition] }, staff); staff,
} = options;
return this.getMultipleRows(
{ AND: [this.createGetByIdsFilter(ids!), extraCondition] },
staff,
);
} }
protected createGetRowsFilters(request: RowModelRequest, staff?: UserProfile): LogicalCondition { protected createGetRowsFilters(
let groupConditions: LogicalCondition[] = [] request: RowModelRequest,
staff?: UserProfile,
): LogicalCondition {
let groupConditions: LogicalCondition[] = [];
if (this.isDoingTreeGroup(request)) { if (this.isDoingTreeGroup(request)) {
groupConditions = [ groupConditions = [
{ {
field: 'parent_id', field: 'parent_id',
op: "equals" as OperatorType, op: 'equals' as OperatorType,
value: request.groupKeys[request.groupKeys.length - 1] value: request.groupKeys[request.groupKeys.length - 1],
} },
] ];
} else { } else {
groupConditions = request?.groupKeys?.map((key, index) => ({ groupConditions = request?.groupKeys?.map((key, index) => ({
field: request.rowGroupCols[index].field, field: request.rowGroupCols[index].field,
op: "equals" as OperatorType, op: 'equals' as OperatorType,
value: key value: key,
})) }));
} }
const condition: LogicalCondition = { const condition: LogicalCondition = {
AND: [...groupConditions, ...this.buildFilterConditions(request.filterModel)] AND: [
} ...groupConditions,
...this.buildFilterConditions(request.filterModel),
],
};
return condition; return condition;
} }
private buildFilterConditions(filterModel: any): LogicalCondition[] { private buildFilterConditions(filterModel: any): LogicalCondition[] {
return filterModel return filterModel
? Object.entries(filterModel)?.map(([key, item]) => SQLBuilder.createFilterSql(key === 'ag-Grid-AutoColumn' ? 'name' : key, item)) ? Object.entries(filterModel)?.map(([key, item]) =>
SQLBuilder.createFilterSql(
key === 'ag-Grid-AutoColumn' ? 'name' : key,
item,
),
)
: []; : [];
} }
@ -116,21 +158,30 @@ export abstract class RowModelService {
} }
protected createAggSqlForWrapper(request: RowModelRequest) { protected createAggSqlForWrapper(request: RowModelRequest) {
const { rowGroupCols, valueCols, groupKeys } = request; const { rowGroupCols, valueCols, groupKeys } = request;
return valueCols.map(valueCol => return valueCols.map(
`${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}` (valueCol) =>
`${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}`,
); );
} }
protected createGroupingRowSelect(request: RowModelRequest, wrapperSql: boolean = false): string[] { protected createGroupingRowSelect(
request: RowModelRequest,
wrapperSql: boolean = false,
): string[] {
const { rowGroupCols, valueCols, groupKeys } = request; const { rowGroupCols, valueCols, groupKeys } = request;
const colsToSelect: string[] = []; const colsToSelect: string[] = [];
const rowGroupCol = rowGroupCols[groupKeys!.length]; const rowGroupCol = rowGroupCols[groupKeys!.length];
if (rowGroupCol) { if (rowGroupCol) {
colsToSelect.push(`${rowGroupCol.field} AS ${rowGroupCol.field.replace('.', '_')}`); colsToSelect.push(
`${rowGroupCol.field} AS ${rowGroupCol.field.replace('.', '_')}`,
);
} }
colsToSelect.push(...valueCols.map(valueCol => colsToSelect.push(
`${wrapperSql ? "" : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}` ...valueCols.map(
)); (valueCol) =>
`${wrapperSql ? '' : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`,
),
);
return colsToSelect; return colsToSelect;
} }
@ -141,17 +192,24 @@ export abstract class RowModelService {
: []; : [];
} }
getOrderByColumns(request: RowModelRequest): string[] { getOrderByColumns(request: RowModelRequest): string[] {
const { sortModel, rowGroupCols, groupKeys } = request; const { sortModel, rowGroupCols, groupKeys } = request;
const grouping = this.isDoingGroup(request); const grouping = this.isDoingGroup(request);
const sortParts: string[] = []; const sortParts: string[] = [];
if (sortModel) { if (sortModel) {
const groupColIds = rowGroupCols.map(groupCol => groupCol.id).slice(0, groupKeys.length + 1); const groupColIds = rowGroupCols
sortModel.forEach(item => { .map((groupCol) => groupCol.id)
if (!grouping || (groupColIds.indexOf(item.colId) >= 0 && rowGroupCols[groupKeys.length].field === item.colId)) { .slice(0, groupKeys.length + 1);
const colId = this.keywords.has(item.colId.toUpperCase()) ? `"${item.colId}"` : item.colId; sortModel.forEach((item) => {
if (
!grouping ||
(groupColIds.indexOf(item.colId) >= 0 &&
rowGroupCols[groupKeys.length].field === item.colId)
) {
const colId = this.keywords.has(item.colId.toUpperCase())
? `"${item.colId}"`
: item.colId;
sortParts.push(`${colId} ${item.sort}`); sortParts.push(`${colId} ${item.sort}`);
} }
}); });
@ -165,30 +223,41 @@ export abstract class RowModelService {
isDoingTreeGroup(requset: RowModelRequest): boolean { isDoingTreeGroup(requset: RowModelRequest): boolean {
return requset.rowGroupCols.length === 0 && requset.groupKeys.length > 0; return requset.rowGroupCols.length === 0 && requset.groupKeys.length > 0;
} }
private async getSingleRow(condition: LogicalCondition, staff?: UserProfile): Promise<any> { private async getSingleRow(
const results = await this.getRowsWithFilters(condition, staff) condition: LogicalCondition,
return results[0] staff?: UserProfile,
): Promise<any> {
const results = await this.getRowsWithFilters(condition, staff);
return results[0];
} }
private async getMultipleRows(condition: LogicalCondition, staff?: UserProfile): Promise<any[]> { private async getMultipleRows(
condition: LogicalCondition,
staff?: UserProfile,
): Promise<any[]> {
return this.getRowsWithFilters(condition, staff); return this.getRowsWithFilters(condition, staff);
} }
private async getRowsWithFilters(condition: LogicalCondition, staff?: UserProfile): Promise<any[]> { private async getRowsWithFilters(
condition: LogicalCondition,
staff?: UserProfile,
): Promise<any[]> {
try { try {
let SQL = SQLBuilder.join([ const SQL = SQLBuilder.join([
SQLBuilder.select(this.createUnGroupingRowSelect()), SQLBuilder.select(this.createUnGroupingRowSelect()),
SQLBuilder.from(this.tableName), SQLBuilder.from(this.tableName),
SQLBuilder.join(this.createJoinSql()), SQLBuilder.join(this.createJoinSql()),
SQLBuilder.where(condition) SQLBuilder.where(condition),
]); ]);
// this.logger.debug(SQL) // this.logger.debug(SQL)
const results: any[] = await db.$queryRawUnsafe(SQL); const results: any[] = await db.$queryRawUnsafe(SQL);
let rowDataDto = await Promise.all(results.map(item => this.getRowDto(item, staff))); const rowDataDto = await Promise.all(
results.map((item) => this.getRowDto(item, staff)),
);
// rowDataDto = getUniqueItems(rowDataDto, "id") // rowDataDto = getUniqueItems(rowDataDto, "id")
return rowDataDto return rowDataDto;
} catch (error) { } catch (error) {
this.logger.error('Error executing query:', error); this.logger.error('Error executing query:', error);
throw error; throw error;
@ -202,7 +271,7 @@ export abstract class RowModelService {
SQLBuilder.from(this.tableName), SQLBuilder.from(this.tableName),
SQLBuilder.join(this.createJoinSql(request)), SQLBuilder.join(this.createJoinSql(request)),
SQLBuilder.where(this.createGetRowsFilters(request)), SQLBuilder.where(this.createGetRowsFilters(request)),
SQLBuilder.groupBy(this.buildAggGroupBy()) SQLBuilder.groupBy(this.buildAggGroupBy()),
]); ]);
const result: any[] = await db.$queryRawUnsafe(SQL); const result: any[] = await db.$queryRawUnsafe(SQL);
return result[0]; return result[0];
@ -215,8 +284,9 @@ export abstract class RowModelService {
return []; return [];
} }
protected buildAggSelect(valueCols: any[]): string[] { protected buildAggSelect(valueCols: any[]): string[] {
return valueCols.map(valueCol => return valueCols.map(
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}` (valueCol) =>
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`,
); );
} }
@ -224,15 +294,14 @@ export abstract class RowModelService {
return { return {
field: `${this.tableName}.id`, field: `${this.tableName}.id`,
value: id, value: id,
op: "equals" op: 'equals',
} };
} }
private createGetByIdsFilter(ids: string[]): LogicalCondition { private createGetByIdsFilter(ids: string[]): LogicalCondition {
return { return {
field: `${this.tableName}.id`, field: `${this.tableName}.id`,
value: ids, value: ids,
op: "in" op: 'in',
}; };
} }
} }

View File

@ -1,4 +1,4 @@
import { db, EnrollmentStatus, PostType } from "@nice/common"; import { db, EnrollmentStatus, PostType } from '@nice/common';
// 更新课程评价统计 // 更新课程评价统计
export async function updateCourseReviewStats(courseId: string) { export async function updateCourseReviewStats(courseId: string) {
@ -6,18 +6,23 @@ export async function updateCourseReviewStats(courseId: string) {
where: { where: {
courseId, courseId,
type: PostType.COURSE_REVIEW, type: PostType.COURSE_REVIEW,
deletedAt: null deletedAt: null,
}, },
select: { rating: true } select: { rating: true },
}); });
const numberOfReviews = reviews.length; const numberOfReviews = reviews.length;
const averageRating = numberOfReviews > 0 const averageRating =
? reviews.reduce((sum, review) => sum + review.rating, 0) / numberOfReviews numberOfReviews > 0
? reviews.reduce((sum, review) => sum + review.rating, 0) /
numberOfReviews
: 0; : 0;
return db.course.update({ return db.course.update({
where: { id: courseId }, where: { id: courseId },
data: { numberOfReviews, averageRating } data: {
// numberOfReviews,
//averageRating,
},
}); });
} }
@ -26,21 +31,19 @@ export async function updateCourseEnrollmentStats(courseId: string) {
const completedEnrollments = await db.enrollment.count({ const completedEnrollments = await db.enrollment.count({
where: { where: {
courseId, courseId,
status: EnrollmentStatus.COMPLETED status: EnrollmentStatus.COMPLETED,
} },
}); });
const totalEnrollments = await db.enrollment.count({ const totalEnrollments = await db.enrollment.count({
where: { courseId } where: { courseId },
}); });
const completionRate = totalEnrollments > 0 const completionRate =
? (completedEnrollments / totalEnrollments) * 100 totalEnrollments > 0 ? (completedEnrollments / totalEnrollments) * 100 : 0;
: 0;
return db.course.update({ return db.course.update({
where: { id: courseId }, where: { id: courseId },
data: { data: {
numberOfStudents: totalEnrollments, // numberOfStudents: totalEnrollments,
completionRate // completionRate,
} },
}); });
} }