Java全栈项目--校园快递管理与配送系统(5)

发布于:2025-04-10 ⋅ 阅读:(46) ⋅ 点赞:(0)

源代码续

<template>
  <div class="app-container">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>通知统计</span>
        <div class="header-operations">
          <el-date-picker
            v-model="dateRange"
            type="daterange"
            align="right"
            unlink-panels
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            :picker-options="pickerOptions"
            @change="handleDateRangeChange"
            style="width: 350px">
          </el-date-picker>
          <el-button type="primary" icon="el-icon-refresh" @click="refreshData">刷新</el-button>
          <el-button type="success" icon="el-icon-download" @click="exportData">导出</el-button>
        </div>
      </div>
      
      <!-- 统计卡片 -->
      <el-row :gutter="20">
        <el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6" v-for="(item, index) in statisticsCards" :key="index">
          <el-card class="stat-card" shadow="hover">
            <div class="card-icon">
              <i :class="item.icon" :style="{ color: item.color }"></i>
            </div>
            <div class="card-content">
              <div class="card-title">{{ item.title }}</div>
              <div class="card-value">{{ item.value }}</div>
              <div class="card-footer">
                <span>{{ item.change >= 0 ? '+' : '' }}{{ item.change }}%</span>
                <span>较上期</span>
                <i :class="item.change >= 0 ? 'el-icon-top' : 'el-icon-bottom'" 
                   :style="{ color: item.change >= 0 ? '#67C23A' : '#F56C6C' }"></i>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
      
      <!-- 图表区域 -->
      <el-row :gutter="20" style="margin-top: 20px;">
        <!-- 发送趋势图 -->
        <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
          <el-card shadow="hover">
            <div slot="header" class="clearfix">
              <span>发送趋势</span>
              <el-radio-group v-model="trendGroupBy" size="mini" style="float: right;">
                <el-radio-button label="day">按日</el-radio-button>
                <el-radio-button label="week">按周</el-radio-button>
                <el-radio-button label="month">按月</el-radio-button>
              </el-radio-group>
            </div>
            <div class="chart-container">
              <div ref="trendChart" style="width: 100%; height: 300px;"></div>
            </div>
          </el-card>
        </el-col>
        
        <!-- 通知类型分布图 -->
        <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
          <el-card shadow="hover">
            <div slot="header" class="clearfix">
              <span>通知类型分布</span>
            </div>
            <div class="chart-container">
              <div ref="typeChart" style="width: 100%; height: 300px;"></div>
            </div>
          </el-card>
        </el-col>
      </el-row>
      
      <el-row :gutter="20" style="margin-top: 20px;">
        <!-- 通知渠道分布图 -->
        <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
          <el-card shadow="hover">
            <div slot="header" class="clearfix">
              <span>通知渠道分布</span>
            </div>
            <div class="chart-container">
              <div ref="channelChart" style="width: 100%; height: 300px;"></div>
            </div>
          </el-card>
        </el-col>
        
        <!-- 阅读率统计图 -->
        <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
          <el-card shadow="hover">
            <div slot="header" class="clearfix">
              <span>阅读率统计</span>
              <el-radio-group v-model="readRateGroupBy" size="mini" style="float: right;">
                <el-radio-button label="type">按类型</el-radio-button>
                <el-radio-button label="channel">按渠道</el-radio-button>
              </el-radio-group>
            </div>
            <div class="chart-container">
              <div ref="readRateChart" style="width: 100%; height: 300px;"></div>
            </div>
          </el-card>
        </el-col>
      </el-row>
      
      <!-- 详细数据表格 -->
      <el-card shadow="hover" style="margin-top: 20px;">
        <div slot="header" class="clearfix">
          <span>详细数据</span>
          <el-button-group style="float: right;">
            <el-button size="mini" :type="tableView === 'daily' ? 'primary' : ''" @click="tableView = 'daily'">日报表</el-button>
            <el-button size="mini" :type="tableView === 'type' ? 'primary' : ''" @click="tableView = 'type'">类型报表</el-button>
            <el-button size="mini" :type="tableView === 'channel' ? 'primary' : ''" @click="tableView = 'channel'">渠道报表</el-button>
          </el-button-group>
        </div>
        
        <!-- 日报表 -->
        <el-table v-if="tableView === 'daily'" :data="dailyData" style="width: 100%" border>
          <el-table-column prop="date" label="日期" width="120" />
          <el-table-column prop="sentCount" label="发送数量" width="100" align="center" />
          <el-table-column prop="readCount" label="已读数量" width="100" align="center" />
          <el-table-column prop="readRate" label="阅读率" width="100" align="center">
            <template slot-scope="scope">
              {{ scope.row.readRate }}%
            </template>
          </el-table-column>
          <el-table-column prop="systemCount" label="系统通知" width="100" align="center" />
          <el-table-column prop="expressCount" label="快递通知" width="100" align="center" />
          <el-table-column prop="activityCount" label="活动通知" width="100" align="center" />
          <el-table-column prop="inAppCount" label="站内信" width="100" align="center" />
          <el-table-column prop="smsCount" label="短信" width="100" align="center" />
          <el-table-column prop="emailCount" label="邮件" width="100" align="center" />
          <el-table-column prop="pushCount" label="推送" width="100" align="center" />
        </el-table>
        
        <!-- 类型报表 -->
        <el-table v-if="tableView === 'type'" :data="typeData" style="width: 100%" border>
          <el-table-column prop="type" label="通知类型" width="120">
            <template slot-scope="scope">
              <el-tag :type="getTypeTagType(scope.row.typeId)">{{ scope.row.type }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="sentCount" label="发送数量" width="120" align="center" />
          <el-table-column prop="readCount" label="已读数量" width="120" align="center" />
          <el-table-column prop="readRate" label="阅读率" width="120" align="center">
            <template slot-scope="scope">
              {{ scope.row.readRate }}%
            </template>
          </el-table-column>
          <el-table-column prop="userCount" label="接收用户数" width="120" align="center" />
          <el-table-column prop="avgResponseTime" label="平均响应时间" align="center">
            <template slot-scope="scope">
              {{ scope.row.avgResponseTime }} 分钟
            </template>
          </el-table-column>
        </el-table>
        
        <!-- 渠道报表 -->
        <el-table v-if="tableView === 'channel'" :data="channelData" style="width: 100%" border>
          <el-table-column prop="channel" label="通知渠道" width="120">
            <template slot-scope="scope">
              <el-tag :type="getChannelTagType(scope.row.channelId)">{{ scope.row.channel }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="sentCount" label="发送数量" width="120" align="center" />
          <el-table-column prop="successCount" label="成功数量" width="120" align="center" />
          <el-table-column prop="successRate" label="发送成功率" width="120" align="center">
            <template slot-scope="scope">
              {{ scope.row.successRate }}%
            </template>
          </el-table-column>
          <el-table-column prop="readCount" label="已读数量" width="120" align="center" />
          <el-table-column prop="readRate" label="阅读率" width="120" align="center">
            <template slot-scope="scope">
              {{ scope.row.readRate }}%
            </template>
          </el-table-column>
          <el-table-column prop="avgResponseTime" label="平均响应时间" align="center">
            <template slot-scope="scope">
              {{ scope.row.avgResponseTime }} 分钟
            </template>
          </el-table-column>
        </el-table>
        
        <div class="pagination-container">
          <el-pagination
            background
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            :current-page="pagination.currentPage"
            :page-sizes="[10, 20, 30, 50]"
            :page-size="pagination.pageSize"
            layout="total, sizes, prev, pager, next, jumper"
            :total="pagination.total">
          </el-pagination>
        </div>
      </el-card>
    </el-card>
  </div>
</template>

<script>
import * as echarts from 'echarts'
import { NotificationType, NotificationChannel, getTypeTagType, getChannelTagType } from '@/utils/notification'

export default {
  name: 'NotificationStatistics',
  data() {
    return {
      // 日期范围选择器配置
      pickerOptions: {
        shortcuts: [
          {
            text: '最近一周',
            onClick(picker) {
              const end = new Date()
              const start = new Date()
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
              picker.$emit('pick', [start, end])
            }
          },
          {
            text: '最近一个月',
            onClick(picker) {
              const end = new Date()
              const start = new Date()
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
              picker.$emit('pick', [start, end])
            }
          },
          {
            text: '最近三个月',
            onClick(picker) {
              const end = new Date()
              const start = new Date()
              start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
              picker.$emit('pick', [start, end])
            }
          }
        ]
      },
      // 日期范围
      dateRange: [new Date(new Date().getTime() - 3600 * 1000 * 24 * 30), new Date()],
      // 统计卡片数据
      statisticsCards: [
        {
          title: '总发送量',
          value: 12580,
          icon: 'el-icon-s-promotion',
          color: '#409EFF',
          change: 15.8
        },
        {
          title: '阅读率',
          value: '78.3%',
          icon: 'el-icon-view',
          color: '#67C23A',
          change: 5.2
        },
        {
          title: '成功率',
          value: '99.5%',
          icon: 'el-icon-check',
          color: '#E6A23C',
          change: 0.3
        },
        {
          title: '平均响应时间',
          value: '25分钟',
          icon: 'el-icon-time',
          color: '#F56C6C',
          change: -10.5
        }
      ],
      // 趋势图分组方式
      trendGroupBy: 'day',
      // 阅读率图分组方式
      readRateGroupBy: 'type',
      // 表格视图
      tableView: 'daily',
      // 分页信息
      pagination: {
        currentPage: 1,
        pageSize: 10,
        total: 0
      },
      // 图表实例
      trendChart: null,
      typeChart: null,
      channelChart: null,
      readRateChart: null,
      // 日报表数据
      dailyData: [],
      // 类型报表数据
      typeData: [],
      // 渠道报表数据
      channelData: []
    }
  },
  mounted() {
    this.initCharts()
    this.fetchData()
    window.addEventListener('resize', this.resizeCharts)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizeCharts)
    if (this.trendChart) this.trendChart.dispose()
    if (this.typeChart) this.typeChart.dispose()
    if (this.channelChart) this.channelChart.dispose()
    if (this.readRateChart) this.readRateChart.dispose()
  },
  watch: {
    trendGroupBy() {
      this.updateTrendChart()
    },
    readRateGroupBy() {
      this.updateReadRateChart()
    }
  },
  methods: {
    // 初始化图表
    initCharts() {
      this.trendChart = echarts.init(this.$refs.trendChart)
      this.typeChart = echarts.init(this.$refs.typeChart)
      this.channelChart = echarts.init(this.$refs.channelChart)
      this.readRateChart = echarts.init(this.$refs.readRateChart)
      
      this.updateTrendChart()
      this.updateTypeChart()
      this.updateChannelChart()
      this.updateReadRateChart()
    },
    
    // 更新趋势图
    updateTrendChart() {
      // 模拟数据
      let xAxisData = []
      let sentData = []
      let readData = []
      
      if (this.trendGroupBy === 'day') {
        xAxisData = ['4-1', '4-2', '4-3', '4-4', '4-5', '4-6', '4-7', '4-8', '4-9', '4-10']
        sentData = [120, 132, 101, 134, 90, 230, 210, 182, 191, 234]
        readData = [90, 110, 80, 100, 70, 180, 160, 140, 150, 180]
      } else if (this.trendGroupBy === 'week') {
        xAxisData = ['第1周', '第2周', '第3周', '第4周']
        sentData = [520, 632, 701, 834]
        readData = [410, 520, 580, 690]
      } else {
        xAxisData = ['1月', '2月', '3月', '4月']
        sentData = [1200, 1300, 1400, 1800]
        readData = [900, 1000, 1100, 1400]
      }
      
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        legend: {
          data: ['发送量', '已读量']
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: xAxisData
        },
        yAxis: {
          type: 'value'
        },
        series: [
          {
            name: '发送量',
            type: 'bar',
            data: sentData,
            itemStyle: {
              color: '#409EFF'
            }
          },
          {
            name: '已读量',
            type: 'bar',
            data: readData,
            itemStyle: {
              color: '#67C23A'
            }
          }
        ]
      }
      
      this.trendChart.setOption(option)
    },
    
    // 更新类型分布图
    updateTypeChart() {
      const option = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b}: {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          right: 10,
          top: 'center',
          data: ['系统通知', '快递通知', '活动通知', '其他通知']
        },
        series: [
          {
            name: '通知类型',
            type: 'pie',
            radius: ['50%', '70%'],
            avoidLabelOverlap: false,
            label: {
              show: false,
              position: 'center'
            },
            emphasis: {
              label: {
                show: true,
                fontSize: '18',
                fontWeight: 'bold'
              }
            },
            labelLine: {
              show: false
            },
            data: [
              { value: 4500, name: '系统通知' },
              { value: 3500, name: '快递通知' },
              { value: 3000, name: '活动通知' },
              { value: 1500, name: '其他通知' }
            ],
            itemStyle: {
              normal: {
                color: function(params) {
                  const colorList = ['#409EFF', '#67C23A', '#E6A23C', '#909399']
                  return colorList[params.dataIndex]
                }
              }
            }
          }
        ]
      }
      
      this.typeChart.setOption(option)
    },
    
    // 更新渠道分布图
    updateChannelChart() {
      const option = {
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b}: {c} ({d}%)'
        },
        legend: {
          orient: 'vertical',
          right: 10,
          top: 'center',
          data: ['站内信', '短信', '邮件', '推送']
        },
        series: [
          {
            name: '通知渠道',
            type: 'pie',
            radius: ['50%', '70%'],
            avoidLabelOverlap: false,
            label: {
              show: false,
              position: 'center'
            },
            emphasis: {
              label: {
                show: true,
                fontSize: '18',
                fontWeight: 'bold'
              }
            },
            labelLine: {
              show: false
            },
            data: [
              { value: 5000, name: '站内信' },
              { value: 3000, name: '短信' },
              { value: 2500, name: '邮件' },
              { value: 2000, name: '推送' }
            ],
            itemStyle: {
              normal: {
                color: function(params) {
                  const colorList = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
                  return colorList[params.dataIndex]
                }
              }
            }
          }
        ]
      }
      
      this.channelChart.setOption(option)
    },
    
    // 更新阅读率图
    updateReadRateChart() {
      let xAxisData = []
      let seriesData = []
      
      if (this.readRateGroupBy === 'type') {
        xAxisData = ['系统通知', '快递通知', '活动通知', '其他通知']
        seriesData = [85, 92, 75, 65]
      } else {
        xAxisData = ['站内信', '短信', '邮件', '推送']
        seriesData = [90, 70, 80, 60]
      }
      
      const option = {
        tooltip: {
          trigger: 'axis',
          formatter: '{b}: {c}%'
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: xAxisData
        },
        yAxis: {
          type: 'value',
          min: 0,
          max: 100,
          axisLabel: {
            formatter: '{value}%'
          }
        },
        series: [
          {
            name: '阅读率',
            type: 'bar',
            data: seriesData,
            itemStyle: {
              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#83bff6' },
                { offset: 0.5, color: '#188df0' },
                { offset: 1, color: '#188df0' }
              ])
            },
            emphasis: {
              itemStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                  { offset: 0, color: '#2378f7' },
                  { offset: 0.7, color: '#2378f7' },
                  { offset: 1, color: '#83bff6' }
                ])
              }
            }
          }
        ]
      }
      
      this.readRateChart.setOption(option)
    },
    
    // 调整图表大小
    resizeCharts() {
      if (this.trendChart) this.trendChart.resize()
      if (this.typeChart) this.typeChart.resize()
      if (this.channelChart) this.channelChart.resize()
      if (this.readRateChart) this.readRateChart.resize()
    },
    
    // 获取数据
    fetchData() {
      // 模拟获取数据
      this.generateMockData()
    },
    
    // 生成模拟数据
    generateMockData() {
      // 生成日报表数据
      this.dailyData = []
      for (let i = 0; i < 30; i++) {
        const date = new Date()
        date.setDate(date.getDate() - i)
        const dateStr = `${date.getMonth() + 1}-${date.getDate()}`
        
        const sentCount = Math.floor(Math.random() * 200) + 100
        const readCount = Math.floor(sentCount * (Math.random() * 0.3 + 0.6))
        const readRate = Math.round((readCount / sentCount) * 100)
        
        const systemCount = Math.floor(sentCount * 0.4)
        const expressCount = Math.floor(sentCount * 0.3)
        const activityCount = Math.floor(sentCount * 0.2)
        const otherCount = sentCount - systemCount - expressCount - activityCount
        
        const inAppCount = Math.floor(sentCount * 0.5)
        const smsCount = Math.floor(sentCount * 0.2)
        const emailCount = Math.floor(sentCount * 0.2)
        const pushCount = sentCount - inAppCount - smsCount - emailCount
        
        this.dailyData.push({
          date: dateStr,
          sentCount,
          readCount,
          readRate,
          systemCount,
          expressCount,
          activityCount,
          otherCount,
          inAppCount,
          smsCount,
          emailCount,
          pushCount
        })
      }
      
      // 生成类型报表数据
      this.typeData = [
        {
          typeId: NotificationType.SYSTEM,
          type: '系统通知',
          sentCount: 4500,
          readCount: 3825,
          readRate: 85,
          userCount: 1200,
          avgResponseTime: 30
        },
        {
          typeId: NotificationType.EXPRESS,
          type: '快递通知',
          sentCount: 3500,
          readCount: 3220,
          readRate: 92,
          userCount: 950,
          avgResponseTime: 15
        },
        {
          typeId: NotificationType.ACTIVITY,
          type: '活动通知',
          sentCount: 3000,
          readCount: 2250,
          readRate: 75,
          userCount: 800,
          avgResponseTime: 45
        },
        {
          typeId: 4,
          type: '其他通知',
          sentCount: 1500,
          readCount: 975,
          readRate: 65,
          userCount: 500,
          avgResponseTime: 60
        }
      ]
      
      // 生成渠道报表数据
      this.channelData = [
        {
          channelId: NotificationChannel.IN_APP,
          channel: '站内信',
          sentCount: 5000,
          successCount: 5000,
          successRate: 100,
          readCount: 4500,
          readRate: 90,
          avgResponseTime: 35
        },
        {
          channelId: NotificationChannel.SMS,
          channel: '短信',
          sentCount: 3000,
          successCount: 2970,
          successRate: 99,
          readCount: 2100,
          readRate: 70,
          avgResponseTime: 20
        },
        {
          channelId: NotificationChannel.EMAIL,
          channel: '邮件',
          sentCount: 2500,
          successCount: 2450,
          successRate: 98,
          readCount: 2000,
          readRate: 80,
          avgResponseTime: 60
        },
        {
          channelId: NotificationChannel.PUSH,
          channel: '推送',
          sentCount: 2000,
          successCount: 1980,
          successRate: 99,
          readCount: 1200,
          readRate: 60,
          avgResponseTime: 25
        }
      ]
      
      this.pagination.total = this.dailyData.length
    },
    
    // 处理日期范围变化
    handleDateRangeChange(val) {
      if (val) {
        // 实际项目中应该根据日期范围重新获取数据
        this.fetchData()
      }
    },
    
    // 刷新数据
    refreshData() {
      this.fetchData()
      this.updateTrendChart()
      this.updateTypeChart()
      this.updateChannelChart()
      this.updateReadRateChart()
    },
    
    // 导出数据
    exportData() {
      this.$message({
        message: '数据导出成功',
        type: 'success'
      })
    },
    
    // 处理分页大小变化
    handleSizeChange(size) {
      this.pagination.pageSize = size
      this.fetchData()
    },
    
    // 处理页码变化
    handleCurrentChange(page) {
      this.pagination.currentPage = page
      this.fetchData()
    },
    
    // 获取通知类型对应的标签类型
    getTypeTagType,
    
    // 获取通知渠道对应的标签类型
    getChannelTagType
  }
}
</script>

<style lang="scss" scoped>
.header-operations {
  float: right;
  display: flex;
  align-items: center;
  
  .el-button {
    margin-left: 10px;
  }
}

.stat-card {
  height: 120px;
  margin-bottom: 20px;
  
  .card-icon {
    float: left;
    font-size: 48px;
    padding: 10px;
  }
  
  .card-content {
    margin-left: 70px;
    
    .card-title {
      font-size: 14px;
      color: #909399;
    }
    
    .card-value {
      font-size: 24px;
      font-weight: bold;
      margin: 10px 0;
    }
    
    .card-footer {
      font-size: 12px;
      color: #909399;
      
      i {
        margin-left: 5px;
      }
    }
  }
}

.chart-container {
  padding: 10px;
}

.pagination-container {
  margin-top: 20px;
  text-align: center;
}
</style>

express-ui\src\views\notification\template-editor.vue

<template>
  <div class="app-container">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>{{ isEdit ? '编辑通知模板' : '创建通知模板' }}</span>
        <el-button style="float: right; padding: 3px 0" type="text" @click="goBack">返回列表</el-button>
      </div>
      
      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="模板名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入模板名称"></el-input>
        </el-form-item>
        
        <el-form-item label="模板代码" prop="code">
          <el-input v-model="form.code" placeholder="请输入模板代码(唯一标识符)"></el-input>
        </el-form-item>
        
        <el-form-item label="通知类型" prop="type">
          <el-select v-model="form.type" placeholder="请选择通知类型">
            <el-option :label="'系统通知'" :value="1"></el-option>
            <el-option :label="'快递通知'" :value="2"></el-option>
            <el-option :label="'活动通知'" :value="3"></el-option>
          </el-select>
        </el-form-item>
        
        <el-form-item label="适用渠道" prop="channels">
          <el-checkbox-group v-model="form.channels">
            <el-checkbox :label="1">站内信</el-checkbox>
            <el-checkbox :label="2">短信</el-checkbox>
            <el-checkbox :label="3">邮件</el-checkbox>
            <el-checkbox :label="4">推送</el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        
        <el-form-item label="模板标题" prop="title">
          <el-input v-model="form.title" placeholder="请输入模板标题">
            <template slot="append">
              <el-button @click="showVariableSelector('title')">插入变量</el-button>
            </template>
          </el-input>
        </el-form-item>
        
        <el-form-item label="模板内容" prop="content">
          <el-tabs v-model="activeTab" type="card">
            <el-tab-pane label="编辑器" name="editor">
              <el-input
                type="textarea"
                v-model="form.content"
                :rows="10"
                placeholder="请输入模板内容,可使用 {{变量名}} 作为占位符">
              </el-input>
              <div class="editor-toolbar">
                <el-button size="small" @click="showVariableSelector('content')">插入变量</el-button>
                <el-button size="small" @click="formatContent">格式化内容</el-button>
              </div>
            </el-tab-pane>
            <el-tab-pane label="预览" name="preview">
              <div class="preview-container">
                <div class="preview-title">{{ previewTitle }}</div>
                <div class="preview-content" v-html="previewContent"></div>
              </div>
            </el-tab-pane>
          </el-tabs>
        </el-form-item>
        
        <el-form-item label="变量列表">
          <el-table :data="variables" style="width: 100%" border>
            <el-table-column prop="name" label="变量名" width="180"></el-table-column>
            <el-table-column prop="description" label="描述">
              <template slot-scope="scope">
                <el-input v-model="scope.row.description" placeholder="请输入变量描述"></el-input>
              </template>
            </el-table-column>
            <el-table-column prop="defaultValue" label="默认值" width="180">
              <template slot-scope="scope">
                <el-input v-model="scope.row.defaultValue" placeholder="请输入默认值"></el-input>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="120" align="center">
              <template slot-scope="scope">
                <el-button type="text" size="small" @click="removeVariable(scope.$index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <div class="table-footer">
            <el-button type="primary" size="small" @click="addVariable">添加变量</el-button>
          </div>
        </el-form-item>
        
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio :label="1">启用</el-radio>
            <el-radio :label="0">禁用</el-radio>
          </el-radio-group>
        </el-form-item>
        
        <el-form-item label="备注" prop="remark">
          <el-input type="textarea" v-model="form.remark" :rows="3" placeholder="请输入备注信息"></el-input>
        </el-form-item>
        
        <el-form-item>
          <el-button type="primary" @click="submitForm">保存</el-button>
          <el-button @click="resetForm">重置</el-button>
          <el-button type="success" @click="testTemplate">测试模板</el-button>
        </el-form-item>
      </el-form>
      
      <!-- 变量选择器对话框 -->
      <el-dialog title="插入变量" :visible.sync="variableSelectorVisible" width="500px" append-to-body>
        <el-form :inline="true" class="variable-form">
          <el-form-item label="变量名">
            <el-select v-model="selectedVariable" placeholder="选择变量" filterable allow-create>
              <el-option
                v-for="item in variables"
                :key="item.name"
                :label="item.name"
                :value="item.name">
                <span>{{ item.name }}</span>
                <span style="float: right; color: #8492a6; font-size: 13px">{{ item.description }}</span>
              </el-option>
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="insertVariable">插入</el-button>
          </el-form-item>
        </el-form>
        <div class="variable-list">
          <p>常用变量:</p>
          <el-tag
            v-for="(item, index) in commonVariables"
            :key="index"
            @click="quickInsertVariable(item)"
            class="variable-tag">
            {{ item }}
          </el-tag>
        </div>
      </el-dialog>
      
      <!-- 测试模板对话框 -->
      <el-dialog title="测试模板" :visible.sync="testDialogVisible" width="600px" append-to-body>
        <el-form label-width="100px">
          <el-form-item
            v-for="(variable, index) in testVariables"
            :key="index"
            :label="variable.name">
            <el-input v-model="variable.value" :placeholder="'请输入' + variable.name + '的值'"></el-input>
          </el-form-item>
        </el-form>
        <div class="test-preview">
          <div class="preview-title">
            <h4>预览效果</h4>
          </div>
          <div class="preview-title">{{ testTitle }}</div>
          <div class="preview-content" v-html="testContent"></div>
        </div>
        <div slot="footer" class="dialog-footer">
          <el-button @click="testDialogVisible = false">关闭</el-button>
          <el-button type="primary" @click="refreshTestPreview">刷新预览</el-button>
        </div>
      </el-dialog>
    </el-card>
  </div>
</template>

<script>
import { extractTemplateVariables, replaceTemplateVariables, formatContent } from '@/utils/notification'

export default {
  name: 'NotificationTemplateEditor',
  data() {
    return {
      isEdit: false,
      templateId: null,
      activeTab: 'editor',
      form: {
        name: '',
        code: '',
        type: 1,
        channels: [1],
        title: '',
        content: '',
        status: 1,
        remark: ''
      },
      rules: {
        name: [
          { required: true, message: '请输入模板名称', trigger: 'blur' },
          { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
        ],
        code: [
          { required: true, message: '请输入模板代码', trigger: 'blur' },
          { pattern: /^[a-zA-Z0-9_]+$/, message: '只能包含字母、数字和下划线', trigger: 'blur' }
        ],
        type: [
          { required: true, message: '请选择通知类型', trigger: 'change' }
        ],
        channels: [
          { type: 'array', required: true, message: '请至少选择一个适用渠道', trigger: 'change' }
        ],
        title: [
          { required: true, message: '请输入模板标题', trigger: 'blur' }
        ],
        content: [
          { required: true, message: '请输入模板内容', trigger: 'blur' }
        ]
      },
      variables: [],
      commonVariables: ['userName', 'userId', 'date', 'time', 'expressCode', 'expressCompany', 'activityName', 'location'],
      variableSelectorVisible: false,
      currentField: '',
      selectedVariable: '',
      testDialogVisible: false,
      testVariables: []
    }
  },
  computed: {
    previewTitle() {
      return this.form.title || '模板标题预览'
    },
    previewContent() {
      return this.form.content ? formatContent(this.form.content) : '模板内容预览'
    },
    testTitle() {
      return replaceTemplateVariables(this.form.title, this.testVariables) || '模板标题预览'
    },
    testContent() {
      const content = replaceTemplateVariables(this.form.content, this.testVariables) || '模板内容预览'
      return formatContent(content)
    }
  },
  created() {
    // 检查是否是编辑模式
    const id = this.$route.params.id
    if (id) {
      this.isEdit = true
      this.templateId = id
      this.getTemplateDetail(id)
    }
  },
  watch: {
    'form.content': function(val) {
      this.updateVariables()
    },
    'form.title': function(val) {
      this.updateVariables()
    }
  },
  methods: {
    // 获取模板详情
    getTemplateDetail(id) {
      // 实际项目中应该从API获取模板详情
      // getNotificationTemplate(id).then(response => {
      //   this.form = response.data
      //   this.updateVariables()
      // })
      
      // 模拟获取数据
      setTimeout(() => {
        this.form = {
          name: '快递到达通知',
          code: 'express_arrival',
          type: 2,
          channels: [1, 2, 4],
          title: '您的快递已到达【{{location}}】',
          content: '尊敬的 {{userName}},\n\n您的快递({{expressCompany}} - {{expressCode}})已到达【{{location}}】,请凭取件码 {{pickupCode}} 及时领取。\n\n取件时间:{{startTime}} - {{endTime}}\n\n如有问题,请联系快递员:{{courierName}}({{courierPhone}})',
          status: 1,
          remark: '用于通知用户快递已到达指定位置,提醒及时取件。'
        }
        this.updateVariables()
      }, 500)
    },
    
    // 更新变量列表
    updateVariables() {
      const titleVariables = extractTemplateVariables(this.form.title)
      const contentVariables = extractTemplateVariables(this.form.content)
      
      // 合并变量
      const allVariables = [...titleVariables, ...contentVariables]
      
      // 去重
      const uniqueVariables = allVariables.filter((v, i, a) => a.findIndex(t => t.name === v.name) === i)
      
      // 保留已有变量的描述和默认值
      const newVariables = uniqueVariables.map(variable => {
        const existingVariable = this.variables.find(v => v.name === variable.name)
        return {
          name: variable.name,
          placeholder: variable.placeholder,
          description: existingVariable ? existingVariable.description : '',
          defaultValue: existingVariable ? existingVariable.defaultValue : ''
        }
      })
      
      this.variables = newVariables
    },
    
    // 添加变量
    addVariable() {
      this.variables.push({
        name: '',
        placeholder: '',
        description: '',
        defaultValue: ''
      })
    },
    
    // 移除变量
    removeVariable(index) {
      this.variables.splice(index, 1)
    },
    
    // 显示变量选择器
    showVariableSelector(field) {
      this.currentField = field
      this.selectedVariable = ''
      this.variableSelectorVisible = true
    },
    
    // 插入变量
    insertVariable() {
      if (!this.selectedVariable) return
      
      const variable = `{{${this.selectedVariable}}}`
      
      if (this.currentField === 'title') {
        this.form.title += variable
      } else if (this.currentField === 'content') {
        // 获取光标位置并插入变量
        const textarea = document.querySelector('textarea')
        if (textarea) {
          const start = textarea.selectionStart
          const end = textarea.selectionEnd
          this.form.content = this.form.content.substring(0, start) + variable + this.form.content.substring(end)
        } else {
          this.form.content += variable
        }
      }
      
      // 添加到变量列表(如果不存在)
      if (!this.variables.find(v => v.name === this.selectedVariable)) {
        this.variables.push({
          name: this.selectedVariable,
          placeholder: `{{${this.selectedVariable}}}`,
          description: '',
          defaultValue: ''
        })
      }
      
      this.variableSelectorVisible = false
    },
    
    // 快速插入变量
    quickInsertVariable(variable) {
      this.selectedVariable = variable
      this.insertVariable()
    },
    
    // 格式化内容
    formatContent() {
      this.form.content = this.form.content.trim()
    },
    
    // 测试模板
    testTemplate() {
      this.testVariables = this.variables.map(variable => ({
        name: variable.name,
        placeholder: variable.placeholder,
        value: variable.defaultValue || ''
      }))
      
      this.testDialogVisible = true
    },
    
    // 刷新测试预览
    refreshTestPreview() {
      // 不需要做任何事情,因为计算属性会自动更新
    },
    
    // 提交表单
    submitForm() {
      this.$refs.form.validate(valid => {
        if (valid) {
          const data = {
            ...this.form,
            variables: this.variables
          }
          
          if (this.isEdit) {
            // 更新模板
            // updateNotificationTemplate(this.templateId, data).then(response => {
            //   this.$message.success('模板更新成功')
            //   this.goBack()
            // })
            
            // 模拟更新成功
            this.$message({
              message: '模板更新成功',
              type: 'success'
            })
            this.goBack()
          } else {
            // 创建模板
            // createNotificationTemplate(data).then(response => {
            //   this.$message.success('模板创建成功')
            //   this.goBack()
            // })
            
            // 模拟创建成功
            this.$message({
              message: '模板创建成功',
              type: 'success'
            })
            this.goBack()
          }
        }
      })
    },
    
    // 重置表单
    resetForm() {
      this.$refs.form.resetFields()
      if (this.isEdit) {
        this.getTemplateDetail(this.templateId)
      } else {
        this.variables = []
      }
    },
    
    // 返回列表
    goBack() {
      this.$router.push('/notification/template')
    }
  }
}
</script>

<style lang="scss" scoped>
.editor-toolbar {
  margin-top: 10px;
  text-align: right;
}

.table-footer {
  margin-top: 10px;
  text-align: right;
}

.preview-container {
  padding: 20px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  min-height: 200px;
  
  .preview-title {
    font-size: 16px;
    font-weight: bold;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 1px solid #ebeef5;
  }
  
  .preview-content {
    white-space: pre-wrap;
    line-height: 1.5;
  }
}

.variable-list {
  margin-top: 20px;
  
  p {
    margin-bottom: 10px;
  }
  
  .variable-tag {
    margin-right: 10px;
    margin-bottom: 10px;
    cursor: pointer;
  }
}

.test-preview {
  margin-top: 20px;
  padding: 15px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  
  .preview-title {
    margin-bottom: 15px;
    
    h4 {
      margin: 0 0 10px 0;
      padding-bottom: 10px;
      border-bottom: 1px dashed #ebeef5;
    }
  }
  
  .preview-content {
    white-space: pre-wrap;
    line-height: 1.5;
  }
}
</style>

express-ui\src\views\notification\template.vue

<template>
  <div class="app-container">
    <el-card class="box-card">
      <div slot="header" class="clearfix">
        <span>通知模板管理</span>
        <el-button style="float: right; padding: 3px 0" type="text" @click="handleAdd">新增模板</el-button>
      </div>
      
      <!-- 搜索区域 -->
      <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
        <el-form-item label="模板名称" prop="name">
          <el-input v-model="queryParams.name" placeholder="请输入模板名称" clearable size="small" @keyup.enter.native="handleQuery" />
        </el-form-item>
        <el-form-item label="模板类型" prop="type">
          <el-select v-model="queryParams.type" placeholder="模板类型" clearable size="small">
            <el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
        </el-form-item>
      </el-form>

      <!-- 表格工具栏 -->
      <el-row :gutter="10" class="mb8">
        <el-col :span="1.5">
          <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
        </el-col>
        <el-col :span="1.5">
          <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
        </el-col>
        <el-col :span="1.5">
          <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
        </el-col>
        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>

      <!-- 数据表格 -->
      <el-table v-loading="loading" :data="templateList" @selection-change="handleSelectionChange">
        <el-table-column type="selection" width="55" align="center" />
        <el-table-column label="ID" align="center" prop="id" width="80" />
        <el-table-column label="模板名称" align="center" prop="name" :show-overflow-tooltip="true" />
        <el-table-column label="模板类型" align="center" prop="type">
          <template slot-scope="scope">
            <el-tag :type="scope.row.type === 1 ? 'primary' : scope.row.type === 2 ? 'success' : 'info'">
              {{ typeFormat(scope.row) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="适用渠道" align="center" prop="channel">
          <template slot-scope="scope">
            <el-tag v-if="scope.row.channel.includes(1)" type="primary" class="channel-tag">站内信</el-tag>
            <el-tag v-if="scope.row.channel.includes(2)" type="success" class="channel-tag">短信</el-tag>
            <el-tag v-if="scope.row.channel.includes(3)" type="warning" class="channel-tag">邮件</el-tag>
            <el-tag v-if="scope.row.channel.includes(4)" type="danger" class="channel-tag">推送</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="创建时间" align="center" prop="createTime" width="160">
          <template slot-scope="scope">
            <span>{{ scope.row.createTime }}</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
          <template slot-scope="scope">
            <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
            <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      
      <!-- 分页 -->
      <pagination
        v-show="total > 0"
        :total="total"
        :page.sync="queryParams.pageNum"
        :limit.sync="queryParams.pageSize"
        @pagination="getList"
      />

      <!-- 添加或修改通知模板对话框 -->
      <el-dialog :title="title" :visible.sync="open" width="780px" append-to-body>
        <el-form ref="form" :model="form" :rules="rules" label-width="100px">
          <el-row>
            <el-col :span="12">
              <el-form-item label="模板名称" prop="name">
                <el-input v-model="form.name" placeholder="请输入模板名称" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="模板类型" prop="type">
                <el-select v-model="form.type" placeholder="请选择模板类型">
                  <el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="适用渠道" prop="channel">
                <el-checkbox-group v-model="form.channel">
                  <el-checkbox :label="1">站内信</el-checkbox>
                  <el-checkbox :label="2">短信</el-checkbox>
                  <el-checkbox :label="3">邮件</el-checkbox>
                  <el-checkbox :label="4">推送</el-checkbox>
                </el-checkbox-group>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="模板内容" prop="content">
                <el-input v-model="form.content" type="textarea" placeholder="请输入模板内容" :rows="8">
                  <template slot="prepend">
                    <div class="template-variables">
                      <p>可用变量:</p>
                      <p>{{userName}} - 用户名</p>
                      <p>{{expressCode}} - 快递单号</p>
                      <p>{{expressCompany}} - 快递公司</p>
                      <p>{{pickupCode}} - 取件码</p>
                      <p>{{deliveryTime}} - 送达时间</p>
                      <p>{{deliveryLocation}} - 送达地点</p>
                    </div>
                  </template>
                </el-input>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="备注" prop="remark">
                <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" :rows="3" />
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
        <div slot="footer" class="dialog-footer">
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </el-dialog>

      <!-- 通知模板详情对话框 -->
      <el-dialog title="模板详情" :visible.sync="openView" width="700px" append-to-body>
        <el-descriptions :column="2" border>
          <el-descriptions-item label="模板名称">{{ form.name }}</el-descriptions-item>
          <el-descriptions-item label="模板类型">{{ typeFormat(form) }}</el-descriptions-item>
          <el-descriptions-item label="适用渠道" :span="2">
            <el-tag v-if="form.channel && form.channel.includes(1)" type="primary" class="channel-tag">站内信</el-tag>
            <el-tag v-if="form.channel && form.channel.includes(2)" type="success" class="channel-tag">短信</el-tag>
            <el-tag v-if="form.channel && form.channel.includes(3)" type="warning" class="channel-tag">邮件</el-tag>
            <el-tag v-if="form.channel && form.channel.includes(4)" type="danger" class="channel-tag">推送</el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="创建时间" :span="2">{{ form.createTime }}</el-descriptions-item>
          <el-descriptions-item label="模板内容" :span="2">
            <div style="white-space: pre-wrap;">{{ form.content }}</div>
          </el-descriptions-item>
          <el-descriptions-item label="备注" :span="2">
            <div style="white-space: pre-wrap;">{{ form.remark }}</div>
          </el-descriptions-item>
        </el-descriptions>
        <div slot="footer" class="dialog-footer">
          <el-button @click="openView = false">关 闭</el-button>
        </div>
      </el-dialog>
    </el-card>
  </div>
</template>

<script>
import { listTemplate, getTemplate, delTemplate, addTemplate, updateTemplate } from '@/api/notification'

export default {
  name: 'NotificationTemplate',
  data() {
    return {
      // 遮罩层
      loading: true,
      // 选中数组
      ids: [],
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 模板表格数据
      templateList: [],
      // 弹出层标题
      title: '',
      // 是否显示弹出层
      open: false,
      // 是否显示详情弹出层
      openView: false,
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        name: undefined,
        type: undefined
      },
      // 表单参数
      form: {},
      // 表单校验
      rules: {
        name: [
          { required: true, message: '模板名称不能为空', trigger: 'blur' }
        ],
        content: [
          { required: true, message: '模板内容不能为空', trigger: 'blur' }
        ],
        type: [
          { required: true, message: '模板类型不能为空', trigger: 'change' }
        ],
        channel: [
          { required: true, message: '适用渠道不能为空', trigger: 'change', type: 'array' }
        ]
      },
      // 模板类型选项
      typeOptions: [
        { value: '1', label: '系统通知' },
        { value: '2', label: '快递通知' },
        { value: '3', label: '活动通知' }
      ]
    }
  },
  created() {
    this.getList()
  },
  methods: {
    /** 查询模板列表 */
    getList() {
      this.loading = true
      // 模拟数据,实际项目中应该调用API
      this.templateList = [
        {
          id: 1,
          name: '系统维护通知模板',
          type: 1,
          channel: [1, 3],
          content: '尊敬的{{userName}},系统将于{{startTime}}至{{endTime}}进行系统维护,届时系统将暂停服务,请提前做好准备。',
          remark: '用于系统维护时通知用户',
          createTime: '2023-04-20 10:00:00'
        },
        {
          id: 2,
          name: '快递到达通知模板',
          type: 2,
          channel: [1, 2, 4],
          content: '您好,{{userName}},您的快递({{expressCompany}} {{expressCode}})已到达校园快递站,取件码:{{pickupCode}},请及时前往领取。',
          remark: '用于快递到达时通知用户',
          createTime: '2023-04-21 14:30:00'
        },
        {
          id: 3,
          name: '活动邀请模板',
          type: 3,
          channel: [1, 3],
          content: '亲爱的{{userName}},诚邀您参加{{activityName}}活动,时间:{{activityTime}},地点:{{activityLocation}},期待您的参与!',
          remark: '用于活动邀请',
          createTime: '2023-04-22 09:15:00'
        }
      ]
      this.total = this.templateList.length
      this.loading = false
      
      // 实际项目中的API调用
      // listTemplate(this.queryParams).then(response => {
      //   this.templateList = response.data.rows
      //   this.total = response.data.total
      //   this.loading = false
      // })
    },
    // 模板类型字典翻译
    typeFormat(row) {
      return this.selectDictLabel(this.typeOptions, row.type)
    },
    // 字典翻译
    selectDictLabel(datas, value) {
      const actions = []
      Object.keys(datas).some(key => {
        if (datas[key].value == value) {
          actions.push(datas[key].label)
          return true
        }
      })
      return actions.join('')
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNum = 1
      this.getList()
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm('queryForm')
      this.handleQuery()
    },
    /** 新增按钮操作 */
    handleAdd() {
      this.reset()
      this.open = true
      this.title = '添加模板'
    },
    /** 修改按钮操作 */
    handleUpdate(row) {
      this.reset()
      const id = row.id || this.ids[0]
      // 实际项目中应该调用API获取详情
      // getTemplate(id).then(response => {
      //   this.form = response.data
      //   this.open = true
      //   this.title = '修改模板'
      // })
      
      // 模拟数据
      this.form = JSON.parse(JSON.stringify(row))
      this.open = true
      this.title = '修改模板'
    },
    /** 查看详情按钮操作 */
    handleView(row) {
      this.reset()
      const id = row.id
      // 实际项目中应该调用API获取详情
      // getTemplate(id).then(response => {
      //   this.form = response.data
      //   this.openView = true
      // })
      
      // 模拟数据
      this.form = JSON.parse(JSON.stringify(row))
      this.openView = true
    },
    /** 提交按钮 */
    submitForm() {
      this.$refs['form'].validate(valid => {
        if (valid) {
          if (this.form.id) {
            // updateTemplate(this.form).then(response => {
            //   this.$modal.msgSuccess('修改成功')
            //   this.open = false
            //   this.getList()
            // })
            this.$message.success('修改成功')
            this.open = false
            this.getList()
          } else {
            // addTemplate(this.form).then(response => {
            //   this.$modal.msgSuccess('新增成功')
            //   this.open = false
            //   this.getList()
            // })
            this.$message.success('新增成功')
            this.open = false
            this.getList()
          }
        }
      })
    },
    /** 删除按钮操作 */
    handleDelete(row) {
      const ids = row.id || this.ids
      this.$confirm('是否确认删除模板编号为"' + ids + '"的数据项?', '警告', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        // delTemplate(ids).then(() => {
        //   this.getList()
        //   this.$modal.msgSuccess('删除成功')
        // })
        this.$message.success('删除成功')
        this.getList()
      }).catch(() => {})
    },
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id)
      this.single = selection.length !== 1
      this.multiple = !selection.length
    },
    /** 重置表单数据 */
    reset() {
      this.form = {
        id: undefined,
        name: undefined,
        content: undefined,
        type: 1,
        channel: [1],
        remark: undefined
      }
      this.resetForm('form')
    },
    /** 取消按钮 */
    cancel() {
      this.open = false
      this.reset()
    }
  }
}
</script>

<style lang="scss" scoped>
.channel-tag {
  margin-right: 5px;
}
.template-variables {
  padding: 5px 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
  margin-bottom: 10px;
  
  p {
    margin: 5px 0;
    font-size: 12px;
    color: #606266;
  }
}
</style>

user-service\pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <groupId>com.campus.express</groupId>
    <artifactId>user-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>user-service</name>
    <description>User Service for Campus Express Management System</description>
    
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.3</spring-cloud.version>
        <jjwt.version>0.11.5</jjwt.version>
        <mapstruct.version>1.5.3.Final</mapstruct.version>
        <lombok.version>1.18.24</lombok.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- Spring Cloud -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        
        <!-- Database -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Lombok & MapStruct -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
        
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

user-service\src\main\java\com\campus\express\user\UserServiceApplication.java

package com.campus.express.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * User Service Application
 * 
 * 用户服务应用程序入口类
 * 
 * @author Campus Express Team
 */
@SpringBootApplication
@EnableEurekaClient
public class UserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

user-service\src\main\java\com\campus\express\user\config\CorsConfig.java

package com.campus.express.user.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * CORS配置
 * 允许跨域请求
 */
@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        
        // 允许跨域的头部信息
        config.addAllowedHeader("*");
        // 允许跨域的方法
        config.addAllowedMethod("*");
        // 允许跨域的来源
        config.addAllowedOrigin("*");
        // 允许携带cookie信息
        config.setAllowCredentials(true);
        // 预检请求的缓存时间
        config.setMaxAge(3600L);
        
        // 添加映射路径,拦截所有请求
        source.registerCorsConfiguration("/**", config);
        
        return new CorsFilter(source);
    }
}

user-service\src\main\java\com\campus\express\user\config\DataInitializer.java

package com.campus.express.user.config;

import com.campus.express.user.model.Role;
import com.campus.express.user.model.User;
import com.campus.express.user.repository.RoleRepository;
import com.campus.express.user.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;

/**
 * 数据初始化器
 * 用于初始化基础数据,如角色和管理员账号
 */
@Slf4j
@Component
public class DataInitializer implements CommandLineRunner {

    @Autowired
    private RoleRepository roleRepository;
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void run(String... args) throws Exception {
        log.info("开始初始化基础数据...");
        
        // 初始化角色
        initRoles();
        
        // 初始化管理员账号
        initAdminUser();
        
        log.info("基础数据初始化完成");
    }
    
    /**
     * 初始化角色
     */
    private void initRoles() {
        // 检查角色是否已存在
        if (roleRepository.count() > 0) {
            log.info("角色数据已存在,跳过初始化");
            return;
        }
        
        // 创建角色
        Role adminRole = new Role();
        adminRole.setName(Role.RoleName.ROLE_ADMIN);
        
        Role staffRole = new Role();
        staffRole.setName(Role.RoleName.ROLE_STAFF);
        
        Role studentRole = new Role();
        studentRole.setName(Role.RoleName.ROLE_STUDENT);
        
        Role courierRole = new Role();
        courierRole.setName(Role.RoleName.ROLE_COURIER);
        
        // 保存角色
        roleRepository.save(adminRole);
        roleRepository.save(staffRole);
        roleRepository.save(studentRole);
        roleRepository.save(courierRole);
        
        log.info("角色初始化完成");
    }
    
    /**
     * 初始化管理员账号
     */
    private void initAdminUser() {
        // 检查管理员账号是否已存在
        if (userRepository.existsByUsername("admin")) {
            log.info("管理员账号已存在,跳过初始化");
            return;
        }
        
        // 获取管理员角色
        Role adminRole = roleRepository.findByName(Role.RoleName.ROLE_ADMIN)
                .orElseThrow(() -> new RuntimeException("管理员角色不存在"));
        
        // 创建管理员账号
        User adminUser = User.builder()
                .username("admin")
                .password(passwordEncoder.encode("admin123"))
                .realName("系统管理员")
                .phone("13800000000")
                .email("admin@campus.com")
                .userType(0) // 管理员类型
                .status(1) // 启用状态
                .build();
        
        // 设置角色
        Set<Role> roles = new HashSet<>();
        roles.add(adminRole);
        adminUser.setRoles(roles);
        
        // 保存管理员账号
        userRepository.save(adminUser);
        
        log.info("管理员账号初始化完成");
    }
}

user-service\src\main\java\com\campus\express\user\config\JwtAuthenticationEntryPoint.java

package com.campus.express.user.config;

import com.campus.express.user.dto.ApiResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

/**
 * JWT认证入口点
 * 
 * 用于处理未认证的请求,返回401错误
 */
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        log.error("Unauthorized error: {}", authException.getMessage());
        
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        ApiResponse<?> apiResponse = ApiResponse.unauthorized("未授权:" + authException.getMessage());
        
        OutputStream outputStream = response.getOutputStream();
        new ObjectMapper().writeValue(outputStream, apiResponse);
        outputStream.flush();
    }
}

user-service\src\main\java\com\campus\express\user\config\JwtAuthenticationFilter.java

package com.campus.express.user.config;

import com.campus.express.user.util.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * JWT认证过滤器
 * 
 * 用于从请求中提取JWT令牌并验证用户身份
 */
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsService userDetailsService;

    @Value("${jwt.header}")
    private String headerName;

    @Value("${jwt.prefix}")
    private String tokenPrefix;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUsernameFromJwtToken(jwt);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            log.error("Cannot set user authentication: {}", e.getMessage());
        }

        filterChain.doFilter(request, response);
    }

    /**
     * 从请求中提取JWT令牌
     * 
     * @param request HTTP请求
     * @return JWT令牌,如果不存在则返回null
     */
    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader(headerName);

        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith(tokenPrefix + " ")) {
            return headerAuth.substring(tokenPrefix.length() + 1);
        }

        return null;
    }
}

user-service\src\main\java\com\campus\express\user\config\SecurityConfig.java

package com.campus.express.user.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * 安全配置类
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .authorizeRequests()
            .antMatchers("/auth/**").permitAll()
            .antMatchers("/public/**").permitAll()
            .antMatchers("/actuator/**").permitAll()
            .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

user-service\src\main\java\com\campus\express\user\controller\AuthController.java

package com.campus.express.user.controller;

import com.campus.express.user.dto.ApiResponse;
import com.campus.express.user.dto.JwtResponse;
import com.campus.express.user.dto.LoginRequest;
import com.campus.express.user.dto.SignupRequest;
import com.campus.express.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * 认证控制器
 * 处理用户注册和登录请求
 */
@Slf4j
@RestController
@RequestMapping("/auth")
@CrossOrigin(origins = "*", maxAge = 3600)
public class AuthController {

    @Autowired
    private UserService userService;

    /**
     * 用户注册
     *
     * @param signupRequest 注册请求
     * @return 注册结果
     */
    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signupRequest) {
        log.info("收到用户注册请求: {}", signupRequest.getUsername());
        return ResponseEntity.ok(ApiResponse.success("用户注册成功", userService.registerUser(signupRequest)));
    }

    /**
     * 用户登录
     *
     * @param loginRequest 登录请求
     * @return JWT响应
     */
    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
        log.info("收到用户登录请求: {}", loginRequest.getUsername());
        JwtResponse jwtResponse = userService.authenticateUser(loginRequest);
        return ResponseEntity.ok(ApiResponse.success("登录成功", jwtResponse));
    }
}

user-service\src\main\java\com\campus\express\user\controller\RoleController.java

package com.campus.express.user.controller;

import com.campus.express.user.dto.ApiResponse;
import com.campus.express.user.model.Role;
import com.campus.express.user.repository.RoleRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * 角色控制器
 * 处理角色相关请求
 */
@Slf4j
@RestController
@RequestMapping("/roles")
@CrossOrigin(origins = "*", maxAge = 3600)
public class RoleController {

    @Autowired
    private RoleRepository roleRepository;

    /**
     * 获取所有角色
     *
     * @return 角色列表
     */
    @GetMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> getAllRoles() {
        List<Role> roles = roleRepository.findAll();
        return ResponseEntity.ok(ApiResponse.success(roles));
    }
}

user-service\src\main\java\com\campus\express\user\controller\UserController.java

package com.campus.express.user.controller;

import com.campus.express.user.dto.ApiResponse;
import com.campus.express.user.dto.UserDTO;
import com.campus.express.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;

/**
 * 用户控制器
 * 处理用户管理相关请求
 */
@Slf4j
@RestController
@RequestMapping("/users")
@CrossOrigin(origins = "*", maxAge = 3600)
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 获取当前用户信息
     *
     * @return 当前用户信息
     */
    @GetMapping("/me")
    public ResponseEntity<?> getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        UserDTO userDTO = userService.getUserByUsername(userDetails.getUsername());
        return ResponseEntity.ok(ApiResponse.success(userDTO));
    }

    /**
     * 获取用户信息
     *
     * @param id 用户ID
     * @return 用户信息
     */
    @GetMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @securityService.isCurrentUser(#id)")
    public ResponseEntity<?> getUserById(@PathVariable Long id) {
        UserDTO userDTO = userService.getUserById(id);
        return ResponseEntity.ok(ApiResponse.success(userDTO));
    }

    /**
     * 分页获取用户列表
     *
     * @param page     页码
     * @param size     每页大小
     * @param sort     排序字段
     * @param userType 用户类型
     * @param keyword  关键字
     * @return 用户列表
     */
    @GetMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> getUserList(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sort,
            @RequestParam(required = false) Integer userType,
            @RequestParam(required = false) String keyword) {
        
        Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, sort));
        Page<UserDTO> userPage = userService.getUserList(userType, keyword, pageable);
        
        Map<String, Object> response = new HashMap<>();
        response.put("content", userPage.getContent());
        response.put("currentPage", userPage.getNumber());
        response.put("totalItems", userPage.getTotalElements());
        response.put("totalPages", userPage.getTotalPages());
        
        return ResponseEntity.ok(ApiResponse.success(response));
    }

    /**
     * 更新用户信息
     *
     * @param id      用户ID
     * @param userDTO 用户信息
     * @return 更新后的用户信息
     */
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @securityService.isCurrentUser(#id)")
    public ResponseEntity<?> updateUser(@PathVariable Long id, @Valid @RequestBody UserDTO userDTO) {
        UserDTO updatedUser = userService.updateUser(id, userDTO);
        return ResponseEntity.ok(ApiResponse.success("用户信息更新成功", updatedUser));
    }

    /**
     * 更新用户状态
     *
     * @param id     用户ID
     * @param status 状态
     * @return 更新后的用户信息
     */
    @PutMapping("/{id}/status")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> updateUserStatus(@PathVariable Long id, @RequestParam Integer status) {
        UserDTO updatedUser = userService.updateUserStatus(id, status);
        return ResponseEntity.ok(ApiResponse.success("用户状态更新成功", updatedUser));
    }

    /**
     * 删除用户
     *
     * @param id 用户ID
     * @return 删除结果
     */
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.ok(ApiResponse.success("用户删除成功"));
    }

    /**
     * 修改密码
     *
     * @param id          用户ID
     * @param oldPassword 旧密码
     * @param newPassword 新密码
     * @return 修改结果
     */
    @PutMapping("/{id}/password")
    @PreAuthorize("@securityService.isCurrentUser(#id)")
    public ResponseEntity<?> changePassword(
            @PathVariable Long id,
            @RequestParam String oldPassword,
            @RequestParam String newPassword) {
        
        boolean result = userService.changePassword(id, oldPassword, newPassword);
        return ResponseEntity.ok(ApiResponse.success("密码修改成功"));
    }
}

user-service\src\main\java\com\campus\express\user\dto\ApiResponse.java

package com.campus.express.user.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * API响应包装类
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
    
    private Integer code;
    private String message;
    private T data;
    private LocalDateTime timestamp;
    
    /**
     * 创建成功响应
     * 
     * @param data 响应数据
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> success(T data) {
        return ApiResponse.<T>builder()
                .code(200)
                .message("操作成功")
                .data(data)
                .timestamp(LocalDateTime.now())
                .build();
    }
    
    /**
     * 创建成功响应(无数据)
     * 
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> success() {
        return ApiResponse.<T>builder()
                .code(200)
                .message("操作成功")
                .timestamp(LocalDateTime.now())
                .build();
    }
    
    /**
     * 创建成功响应(自定义消息)
     * 
     * @param message 响应消息
     * @param data 响应数据
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> success(String message, T data) {
        return ApiResponse.<T>builder()
                .code(200)
                .message(message)
                .data(data)
                .timestamp(LocalDateTime.now())
                .build();
    }
    
    /**
     * 创建错误响应
     * 
     * @param code 错误码
     * @param message 错误消息
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> error(Integer code, String message) {
        return ApiResponse.<T>builder()
                .code(code)
                .message(message)
                .timestamp(LocalDateTime.now())
                .build();
    }
    
    /**
     * 创建错误响应(400 Bad Request)
     * 
     * @param message 错误消息
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> badRequest(String message) {
        return error(400, message);
    }
    
    /**
     * 创建错误响应(401 Unauthorized)
     * 
     * @param message 错误消息
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> unauthorized(String message) {
        return error(401, message);
    }
    
    /**
     * 创建错误响应(403 Forbidden)
     * 
     * @param message 错误消息
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> forbidden(String message) {
        return error(403, message);
    }
    
    /**
     * 创建错误响应(404 Not Found)
     * 
     * @param message 错误消息
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> notFound(String message) {
        return error(404, message);
    }
    
    /**
     * 创建错误响应(500 Internal Server Error)
     * 
     * @param message 错误消息
     * @param <T> 数据类型
     * @return API响应对象
     */
    public static <T> ApiResponse<T> serverError(String message) {
        return error(500, message);
    }
}

user-service\src\main\java\com\campus\express\user\dto\JwtResponse.java

package com.campus.express.user.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * JWT认证响应DTO
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JwtResponse {
    
    private String token;
    private String type = "Bearer";
    private Long id;
    private String username;
    private String realName;
    private String phone;
    private Integer userType;
    private List<String> roles;
    
    public JwtResponse(String token, Long id, String username, String realName, String phone, Integer userType, List<String> roles) {
        this.token = token;
        this.id = id;
        this.username = username;
        this.realName = realName;
        this.phone = phone;
        this.userType = userType;
        this.roles = roles;
    }
}

user-service\src\main\java\com\campus\express\user\dto\LoginRequest.java

package com.campus.express.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;

/**
 * 用户登录请求DTO
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {
    
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    private String password;
}

user-service\src\main\java\com\campus\express\user\dto\PasswordChangeRequest.java

package com.campus.express.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

/**
 * 密码修改请求DTO
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PasswordChangeRequest {
    
    @NotBlank(message = "旧密码不能为空")
    private String oldPassword;
    
    @NotBlank(message = "新密码不能为空")
    @Size(min = 6, max = 40, message = "新密码长度必须在6-40个字符之间")
    private String newPassword;
    
    @NotBlank(message = "确认密码不能为空")
    private String confirmPassword;
}

user-service\src\main\java\com\campus\express\user\dto\SignupRequest.java

package com.campus.express.user.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.util.Set;

/**
 * 用户注册请求DTO
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SignupRequest {
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 40, message = "密码长度必须在6-40个字符之间")
    private String password;
    
    @NotBlank(message = "真实姓名不能为空")
    @Size(max = 50, message = "真实姓名长度不能超过50个字符")
    private String realName;
    
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    private Integer userType; // 1-学生,2-教职工,3-快递员,4-管理员
    
    private String studentId;
    
    private String department;
    
    private String dormitory;
    
    private Set<String> roles;
}

user-service\src\main\java\com\campus\express\user\dto\UserDTO.java

package com.campus.express.user.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.Set;

/**
 * 用户数据传输对象
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    
    private Long id;
    private String username;
    private String realName;
    private String phone;
    private String email;
    private Integer userType;
    private String studentId;
    private String department;
    private String dormitory;
    private String avatar;
    private Integer status;
    private Set<String> roles;
    private LocalDateTime createdTime;
    private LocalDateTime updatedTime;
}

user-service\src\main\java\com\campus\express\user\exception\BadRequestException.java

package com.campus.express.user.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * 请求参数错误异常
 */
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
    
    public BadRequestException(String message) {
        super(message);
    }
    
    public BadRequestException(String message, Throwable cause) {
        super(message, cause);
    }
}

user-service\src\main\java\com\campus\express\user\exception\GlobalExceptionHandler.java

package com.campus.express.user.exception;

import com.campus.express.user.dto.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;

/**
 * 全局异常处理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理资源未找到异常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<?> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        log.error("Resource not found: {}", ex.getMessage());
        return ApiResponse.notFound(ex.getMessage());
    }

    /**
     * 处理请求参数错误异常
     */
    @ExceptionHandler(BadRequestException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<?> handleBadRequestException(BadRequestException ex, WebRequest request) {
        log.error("Bad request: {}", ex.getMessage());
        return ApiResponse.badRequest(ex.getMessage());
    }

    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<?> handleValidationExceptions(MethodArgumentNotValidException ex, WebRequest request) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        log.error("Validation error: {}", errors);
        return ApiResponse.badRequest("参数校验失败").setData(errors);
    }

    /**
     * 处理约束违反异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<?> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
        log.error("Constraint violation: {}", ex.getMessage());
        return ApiResponse.badRequest(ex.getMessage());
    }

    /**
     * 处理认证异常
     */
    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ApiResponse<?> handleAuthenticationException(AuthenticationException ex, WebRequest request) {
        log.error("Authentication error: {}", ex.getMessage());
        return ApiResponse.unauthorized("认证失败:" + ex.getMessage());
    }

    /**
     * 处理凭证错误异常
     */
    @ExceptionHandler(BadCredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ApiResponse<?> handleBadCredentialsException(BadCredentialsException ex, WebRequest request) {
        log.error("Bad credentials: {}", ex.getMessage());
        return ApiResponse.unauthorized("用户名或密码错误");
    }

    /**
     * 处理访问拒绝异常
     */
    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ApiResponse<?> handleAccessDeniedException(AccessDeniedException ex, WebRequest request) {
        log.error("Access denied: {}", ex.getMessage());
        return ApiResponse.forbidden("没有权限访问此资源");
    }

    /**
     * 处理所有其他异常
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<?> handleAllUncaughtException(Exception ex, WebRequest request) {
        log.error("Internal server error: ", ex);
        return ApiResponse.serverError("服务器内部错误:" + ex.getMessage());
    }
}

user-service\src\main\java\com\campus\express\user\exception\ResourceNotFoundException.java

package com.campus.express.user.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * 资源未找到异常
 */
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    
    private String resourceName;
    private String fieldName;
    private Object fieldValue;
    
    public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }
    
    public String getResourceName() {
        return resourceName;
    }
    
    public String getFieldName() {
        return fieldName;
    }
    
    public Object getFieldValue() {
        return fieldValue;
    }
}

user-service\src\main\java\com\campus\express\user\model\Role.java

package com.campus.express.user.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

/**
 * 角色实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "roles")
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    @Column(length = 20, unique = true, nullable = false)
    private RoleName name;

    @Column(length = 100)
    private String description;

    public enum RoleName {
        ROLE_STUDENT,    // 学生角色
        ROLE_STAFF,      // 教职工角色
        ROLE_COURIER,    // 快递员角色
        ROLE_ADMIN       // 管理员角色
    }
}

user-service\src\main\java\com\campus\express\user\model\User.java

package com.campus.express.user.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;

/**
 * 用户实体类
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 3, max = 50)
    @Column(unique = true, nullable = false)
    private String username;

    @NotBlank
    @Size(min = 6, max = 100)
    @Column(nullable = false)
    private String password;

    @Column(name = "real_name")
    private String realName;

    @NotBlank
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    @Column(unique = true, nullable = false)
    private String phone;

    @Email
    @Column
    private String email;

    @Column(name = "user_type", nullable = false)
    private Integer userType; // 1-学生,2-教职工,3-快递员,4-管理员

    @Column(name = "student_id")
    private String studentId;

    @Column
    private String department;

    @Column
    private String dormitory;

    @Column
    private String avatar;

    @Column(nullable = false)
    private Integer status; // 0-禁用,1-启用

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_roles",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();

    @CreationTimestamp
    @Column(name = "created_time", nullable = false, updatable = false)
    private LocalDateTime createdTime;

    @UpdateTimestamp
    @Column(name = "updated_time", nullable = false)
    private LocalDateTime updatedTime;
}

user-service\src\main\java\com\campus\express\user\repository\RoleRepository.java

package com.campus.express.user.repository;

import com.campus.express.user.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

/**
 * 角色数据访问接口
 */
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
    
    /**
     * 根据角色名查找角色
     * 
     * @param name 角色名
     * @return 角色对象
     */
    Optional<Role> findByName(Role.RoleName name);
}

user-service\src\main\java\com\campus\express\user\repository\UserRepository.java

package com.campus.express.user.repository;

import com.campus.express.user.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import java.util.Optional;

/**
 * 用户数据访问接口
 */
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
    
    /**
     * 根据用户名查找用户
     * 
     * @param username 用户名
     * @return 用户对象
     */
    Optional<User> findByUsername(String username);
    
    /**
     * 根据手机号查找用户
     * 
     * @param phone 手机号
     * @return 用户对象
     */
    Optional<User> findByPhone(String phone);
    
    /**
     * 根据邮箱查找用户
     * 
     * @param email 邮箱
     * @return 用户对象
     */
    Optional<User> findByEmail(String email);
    
    /**
     * 检查用户名是否存在
     * 
     * @param username 用户名
     * @return 是否存在
     */
    boolean existsByUsername(String username);
    
    /**
     * 检查手机号是否存在
     * 
     * @param phone 手机号
     * @return 是否存在
     */
    boolean existsByPhone(String phone);
    
    /**
     * 检查邮箱是否存在
     * 
     * @param email 邮箱
     * @return 是否存在
     */
    boolean existsByEmail(String email);
}

user-service\src\main\java\com\campus\express\user\service\SecurityService.java

package com.campus.express.user.service;

import com.campus.express.user.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

/**
 * 安全服务
 * 提供安全相关的辅助方法
 */
@Slf4j
@Service
public class SecurityService {

    @Autowired
    private UserRepository userRepository;

    /**
     * 判断当前用户是否为指定ID的用户
     *
     * @param userId 用户ID
     * @return 是否为当前用户
     */
    public boolean isCurrentUser(Long userId) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !authentication.isAuthenticated()) {
            return false;
        }

        Object principal = authentication.getPrincipal();
        if (!(principal instanceof UserDetails)) {
            return false;
        }

        String username = ((UserDetails) principal).getUsername();
        return userRepository.findByUsername(username)
                .map(user -> user.getId().equals(userId))
                .orElse(false);
    }
}

user-service\src\main\java\com\campus\express\user\service\UserDetailsServiceImpl.java

package com.campus.express.user.service;

import com.campus.express.user.model.User;
import com.campus.express.user.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 用户详情服务实现类
 * 
 * 实现Spring Security的UserDetailsService接口,用于加载用户信息
 */
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("未找到用户名为 " + username + " 的用户"));

        if (user.getStatus() == 0) {
            throw new UsernameNotFoundException("用户 " + username + " 已被禁用");
        }

        List<SimpleGrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName().name()))
                .collect(Collectors.toList());

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                authorities
        );
    }
}

user-service\src\main\java\com\campus\express\user\service\UserService.java

package com.campus.express.user.service;

import com.campus.express.user.dto.JwtResponse;
import com.campus.express.user.dto.LoginRequest;
import com.campus.express.user.dto.SignupRequest;
import com.campus.express.user.dto.UserDTO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

/**
 * 用户服务接口
 */
public interface UserService {
    
    /**
     * 用户注册
     * 
     * @param signupRequest 注册请求
     * @return 用户DTO
     */
    UserDTO registerUser(SignupRequest signupRequest);
    
    /**
     * 用户登录
     * 
     * @param loginRequest 登录请求
     * @return JWT响应
     */
    JwtResponse authenticateUser(LoginRequest loginRequest);
    
    /**
     * 获取用户信息
     * 
     * @param id 用户ID
     * @return 用户DTO
     */
    UserDTO getUserById(Long id);
    
    /**
     * 获取用户信息
     * 
     * @param username 用户名
     * @return 用户DTO
     */
    UserDTO getUserByUsername(String username);
    
    /**
     * 分页获取用户列表
     * 
     * @param userType 用户类型
     * @param keyword 关键字
     * @param pageable 分页参数
     * @return 用户DTO分页
     */
    Page<UserDTO> getUserList(Integer userType, String keyword, Pageable pageable);
    
    /**
     * 更新用户信息
     * 
     * @param id 用户ID
     * @param userDTO 用户DTO
     * @return 更新后的用户DTO
     */
    UserDTO updateUser(Long id, UserDTO userDTO);
    
    /**
     * 更新用户状态
     * 
     * @param id 用户ID
     * @param status 状态
     * @return 更新后的用户DTO
     */
    UserDTO updateUserStatus(Long id, Integer status);
    
    /**
     * 删除用户
     * 
     * @param id 用户ID
     */
    void deleteUser(Long id);
    
    /**
     * 修改密码
     * 
     * @param id 用户ID
     * @param oldPassword 旧密码
     * @param newPassword 新密码
     * @return 是否成功
     */
    boolean changePassword(Long id, String oldPassword, String newPassword);
}

user-service\src\main\java\com\campus\express\user\service\UserServiceImpl.java

package com.campus.express.user.service;

import com.campus.express.user.dto.JwtResponse;
import com.campus.express.user.dto.LoginRequest;
import com.campus.express.user.dto.SignupRequest;
import com.campus.express.user.dto.UserDTO;
import com.campus.express.user.exception.BadRequestException;
import com.campus.express.user.exception.ResourceNotFoundException;
import com.campus.express.user.model.Role;
import com.campus.express.user.model.User;
import com.campus.express.user.repository.RoleRepository;
import com.campus.express.user.repository.UserRepository;
import com.campus.express.user.util.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.Predicate;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 用户服务实现类
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    @Transactional
    public UserDTO registerUser(SignupRequest signupRequest) {
        // 验证用户名是否已存在
        if (userRepository.existsByUsername(signupRequest.getUsername())) {
            throw new BadRequestException("用户名已存在");
        }

        // 验证手机号是否已存在
        if (userRepository.existsByPhone(signupRequest.getPhone())) {
            throw new BadRequestException("手机号已被注册");
        }

        // 验证邮箱是否已存在
        if (StringUtils.hasText(signupRequest.getEmail()) && userRepository.existsByEmail(signupRequest.getEmail())) {
            throw new BadRequestException("邮箱已被注册");
        }

        // 创建用户对象
        User user = User.builder()
                .username(signupRequest.getUsername())
                .password(passwordEncoder.encode(signupRequest.getPassword()))
                .realName(signupRequest.getRealName())
                .phone(signupRequest.getPhone())
                .email(signupRequest.getEmail())
                .userType(signupRequest.getUserType())
                .studentId(signupRequest.getStudentId())
                .department(signupRequest.getDepartment())
                .dormitory(signupRequest.getDormitory())
                .status(1) // 默认启用
                .build();

        // 设置用户角色
        Set<Role> roles = new HashSet<>();
        if (signupRequest.getRoles() == null || signupRequest.getRoles().isEmpty()) {
            // 默认角色
            Role userRole = roleRepository.findByName(Role.RoleName.ROLE_STUDENT)
                    .orElseThrow(() -> new RuntimeException("默认角色不存在"));
            roles.add(userRole);
        } else {
            signupRequest.getRoles().forEach(role -> {
                switch (role) {
                    case "admin":
                        Role adminRole = roleRepository.findByName(Role.RoleName.ROLE_ADMIN)
                                .orElseThrow(() -> new RuntimeException("管理员角色不存在"));
                        roles.add(adminRole);
                        break;
                    case "staff":
                        Role staffRole = roleRepository.findByName(Role.RoleName.ROLE_STAFF)
                                .orElseThrow(() -> new RuntimeException("教职工角色不存在"));
                        roles.add(staffRole);
                        break;
                    case "courier":
                        Role courierRole = roleRepository.findByName(Role.RoleName.ROLE_COURIER)
                                .orElseThrow(() -> new RuntimeException("快递员角色不存在"));
                        roles.add(courierRole);
                        break;
                    default:
                        Role studentRole = roleRepository.findByName(Role.RoleName.ROLE_STUDENT)
                                .orElseThrow(() -> new RuntimeException("学生角色不存在"));
                        roles.add(studentRole);
                }
            });
        }
        user.setRoles(roles);

        // 保存用户
        User savedUser = userRepository.save(user);
        log.info("用户注册成功: {}", savedUser.getUsername());

        // 转换为DTO并返回
        return convertToDTO(savedUser);
    }

    @Override
    public JwtResponse authenticateUser(LoginRequest loginRequest) {
        // 认证用户
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));

        // 设置认证信息
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 生成JWT令牌
        String jwt = jwtUtils.generateJwtToken(authentication);

        // 获取用户详情
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        // 获取用户角色
        List<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        // 获取用户信息
        User user = userRepository.findByUsername(userDetails.getUsername())
                .orElseThrow(() -> new ResourceNotFoundException("User", "username", userDetails.getUsername()));

        log.info("用户登录成功: {}", user.getUsername());

        // 返回JWT响应
        return new JwtResponse(
                jwt,
                user.getId(),
                user.getUsername(),
                user.getRealName(),
                user.getPhone(),
                user.getUserType(),
                roles
        );
    }

    @Override
    @Transactional(readOnly = true)
    public UserDTO getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
        return convertToDTO(user);
    }

    @Override
    @Transactional(readOnly = true)
    public UserDTO getUserByUsername(String username) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new ResourceNotFoundException("User", "username", username));
        return convertToDTO(user);
    }

    @Override
    @Transactional(readOnly = true)
    public Page<UserDTO> getUserList(Integer userType, String keyword, Pageable pageable) {
        Specification<User> spec = (root, query, criteriaBuilder) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            // 用户类型过滤
            if (userType != null) {
                predicates.add(criteriaBuilder.equal(root.get("userType"), userType));
            }
            
            // 关键字搜索
            if (StringUtils.hasText(keyword)) {
                List<Predicate> keywordPredicates = new ArrayList<>();
                keywordPredicates.add(criteriaBuilder.like(root.get("username"), "%" + keyword + "%"));
                keywordPredicates.add(criteriaBuilder.like(root.get("realName"), "%" + keyword + "%"));
                keywordPredicates.add(criteriaBuilder.like(root.get("phone"), "%" + keyword + "%"));
                keywordPredicates.add(criteriaBuilder.like(root.get("email"), "%" + keyword + "%"));
                predicates.add(criteriaBuilder.or(keywordPredicates.toArray(new Predicate[0])));
            }
            
            return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
        
        Page<User> userPage = userRepository.findAll(spec, pageable);
        return userPage.map(this::convertToDTO);
    }

    @Override
    @Transactional
    public UserDTO updateUser(Long id, UserDTO userDTO) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
        
        // 更新用户信息
        if (StringUtils.hasText(userDTO.getRealName())) {
            user.setRealName(userDTO.getRealName());
        }
        
        if (StringUtils.hasText(userDTO.getPhone()) && !user.getPhone().equals(userDTO.getPhone())) {
            if (userRepository.existsByPhone(userDTO.getPhone())) {
                throw new BadRequestException("手机号已被注册");
            }
            user.setPhone(userDTO.getPhone());
        }
        
        if (StringUtils.hasText(userDTO.getEmail()) && !Objects.equals(user.getEmail(), userDTO.getEmail())) {
            if (userRepository.existsByEmail(userDTO.getEmail())) {
                throw new BadRequestException("邮箱已被注册");
            }
            user.setEmail(userDTO.getEmail());
        }
        
        if (userDTO.getUserType() != null) {
            user.setUserType(userDTO.getUserType());
        }
        
        if (StringUtils.hasText(userDTO.getStudentId())) {
            user.setStudentId(userDTO.getStudentId());
        }
        
        if (StringUtils.hasText(userDTO.getDepartment())) {
            user.setDepartment(userDTO.getDepartment());
        }
        
        if (StringUtils.hasText(userDTO.getDormitory())) {
            user.setDormitory(userDTO.getDormitory());
        }
        
        if (StringUtils.hasText(userDTO.getAvatar())) {
            user.setAvatar(userDTO.getAvatar());
        }
        
        // 保存用户
        User updatedUser = userRepository.save(user);
        log.info("用户信息更新成功: {}", updatedUser.getUsername());
        
        return convertToDTO(updatedUser);
    }

    @Override
    @Transactional
    public UserDTO updateUserStatus(Long id, Integer status) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
        
        user.setStatus(status);
        
        User updatedUser = userRepository.save(user);
        log.info("用户状态更新成功: {}, 状态: {}", updatedUser.getUsername(), status);
        
        return convertToDTO(updatedUser);
    }

    @Override
    @Transactional
    public void deleteUser(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
        
        userRepository.delete(user);
        log.info("用户删除成功: {}", user.getUsername());
    }

    @Override
    @Transactional
    public boolean changePassword(Long id, String oldPassword, String newPassword) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
        
        // 验证旧密码
        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
            throw new BadRequestException("旧密码不正确");
        }
        
        // 更新密码
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);
        log.info("用户密码修改成功: {}", user.getUsername());
        
        return true;
    }
    
    /**
     * 将用户实体转换为DTO
     * 
     * @param user 用户实体
     * @return 用户DTO
     */
    private UserDTO convertToDTO(User user) {
        Set<String> roles = user.getRoles().stream()
                .map(role -> role.getName().name())
                .collect(Collectors.toSet());
        
        return UserDTO.builder()
                .id(user.getId())
                .username(user.getUsername())
                .realName(user.getRealName())
                .phone(user.getPhone())
                .email(user.getEmail())
                .userType(user.getUserType())
                .studentId(user.getStudentId())
                .department(user.getDepartment())
                .dormitory(user.getDormitory())
                .avatar(user.getAvatar())
                .status(user.getStatus())
                .roles(roles)
                .createdTime(user.getCreatedTime())
                .updatedTime(user.getUpdatedTime())
                .build();
    }
}

user-service\src\main\java\com\campus\express\user\util\JwtUtils.java

package com.campus.express.user.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;

/**
 * JWT工具类,用于生成和验证JWT令牌
 */
@Slf4j
@Component
public class JwtUtils {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.expiration}")
    private int jwtExpirationMs;

    /**
     * 生成JWT令牌
     *
     * @param authentication 认证信息
     * @return JWT令牌
     */
    public String generateJwtToken(Authentication authentication) {
        UserDetails userPrincipal = (UserDetails) authentication.getPrincipal();
        
        return Jwts.builder()
                .setSubject(userPrincipal.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
                .signWith(getSigningKey(), SignatureAlgorithm.HS512)
                .compact();
    }

    /**
     * 从JWT令牌中获取用户名
     *
     * @param token JWT令牌
     * @return 用户名
     */
    public String getUsernameFromJwtToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    /**
     * 验证JWT令牌
     *
     * @param authToken JWT令牌
     * @return 是否有效
     */
    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(getSigningKey())
                    .build()
                    .parseClaimsJws(authToken);
            return true;
        } catch (MalformedJwtException e) {
            log.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            log.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            log.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            log.error("JWT claims string is empty: {}", e.getMessage());
        } catch (Exception e) {
            log.error("JWT validation error: {}", e.getMessage());
        }
        return false;
    }
    
    /**
     * 获取签名密钥
     *
     * @return 签名密钥
     */
    private Key getSigningKey() {
        byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

user-service\src\main\resources\application.yml

server:
  port: 8081
  servlet:
    context-path: /api/users

spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql://localhost:3306/campus_express_user?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

# JWT配置
jwt:
  secret: campus_express_secret_key_2025_04_08_very_secure_and_long_key
  expiration: 86400000  # 24小时
  header: Authorization
  prefix: Bearer

# 日志配置
logging:
  level:
    com.campus.express.user: DEBUG
    org.springframework.web: INFO
    org.hibernate: INFO

user-service\target\classes\application.yml

server:
  port: 8081
  servlet:
    context-path: /api/users

spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql://localhost:3306/campus_express_user?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: Asia/Shanghai

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${server.port}

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

# JWT配置
jwt:
  secret: campus_express_secret_key_2025_04_08_very_secure_and_long_key
  expiration: 86400000  # 24小时
  header: Authorization
  prefix: Bearer

# 日志配置
logging:
  level:
    com.campus.express.user: DEBUG
    org.springframework.web: INFO
    org.hibernate: INFO