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

View File

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

View File

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