@startuml
' 定义接口
interface NoticeMapper {
    + insert(Notice)
    + update(Notice)
    + deleteById(Long)
    + selectById(Long): Notice
    + selectByCondition(String, String, Date): List~Notice~
}

class Notice {
    - id: Long
    - title: String
    - content: String
    - createTime: Date
    - updateTime: Date
    + getId(): Long
    + setId(Long): void
    + getTitle(): String
    + setTitle(String): void
    + getContent(): String
    + setContent(String): void
    + getCreateTime(): Date
    + setCreateTime(Date): void
    + getUpdateTime(): Date
    + setUpdateTime(Date): void
}

class NoticeService {
    - noticeMapper: NoticeMapper
    + createNotice(Notice): Notice
    + updateNotice(Notice): Notice
    + deleteNotice(Long): void
    + getNoticeById(Long): Notice
    + searchNotices(String, String, Date): List~Notice~
}

class NoticeController {
    - noticeService: NoticeService
    + create(Notice): Notice
    + update(Long, Notice): Notice
    + delete(Long): void
    + get(Long): Notice
    + search(String, String, Date): List~Notice~
}

' 关系定义

NoticeMapper <|.. NoticeService : 实现

NoticeService ..> NoticeMapper : 依赖
NoticeController ..> NoticeService : 依赖

NoticeService "1" -- "0..*" Notice : 关联
NoticeMapper "1" -- "0..*" Notice : 关联

@enduml根据上面的代码规范，帮我画类图。关系定义要用中文。package com.example.springboot.controller;

import com.example.springboot.entity.Feedback;
import com.example.springboot.service.FeedbackService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/feedback")
public class FeedbackController {

    private final FeedbackService feedbackService;

    @Autowired
    public FeedbackController(FeedbackService feedbackService) {
        this.feedbackService = feedbackService;
    }

    /**
     * 用户提交反馈
     */
    @PostMapping("/user/add")
    public String addFeedback(@RequestBody Feedback feedback, @RequestParam Integer userId) {
        System.out.println("POST /user/add -> userId = " + userId);
        if (userId == null || userId <= 0) {
            return "请先登录";
        }
        feedback.setUserId(userId);
        int result = feedbackService.addFeedback(feedback);
        return result > 0 ? "反馈提交成功" : "反馈提交失败";
    }

    @GetMapping("/user/list")
    public List<Feedback> getUserFeedback(@RequestParam Integer userId) {
        System.out.println("GET /user/list -> userId = " + userId);
        if (userId == null || userId <= 0) {
            return null;
        }
        return feedbackService.getFeedbackByUser(userId);
    }

    @DeleteMapping("/user/delete/{id}")
    public String deleteUserFeedback(@PathVariable Integer id, @RequestParam Integer userId) {
        System.out.println("DELETE /user/delete/" + id + " -> userId = " + userId);
        if (userId == null || userId <= 0) {
            return "请先登录";
        }
        Feedback feedback = feedbackService.getFeedbackById(id);
        if (feedback == null || !feedback.getUserId().equals(userId)) {
            return "删除失败，可能不是您的反馈或不存在";
        }
        int result = feedbackService.deleteFeedback(id, userId);
        return result > 0 ? "删除成功" : "删除失败";
    }

    /**
     * 管理员查询所有反馈接口
     * 不需要传 userId，且不做权限验证，直接返回所有反馈
     */
    @GetMapping("/list/all")
    public List<Feedback> getAllFeedback() {
        System.out.println("GET /list/all 请求到达");
        return feedbackService.getAllFeedback();
    }
//  管理员回复反馈接口
    @PutMapping("/reply/{id}")
    public String replyFeedback(@PathVariable Integer id, @RequestBody Map<String, String> body) {
        String reply = body.get("reply");
        int result = feedbackService.replyFeedback(id, reply);
        return result > 0 ? "回复成功" : "回复失败";
    }


}
package com.example.springboot.entity;

import java.time.LocalDateTime;

public class Feedback {
    private Integer id;
    private String feedbackType;
    private String phone;
    private String content;
    private String reply;
    private Integer userId;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    // getter setter
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFeedbackType() {
        return feedbackType;
    }

    public void setFeedbackType(String feedbackType) {
        this.feedbackType = feedbackType;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getReply() {
        return reply;
    }

    public void setReply(String reply) {
        this.reply = reply;
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public LocalDateTime getCreateTime() {
        return createTime;
    }

    public void setCreateTime(LocalDateTime createTime) {
        this.createTime = createTime;
    }

    public LocalDateTime getUpdateTime() {
        return updateTime;
    }

    public void setUpdateTime(LocalDateTime updateTime) {
        this.updateTime = updateTime;
    }
}
package com.example.springboot.mapper;

import com.example.springboot.entity.Feedback;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface FeedbackMapper {

    // 插入反馈
    int insertFeedback(Feedback feedback);

    // 根据id删除反馈
    int deleteById(@Param("id") Integer id);

    // 查询指定用户自己的反馈列表
    List<Feedback> selectByUserId(@Param("userId") Integer userId);

    // 查询所有反馈（管理员用）
    List<Feedback> selectAll();

    // 根据id查询反馈详情
    Feedback selectById(@Param("id") Integer id);

    // 管理员回复更新反馈
    int updateReply(@Param("id") Integer id, @Param("reply") String reply);
}
package com.example.springboot.service;

import com.example.springboot.entity.Feedback;
import com.example.springboot.mapper.FeedbackMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class FeedbackService {

    private final FeedbackMapper feedbackMapper;

    public FeedbackService(FeedbackMapper feedbackMapper) {
        this.feedbackMapper = feedbackMapper;
    }

    /**
     * 用户添加反馈
     */
    @Transactional
    public int addFeedback(Feedback feedback) {
        return feedbackMapper.insertFeedback(feedback);
    }

    /**
     * 用户删除自己的反馈
     */
    @Transactional
    public int deleteFeedback(Integer id, Integer userId) {
        Feedback feedback = feedbackMapper.selectById(id);
        if(feedback == null) {
            return 0;
        }
        // 只有反馈属于该用户或管理员可以删除
        if(feedback.getUserId().equals(userId)) {
            return feedbackMapper.deleteById(id);
        }
        return 0;
    }

    /**
     * 查询指定用户的反馈
     */
    public List<Feedback> getFeedbackByUser(Integer userId) {
        return feedbackMapper.selectByUserId(userId);
    }

    /**
     * 管理员查询所有反馈
     */
    public List<Feedback> getAllFeedback() {
        return feedbackMapper.selectAll();
    }

    /**
     * 管理员回复反馈
     */
    @Transactional
    public int replyFeedback(Integer id, String reply) {
        return feedbackMapper.updateReply(id, reply);
    }

    /**
     * 根据ID查询反馈详情
     */
    public Feedback getFeedbackById(Integer id) {
        return feedbackMapper.selectById(id);
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springboot.mapper.FeedbackMapper">

    <insert id="insertFeedback" parameterType="com.example.springboot.entity.Feedback" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO feedback (feedback_type, phone, content, user_id)
        VALUES (#{feedbackType}, #{phone}, #{content}, #{userId})
    </insert>

    <delete id="deleteById" parameterType="int">
        DELETE FROM feedback WHERE id = #{id}
    </delete>

    <select id="selectByUserId" resultType="com.example.springboot.entity.Feedback" parameterType="int">
        SELECT id, feedback_type, phone, content, reply, user_id, create_time, update_time
        FROM feedback
        WHERE user_id = #{userId}
        ORDER BY create_time DESC
    </select>

    <select id="selectAll" resultType="com.example.springboot.entity.Feedback">
        SELECT id, feedback_type, phone, content, reply, user_id, create_time, update_time
        FROM feedback
        ORDER BY create_time DESC
    </select>

    <select id="selectById" parameterType="int" resultType="com.example.springboot.entity.Feedback">
        SELECT id, feedback_type, phone, content, reply, user_id, create_time, update_time
        FROM feedback
        WHERE id = #{id}
    </select>

    <update id="updateReply" parameterType="map">
        UPDATE feedback SET reply = #{reply}, update_time = NOW() WHERE id = #{id}
    </update>

</mapper>
<template>
  <el-card class="box-card" style="max-width: 900px; margin: 20px auto;">
    <div class="header">
      <h2>管理员反馈管理</h2>
      <el-button
          @click="fetchAllFeedback"
          type="primary"
          size="medium"
          class="refresh-btn"
      >
        <i class="el-icon-refresh" /> 刷新
      </el-button>
    </div>

    <el-table
        :data="feedbacks"
        style="width: 100%"
        v-loading="loading"
        :row-key="row => row.id"
        :expand-row-keys="expandedRows"
        @expand-change="handleExpand"
    >
      <el-table-column type="expand">
        <template #default="{ row }">
          <el-form label-position="top" @submit.prevent>
            <el-form-item label="用户回复">
              <el-input
                  type="textarea"
                  rows="3"
                  v-model="replyMap[row.id]"
                  placeholder="填写回复"
              />
            </el-form-item>
            <el-form-item>
              <el-button
                  type="primary"
                  size="small"
                  @click="submitReply(row.id)"
              >
                提交回复
              </el-button>
            </el-form-item>
          </el-form>
        </template>
      </el-table-column>

      <el-table-column prop="id" label="ID" width="70" />
      <el-table-column prop="userId" label="用户ID" width="90" />
      <el-table-column prop="feedbackType" label="反馈类型" width="120" />
      <el-table-column prop="phone" label="手机号" width="140" />
      <el-table-column prop="content" label="内容" />
      <el-table-column prop="reply" label="回复" width="200" />
    </el-table>
  </el-card>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage } from 'element-plus'

// 反馈数组
const feedbacks = ref([])

// 对展开行对应的回复输入框绑定内容，key为反馈id，value为输入内容
const replyMap = reactive({})

// 当前正在加载状态
const loading = ref(false)

// 当前展开行id列表
const expandedRows = ref([])

// 获取所有反馈（管理员）
const fetchAllFeedback = async () => {
  loading.value = true
  try {
    const res = await axios.get('http://localhost:8080/feedback/list/all')
    if (Array.isArray(res.data)) {
      feedbacks.value = res.data
      res.data.forEach((item) => {
        replyMap[item.id] = item.reply || ''
      })
    } else {
      feedbacks.value = []
      ElMessage.error('获取反馈列表失败，数据格式不正确')
    }
  } catch (error) {
    console.error(error)
    ElMessage.error('获取反馈失败')
  } finally {
    loading.value = false
  }
}

// 提交管理员回复
const submitReply = async (id) => {
  const replyContent = replyMap[id]?.trim()
  if (!replyContent) {
    ElMessage.warning('回复内容不能为空')
    return
  }
  try {
    await axios.put(`http://localhost:8080/feedback/reply/${id}`, {
      reply: replyContent,
    })
    ElMessage.success('回复提交成功')
    // 刷新列表并更新replyMap
    await fetchAllFeedback()
    // 保持当前展开状态
    if (!expandedRows.value.includes(id)) {
      expandedRows.value.push(id)
    }
  } catch (error) {
    console.error(error)
    ElMessage.error('回复提交失败')
  }
}

// 展开事件，用于保持 expandedRows 中的内容
const handleExpand = (row, expanded) => {
  if (expanded) {
    if (!expandedRows.value.includes(row.id)) {
      expandedRows.value.push(row.id)
    }
  } else {
    const idx = expandedRows.value.indexOf(row.id)
    if (idx >= 0) {
      expandedRows.value.splice(idx, 1)
    }
  }
}

// 页面加载时，获取所有反馈
onMounted(() => {
  fetchAllFeedback()
})
</script>

<style scoped>
.box-card {
  padding: 20px;
}

/* header 容器 使用 flex */
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

/* 去掉 h2 默认上下 margin，防止垂直偏离 */
.header h2 {
  margin: 0;
  font-weight: 700;
  font-size: 24px;
}

/* 刷新按钮样式 */
.refresh-btn {
  min-width: 90px;
  font-weight: 600;
  padding: 6px 20px;
  border-radius: 6px;
  box-shadow: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  transition: background-color 0.3s ease;
  user-select: none;
}

.refresh-btn i {
  font-size: 16px;
  line-height: 1;
}

/* 鼠标悬浮 */
.refresh-btn:hover {
  background-color: #3a8ee6;
}

/* 聚焦时的阴影 */
.refresh-btn:focus-visible {
  box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.4);
}
</style>
<template>
  <el-card class="box-card" style="max-width: 800px; margin: 20px auto;">
    <div class="user-feedback-header">
      <h2>我的反馈列表</h2>
      <el-button
          @click="fetchUserFeedback"
          type="primary"
          size="medium"
          class="refresh-btn"
      >
        <i class="el-icon-refresh" /> 刷新
      </el-button>
    </div>

    <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
      <el-form-item label="反馈类型" prop="feedbackType">
        <el-select v-model="form.feedbackType" placeholder="请选择反馈类型">
          <el-option label="系统故障" value="系统故障"></el-option>
          <el-option label="优化意见" value="优化意见"></el-option>
        </el-select>
      </el-form-item>

      <el-form-item label="手机号" prop="phone">
        <el-input v-model="form.phone" placeholder="请输入手机号"></el-input>
      </el-form-item>

      <el-form-item label="建议内容" prop="content">
        <el-input
            type="textarea"
            :rows="4"
            v-model="form.content"
            placeholder="请输入您的建议内容"
        ></el-input>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="submitFeedback">提交反馈</el-button>
        <el-button @click="resetForm">重置</el-button>
      </el-form-item>
    </el-form>

    <el-divider></el-divider>

    <h2>我的反馈列表</h2>
    <el-table :data="feedbacks" style="width: 100%" v-loading="loading">
      <el-table-column prop="feedbackType" label="反馈类型" width="120"></el-table-column>
      <el-table-column prop="phone" label="手机号" width="140"></el-table-column>
      <el-table-column prop="content" label="内容" />
      <el-table-column label="回复" width="200">
        <template #default="{ row }">
          <span v-if="row.reply">{{ row.reply }}</span>
          <span v-else style="color: #999">暂无回复</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="100">
        <template #default="{ row }">
          <el-button type="danger" size="mini" @click="deleteFeedback(row.id)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-card>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'

const router = useRouter()

// 获取并解析userId，并做严格校验
const rawUserId = localStorage.getItem('userId')
const parsedUserId = parseInt(rawUserId)

if (!rawUserId || isNaN(parsedUserId) || parsedUserId <= 0) {
  console.error('无效userId:', rawUserId)
  ElMessage.error('请先登录')
  router.push('/login')
}

const userId = parsedUserId

const formRef = ref(null)
const form = ref({
  feedbackType: '',
  phone: '',
  content: ''
})

const rules = {
  feedbackType: [{ required: true, message: '请选择反馈类型', trigger: 'change' }],
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入有效手机号', trigger: 'blur' }
  ],
  content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
}

const feedbacks = ref([])
const loading = ref(false)

async function fetchUserFeedback() {
  loading.value = true
  try {
    console.log('fetchUserFeedback userId:', userId)
    const resp = await axios.get('http://localhost:8080/feedback/user/list', {
      params: { userId }
    })
    if (resp.data) {
      feedbacks.value = resp.data
    }
  } catch (error) {
    console.error('获取反馈失败', error)
    ElMessage.error('获取反馈失败')
  } finally {
    loading.value = false
  }
}

async function submitFeedback() {
  formRef.value.validate(async valid => {
    if (!valid) return
    try {
      const payload = {
        feedbackType: form.value.feedbackType,
        phone: form.value.phone,
        content: form.value.content
      }
      console.log('submitFeedback payload, userId:', payload, userId)
      const resp = await axios.post('http://localhost:8080/feedback/user/add', payload, {
        params: { userId }
      })

      if (resp.data === '反馈提交成功') {
        ElMessage.success(resp.data)
        resetForm()
        fetchUserFeedback()
      } else {
        ElMessage.error(resp.data || '提交失败')
      }
    } catch (error) {
      console.error('提交失败', error)
      ElMessage.error('提交失败')
    }
  })
}

function resetForm() {
  form.value.feedbackType = ''
  form.value.phone = ''
  form.value.content = ''
  formRef.value.clearValidate()
}

async function deleteFeedback(id) {
  try {
    await ElMessageBox.confirm('确认删除该反馈吗？', '提示', { type: 'warning' })
    console.log('deleteFeedback id, userId:', id, userId)
    const resp = await axios.delete(`http://localhost:8080/feedback/user/delete/${id}`, {
      params: { userId }
    })
    if (resp.data === '删除成功') {
      ElMessage.success('删除成功')
      fetchUserFeedback()
    } else {
      ElMessage.error(resp.data || '删除失败')
    }
  } catch {
    // 用户取消，不处理
  }
}

onMounted(() => {
  fetchUserFeedback()
})
</script>

<style scoped>
.box-card {
  padding: 20px;
}

.user-feedback-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

.user-feedback-header h2 {
  margin: 0;
  font-weight: 700;
  font-size: 24px;
}

.refresh-btn {
  min-width: 90px;
  font-weight: 600;
  padding: 6px 20px;
  border-radius: 6px;
  box-shadow: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  transition: background-color 0.3s ease;
  user-select: none;
}

.refresh-btn i {
  font-size: 16px;
  line-height: 1;
}

.refresh-btn:hover {
  background-color: #3a8ee6;
}

.refresh-btn:focus-visible {
  box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.4);
}
</style>