From 4180eae9446eb170ca5fc24106d45cee6a043eeb Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Mon, 6 Jul 2026 11:13:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=84=8F=E8=A7=81=E5=8F=8D=E9=A6=88?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增完整反馈闭环: - 后端: Feedback 模块 (schema+service+controller+module) - POST /api/feedback (用户提交) - GET /api/feedback (管理员列表) - PATCH /api/feedback/:id/resolve (管理员标记已处理) - 前端: pages/feedback/feedback.vue - 三种反馈类型: 问题反馈/改进建议/点赞鼓励 - 文本输入 + 联系方式选填 - 提交后显示成功提示 - 用户页新增'意见反馈'菜单入口 - 管理后台可通过 API 查看和管理反馈 --- backend/src/app.module.ts | 2 + .../modules/feedback/feedback.controller.ts | 31 +++++ .../src/modules/feedback/feedback.module.ts | 13 ++ .../src/modules/feedback/feedback.schema.ts | 24 ++++ .../src/modules/feedback/feedback.service.ts | 26 ++++ zhiyin-app/src/pages.json | 3 +- zhiyin-app/src/pages/feedback/feedback.vue | 113 ++++++++++++++++++ zhiyin-app/src/pages/user/user.vue | 6 + 8 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 backend/src/modules/feedback/feedback.controller.ts create mode 100644 backend/src/modules/feedback/feedback.module.ts create mode 100644 backend/src/modules/feedback/feedback.schema.ts create mode 100644 backend/src/modules/feedback/feedback.service.ts create mode 100644 zhiyin-app/src/pages/feedback/feedback.vue 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 @@ +