diff --git a/docs/API接口说明-反馈功能.md b/docs/API接口说明-反馈功能.md new file mode 100644 index 0000000..2400750 --- /dev/null +++ b/docs/API接口说明-反馈功能.md @@ -0,0 +1,298 @@ +# API 接口说明 - 问题反馈功能 + +## 问题反馈相关接口 + +### 1. 创建反馈 + +**接口地址:** `POST /feedback` + +**请求头:** +``` +Authorization: your-token(可选,支持匿名提交) +``` + +**请求参数:** +```json +{ + "feedbackType": "功能建议", // 问题类型(必填) + "title": "希望增加夜间模式", // 问题标题(必填) + "content": "详细描述...", // 问题描述(可选) + "contact": "user@example.com" // 联系方式(可选) +} +``` + +**响应示例:** +```json +{ + "code": 200, + "message": "反馈提交成功", + "data": null +} +``` + +**说明:** +- 支持匿名提交,如果已登录会自动关联用户ID +- 问题类型建议:功能建议、Bug反馈、使用问题、其他 +- 联系方式用于后续沟通,建议填写邮箱或手机号 +- 反馈默认状态为"待处理" + +--- + +### 2. 查询当前用户的反馈列表 + +**接口地址:** `GET /feedback/my` + +**请求头:** +``` +Authorization: your-token +``` + +**响应示例:** +```json +{ + "code": 200, + "message": "success", + "data": [ + { + "id": 1, + "userId": 1, + "feedbackType": "功能建议", + "title": "希望增加夜间模式", + "content": "详细描述...", + "contact": "user@example.com", + "status": 0, + "createTime": "2025-01-01T12:00:00", + "updateTime": "2025-01-01T12:00:00" + } + ] +} +``` + +**说明:** +- 需要登录 +- 返回当前用户提交的所有反馈 +- 按创建时间倒序排序 + +--- + +### 3. 根据ID查询反馈详情 + +**接口地址:** `GET /feedback/{id}` + +**响应示例:** +```json +{ + "code": 200, + "message": "success", + "data": { + "id": 1, + "userId": 1, + "feedbackType": "功能建议", + "title": "希望增加夜间模式", + "content": "详细描述...", + "contact": "user@example.com", + "status": 0, + "createTime": "2025-01-01T12:00:00", + "updateTime": "2025-01-01T12:00:00" + } +} +``` + +**说明:** +- 不需要登录 +- 根据反馈ID查询详情 + +--- + +### 4. 分页查询反馈列表 + +**接口地址:** `GET /feedback/page` + +**请求参数:** +``` +current: 当前页码(默认1) +size: 每页数量(默认10) +userId: 用户ID(可选,用于筛选指定用户的反馈) +feedbackType: 问题类型(可选,用于筛选指定类型的反馈) +status: 状态(可选,用于筛选指定状态的反馈) +``` + +**请求示例:** +``` +GET /feedback/page?current=1&size=10&feedbackType=功能建议&status=0 +``` + +**响应示例:** +```json +{ + "code": 200, + "message": "success", + "data": { + "records": [ + { + "id": 1, + "userId": 1, + "feedbackType": "功能建议", + "title": "希望增加夜间模式", + "content": "详细描述...", + "contact": "user@example.com", + "status": 0, + "createTime": "2025-01-01T12:00:00", + "updateTime": "2025-01-01T12:00:00" + } + ], + "total": 100, + "size": 10, + "current": 1, + "pages": 10 + } +} +``` + +**说明:** +- 不需要登录 +- 支持分页和多条件筛选 +- 按创建时间倒序排序 + +--- + +### 5. 更新反馈状态 + +**接口地址:** `PUT /feedback/{id}/status` + +**请求参数:** +``` +status: 状态值(0-待处理 1-处理中 2-已完成 3-已关闭) +``` + +**请求示例:** +``` +PUT /feedback/1/status?status=1 +``` + +**响应示例:** +```json +{ + "code": 200, + "message": "状态更新成功", + "data": null +} +``` + +**说明:** +- 不需要登录 +- 用于管理员更新反馈处理状态 +- 状态说明: + - 0: 待处理 + - 1: 处理中 + - 2: 已完成 + - 3: 已关闭 + +--- + +### 6. 删除反馈 + +**接口地址:** `DELETE /feedback/{id}` + +**响应示例:** +```json +{ + "code": 200, + "message": "删除成功", + "data": null +} +``` + +**说明:** +- 不需要登录 +- 用于管理员删除反馈 + +--- + +## 数据库说明 + +### 反馈表结构 + +**建表 SQL:** `src/main/resources/db/feedback.sql` + +```sql +CREATE TABLE `app_feedback` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '反馈ID', + `user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID', + `feedback_type` VARCHAR(50) NOT NULL COMMENT '问题类型', + `title` VARCHAR(200) NOT NULL COMMENT '问题标题', + `content` TEXT DEFAULT NULL COMMENT '问题描述', + `contact` VARCHAR(100) DEFAULT NULL COMMENT '联系方式', + `status` TINYINT(1) DEFAULT 0 COMMENT '处理状态:0-待处理 1-处理中 2-已完成 3-已关闭', + `create_time` DATETIME DEFAULT NULL COMMENT '创建时间', + `update_time` DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题反馈表'; +``` + +--- + +## 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 200 | 成功 | +| 500 | 失败 | + +--- + +## 开发说明 + +### 代码结构 + +``` +com.corewing.app/ +├── controller/ +│ └── AppFeedbackController.java # 反馈控制器 +├── dto/ +│ └── FeedbackRequest.java # 反馈请求参数 +├── entity/ +│ └── AppFeedback.java # 反馈实体类 +├── mapper/ +│ └── AppFeedbackMapper.java # 反馈Mapper接口 +└── service/ + ├── AppFeedbackService.java # 反馈服务接口 + └── impl/ + └── AppFeedbackServiceImpl.java # 反馈服务实现 +``` + +### 功能特性 + +1. **匿名提交** - 支持匿名用户提交反馈,不需要登录 +2. **自动关联** - 已登录用户提交反馈时自动关联用户ID +3. **状态管理** - 完整的反馈状态流转:待处理 → 处理中 → 已完成/已关闭 +4. **分页查询** - 支持分页和多条件筛选 +5. **MyBatis Plus** - 使用 MyBatis Plus 实现数据持久化 +6. **自动填充** - 创建时间和更新时间自动填充 + +### 权限说明 + +- 所有反馈接口都不需要身份认证 +- `/feedback` 和 `/feedback/**` 已在 Sa-Token 配置中加入白名单 +- 管理员可以通过状态更新和删除接口管理反馈 + +### 使用流程 + +1. **用户提交反馈** + - 访问 `POST /feedback` 接口 + - 填写问题类型、标题、描述、联系方式 + - 系统自动创建反馈记录 + +2. **查看反馈列表** + - 已登录用户:访问 `GET /feedback/my` 查看自己的反馈 + - 管理员:访问 `GET /feedback/page` 查看所有反馈 + +3. **处理反馈** + - 管理员通过 `PUT /feedback/{id}/status` 更新反馈状态 + - 状态流转:待处理 → 处理中 → 已完成 + +4. **查看详情** + - 通过 `GET /feedback/{id}` 查看反馈详情 diff --git a/docs/API接口说明.md b/docs/API接口说明.md index eb8c7bd..a785ef4 100644 --- a/docs/API接口说明.md +++ b/docs/API接口说明.md @@ -1,6 +1,6 @@ # API 接口说明 -## 用户相关接 +## 用户相关接口 ### 1. 发送验证码 diff --git a/src/main/java/com/corewing/app/config/SaTokenConfig.java b/src/main/java/com/corewing/app/config/SaTokenConfig.java index 3a051ff..7ee33fd 100644 --- a/src/main/java/com/corewing/app/config/SaTokenConfig.java +++ b/src/main/java/com/corewing/app/config/SaTokenConfig.java @@ -23,6 +23,8 @@ public class SaTokenConfig implements WebMvcConfigurer { .addPathPatterns("/**") // 排除登录、注册、发送验证码接口 .excludePathPatterns("/user/login", "/user/register", "/user/sendCode") + // 排除反馈接口(支持匿名提交) + .excludePathPatterns("/feedback", "/feedback/**") // 排除静态资源 .excludePathPatterns("/", "/index.html", "/*.html", "/*.css", "/*.js", "/*.ico", "/static/**") // 排除 Druid 监控 diff --git a/src/main/java/com/corewing/app/controller/AppFeedbackController.java b/src/main/java/com/corewing/app/controller/AppFeedbackController.java new file mode 100644 index 0000000..62af9b7 --- /dev/null +++ b/src/main/java/com/corewing/app/controller/AppFeedbackController.java @@ -0,0 +1,145 @@ +package com.corewing.app.controller; + +import cn.dev33.satoken.stp.StpUtil; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.corewing.app.common.Result; +import com.corewing.app.dto.FeedbackRequest; +import com.corewing.app.entity.AppFeedback; +import com.corewing.app.service.AppFeedbackService; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 问题反馈 Controller + */ +@RestController +@RequestMapping("/feedback") +public class AppFeedbackController { + + private final AppFeedbackService feedbackService; + + public AppFeedbackController(AppFeedbackService feedbackService) { + this.feedbackService = feedbackService; + } + + /** + * 创建反馈(支持匿名提交) + */ + @PostMapping + public Result create(@RequestBody FeedbackRequest request) { + try { + // 尝试获取当前登录用户ID(如果未登录则为null) + Long userId = null; + try { + userId = StpUtil.getLoginIdAsLong(); + } catch (Exception e) { + // 未登录,userId 保持为 null + } + + AppFeedback feedback = new AppFeedback(); + feedback.setUserId(userId); + feedback.setFeedbackType(request.getFeedbackType()); + feedback.setTitle(request.getTitle()); + feedback.setContent(request.getContent()); + feedback.setContact(request.getContact()); + + boolean success = feedbackService.createFeedback(feedback); + if (success) { + return Result.success("反馈提交成功"); + } + return Result.error("反馈提交失败"); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 查询当前用户的反馈列表 + */ + @GetMapping("/my") + public Result> getMyFeedbackList() { + try { + // 获取当前登录用户ID + Long userId = StpUtil.getLoginIdAsLong(); + List feedbackList = feedbackService.listByUserId(userId); + return Result.success(feedbackList); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 根据ID查询反馈详情 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + try { + AppFeedback feedback = feedbackService.getById(id); + if (feedback != null) { + return Result.success(feedback); + } + return Result.error("反馈不存在"); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 分页查询反馈列表 + * + * @param current 当前页码 + * @param size 每页数量 + * @param userId 用户ID(可选) + * @param feedbackType 问题类型(可选) + * @param status 状态(可选) + */ + @GetMapping("/page") + public Result> getPageList( + @RequestParam(defaultValue = "1") Long current, + @RequestParam(defaultValue = "10") Long size, + @RequestParam(required = false) Long userId, + @RequestParam(required = false) String feedbackType, + @RequestParam(required = false) Integer status) { + try { + Page page = new Page<>(current, size); + IPage pageResult = feedbackService.pageList(page, userId, feedbackType, status); + return Result.success(pageResult); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 更新反馈状态 + */ + @PutMapping("/{id}/status") + public Result updateStatus(@PathVariable Long id, @RequestParam Integer status) { + try { + boolean success = feedbackService.updateStatus(id, status); + if (success) { + return Result.success("状态更新成功"); + } + return Result.error("状态更新失败"); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 删除反馈 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + try { + boolean success = feedbackService.removeById(id); + if (success) { + return Result.success("删除成功"); + } + return Result.error("删除失败"); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } +} diff --git a/src/main/java/com/corewing/app/dto/FeedbackRequest.java b/src/main/java/com/corewing/app/dto/FeedbackRequest.java new file mode 100644 index 0000000..788cc3f --- /dev/null +++ b/src/main/java/com/corewing/app/dto/FeedbackRequest.java @@ -0,0 +1,34 @@ +package com.corewing.app.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 创建反馈请求 + */ +@Data +public class FeedbackRequest { + + /** + * 问题类型 + */ + @NotBlank(message = "问题类型不能为空") + private String feedbackType; + + /** + * 问题标题 + */ + @NotBlank(message = "问题标题不能为空") + private String title; + + /** + * 问题描述 + */ + private String content; + + /** + * 联系方式 + */ + private String contact; +} diff --git a/src/main/java/com/corewing/app/entity/AppFeedback.java b/src/main/java/com/corewing/app/entity/AppFeedback.java new file mode 100644 index 0000000..a5c80fb --- /dev/null +++ b/src/main/java/com/corewing/app/entity/AppFeedback.java @@ -0,0 +1,69 @@ +package com.corewing.app.entity; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 问题反馈实体类 + */ +@Data +@TableName("app_feedback") +public class AppFeedback implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 反馈ID + */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 问题类型 + */ + private String feedbackType; + + /** + * 问题标题 + */ + private String title; + + /** + * 问题描述 + */ + private String content; + + /** + * 联系方式 + */ + private String contact; + + /** + * 处理状态:0-待处理 1-处理中 2-已完成 3-已关闭 + */ + private Integer status; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/corewing/app/handler/GlobalExceptionHandler.java b/src/main/java/com/corewing/app/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..ebc47c1 --- /dev/null +++ b/src/main/java/com/corewing/app/handler/GlobalExceptionHandler.java @@ -0,0 +1,57 @@ +package com.corewing.app.handler; + +import cn.dev33.satoken.exception.NotLoginException; +import com.corewing.app.common.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 全局异常处理器 + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 处理 Sa-Token 未登录异常 + */ + @ExceptionHandler(NotLoginException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleNotLoginException(NotLoginException e) { + // 根据不同的场景返回不同的提示 + String message; + switch (e.getType()) { + case NotLoginException.NOT_TOKEN: + message = "未提供登录凭证"; + break; + case NotLoginException.INVALID_TOKEN: + message = "登录凭证无效"; + break; + case NotLoginException.TOKEN_TIMEOUT: + message = "登录已过期,请重新登录"; + break; + case NotLoginException.BE_REPLACED: + message = "账号已在其他地方登录"; + break; + case NotLoginException.KICK_OUT: + message = "账号已被踢下线"; + break; + default: + message = "未登录,请先登录"; + } + return Result.error(HttpStatus.UNAUTHORIZED.value(), message); + } + + /** + * 处理其他异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e) { + log.error(e.getMessage()); + return Result.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误:" + e.getMessage()); + } +} diff --git a/src/main/java/com/corewing/app/mapper/AppFeedbackMapper.java b/src/main/java/com/corewing/app/mapper/AppFeedbackMapper.java new file mode 100644 index 0000000..971760a --- /dev/null +++ b/src/main/java/com/corewing/app/mapper/AppFeedbackMapper.java @@ -0,0 +1,13 @@ +package com.corewing.app.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.corewing.app.entity.AppFeedback; +import org.apache.ibatis.annotations.Mapper; + +/** + * 问题反馈 Mapper 接口 + */ +@Mapper +public interface AppFeedbackMapper extends BaseMapper { + +} diff --git a/src/main/java/com/corewing/app/service/AppFeedbackService.java b/src/main/java/com/corewing/app/service/AppFeedbackService.java new file mode 100644 index 0000000..e106575 --- /dev/null +++ b/src/main/java/com/corewing/app/service/AppFeedbackService.java @@ -0,0 +1,50 @@ +package com.corewing.app.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.corewing.app.entity.AppFeedback; + +import java.util.List; + +/** + * 问题反馈 Service 接口 + */ +public interface AppFeedbackService extends IService { + + /** + * 创建反馈 + * + * @param feedback 反馈信息 + * @return 是否成功 + */ + boolean createFeedback(AppFeedback feedback); + + /** + * 根据用户ID查询反馈列表 + * + * @param userId 用户ID + * @return 反馈列表 + */ + List listByUserId(Long userId); + + /** + * 分页查询反馈列表 + * + * @param page 分页参数 + * @param userId 用户ID(可选) + * @param feedbackType 问题类型(可选) + * @param status 状态(可选) + * @return 分页结果 + */ + IPage pageList(Page page, Long userId, String feedbackType, Integer status); + + /** + * 更新反馈状态 + * + * @param id 反馈ID + * @param status 状态 + * @return 是否成功 + */ + boolean updateStatus(Long id, Integer status); +} diff --git a/src/main/java/com/corewing/app/service/impl/AppFeedbackServiceImpl.java b/src/main/java/com/corewing/app/service/impl/AppFeedbackServiceImpl.java new file mode 100644 index 0000000..79766d1 --- /dev/null +++ b/src/main/java/com/corewing/app/service/impl/AppFeedbackServiceImpl.java @@ -0,0 +1,77 @@ +package com.corewing.app.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.corewing.app.entity.AppFeedback; +import com.corewing.app.mapper.AppFeedbackMapper; +import com.corewing.app.service.AppFeedbackService; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * 问题反馈 Service 实现类 + */ +@Service +public class AppFeedbackServiceImpl extends ServiceImpl implements AppFeedbackService { + + @Override + public boolean createFeedback(AppFeedback feedback) { + // 设置默认状态为待处理 + if (feedback.getStatus() == null) { + feedback.setStatus(0); + } + return this.save(feedback); + } + + @Override + public List listByUserId(Long userId) { + if (userId == null) { + throw new RuntimeException("用户ID不能为空"); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(AppFeedback::getUserId, userId) + .orderByDesc(AppFeedback::getCreateTime); + return this.list(wrapper); + } + + @Override + public IPage pageList(Page page, Long userId, String feedbackType, Integer status) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + // 条件查询 + if (userId != null) { + wrapper.eq(AppFeedback::getUserId, userId); + } + if (StringUtils.hasText(feedbackType)) { + wrapper.eq(AppFeedback::getFeedbackType, feedbackType); + } + if (status != null) { + wrapper.eq(AppFeedback::getStatus, status); + } + + // 按创建时间倒序排序 + wrapper.orderByDesc(AppFeedback::getCreateTime); + + return this.page(page, wrapper); + } + + @Override + public boolean updateStatus(Long id, Integer status) { + if (id == null) { + throw new RuntimeException("反馈ID不能为空"); + } + if (status == null) { + throw new RuntimeException("状态不能为空"); + } + + AppFeedback feedback = new AppFeedback(); + feedback.setId(id); + feedback.setStatus(status); + + return this.updateById(feedback); + } +} diff --git a/src/main/resources/db/feedback.sql b/src/main/resources/db/feedback.sql new file mode 100644 index 0000000..c6ff390 --- /dev/null +++ b/src/main/resources/db/feedback.sql @@ -0,0 +1,18 @@ +-- 问题反馈表 +DROP TABLE IF EXISTS `app_feedback`; + +CREATE TABLE `app_feedback` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '反馈ID', + `user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID', + `feedback_type` VARCHAR(50) NOT NULL COMMENT '问题类型', + `title` VARCHAR(200) NOT NULL COMMENT '问题标题', + `content` TEXT DEFAULT NULL COMMENT '问题描述', + `contact` VARCHAR(100) DEFAULT NULL COMMENT '联系方式', + `status` TINYINT(1) DEFAULT 0 COMMENT '处理状态:0-待处理 1-处理中 2-已完成 3-已关闭', + `create_time` DATETIME DEFAULT NULL COMMENT '创建时间', + `update_time` DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问题反馈表';