Java全栈项目--校园快递管理与配送系统(5)
源代码续
<template><div class="app-container"><el-card class="box-card"><div slot="header" class="clearfix"><span>通知统计</span><div class="header-operations"><el-date-pickerv-model="dateRange"type="daterange"align="right"unlink-panelsrange-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-paginationbackground@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) + 100const 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 - activityCountconst 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 - emailCountthis.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 = sizethis.fetchData()},// 处理页码变化handleCurrentChange(page) {this.pagination.currentPage = pagethis.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-inputtype="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-optionv-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-tagv-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-itemv-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.idif (id) {this.isEdit = truethis.templateId = idthis.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 = fieldthis.selectedVariable = ''this.variableSelectorVisible = true},// 插入变量insertVariable() {if (!this.selectedVariable) returnconst 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.selectionStartconst end = textarea.selectionEndthis.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 = variablethis.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><!-- 分页 --><paginationv-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// 模拟数据,实际项目中应该调用APIthis.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.lengththis.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 = 1this.getList()},/** 重置按钮操作 */resetQuery() {this.resetForm('queryForm')this.handleQuery()},/** 新增按钮操作 */handleAdd() {this.reset()this.open = truethis.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 = truethis.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 = falsethis.getList()} else {// addTemplate(this.form).then(response => {// this.$modal.msgSuccess('新增成功')// this.open = false// this.getList()// })this.$message.success('新增成功')this.open = falsethis.getList()}}})},/** 删除按钮操作 */handleDelete(row) {const ids = row.id || this.idsthis.$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 !== 1this.multiple = !selection.length},/** 重置表单数据 */reset() {this.form = {id: undefined,name: undefined,content: undefined,type: 1,channel: [1],remark: undefined}this.resetForm('form')},/** 取消按钮 */cancel() {this.open = falsethis.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 {@Beanpublic 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 {@Autowiredprivate RoleRepository roleRepository;@Autowiredprivate UserRepository userRepository;@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic 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 {@Overridepublic 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 {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate UserDetailsService userDetailsService;@Value("${jwt.header}")private String headerName;@Value("${jwt.prefix}")private String tokenPrefix;@Overrideprotected 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 {@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate JwtAuthenticationEntryPoint unauthorizedHandler;@Beanpublic JwtAuthenticationFilter jwtAuthenticationFilter() {return new JwtAuthenticationFilter();}@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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@Columnprivate String email;@Column(name = "user_type", nullable = false)private Integer userType; // 1-学生,2-教职工,3-快递员,4-管理员@Column(name = "student_id")private String studentId;@Columnprivate String department;@Columnprivate String dormitory;@Columnprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate UserRepository userRepository;@Autowiredprivate RoleRepository roleRepository;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtUtils jwtUtils;@Override@Transactionalpublic 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);}@Overridepublic 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@Transactionalpublic 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@Transactionalpublic 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@Transactionalpublic void deleteUser(Long id) {User user = userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User", "id", id));userRepository.delete(user);log.info("用户删除成功: {}", user.getUsername());}@Override@Transactionalpublic 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: 8081servlet:context-path: /api/usersspring:application:name: user-servicedatasource:url: jdbc:mysql://localhost:3306/campus_express_user?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: updateshow-sql: trueproperties:hibernate:format_sql: truedialect: org.hibernate.dialect.MySQL8Dialectjackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: Asia/Shanghaieureka:client:service-url:defaultZone: http://localhost:8761/eureka/instance:prefer-ip-address: trueinstance-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_keyexpiration: 86400000 # 24小时header: Authorizationprefix: Bearer# 日志配置
logging:level:com.campus.express.user: DEBUGorg.springframework.web: INFOorg.hibernate: INFO
user-service\target\classes\application.yml
server:port: 8081servlet:context-path: /api/usersspring:application:name: user-servicedatasource:url: jdbc:mysql://localhost:3306/campus_express_user?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: updateshow-sql: trueproperties:hibernate:format_sql: truedialect: org.hibernate.dialect.MySQL8Dialectjackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: Asia/Shanghaieureka:client:service-url:defaultZone: http://localhost:8761/eureka/instance:prefer-ip-address: trueinstance-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_keyexpiration: 86400000 # 24小时header: Authorizationprefix: Bearer# 日志配置
logging:level:com.campus.express.user: DEBUGorg.springframework.web: INFOorg.hibernate: INFO
相关文章:
Java全栈项目--校园快递管理与配送系统(5)
源代码续 <template><div class"app-container"><el-card class"box-card"><div slot"header" class"clearfix"><span>通知统计</span><div class"header-operations"><el-d…...
UE5 本地化
文章目录 打开本地化面板设置本地化翻译设置文本收集路径添加语言收集需要翻译的文本手动翻译导入导出编译 使用本地化启动代码修改语言 打开本地化面板 UE4: UE5: 设置本地化翻译 设置文本收集路径 UE5可以自动帮我们收集需要显示的文本ÿ…...
用c语言写一个linux进程之间通信(聊天)的简单程序
使用talk 用户在同一台机器上talk指令格式如下: talk 用户名ip地址 [用户终端号] 如果用户只登录了一个终端,那么可以不写用户终端号,如: talk userlocalhost可以使用who指令来查看当前有哪些用户登录,他的终端号…...
同时支持Vue2/Vue3的图片懒加载组件(支持懒加载 v-html 指令梆定的 html 内容)
🚀 vue-lazyload-imgs(LazyLoadImgs) 组件简介 详情见:https://npmjs.com/package/vue-lazyload-imgs 安装方法: npm i vue-lazyload-imgs(不要安装为开发依赖,应为产品依赖) 适用环…...
Qt容器类在元对象系统中使用
解释 “QVector没有被注册到Qt的元对象系统中”这句话的意思是:QVector<double>这种数据类型没有被Qt的元对象系统(Meta-Object System)识别和管理。Qt的元对象系统是Qt框架的核心部分,它提供了信号与槽机制、动态属性系统…...
Qt中的信号与槽及其自定义
信号源:哪个控件发的信号 信号的类型:用户进行不同的操作就会触发不同的信号 如点击按钮,在输入框移动光标,勾选一个复选框,选 择一个下拉框 信号的处理方式:槽(slot)----也就是函数,Qt中用con…...
【已完结STM32】--自学江协科技笔记汇总
以下学习笔记代码均来自b站江协科技视频 笔记汇总完结 文章笔记对应江科大视频新建工程【2-2】新建工程江科大STM32-GPIO输出 点亮LED,LED闪烁,LED流水灯,蜂鸣器(学习笔记)_unit32-t rcc-apb2periph-CSDN博客 【3-1】…...
科技快讯 | 索诺瓦携手清华大学共筑听力无障碍未来;中国探月工程总设计师:未来月球上能打电话;Shopify要求员工证明AI无法取代其工作
索诺瓦携手清华大学共筑听力无障碍未来 2024年末,60岁以上人口超3.1亿,听力损失比例高达11%。清华大学无障碍发展研究院与索诺瓦集团深化合作,共同推动听力无障碍环境建设。2023年9月,《无障碍环境建设法》实施,2024年…...
[实战] 天线阵列波束成形原理详解与仿真实战(完整代码)
天线阵列波束成形原理详解与仿真实战 1. 引言 在无线通信、雷达和声学系统中,波束成形(Beamforming)是一种通过调整天线阵列中各个阵元的信号相位和幅度,将电磁波能量集中在特定方向的技术。其核心目标是通过空间滤波增强目标方…...
北京自在科技:让万物接入苹果Find My网络的″钥匙匠″
在AirTag掀起全球防丢热潮的今天,越来越多的第三方产品开始接入苹果Find My网络——从充电宝到电动车,从行李箱到保温杯,用户只需打开iPhone的「查找」App,就能实时定位这些物品。 北京自在科技有限责任公司早在苹果推出Find My开…...
区块链是怎么存储块怎么找到前一个块
前言:学习区块链的过程中在想怎么管理区块链呢 📌 推荐项目回顾: 👉 Jeiwan 的 blockchain_go 项目 GitHub 地址:https://github.com/Jeiwan/blockchain_go ❓它是怎么存储区块 & 找前一个区块的? 项…...
聚类算法 ap 聚类 谱聚类
AP聚类(Affinity Propagation Clustering)是一种基于消息传递的聚类算法,由Brendan J. Frey和Delbert Dueck于2007年提出。与传统的聚类算法(如K-Means)不同,AP聚类不需要预先指定聚类数量,而是…...
习题与正则表达式
思路: 二分查找: left 1(最小可能距离),right L(最大可能距离)。 每次取 mid (left right) / 2,判断是否可以通过增设 ≤ K 个路标使得所有相邻路标的距离 ≤ mid。 贪心验证…...
数据库管理工具实战:IDEA 与 DBeaver 连接 TDengine(一)
一、引言 在当今数字化时代,数据如同企业的生命线,而数据库则是承载这些宝贵数据的关键基础设施。TDengine 作为一款高性能的时序数据库,在物联网、工业互联网、车联网、IT 运维等众多领域中发挥着举足轻重的作用。它以其卓越的性能、高效的…...
聚类Clustering和分类Classification的区别
目的: 聚类:旨在将数据集中的样本分成若干组(簇),使得同一组内的样本在某种意义上更相似,而不同组的样本差异更大。聚类是一种探索性分析,用于发现数据中的自然结构。分类:旨在根据已…...
对比 redis keys 命令 ,下次面试说用 scan
Redis SCAN 命令使用指南 SCAN 是 Redis 提供的一种非阻塞迭代器命令,用于逐步遍历 Redis 数据库中的键。相比于 KEYS 命令,SCAN 不会一次性加载所有键,因此对性能的影响较小,适合在生产环境中使用。 以下是关于 SCAN 的详细用法…...
【Java设计模式】第6章 抽象工厂模式讲解
6. 抽象工厂模式 6.1 抽象工厂讲解 定义:提供一个接口创建一系列相关或依赖对象,无需指定具体类。核心概念: 产品等级结构:同一类型的不同产品(如Java视频、Python视频)。产品族:同一工厂生产的多个产品(如Java视频 + Java手记)。适用场景: 需要创建多个相关联的产品…...
设计模式 - 代理模式
代理模式 代理模式是一种结构型设计模式,它允许你提供一个代理对象来控制对另一个对象的访问。代理模式可以在不改变原始对象的情况下,增强或控制对原始对象的访问。代理模式通常用于延迟加载、访问控制、日志记录、性能监控等场景。 1.静态代理 静态…...
Unity-Xlua热更和AssetBundle详解
从今天开始我们深入Unity的组件,最近的一系列事让我明白学习技术不能只有广度,如走马观花,虽然你可能确实学到了皮毛,但是没有足够深厚的理解,是无法融入自己的东西的,那么你就只是在用别人的工具而不是自己…...
【企业级数据安全】掌握高性能Log4j2敏感信息脱敏方案
前言 在数据安全合规日益严格的今天,日志中的敏感信息保护已成为企业IT建设的必备环节。本文带您深入了解如何打造一套高性能、可实时配置的Log4j2日志脱敏插件,轻松应对各类敏感数据保护需求,让您的系统既满足合规要求,又不牺牲…...
力扣刷题——606.根据二叉树创建字符串
给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。 空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映…...
图像处理中的 Gaussina Blur 和 SIFT 算法
Gaussina Blur 高斯模糊 高斯模糊的数学定义 高斯模糊是通过 高斯核(Gaussian Kernel) 对图像进行卷积操作实现的. 二维高斯函数定义为 G ( x , y , σ ) 1 2 π σ 2 e − x 2 y 2 2 σ 2 G(x, y, \sigma) \frac{1}{2\pi \sigma^2} e^{-\frac{x^2 y^2}{2\sigma^2}} G(x…...
AWS区块链游戏场景技术解决方案:全球节点与去中心化架构实践
一、区块链游戏的技术挑战与架构需求 区块链游戏作为Web3领域的重要应用场景,其技术架构需要满足以下核心需求: 分布式账本的高效同步与共识验证 智能合约的安全执行环境 全球玩家的低延迟交互体验 动态扩展的节点网络支持 海量NFT资产的可靠存储 …...
AWS VPC深度解析:构建安全可靠的云网络基础设施
1. 引言 在云计算时代,网络基础设施的重要性不言而喻。Amazon Web Services (AWS) 的Virtual Private Cloud (VPC)为用户提供了一个强大而灵活的网络环境,使他们能够在AWS云中构建安全、可扩展的应用程序。本文将全面剖析AWS VPC的核心特性,帮助读者深入理解如何利用VPC构建高…...
青少年编程与数学 02-016 Python数据结构与算法 08课题、图
青少年编程与数学 02-016 Python数据结构与算法 08课题、图 一、图1. 图的基本概念1.1 定义1.2 顶点和边1.3 图的分类1.4 特殊术语 2. 图的表示方法1. 邻接矩阵(Adjacency Matrix)2. 邻接表(Adjacency List)3. 边列表(…...
微信小程序:动态表格实现,表头单元格数据完全从data中获取,宽度自定义,自定义文本框,行勾选,样式效果,横向滚动表格(解决背景色不足的问题)等
一、样式效果 二、代码 1、wxml <view class"line flex flex-center"><view class"none" wx:if"{{info.length 0}}">暂无料号</view><view wx:else class"table-container"><!-- 动态生成表头 -->&…...
MySQL学习笔记集--游标
游标 在MySQL中,游标(Cursor)是一种数据库对象,它允许您逐行处理查询结果集。游标通常与存储过程一起使用,因为它们需要在存储过程或函数中声明和操作。游标的使用涉及几个步骤:声明游标、打开游标、从游标…...
Microsoft Defender Antivirus Service服务占用CPU过高
下载火绒安全,用它替代 Microsoft Defender,并关闭 Microsoft Defender 两步禁用Windows Defender Antivirus Service_microsoft defender antivirus service-CSDN博客 Windows10/11家庭版 关闭方法 按 ‘Win键R’,输入 “regedit”&#…...
Ansible(7)——管理机密与事实
目录 一、管理机密: 1、Ansible Vault : 2、ansible-vault 命令行工具: (1)创建加密文件: (2)查看加密文件: (3)编辑现有加密文件…...
consul服务注册与发现(go)-学习笔记
参考博客 1、服务实例接口与默认实现 type ServiceInstance interface {// 获取服务实例的唯一IDGetInstanceId() string// 获取服务IDGetServiceId() string// 获取服务实例的主机名或IP地址GetHost() string// 获取服务实例的端口号GetPort() int// 判断服务实例是否使用HT…...
golang-defer延迟机制
defer延迟机制 defer是什么 defer是go中一种延迟调用机制。 执行时机 defer后面的函数只有在当前函数执行完毕后才能执行。 执行顺序 将延迟的语句按defer的逆序进行执行,也就是说先被defer的语句最后被执行,最后被defer的语句,最先被执…...
字符串哈希算法详解:原理、实现与应用
字符串哈希是一种高效处理字符串匹配和比较的技术,它通过将字符串映射为一个唯一的数值(哈希值),从而在O(1)时间内完成子串的比较。本文将结合代码实现,详细讲解前缀哈希法的工作原理,并通过流程图逐步解析…...
python-Leetcode 65.搜索旋转排序数组
题目: 整数数组nums按升序排列,数组中的值互不相同 在传递给函数之前,nums在预先未知的某个小标K上进行了旋转,使数组变为[nums[k], nums[k1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]],小标从0开始计数。…...
蓝桥杯 C/C++ 组历届真题合集速刷(二)
一、0ASC - 蓝桥云课 (单位换算)算法代码: #include <iostream> using namespace std; int main() {printf("%d",L);return 0; } 二、0时间显示 - 蓝桥云课 (单位换算)算法代码: #inclu…...
react的redux总结
目录 一、Antd 1.1、基本使用 1.2、自定义主题 二、Redux 2.1、工作流程 2.2、理解react-redux 2.3、优化 2.3.1、简写mapDispatch 2.3.2、Provider组件 2.4、数据共享 2.4.1、编写Person组件 2.4.2、Person组件的reducer 2.4.3、完成数据共享 2.5、求和案例 2.…...
MySQL视图
一、视图的本质与分类 1. 定义 虚拟表:视图不存储数据,本质是保存的查询语句(SELECT),每次访问视图时动态执行查询并返回结果。 逻辑抽象:基于一个或多个基表(或视图)创建…...
程序化广告行业(69/89):电商素材制作与展示策略解析
程序化广告行业(69/89):电商素材制作与展示策略解析 在如今数字化营销的浪潮中,程序化广告成为众多企业精准触达目标客户的有力武器。作为一名在广告技术领域摸爬滚打多年的从业者,深知学习是不断进步的阶梯ÿ…...
【PCB工艺】发光二极管的原理
你真的知道发光二极管为什么会发光吗? 而为什么另一部分二极管不会发光呢? 这篇文章解释元器件发光二极管(LED)的底层原理。 发光二极管(LED, Light Emitting Diode) 是一种能够将电能转换为光能的半导体…...
探秘 DeepSeek:开源生态如何推动 AI 技术普惠?
探秘 DeepSeek:开源生态如何推动 AI 技术普惠? 引言 在人工智能(AI)领域,技术的快速发展和广泛应用正在深刻改变我们的生活。然而,AI 的发展往往伴随着资源和技术的集中化问题,大型科技公司凭借其雄厚的资金和人才优势占据了主导地位,而中小企业、研究机构和个人开发…...
远程主机可能不符合glibc和libstdc++ VS Code服务器的先决条件
这是因为我最近更新了vscode, 服务器中有个GLIBC库,VSCode>1.86.0版本对 低于v2.28.0版本的GLIBC不再满足需求。 解决办法 回退到之前能够连接服务器的版本。我之前用的是January 2025 (version 1.97) vscode旧版本下载地址...
JVM性能调优:参数配置×内存诊断×GC调优实战
🚀前言 “你的Java应用是否还在经历莫名卡顿?半夜被OOM报警惊醒?GC日志像天书看不懂? 本文将用20个真实案例50个关键参数,带你掌握: 参数调优:如何用-XX:UseG1GC让GC暂停从秒级降到毫秒级&…...
pg_waldump 使用方法和输出验证
目录 pg_waldump 使用方法和输出验证一、pg_waldump 基础用法二、验证输出文件正确性三、关键参数 -p 的作用四、验证示例五、注意事项 pg_waldump 使用方法和输出验证 一、pg_waldump 基础用法 命令格式 pg_waldump [选项] [WAL文件路径]-p, --pgdataDIR:指定 Pos…...
Android 定制飞行模式和通话中设置菜单置灰
业务背景 定制需求实现 目标:通话中禁用移动网络设置中的网络模式和APN入口。 Google原生行为分析 在原生Android中: 飞行模式: 无法在通话中开启:系统会自动阻止,因飞行模式会断开通话所需的射频。APN/网络模式修改…...
C# System.Text.Json 中 ReferenceHandling 使用详解
总目录 一、什么是 ReferenceHandling? 1. 概述 ReferenceHandling 是 System.Text.Json 中用于处理对象引用(循环引用或重复引用)的选项。它允许开发者在序列化和反序列化时控制如何处理对象之间的引用关系。 默认情况下,Syst…...
【开发经验】调试OpenBMC Redfish EventService功能
EventService功能是Redfish规范中定义的一种事件日志的发送方式。用户可以设置订阅者信息(通常是一个web服务器),当产生事件日志时,OpenBMC可以根据用户设置的订阅者信息与对日志的筛选设置,将事件日志发送到订阅者。 相比于传统的SNMPTrap日…...
【AI工具】FastGPT:开启高效智能问答新征程
前言 在人工智能飞速发展的当下,各类 AI 工具如雨后春笋般涌现。FastGPT 作为一款基于大语言模型(LLM)的知识图谱问答系统,凭借其强大的数据处理和模型调校能力,为用户带来了便捷的使用体验。今天,就让我们…...
4.8学习总结 贪心算法+Stream流
贪心算法: 找到局部最优->从而推导全局最优。 Java练习: 获取随机验证码: import java.util.*; import java.util.function.BiConsumer; public class test {public static void main(String[] args) {System.out.println(createCode(…...
入选ICLR‘25 Spotlight!深度强化学习(DRL)迎来新突破!
近年来,深度强化学习相关的成果在顶会顶刊上接受度普遍较高,经常上榜ICLR、Nature、Science等。比如ICLR 2025上的一篇Spotlight,由清华团队提出,介绍了一种SmODE网路,让深度强化学习的控制更加丝滑! 另外…...
【学习笔记】HTTP和HTTPS的核心区别及工作原理
一、基础概念 HTTP(超文本传输协议):明文传输数据,默认端口80,容易被窃听或篡改。 HTTPS(HTTP SSL/TLS):通过加密传输数据,默认端口443,保障安全性。 二、…...
gbase8s之数据字典导出脚本(完美)
有时我们需要将表结构转换成数据库设计文档(WORD或者其他格式),这时需要使用脚本将表结构导出,转换成可用格式。 该脚本适用于GBase 8s小版本号在3.0之后的版本(含有syscolumnsext、syscomments以及syscolcomments表&a…...