Sfoglia il codice sorgente

feat(feedback): 添加用户反馈功能

- 新增用户反馈页面,支持选择反馈类型、输入内容和上传图片
- 实现反馈提交接口,包括数据校验和存储逻辑
- 集成MinIO用于反馈图片的上传与管理
- 在网关层添加反馈服务路由配置
- 增加反馈相关API接口定义及调用封装
- 更新页面导航配置以支持反馈页面访问
- 添加MinIO依赖及相关配置项以启用图片存储功能
- 创建反馈实体类及其数据库映射结构
- 实现反馈上传工具类确保图片安全上传至MinIO
- 开发反馈控制层和服务层处理业务逻辑
- 完善前端图片上传交互流程并优化用户体验
李思佳 1 mese fa
parent
commit
22060cb5bf

+ 8 - 0
LiangZhiYUMao/pages.json

@@ -306,6 +306,14 @@
 				"navigationBarTitleText": "关于我们",
 				"navigationStyle": "default"
 			}
+		},
+		{
+			"path": "pages/feedback/index",
+			"style": {
+				"navigationBarTitleText": "用户反馈",
+				"navigationBarBackgroundColor": "#E91E63",
+				"navigationBarTextStyle": "white"
+			}
 		}
 	],
 	"globalStyle": {

+ 413 - 0
LiangZhiYUMao/pages/feedback/index.vue

@@ -0,0 +1,413 @@
+<template>
+  <view class="feedback-page">
+    <!-- 页面标题 -->
+    <view class="page-header">
+      <view class="header-left" @click="goBack">
+        <text class="back-icon">←</text>
+      </view>
+      <view class="header-title">用户反馈</view>
+      <view class="header-right"></view>
+    </view>
+
+    <!-- 反馈内容 -->
+    <view class="feedback-content">
+      <view class="section">
+        <view class="section-title">反馈类型</view>
+        <view class="feedback-type">
+          <view class="type-item"
+                :class="{ active: feedbackType === '意见建议' }"
+                @click="feedbackType = '意见建议'">
+            <text class="type-text">意见建议</text>
+          </view>
+          <view class="type-item"
+                :class="{ active: feedbackType === '功能改进' }"
+                @click="feedbackType = '功能改进'">
+            <text class="type-text">功能改进</text>
+          </view>
+          <view class="type-item"
+                :class="{ active: feedbackType === 'bug报告' }"
+                @click="feedbackType = 'bug报告'">
+            <text class="type-text">bug报告</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="section">
+        <view class="section-title">反馈内容</view>
+        <textarea
+            class="feedback-input"
+            placeholder="请详细描述您的问题或建议..."
+            v-model="feedbackContent"
+            maxlength="500"
+            auto-height
+        ></textarea>
+        <view class="word-count">{{ feedbackContent.length }}/500</view>
+      </view>
+
+      <view class="section">
+        <view class="section-title">上传图片(可选)</view>
+        <view class="image-uploader">
+          <view class="upload-item" v-for="(image, index) in images" :key="index">
+            <image class="uploaded-image" :src="image" mode="aspectFill"></image>
+            <view class="delete-icon" @click="deleteImage(index)">×</view>
+          </view>
+          <view class="upload-btn" @click="chooseImage" v-if="images.length < 3">
+            <text class="upload-icon">+</text>
+            <text class="upload-text">添加图片</text>
+          </view>
+        </view>
+        <view class="upload-tip">最多可上传3张图片</view>
+      </view>
+    </view>
+
+    <!-- 提交按钮 -->
+    <view class="submit-section">
+      <button class="submit-btn" @click="submitFeedback" :disabled="!feedbackContent.trim()">
+        提交反馈
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js';
+
+export default {
+  data() {
+    return {
+      feedbackType: '意见建议',
+      feedbackContent: '',
+      images: [],
+      uploadedImages: []
+    };
+  },
+  onLoad() {
+    // 确保 __route__ 属性存在,避免渲染错误
+    if (!this.__route__) {
+      this.__route__ = '/pages/feedback/index';
+    }
+  },
+  methods: {
+    // 返回上一页
+    goBack() {
+      uni.navigateBack();
+    },
+
+    // 选择图片
+    chooseImage() {
+      uni.chooseImage({
+        count: 3 - this.images.length,
+        sizeType: ['compressed'],
+        sourceType: ['album', 'camera'],
+        success: (res) => {
+          this.images = [...this.images, ...res.tempFilePaths];
+        }
+      });
+    },
+
+    // 删除图片
+    deleteImage(index) {
+      this.images.splice(index, 1);
+      this.uploadedImages.splice(index, 1);
+    },
+
+    // 上传单张图片
+    async uploadSingleImage(filePath) {
+      try {
+        console.log('上传图片:', filePath);
+        // 调用后端的图片上传接口
+        const imageUrl = await api.feedback.uploadImage(filePath);
+        return imageUrl;
+      } catch (error) {
+        console.error('上传图片失败:', error);
+        throw error;
+      }
+    },
+    
+    // 上传所有图片
+    async uploadAllImages() {
+      const uploadedUrls = [];
+      for (let i = 0; i < this.images.length; i++) {
+        if (!this.uploadedImages[i]) {
+          const url = await this.uploadSingleImage(this.images[i]);
+          if (url) {
+            this.uploadedImages[i] = url;
+          }
+        }
+        uploadedUrls.push(this.uploadedImages[i]);
+      }
+      return uploadedUrls.filter(url => url);
+    },
+
+    // 提交反馈
+    async submitFeedback() {
+      if (!this.feedbackContent.trim()) {
+        uni.showToast({
+          title: '请输入反馈内容',
+          icon: 'none'
+        });
+        return;
+      }
+
+      uni.showLoading({
+        title: '提交中...'
+      });
+
+      try {
+        // 上传图片
+        const imageUrls = await this.uploadAllImages();
+        
+        // 获取当前用户ID
+        const userInfo = uni.getStorageSync('userInfo');
+        const userId = userInfo?.userId || uni.getStorageSync('userId');
+        
+        // 转换反馈类型为数字格式(1=意见建议,2=功能改进,3=bug报告)
+        let feedbackTypeNum = 1;
+        if (this.feedbackType === '功能改进') {
+          feedbackTypeNum = 2;
+        } else if (this.feedbackType === 'bug报告') {
+          feedbackTypeNum = 3;
+        }
+        
+        // 构造反馈数据,与后端接口字段匹配
+        const feedbackData = {
+          userId: userId,
+          feedbackType: feedbackTypeNum,
+          content: this.feedbackContent,
+          imageUrls: imageUrls.join(',') // 将图片URL数组转换为逗号分隔的字符串
+        };
+        
+        // 提交反馈
+        await api.feedback.submit(feedbackData);
+        
+        uni.hideLoading();
+        uni.showToast({
+          title: '反馈提交成功',
+          icon: 'success'
+        });
+        
+        // 提交成功后返回上一页
+        setTimeout(() => {
+          this.goBack();
+        }, 1500);
+      } catch (error) {
+        uni.hideLoading();
+        console.error('提交反馈失败:', error);
+        let errorMsg = '反馈提交失败,请稍后重试';
+        if (error && error.message) {
+          errorMsg = error.message;
+        } else if (error && error.errMsg) {
+          errorMsg = error.errMsg;
+        }
+        uni.showToast({
+          title: errorMsg,
+          icon: 'none'
+        });
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.feedback-page {
+  min-height: 100vh;
+  background: #F5F5F5;
+}
+
+/* 页面标题 */
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 88rpx;
+  background: #FFFFFF;
+  padding: 0 30rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+  position: sticky;
+  top: 0;
+  z-index: 99;
+}
+
+.header-left,
+.header-right {
+  width: 60rpx;
+}
+
+.back-icon {
+  font-size: 40rpx;
+  color: #333333;
+}
+
+.header-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333333;
+}
+
+/* 反馈内容 */
+.feedback-content {
+  padding: 30rpx;
+}
+
+.section {
+  background: #FFFFFF;
+  border-radius: 12rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+}
+
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333333;
+  margin-bottom: 20rpx;
+}
+
+/* 反馈类型 */
+.feedback-type {
+  display: flex;
+  gap: 20rpx;
+  flex-wrap: wrap;
+}
+
+.type-item {
+  padding: 16rpx 32rpx;
+  background: #F5F5F5;
+  border-radius: 20rpx;
+  border: 2rpx solid #E0E0E0;
+  transition: all 0.2s ease;
+}
+
+.type-item.active {
+  background: #FFE5F1;
+  border-color: #E91E63;
+}
+
+.type-text {
+  font-size: 26rpx;
+  color: #666666;
+}
+
+.type-item.active .type-text {
+  color: #E91E63;
+  font-weight: 500;
+}
+
+/* 反馈输入 */
+.feedback-input {
+  width: 100%;
+  min-height: 200rpx;
+  font-size: 28rpx;
+  color: #333333;
+  border: 2rpx solid #E0E0E0;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  box-sizing: border-box;
+  resize: none;
+}
+
+.feedback-input::placeholder {
+  color: #CCCCCC;
+}
+
+.word-count {
+  text-align: right;
+  font-size: 24rpx;
+  color: #999999;
+  margin-top: 10rpx;
+}
+
+/* 图片上传 */
+.image-uploader {
+  display: flex;
+  gap: 20rpx;
+  flex-wrap: wrap;
+}
+
+.upload-item {
+  width: 200rpx;
+  height: 200rpx;
+  position: relative;
+  border-radius: 12rpx;
+  overflow: hidden;
+  border: 2rpx solid #E0E0E0;
+}
+
+.uploaded-image {
+  width: 100%;
+  height: 100%;
+}
+
+.delete-icon {
+  position: absolute;
+  top: 10rpx;
+  right: 10rpx;
+  width: 40rpx;
+  height: 40rpx;
+  background: rgba(0, 0, 0, 0.5);
+  color: #FFFFFF;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 30rpx;
+  font-weight: bold;
+}
+
+.upload-btn {
+  width: 200rpx;
+  height: 200rpx;
+  border: 2rpx dashed #E0E0E0;
+  border-radius: 12rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: #F9F9F9;
+}
+
+.upload-icon {
+  font-size: 60rpx;
+  color: #CCCCCC;
+  margin-bottom: 10rpx;
+}
+
+.upload-text {
+  font-size: 24rpx;
+  color: #999999;
+}
+
+.upload-tip {
+  font-size: 22rpx;
+  color: #999999;
+  margin-top: 10rpx;
+}
+
+/* 提交按钮 */
+.submit-section {
+  padding: 30rpx;
+  background: #FFFFFF;
+  margin-top: 30rpx;
+}
+
+.submit-btn {
+  width: 100%;
+  height: 90rpx;
+  background: #E91E63;
+  color: #FFFFFF;
+  font-size: 32rpx;
+  font-weight: bold;
+  border-radius: 45rpx;
+  border: none;
+}
+
+.submit-btn:disabled {
+  background: #CCCCCC;
+}
+
+.submit-btn::after {
+  border: none;
+}
+
+</style>

+ 41 - 17
LiangZhiYUMao/pages/mine/index.vue

@@ -188,6 +188,14 @@
 				<text class="menu-arrow">›</text>
 			</view>
 			
+			<view class="menu-item" @click="goToPage('feedback')">
+				<view class="menu-left">
+					<text class="menu-icon">💬</text>
+					<text class="menu-text">用户反馈</text>
+				</view>
+				<text class="menu-arrow">›</text>
+			</view>
+
 			<view class="menu-item" @click="goToPage('blacklist')">
 				<view class="menu-left">
 					<text class="menu-icon">📋</text>
@@ -195,7 +203,7 @@
 				</view>
 				<text class="menu-arrow">›</text>
 			</view>
-			
+
 			<view class="menu-item" @click="goToPage('settings')">
 				<view class="menu-left">
 					<text class="menu-icon">⚙️</text>
@@ -900,22 +908,38 @@
 								})
 							}
 						})
-					} else if (page === 'settings') {
-						console.log('✅ 跳转到设置页面')
-						// 跳转到设置页面
-						uni.navigateTo({
-							url: '/pages/settings/index',
-							success: () => {
-								console.log('✅ 设置页面跳转成功')
-							},
-							fail: (err) => {
-								console.error('❌ 设置页面跳转失败:', err)
-								uni.showToast({
-									title: '页面跳转失败',
-									icon: 'none'
-								})
-							}
-						})
+					} else if (page === 'feedback') {
+							console.log('✅ 跳转到用户反馈页面')
+							// 跳转到用户反馈页面
+							uni.navigateTo({
+								url: '/pages/feedback/index',
+								success: () => {
+									console.log('✅ 用户反馈页面跳转成功')
+								},
+								fail: (err) => {
+									console.error('❌ 用户反馈页面跳转失败:', err)
+									uni.showToast({
+										title: '页面跳转失败',
+										icon: 'none'
+									})
+								}
+							})
+						} else if (page === 'settings') {
+							console.log('✅ 跳转到设置页面')
+							// 跳转到设置页面
+							uni.navigateTo({
+								url: '/pages/settings/index',
+								success: () => {
+									console.log('✅ 设置页面跳转成功')
+								},
+								fail: (err) => {
+									console.error('❌ 设置页面跳转失败:', err)
+									uni.showToast({
+										title: '页面跳转失败',
+										icon: 'none'
+									})
+								}
+							})
 					} else if (page === 'customize') {
 						console.log('✅ 跳转到私人定制页面')
 						// 跳转到私人定制页面

+ 44 - 14
LiangZhiYUMao/utils/api.js

@@ -130,7 +130,7 @@ export default {
       data: { phone, code }
     }),
 
-    // 微信登录(直连 login 服务以便开发环境不依赖 appid/secret 配置
+    // 微信登录(直连 login 服务)
     wechatLogin: (data) => request({
       url: 'http://localhost:8087/api/login/wechat/login',
       method: 'POST',
@@ -472,13 +472,6 @@ export default {
       });
     },
 
-    // 发布动态
-    // publish: (data) => request({
-    //   url: '/dynamic/publish',
-    //   method: 'POST',
-    //   data
-    // }),
-
     // 创建个人动态
     createUserDynamic: (payload) => request({
       url: '/dynamic/user',
@@ -614,6 +607,8 @@ export default {
       method: 'DELETE'
     })
   },
+
+  // VIP相关
   vip: {
       // 获取VIP信息(状态、套餐等)
       getInfo: (userId) => request({ 
@@ -636,13 +631,48 @@ export default {
       getOrderStatus: (orderNo) => request({ 
         url: `/vip/order/status?orderNo=${orderNo}` 
       }),
-	  // 新增:查询支付状态(userId + packageId)
-	    checkPayStatus: (userId, packageId) => request({
-	      url: '/vip/checkPayStatus',
-	      method: 'GET',
-	      data: { userId, packageId }
-	    })
+  	  // 新增:查询支付状态(userId + packageId)
+  	    checkPayStatus: (userId, packageId) => request({
+  	      url: '/vip/checkPayStatus',
+  	      method: 'GET',
+  	      data: { userId, packageId }
+  	    })
+  },
+  
+  // 用户反馈
+  feedback: {
+    // 提交用户反馈
+    submit: (data) => request({
+      url: '/feedback/submit',
+      method: 'POST',
+      data
+    }),
+    // 上传反馈图片
+    uploadImage: (filePath) => {
+      return new Promise((resolve, reject) => {
+        uni.uploadFile({
+          url: BASE_URL + '/feedback/upload',
+          filePath: filePath,
+          name: 'file',
+          success: (res) => {
+            try {
+              const data = JSON.parse(res.data)
+              if (data.code === 200 || data.code === 0 || data.success) {
+                resolve(data.data)
+              } else {
+                reject(new Error(data.message || '上传失败'))
+              }
+            } catch (e) {
+              reject(new Error('解析响应数据失败'))
+            }
+          },
+          fail: (error) => {
+            reject(new Error('上传请求失败: ' + (error.errMsg || '未知错误')))
+          }
+        })
+      })
     }
+  }
 }
 
 // 导出 request 函数供其他模块使用

+ 8 - 0
gateway/src/main/resources/application.yml

@@ -102,6 +102,14 @@ spring:
             - Path=/api/user/**
           filters:
             - StripPrefix=0
+        
+        # Essential服务-反馈路由
+        - id: essential-feedback-route
+          uri: http://localhost:1005
+          predicates:
+            - Path=/api/feedback/**
+          filters:
+            - StripPrefix=0
 
         # 首页服务路由(兜底路由)
         - id: homepage-route

+ 7 - 0
service/Essential/pom.xml

@@ -87,5 +87,12 @@
             <artifactId>commons-lang3</artifactId>
             <version>3.12.0</version>
         </dependency>
+        
+        <!-- MinIO -->
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <version>8.5.2</version>
+        </dependency>
     </dependencies>
 </project>

+ 52 - 0
service/Essential/src/main/java/com/zhentao/config/MinioConfig.java

@@ -0,0 +1,52 @@
+package com.zhentao.config;
+
+import io.minio.MinioClient;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * MinIO配置类
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "minio")
+public class MinioConfig {
+    
+    /**
+     * MinIO服务端点
+     */
+    private String endpoint;
+    
+    /**
+     * 访问密钥
+     */
+    private String accessKey;
+    
+    /**
+     * 密钥
+     */
+    private String secretKey;
+    
+    /**
+     * 桶名称
+     */
+    private String bucketName;
+    
+    /**
+     * 用户反馈图片文件夹路径
+     */
+    private String userFeedbackFolder = "user-feedback";
+    
+    /**
+     * 创建MinioClient Bean
+     */
+    @Bean
+    public MinioClient minioClient() {
+        return MinioClient.builder()
+                .endpoint(endpoint)
+                .credentials(accessKey, secretKey)
+                .build();
+    }
+}

+ 135 - 0
service/Essential/src/main/java/com/zhentao/controller/FeedbackController.java

@@ -0,0 +1,135 @@
+package com.zhentao.controller;
+
+import com.zhentao.common.Result;
+import com.zhentao.entity.Feedback;
+import com.zhentao.service.FeedbackService;
+import com.zhentao.util.FeedbackUploadUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 用户反馈控制器
+ */
+@RestController
+@RequestMapping("/api/feedback")
+public class FeedbackController {
+    
+    @Autowired
+    private FeedbackService feedbackService;
+    
+    @Autowired
+    private FeedbackUploadUtil feedbackUploadUtil;
+    
+    /**
+     * 提交用户反馈
+     * @param feedback 反馈信息
+     * @return 提交结果
+     */
+    @PostMapping("/submit")
+    public Result<Boolean> submitFeedback(@RequestBody Feedback feedback) {
+        System.out.println("=== FeedbackController.submitFeedback 接收请求 ===");
+        
+        try {
+            // 验证反馈信息
+            if (feedback == null) {
+                return Result.error("反馈信息不能为空");
+            }
+            
+            System.out.println("接收到的反馈信息:");
+            System.out.println("- 用户ID: " + feedback.getUserId());
+            System.out.println("- 反馈类型: " + feedback.getFeedbackType());
+            System.out.println("- 反馈内容: " + feedback.getContent());
+            System.out.println("- 图片URL: " + feedback.getImageUrls());
+            
+            // 提交反馈
+            Boolean success = feedbackService.submitFeedback(feedback);
+            
+            if (success) {
+                System.out.println("✅ 反馈提交成功");
+                return Result.success("反馈提交成功", true);
+            } else {
+                System.out.println("❌ 反馈提交失败");
+                return Result.error("反馈提交失败");
+            }
+            
+        } catch (RuntimeException e) {
+            System.err.println("❌ RuntimeException: " + e.getMessage());
+            e.printStackTrace();
+            return Result.error(e.getMessage());
+        } catch (Exception e) {
+            System.err.println("❌ Exception: " + e.getMessage());
+            e.printStackTrace();
+            return Result.error("提交反馈失败: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 健康检查接口
+     * @return 服务状态
+     */
+    @GetMapping("/health")
+    public Result<String> healthCheck() {
+        try {
+            return Result.success("Feedback服务运行正常", "healthy");
+        } catch (Exception e) {
+            return Result.error("服务异常: " + e.getMessage());
+        }
+    }
+    
+    /**
+     * 上传反馈图片
+     * @param file 图片文件
+     * @return 上传结果,包含图片URL
+     */
+    @PostMapping("/upload")
+    public Result<String> uploadFeedbackImage(@RequestParam("file") MultipartFile file) {
+        System.out.println("=== 接收反馈图片上传请求 ===");
+        System.out.println("文件名: " + file.getOriginalFilename());
+        System.out.println("文件大小: " + file.getSize() + " bytes");
+        
+        try {
+            // 1. 验证文件
+            if (file.isEmpty()) {
+                return Result.error("文件不能为空");
+            }
+            
+            // 2. 验证文件类型
+            String contentType = file.getContentType();
+            if (contentType == null || !contentType.startsWith("image/")) {
+                return Result.error("只能上传图片文件");
+            }
+            
+            // 3. 验证文件大小(不超过5MB)
+            long maxSize = 5 * 1024 * 1024; // 5MB
+            if (file.getSize() > maxSize) {
+                return Result.error("图片大小不能超过5MB");
+            }
+            
+            // 4. 上传图片到MinIO
+            try (InputStream inputStream = file.getInputStream()) {
+                String imageUrl = feedbackUploadUtil.uploadFeedbackImage(inputStream, file.getOriginalFilename(), contentType);
+                
+                if (imageUrl != null) {
+                    System.out.println("✅ 反馈图片上传成功!URL: " + imageUrl);
+                    return Result.success("图片上传成功", imageUrl);
+                } else {
+                    System.err.println("❌ 上传到MinIO失败");
+                    return Result.error("上传到MinIO失败");
+                }
+            }
+            
+        } catch (IOException e) {
+            System.err.println("❌ 文件处理失败: " + e.getMessage());
+            e.printStackTrace();
+            return Result.error("文件处理失败: " + e.getMessage());
+        } catch (Exception e) {
+            System.err.println("❌ 上传失败: " + e.getMessage());
+            e.printStackTrace();
+            return Result.error("上传失败: " + e.getMessage());
+        }
+    }
+}

+ 60 - 0
service/Essential/src/main/java/com/zhentao/entity/Feedback.java

@@ -0,0 +1,60 @@
+package com.zhentao.entity;
+
+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 com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 用户反馈实体类
+ */
+@Data
+@TableName("user_feedback")
+public class Feedback implements Serializable {
+    
+    /**
+     * 反馈ID(主键)
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+    
+    /**
+     * 提交反馈的用户ID(关联用户表)
+     */
+    @TableField("user_id")
+    private String userId;
+    
+    /**
+     * 反馈类型:1=意见建议,2=功能改进,3=bug报告
+     */
+    @TableField("feedback_type")
+    private Integer feedbackType;
+    
+    /**
+     * 反馈内容(最多500字)
+     */
+    private String content;
+    
+    /**
+     * 上传图片URL(多个用逗号分隔,最多3张)
+     */
+    @TableField("image_urls")
+    private String imageUrls;
+    
+    /**
+     * 提交时间
+     */
+    @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+    
+    /**
+     * 处理状态:0=未处理,1=已处理,2=已回复
+     */
+    private Integer status;
+}

+ 12 - 0
service/Essential/src/main/java/com/zhentao/mapper/FeedbackMapper.java

@@ -0,0 +1,12 @@
+package com.zhentao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zhentao.entity.Feedback;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 用户反馈Mapper接口
+ */
+@Mapper
+public interface FeedbackMapper extends BaseMapper<Feedback> {
+}

+ 16 - 0
service/Essential/src/main/java/com/zhentao/service/FeedbackService.java

@@ -0,0 +1,16 @@
+package com.zhentao.service;
+
+import com.zhentao.entity.Feedback;
+
+/**
+ * 用户反馈服务接口
+ */
+public interface FeedbackService {
+    
+    /**
+     * 提交用户反馈
+     * @param feedback 反馈信息
+     * @return 是否成功
+     */
+    Boolean submitFeedback(Feedback feedback);
+}

+ 62 - 0
service/Essential/src/main/java/com/zhentao/service/impl/FeedbackServiceImpl.java

@@ -0,0 +1,62 @@
+package com.zhentao.service.impl;
+
+import com.zhentao.entity.Feedback;
+import com.zhentao.mapper.FeedbackMapper;
+import com.zhentao.service.FeedbackService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+
+/**
+ * 用户反馈服务实现类
+ */
+@Service
+public class FeedbackServiceImpl implements FeedbackService {
+    
+    @Autowired
+    private FeedbackMapper feedbackMapper;
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean submitFeedback(Feedback feedback) {
+        System.out.println("=== FeedbackServiceImpl.submitFeedback 接收请求 ===");
+        
+        // 数据验证
+        if (feedback == null) {
+            throw new RuntimeException("反馈信息不能为空");
+        }
+        
+        if (feedback.getUserId() == null || feedback.getUserId().isEmpty()) {
+            throw new RuntimeException("用户ID不能为空");
+        }
+        
+        if (feedback.getFeedbackType() == null || feedback.getFeedbackType() < 1 || feedback.getFeedbackType() > 3) {
+            throw new RuntimeException("反馈类型无效");
+        }
+        
+        if (feedback.getContent() == null || feedback.getContent().trim().isEmpty()) {
+            throw new RuntimeException("反馈内容不能为空");
+        }
+        
+        if (feedback.getContent().length() > 500) {
+            throw new RuntimeException("反馈内容不能超过500字");
+        }
+        
+        // 设置默认值
+        if (feedback.getImageUrls() == null) {
+            feedback.setImageUrls("");
+        }
+        
+        feedback.setCreateTime(LocalDateTime.now());
+        feedback.setStatus(0); // 0=未处理
+        
+        // 保存反馈
+        int result = feedbackMapper.insert(feedback);
+        
+        System.out.println("✅ 反馈提交成功,反馈ID: " + feedback.getId());
+        
+        return result > 0;
+    }
+}

+ 122 - 0
service/Essential/src/main/java/com/zhentao/util/FeedbackUploadUtil.java

@@ -0,0 +1,122 @@
+package com.zhentao.util;
+
+import io.minio.BucketExistsArgs;
+import io.minio.MakeBucketArgs;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
+import io.minio.SetBucketPolicyArgs;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.InputStream;
+import java.util.UUID;
+
+/**
+ * 反馈图片上传工具类
+ */
+@Slf4j
+@Component
+public class FeedbackUploadUtil {
+    
+    @Autowired
+    private MinioClient minioClient;
+    
+    @Autowired
+    private com.zhentao.config.MinioConfig minioConfig;
+    
+    /**
+     * 初始化:确保桶存在并设置为公共访问
+     */
+    @PostConstruct
+    public void init() {
+        try {
+            String bucketName = minioConfig.getBucketName();
+            
+            // 检查桶是否存在
+            boolean exists = minioClient.bucketExists(
+                BucketExistsArgs.builder()
+                    .bucket(bucketName)
+                    .build()
+            );
+            
+            if (!exists) {
+                log.info("MinIO桶不存在,开始创建: {}", bucketName);
+                
+                // 创建桶
+                minioClient.makeBucket(
+                    MakeBucketArgs.builder()
+                        .bucket(bucketName)
+                        .build()
+                );
+                
+                log.info("✅ MinIO桶创建成功: {}", bucketName);
+                
+                // 设置桶为公共访问(允许匿名读取)
+                String policy = String.format(
+                    "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::%s/*\"]}]}",
+                    bucketName
+                );
+                
+                minioClient.setBucketPolicy(
+                    SetBucketPolicyArgs.builder()
+                        .bucket(bucketName)
+                        .config(policy)
+                        .build()
+                );
+                
+                log.info("✅ MinIO桶权限设置成功(公共读取): {}", bucketName);
+            } else {
+                log.info("MinIO桶已存在: {}", bucketName);
+            }
+            
+        } catch (Exception e) {
+            log.error("❌ 初始化MinIO失败: {}", e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 上传反馈图片到MinIO
+     * 
+     * @param inputStream 图片文件输入流
+     * @param originalFilename 原始文件名
+     * @param contentType 文件内容类型
+     * @return MinIO中的对象URL(永久有效,不使用预签名)
+     */
+    public String uploadFeedbackImage(InputStream inputStream, String originalFilename, String contentType) {
+        try {
+            // 1. 生成唯一文件名
+            String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
+            String fileName = "feedback_" + System.currentTimeMillis() + "_" + UUID.randomUUID().toString().replace("-", "") + suffix;
+            
+            // 2. 构建对象路径
+            String objectName = minioConfig.getUserFeedbackFolder() + "/" + fileName;
+            
+            log.info("开始上传反馈图片到MinIO: objectName={}", objectName);
+            
+            // 3. 上传到MinIO
+            minioClient.putObject(
+                PutObjectArgs.builder()
+                    .bucket(minioConfig.getBucketName())
+                    .object(objectName)
+                    .stream(inputStream, -1, 10485760) // 10MB chunk size
+                    .contentType(contentType)
+                    .build()
+            );
+            
+            // 4. 生成公共访问URL(不使用预签名,永久有效)
+            String imageUrl = String.format("%s/%s/%s", 
+                minioConfig.getEndpoint(),
+                minioConfig.getBucketName(),
+                objectName);
+            
+            log.info("✅ 反馈图片上传成功: {}", imageUrl);
+            return imageUrl;
+            
+        } catch (Exception e) {
+            log.error("❌ 上传反馈图片失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+}

+ 6 - 8
service/Essential/src/main/resources/application.yml

@@ -64,14 +64,12 @@ mybatis-plus:
       logic-delete-field: isDeleted
       logic-delete-value: 1
       logic-not-delete-value: 0
-# MinIO配置(存储空间不足,暂时停用)
-# minio:
-#   endpoint: http://115.190.125.125:9000
-#   accessKey: minioadmin
-#   secretKey: minioadmin
-#   bucketName: avatars
-#   bannerBucket: banners
-#   avatarFolder: 红娘
+# MinIO配置
+minio:
+  endpoint: http://115.190.125.125:9000
+  accessKey: minioadmin
+  secretKey: minioadmin
+  bucketName: user-feedback
 
 # 头像本地存储配置
 avatar: