feat: 意见反馈功能

新增完整反馈闭环:
- 后端: Feedback 模块 (schema+service+controller+module)
  - POST /api/feedback (用户提交)
  - GET /api/feedback (管理员列表)
  - PATCH /api/feedback/:id/resolve (管理员标记已处理)
- 前端: pages/feedback/feedback.vue
  - 三种反馈类型: 问题反馈/改进建议/点赞鼓励
  - 文本输入 + 联系方式选填
  - 提交后显示成功提示
- 用户页新增'意见反馈'菜单入口
- 管理后台可通过 API 查看和管理反馈
This commit is contained in:
yuzhiran
2026-07-06 11:13:08 +08:00
parent 4e4e1ca271
commit 4180eae944
8 changed files with 217 additions and 1 deletions
+2
View File
@@ -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,
@@ -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)
}
}
@@ -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 {}
@@ -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)
@@ -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<Feedback>) {}
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 })
}
}