diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index f5fbe71..2a30582 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -9,6 +9,7 @@ import { APP_GUARD } from '@nestjs/core' import { JwtStrategy } from './common/strategies/jwt.strategy' import { JwtAuthGuard } from './common/guards/jwt-auth.guard' import { AiModule } from './modules/ai/ai.module' +import { FeedbackModule } from './modules/feedback/feedback.module' import { UserModule } from './modules/user/user.module' import { InterviewModule } from './modules/interview/interview.module' import { ResumeModule } from './modules/resume/resume.module' @@ -47,6 +48,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin }]), NestScheduleModule.forRoot(), UserModule, + FeedbackModule, AiModule, InterviewModule, AnalyzeModule, diff --git a/backend/src/modules/feedback/feedback.controller.ts b/backend/src/modules/feedback/feedback.controller.ts new file mode 100644 index 0000000..63f6c8b --- /dev/null +++ b/backend/src/modules/feedback/feedback.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Post, Get, Patch, Param, Body, Query, UseGuards, HttpException, HttpStatus } from '@nestjs/common' +import { FeedbackService } from './feedback.service' +import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard' +import { AdminGuard } from '../../common/guards/admin.guard' +import { CurrentUser } from '../../common/decorators/current-user.decorator' + +@Controller('feedback') +export class FeedbackController { + constructor(private service: FeedbackService) {} + + @UseGuards(JwtAuthGuard) + @Post() + async create(@CurrentUser('userId') userId: string, @Body() body: { type?: string; content: string; contact?: string }) { + if (!body.content || body.content.length < 2) { + throw new HttpException('请填写反馈内容', HttpStatus.BAD_REQUEST) + } + return this.service.create({ userId, type: body.type || 'suggestion', content: body.content, contact: body.contact }) + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Get() + async list(@Query('page') page?: string, @Query('limit') limit?: string) { + return this.service.findAll(Number(page) || 1, Number(limit) || 20) + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch(':id/resolve') + async resolve(@Param('id') id: string) { + return this.service.markResolved(id) + } +} diff --git a/backend/src/modules/feedback/feedback.module.ts b/backend/src/modules/feedback/feedback.module.ts new file mode 100644 index 0000000..f3dfcce --- /dev/null +++ b/backend/src/modules/feedback/feedback.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common' +import { MongooseModule } from '@nestjs/mongoose' +import { FeedbackController } from './feedback.controller' +import { FeedbackService } from './feedback.service' +import { Feedback, FeedbackSchema } from './feedback.schema' + +@Module({ + imports: [MongooseModule.forFeature([{ name: Feedback.name, schema: FeedbackSchema }])], + controllers: [FeedbackController], + providers: [FeedbackService], + exports: [FeedbackService], +}) +export class FeedbackModule {} diff --git a/backend/src/modules/feedback/feedback.schema.ts b/backend/src/modules/feedback/feedback.schema.ts new file mode 100644 index 0000000..95e127b --- /dev/null +++ b/backend/src/modules/feedback/feedback.schema.ts @@ -0,0 +1,24 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' +import { Document, Types } from 'mongoose' + +export type FeedbackDocument = Feedback & Document + +@Schema({ timestamps: true }) +export class Feedback { + @Prop({ type: Types.ObjectId, ref: 'User', required: true }) + userId: Types.ObjectId + + @Prop({ default: 'suggestion' }) + type: string + + @Prop({ required: true }) + content: string + + @Prop({ default: '' }) + contact: string + + @Prop({ default: 'pending' }) + status: string +} + +export const FeedbackSchema = SchemaFactory.createForClass(Feedback) diff --git a/backend/src/modules/feedback/feedback.service.ts b/backend/src/modules/feedback/feedback.service.ts new file mode 100644 index 0000000..2bf4b91 --- /dev/null +++ b/backend/src/modules/feedback/feedback.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common' +import { InjectModel } from '@nestjs/mongoose' +import { Model } from 'mongoose' +import { Feedback } from './feedback.schema' + +@Injectable() +export class FeedbackService { + constructor(@InjectModel(Feedback.name) private model: Model) {} + + async create(data: { userId: string; type: string; content: string; contact?: string }) { + return this.model.create(data) + } + + async findAll(page = 1, limit = 20) { + const skip = (page - 1) * limit + const [items, total] = await Promise.all([ + this.model.find().sort({ createdAt: -1 }).skip(skip).limit(limit).populate('userId', 'nickname phone email').lean(), + this.model.countDocuments(), + ]) + return { items, total, page, limit } + } + + async markResolved(id: string) { + return this.model.findByIdAndUpdate(id, { status: 'resolved' }, { new: true }) + } +} diff --git a/zhiyin-app/src/pages.json b/zhiyin-app/src/pages.json index 86a488c..b5046a5 100644 --- a/zhiyin-app/src/pages.json +++ b/zhiyin-app/src/pages.json @@ -19,7 +19,8 @@ { "path": "pages/privacy/privacy", "style": { "navigationBarTitleText": "隐私政策" } }, { "path": "pages/share/share", "style": { "navigationBarTitleText": "我的分享" } }, { "path": "pages/review/review", "style": { "navigationBarTitleText": "面试复盘分析" } }, - { "path": "pages/career/career", "style": { "navigationBarTitleText": "AI择业顾问" } } + { "path": "pages/career/career", "style": { "navigationBarTitleText": "AI择业顾问" } }, + { "path": "pages/feedback/feedback", "style": { "navigationBarTitleText": "意见反馈" } } ], "tabBar": { "color": "#999999", diff --git a/zhiyin-app/src/pages/feedback/feedback.vue b/zhiyin-app/src/pages/feedback/feedback.vue new file mode 100644 index 0000000..0690e7d --- /dev/null +++ b/zhiyin-app/src/pages/feedback/feedback.vue @@ -0,0 +1,113 @@ +