Java全栈项目实战:校园报修服务系统
项目介绍
校园报修服务系统是一个面向高校的设施维修管理平台,旨在提供便捷的报修服务,提高维修效率,实现校园设施维护的信息化管理。本项目采用前后端分离架构,基于 Spring Boot + Vue.js 技术栈开发。
系统功能
1. 用户端功能
- 在线报修申请
- 维修进度查询
- 报修历史记录
- 维修评价反馈
- 个人信息管理
2. 维修人员功能
- 接收报修任务
- 任务状态更新
- 维修记录管理
- 工作量统计
3. 管理员功能
- 用户管理
- 维修人员管理
- 报修类型管理
- 工单分配
- 数据统计分析
技术架构
后端技术栈
- Spring Boot 2.x
- Spring Security
- MyBatis Plus
- MySQL 8.0
- Redis
- JWT
前端技术栈
- Vue.js 2.x
- Element UI
- Axios
- Vuex
- Vue Router
核心功能实现
1. 报修工单流程
@Service
public class RepairOrderServiceImpl implements RepairOrderService {
@Autowired
private RepairOrderMapper repairOrderMapper;
@Override
@Transactional
public void createOrder(RepairOrder order) {
// 设置工单初始状态
order.setStatus(OrderStatus.PENDING);
order.setCreateTime(new Date());
// 保存工单信息
repairOrderMapper.insert(order);
// 发送通知给相关维修人员
notifyRepairStaff(order);
}
}
2. 实时消息推送
@Component
public class WebSocketServer {
@OnMessage
public void onMessage(String message, Session session) {
// 处理消息推送
JSONObject jsonObject = JSON.parseObject(message);
String userId = jsonObject.getString("userId");
String content = jsonObject.getString("content");
// 推送消息给指定用户
sendMessage(userId, content);
}
}
3. 工单分配算法
public class RepairAssignmentStrategy {
public Staff assignRepairTask(RepairOrder order) {
List<Staff> availableStaff = getAvailableStaff();
// 根据工作量、专业类型等因素计算最优分配
return availableStaff.stream()
.min((s1, s2) -> compareWorkload(s1, s2))
.orElseThrow(() -> new NoAvailableStaffException());
}
}
数据库设计
核心表结构
-- 报修工单表
CREATE TABLE repair_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
type_id INT NOT NULL,
location VARCHAR(100) NOT NULL,
description TEXT,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL,
update_time DATETIME
);
-- 维修人员表
CREATE TABLE repair_staff (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20),
specialty VARCHAR(50),
status TINYINT NOT NULL
);
项目亮点
-
智能分配系统:采用工作量均衡算法,实现维修任务的智能分配
-
实时通知:基于 WebSocket 实现工单状态实时推送
-
评价反馈:引入评分机制,促进服务质量提升
-
数据可视化:使用 ECharts 实现维修数据的图表展示
性能优化
- 缓存优化
@Cacheable(value = "repair", key = "#orderId")
public RepairOrder getOrderDetail(Long orderId) {
return repairOrderMapper.selectById(orderId);
}
- 分页查询优化
public Page<RepairOrder> getOrderList(QueryDTO query) {
return repairOrderMapper.selectPage(
new Page<>(query.getPage(), query.getSize()),
new QueryWrapper<RepairOrder>()
.eq("status", query.getStatus())
.orderByDesc("create_time")
);
}
项目总结
通过本项目的开发,不仅实现了校园报修流程的信息化管理,也积累了全栈开发的实战经验。项目中的难点包括:
- 工单分配算法的优化
- 实时消息推送的实现
- 高并发场景下的性能优化
未来计划继续优化以下方面:
- 引入微服务架构
- 添加移动端应用
- 集成智能预警系统
项目收获
- 掌握了前后端分离项目的完整开发流程
- 深入理解了 Spring Boot 企业级应用开发
- 提升了代码设计和架构能力
- 积累了项目管理和团队协作经验
通过这个项目,不仅提供了一个实用的校园服务平台,也为后续类似项目的开发积累了宝贵经验。
校园报修系统用户端功能详解
1. 在线报修申请
1.1 功能描述
- 用户填写报修信息表单
- 支持图片上传
- 自动定位报修位置
- 选择报修类型和紧急程度
1.2 核心代码实现
前端表单组件:
<template>
<el-form :model="repairForm" :rules="rules" ref="repairForm">
<!-- 报修类型 -->
<el-form-item label="报修类型" prop="type">
<el-select v-model="repairForm.type">
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<!-- 报修位置 -->
<el-form-item label="报修位置" prop="location">
<el-input v-model="repairForm.location">
<el-button slot="append" @click="getLocation">
定位
</el-button>
</el-input>
</el-form-item>
<!-- 问题描述 -->
<el-form-item label="问题描述" prop="description">
<el-input type="textarea" v-model="repairForm.description">
</el-input>
</el-form-item>
<!-- 图片上传 -->
<el-form-item label="现场图片">
<el-upload
action="/api/upload"
list-type="picture-card"
:on-success="handleUploadSuccess">
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
<!-- 紧急程度 -->
<el-form-item label="紧急程度" prop="priority">
<el-rate v-model="repairForm.priority"></el-rate>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
repairForm: {
type: '',
location: '',
description: '',
images: [],
priority: 3
},
rules: {
type: [{ required: true, message: '请选择报修类型' }],
location: [{ required: true, message: '请输入报修位置' }],
description: [{ required: true, message: '请描述问题' }]
}
}
},
methods: {
// 获取当前位置
async getLocation() {
try {
const position = await this.$geolocation.getCurrentPosition()
this.repairForm.location = `${position.coords.latitude},${position.coords.longitude}`
} catch (error) {
this.$message.error('获取位置失败')
}
},
// 图片上传成功回调
handleUploadSuccess(response) {
this.repairForm.images.push(response.url)
},
// 提交表单
async submitForm() {
try {
await this.$refs.repairForm.validate()
const response = await this.$api.repair.submit(this.repairForm)
this.$message.success('报修提交成功')
this.$router.push(`/repair/detail/${response.data.orderId}`)
} catch (error) {
this.$message.error('提交失败')
}
}
}
}
</script>
后端接口实现:
@RestController
@RequestMapping("/api/repair")
public class RepairController {
@Autowired
private RepairOrderService repairOrderService;
@PostMapping("/submit")
public Result submitRepair(@RequestBody RepairDTO repairDTO) {
// 参数校验
ValidateUtils.validateRepairDTO(repairDTO);
// 创建工单
RepairOrder order = RepairOrder.builder()
.userId(SecurityUtils.getCurrentUserId())
.type(repairDTO.getType())
.location(repairDTO.getLocation())
.description(repairDTO.getDescription())
.images(repairDTO.getImages())
.priority(repairDTO.getPriority())
.status(OrderStatus.PENDING)
.createTime(new Date())
.build();
// 保存工单
Long orderId = repairOrderService.createOrder(order);
return Result.success(orderId);
}
}
2. 维修进度查询
2.1 功能描述
- 查看工单当前状态
- 实时推送进度更新
- 与维修人员在线沟通
- 进度时间轴展示
2.2 核心代码实现
前端进度查询组件:
<template>
<div class="repair-progress">
<!-- 工单状态 -->
<div class="status-card">
<el-tag :type="getStatusType(order.status)">
{{ getStatusText(order.status) }}
</el-tag>
</div>
<!-- 进度时间轴 -->
<el-timeline>
<el-timeline-item
v-for="(progress, index) in progressList"
:key="index"
:timestamp="progress.createTime"
:type="progress.type">
{{ progress.content }}
</el-timeline-item>
</el-timeline>
<!-- 在线沟通 -->
<div class="chat-box" v-if="order.status !== 'COMPLETED'">
<div class="messages" ref="messages">
<div v-for="msg in messages" :key="msg.id"
:class="['message', msg.fromUser ? 'user' : 'staff']">
{{ msg.content }}
</div>
</div>
<div class="input-area">
<el-input v-model="messageText" placeholder="输入消息...">
<el-button slot="append" @click="sendMessage">
发送
</el-button>
</el-input>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
order: {},
progressList: [],
messages: [],
messageText: '',
websocket: null
}
},
created() {
this.initWebSocket()
this.loadOrderDetail()
},
methods: {
// 初始化WebSocket连接
initWebSocket() {
const wsUrl = `ws://${location.host}/ws/repair/${this.orderId}`
this.websocket = new WebSocket(wsUrl)
this.websocket.onmessage = this.handleMessage
},
// 处理收到的消息
handleMessage(event) {
const data = JSON.parse(event.data)
if (data.type === 'PROGRESS') {
this.progressList.push(data.progress)
} else if (data.type === 'CHAT') {
this.messages.push(data.message)
this.scrollToBottom()
}
},
// 发送消息
sendMessage() {
if (!this.messageText.trim()) return
this.websocket.send(JSON.stringify({
type: 'CHAT',
content: this.messageText
}))
this.messageText = ''
},
// 加载工单详情
async loadOrderDetail() {
const { data } = await this.$api.repair.getDetail(this.orderId)
this.order = data.order
this.progressList = data.progressList
this.messages = data.messages
},
// 滚动到最新消息
scrollToBottom() {
this.$nextTick(() => {
const messages = this.$refs.messages
messages.scrollTop = messages.scrollHeight
})
}
}
}
</script>
后端WebSocket实现:
@ServerEndpoint("/ws/repair/{orderId}")
@Component
public class RepairWebSocket {
private Session session;
private String orderId;
private static Map<String, Set<RepairWebSocket>> orderClients = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("orderId") String orderId) {
this.session = session;
this.orderId = orderId;
// 添加到连接池
orderClients.computeIfAbsent(orderId, k -> new CopyOnWriteArraySet<>())
.add(this);
}
@OnMessage
public void onMessage(String message) {
JSONObject json = JSON.parseObject(message);
// 处理聊天消息
if ("CHAT".equals(json.getString("type"))) {
// 保存消息记录
ChatMessage chatMessage = new ChatMessage();
chatMessage.setOrderId(orderId);
chatMessage.setContent(json.getString("content"));
chatMessage.setFromUser(true);
chatMessageService.save(chatMessage);
// 广播给该工单的所有连接
broadcast(orderId, JSON.toJSONString(chatMessage));
}
}
// 广播消息
private void broadcast(String orderId, String message) {
Set<RepairWebSocket> clients = orderClients.get(orderId);
if (clients != null) {
clients.forEach(client -> {
try {
client.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
3. 报修历史记录
3.1 功能描述
- 查看历史报修列表
- 支持多条件筛选
- 导出报修记录
- 快速重新报修
3.2 核心代码实现
前端历史记录组件:
<template>
<div class="repair-history">
<!-- 搜索条件 -->
<el-form :inline="true" :model="queryForm">
<el-form-item label="报修类型">
<el-select v-model="queryForm.type" clearable>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryForm.status" clearable>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="queryForm.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="exportHistory">导出</el-button>
</el-form-item>
</el-form>
<!-- 历史列表 -->
<el-table :data="historyList" v-loading="loading">
<el-table-column prop="orderNo" label="工单号" width="120">
</el-table-column>
<el-table-column prop="type" label="类型" width="100">
<template slot-scope="scope">
{{ getTypeText(scope.row.type) }}
</template>
</el-table-column>
<el-table-column prop="location" label="位置">
</el-table-column>
<el-table-column prop="createTime" label="报修时间" width="160">
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button type="text" @click="viewDetail(scope.row)">
查看
</el-button>
<el-button
type="text"
@click="reSubmit(scope.row)"
v-if="scope.row.status === 'COMPLETED'">
再次报修
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@current-change="handlePageChange"
@size-change="handleSizeChange"
:current-page="page"
:page-sizes="[10, 20, 50]"
:page-size="size"
:total="total"
layout="total, sizes, prev, pager, next">
</el-pagination>
</div>
</template>
<script>
export default {
data() {
return {
queryForm: {
type: '',
status: '',
dateRange: []
},
historyList: [],
loading: false,
page: 1,
size: 10,
total: 0
}
},
created() {
this.loadHistory()
},
methods: {
// 加载历史记录
async loadHistory() {
this.loading = true
try {
const params = {
page: this.page,
size: this.size,
type: this.queryForm.type,
status: this.queryForm.status,
startTime: this.queryForm.dateRange[0],
endTime: this.queryForm.dateRange[1]
}
const { data } = await this.$api.repair.getHistory(params)
this.historyList = data.records
this.total = data.total
} finally {
this.loading = false
}
},
// 导出历史记录
async exportHistory() {
const params = {
type: this.queryForm.type,
status: this.queryForm.status,
startTime: this.queryForm.dateRange[0],
endTime: this.queryForm.dateRange[1]
}
const blob = await this.$api.repair.exportHistory(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '报修历史记录.xlsx'
link.click()
window.URL.revokeObjectURL(url)
},
// 再次报修
reSubmit(record) {
this.$router.push({
path: '/repair/submit',
query: {
type: record.type,
location: record.location,
description: record.description
}
})
}
}
}
</script>
后端导出实现:
@Service
public class RepairHistoryServiceImpl implements RepairHistoryService {
@Autowired
private RepairOrderMapper repairOrderMapper;
@Override
public void exportHistory(RepairHistoryQuery query, HttpServletResponse response) {
// 查询数据
List<RepairOrder> orderList = repairOrderMapper.selectHistory(query);
// 创建工作簿
SXSSFWorkbook workbook = new SXSSFWorkbook();
Sheet sheet = workbook.createSheet("报修历史");
// 创建表头
Row headerRow = sheet.createRow(0);
String[] headers = {"工单号", "报修类型", "报修位置", "问题描述",
"报修时间", "完成时间", "状态"};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
}
// 填充数据
for (int i = 0; i < orderList.size(); i++) {
RepairOrder order = orderList.get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(order.getOrderNo());
row.createCell(1).setCellValue(order.getType());
row.createCell(2).setCellValue(order.getLocation());
row.createCell(3).setCellValue(order.getDescription());
row.createCell(4).setCellValue(
DateUtil.format(order.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
row.createCell(5).setCellValue(
DateUtil.format(order.getFinishTime(), "yyyy-MM-dd HH:mm:ss"));
row.createCell(6).setCellValue(order.getStatus());
}
// 输出文件
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition",
"attachment;filename=repair_history.xlsx");
workbook.write(response.getOutputStream());
workbook.close();
}
}
4. 维修评价反馈
4.1 功能描述
- 维修完成后进行评分
- 填写评价内容
- 上传完工照片
- 投诉建议
4.2 核心代码实现
前端评价组件:
<template>
<div class="repair-feedback">
<el-form :model="feedbackForm" :rules="rules" ref="feedbackForm">
<!-- 维修评分 -->
<el-form-item label="维修质量" prop="quality">
<el-rate
v-model="feedbackForm.quality"
:colors="['#99A9BF', '#F7BA2A', '#FF9900']">
</el-rate>
</el-form-item>
<!-- 服务态度 -->
<el-form-item label="服务态度" prop="attitude">
<el-rate
v-model="feedbackForm.attitude"
:colors="['#99A9BF', '#F7BA2A', '#FF9900']">
</el-rate>
</el-form-item>
<!-- 响应速度 -->
<el-form-item label="响应速度" prop="speed">
<el-rate
v-model="feedbackForm.speed"
:colors="['#99A9BF', '#F7BA2A', '#FF9900']">
</el-rate>
</el-form-item>
<!-- 评价内容 -->
<el-form-item label="评价内容" prop="content">
<el-input
type="textarea"
v-model="feedbackForm.content"
:rows="4"
placeholder="请输入评价内容">
</el-input>
</el-form-item>
<!-- 完工照片 -->
<el-form-item label="完工照片">
<el-upload
action="/api/upload"
list-type="picture-card"
:on-success="handleUploadSuccess"
:on-remove="handleRemove">
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
<!-- 投诉建议 -->
<el-form-item label="投诉建议">
<el-input
type="textarea"
v-model="feedbackForm.suggestion"
:rows="4"
placeholder="如有投诉建议请在此填写">
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitFeedback">
提交评价
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data() {
return {
feedbackForm: {
quality: 5,
attitude: 5,
speed: 5,
content: '',
images: [],
suggestion: ''
},
rules: {
quality: [{ required: true, message: '请评价维修质量' }],
attitude: [{ required: true, message: '请评价服务态度' }],
speed: [{ required: true, message: '请评价响应速度' }],
content: [{ required: true, message: '请填写评价内容' }]
}
}
},
methods: {
// 图片上传成功
handleUploadSuccess(response) {
this.feedbackForm.images.push(response.url)
},
// 移除图片
handleRemove(file) {
const index = this.feedbackForm.images.indexOf(file.url)
if (index !== -1) {
this.feedbackForm.images.splice(index, 1)
}
},
// 提交评价
async submitFeedback() {
try {
await this.$refs.feedbackForm.validate()
await this.$api.repair.submitFeedback({
orderId: this.$route.params.orderId,
...this.feedbackForm
})
this.$message.success('评价提交成功')
this.$router.push('/repair/history')
} catch (error) {
this.$message.error('提交失败')
}
}
}
}
</script>
后端评价处理:
@Service
public class RepairFeedbackServiceImpl implements RepairFeedbackService {
@Autowired
private RepairFeedbackMapper feedbackMapper;
@Autowired
private RepairOrderService orderService;
@Autowired
private StaffEvaluationService evaluationService;
@Override
@Transactional
public void submitFeedback(RepairFeedbackDTO feedbackDTO) {
// 创建评价记录
RepairFeedback feedback = RepairFeedback.builder()
.orderId(feedbackDTO.getOrderId())
.quality(feedbackDTO.getQuality())
.attitude(feedbackDTO.getAttitude())
.speed(feedbackDTO.getSpeed())
.content(feedbackDTO.getContent())
.images(feedbackDTO.getImages())
.suggestion(feedbackDTO.getSuggestion())
.createTime(new Date())
.build();
feedbackMapper.insert(feedback);
// 更新工单状态
orderService.updateStatus(
feedbackDTO.getOrderId(),
OrderStatus.EVALUATED
);
// 更新维修人员评分
RepairOrder order = orderService.getById(feedbackDTO.getOrderId());
evaluationService.updateStaffScore(
order.getStaffId(),
calculateScore(feedbackDTO)
);
// 如果有投诉,创建投诉工单
if (StringUtils.isNotEmpty(feedbackDTO.getSuggestion())) {
createComplaint(feedback);
}
}
// 计算综合评分
private double calculateScore(RepairFeedbackDTO feedback) {
return (feedback.getQuality() * 0.4 +
feedback.getAttitude() * 0.3 +
feedback.getSpeed() * 0.3);
}
// 创建投诉工单
private void createComplaint(RepairFeedback feedback) {
ComplaintOrder complaint = ComplaintOrder.builder()
.repairOrderId(feedback.getOrderId())
.content(feedback.getSuggestion())
.status(ComplaintStatus.PENDING)
.createTime(new Date())
.build();
complaintOrderMapper.insert(complaint);
}
}
5. 个人信息管理
5.1 功能描述
- 基本信息维护
- 修改密码
- 常用地址管理
- 消息通知设置
5.2 核心代码实现
前端个人信息组件:
<template>
<div class="user-profile">
<el-tabs v-model="activeTab">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="basic">
<el-form :model="userForm" :rules="rules" ref="userForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" disabled></el-input>
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input v-model="userForm.realName"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="userForm.phone"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="updateBasicInfo">
保存修改
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 修改密码 -->
<el-tab-pane label="修改密码" name="password">
<el-form :model="passwordForm" :rules="passwordRules" ref="passwordForm">
<el-form-item label="原密码" prop="oldPassword">
<el-input
type="password"
v-model="passwordForm.oldPassword">
</el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-input
type="password"
v-model="passwordForm.newPassword">
</el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
type="password"
v-model="passwordForm.confirmPassword">
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="updatePassword">
修改密码
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<!-- 常用地址 -->
<el-tab-pane label="常用地址" name="address">
<div class="address-list">
<el-button type="primary" @click="showAddressDialog">
添加地址
</el-button>
<el-table :data="addressList">
<el-table-column prop="name" label="地址名称">
</el-table-column>
<el-table-column prop="detail" label="详细地址">
</el-table-column>
<el-table-column label="操作" width="150">
<template slot-scope="scope">
<el-button
type="text"
@click="editAddress(scope.row)">
编辑
</el-button>
<el-button
type="text"
@click="deleteAddress(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<!-- 通知设置 -->
<el-tab-pane label="通知设置" name="notification">
<el-form :model="notificationForm">
<el-form-item label="接收系统通知">
<el-switch
v-model="notificationForm.systemNotice">
</el-switch>
</el-form-item>
<el-form-item label="维修进度通知">
<el-switch
v-model="notificationForm.progressNotice">
</el-switch>
</el-form-item>
<el-form-item label="评价提醒">
<el-switch
v-model="notificationForm.feedbackReminder">
</el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveNotificationSettings">
保存设置
</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<!-- 地址编辑对话框 -->
<el-dialog
:title="addressForm.id ? '编辑地址' : '添加地址'"
:visible.sync="addressDialogVisible">
<el-form :model="addressForm" :rules="addressRules" ref="addressForm">
<el-form-item label="地址名称" prop="name">
<el-input v-model="addressForm.name"></el-input>
</el-form-item>
<el-form-item label="详细地址" prop="detail">
<el-input
type="textarea"
v-model="addressForm.detail">
</el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="addressDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveAddress">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
// 密码确认验证
const validateConfirmPassword = (rule, value, callback) => {
if (value !== this.passwordForm.newPassword) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
return {
activeTab: 'basic',
// 基本信息表单
userForm: {
username: '',
realName: '',
phone: '',
email: ''
},
// 密码表单
passwordForm: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
},
// 地址相关
addressList: [],
addressDialogVisible: false,
addressForm: {
id: null,
name: '',
detail: ''
},
// 通知设置
notificationForm: {
systemNotice: true,
progressNotice: true,
feedbackReminder: true
},
// 表单验证规则
rules: {
realName: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
]
},
passwordRules: {
oldPassword: [
{ required: true, message: '请输入原密码', trigger: 'blur' }
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
},
addressRules: {
name: [
{ required: true, message: '请输入地址名称', trigger: 'blur' }
],
detail: [
{ required: true, message: '请输入详细地址', trigger: 'blur' }
]
}
}
},
created() {
this.loadUserInfo()
this.loadAddressList()
this.loadNotificationSettings()
},
methods: {
// 加载用户信息
async loadUserInfo() {
try {
const { data } = await this.$api.user.getUserInfo()
this.userForm = data
} catch (error) {
this.$message.error('获取用户信息失败')
}
},
// 更新基本信息
async updateBasicInfo() {
try {
await this.$refs.userForm.validate()
await this.$api.user.updateUserInfo(this.userForm)
this.$message.success('信息更新成功')
} catch (error) {
this.$message.error('更新失败')
}
},
// 修改密码
async updatePassword() {
try {
await this.$refs.passwordForm.validate()
await this.$api.user.updatePassword(this.passwordForm)
this.$message.success('密码修改成功,请重新登录')
this.$store.dispatch('logout')
this.$router.push('/login')
} catch (error) {
this.$message.error('密码修改失败')
}
},
// 加载地址列表
async loadAddressList() {
try {
const { data } = await this.$api.user.getAddressList()
this.addressList = data
} catch (error) {
this.$message.error('获取地址列表失败')
}
},
// 显示地址编辑框
showAddressDialog(address = null) {
if (address) {
this.addressForm = { ...address }
} else {
this.addressForm = { id: null, name: '', detail: '' }
}
this.addressDialogVisible = true
},
// 保存地址
async saveAddress() {
try {
await this.$refs.addressForm.validate()
if (this.addressForm.id) {
await this.$api.user.updateAddress(this.addressForm)
} else {
await this.$api.user.addAddress(this.addressForm)
}
this.$message.success('保存成功')
this.addressDialogVisible = false
this.loadAddressList()
} catch (error) {
this.$message.error('保存失败')
}
},
// 删除地址
async deleteAddress(address) {
try {
await this.$confirm('确认删除该地址吗?')
await this.$api.user.deleteAddress(address.id)
this.$message.success('删除成功')
this.loadAddressList()
} catch (error) {
if (error !== 'cancel') {
this.$message.error('删除失败')
}
}
},
// 加载通知设置
async loadNotificationSettings() {
try {
const { data } = await this.$api.user.getNotificationSettings()
this.notificationForm = data
} catch (error) {
this.$message.error('获取通知设置失败')
}
},
// 保存通知设置
async saveNotificationSettings() {
try {
await this.$api.user.updateNotificationSettings(this.notificationForm)
this.$message.success('设置保存成功')
} catch (error) {
this.$message.error('保存设置失败')
}
}
}
}
</script>
<style lang="scss" scoped>
.user-profile {
padding: 20px;
.el-tabs {
background: #fff;
padding: 20px;
border-radius: 4px;
}
.address-list {
.el-button {
margin-bottom: 20px;
}
}
.el-form {
max-width: 500px;
}
}
</style>
后端接口实现:
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/info")
public Result getUserInfo() {
UserInfo userInfo = userService.getCurrentUserInfo();
return Result.success(userInfo);
}
@PutMapping("/info")
public Result updateUserInfo(@RequestBody @Validated UserInfoDTO userInfoDTO) {
userService.updateUserInfo(userInfoDTO);
return Result.success();
}
@PutMapping("/password")
public Result updatePassword(@RequestBody @Validated PasswordUpdateDTO passwordDTO) {
userService.updatePassword(passwordDTO);
return Result.success();
}
@GetMapping("/address")
public Result getAddressList() {
List<UserAddress> addressList = userService.getUserAddressList();
return Result.success(addressList);
}
@PostMapping("/address")
public Result addAddress(@RequestBody @Validated UserAddressDTO addressDTO) {
userService.addUserAddress(addressDTO);
return Result.success();
}
@PutMapping("/address/{id}")
public Result updateAddress(
@PathVariable Long id,
@RequestBody @Validated UserAddressDTO addressDTO) {
userService.updateUserAddress(id, addressDTO);
return Result.success();
}
@DeleteMapping("/address/{id}")
public Result deleteAddress(@PathVariable Long id) {
userService.deleteUserAddress(id);
return Result.success();
}
@GetMapping("/notification/settings")
public Result getNotificationSettings() {
NotificationSettings settings = userService.getNotificationSettings();
return Result.success(settings);
}
@PutMapping("/notification/settings")
public Result updateNotificationSettings(
@RequestBody @Validated NotificationSettingsDTO settingsDTO) {
userService.updateNotificationSettings(settingsDTO);
return Result.success();
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserAddressMapper addressMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserInfo getCurrentUserInfo() {
Long userId = SecurityUtils.getCurrentUserId();
return userMapper.selectUserInfo(userId);
}
@Override
@Transactional
public void updateUserInfo(UserInfoDTO userInfoDTO) {
Long userId = SecurityUtils.getCurrentUserId();
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
// 更新用户信息
user.setRealName(userInfoDTO.getRealName());
user.setPhone(userInfoDTO.getPhone());
user.setEmail(userInfoDTO.getEmail());
user.setUpdateTime(new Date());
userMapper.updateById(user);
}
@Override
@Transactional
public void updatePassword(PasswordUpdateDTO passwordDTO) {
Long userId = SecurityUtils.getCurrentUserId();
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
// 验证原密码
if (!passwordEncoder.matches(passwordDTO.getOldPassword(), user.getPassword())) {
throw new BusinessException("原密码不正确");
}
// 更新密码
user.setPassword(passwordEncoder.encode(passwordDTO.getNewPassword()));
user.setUpdateTime(new Date());
userMapper.updateById(user);
}
@Override
public List<UserAddress> getUserAddressList() {
Long userId = SecurityUtils.getCurrentUserId();
return addressMapper.selectByUserId(userId);
}
@Override
@Transactional
public void addUserAddress(UserAddressDTO addressDTO) {
Long userId = SecurityUtils.getCurrentUserId();
UserAddress address = new UserAddress();
address.setUserId(userId);
address.setName(addressDTO.getName());
address.setDetail(addressDTO.getDetail());
address.setCreateTime(new Date());
addressMapper.insert(address);
}
@Override
@Transactional
public void updateUserAddress(Long addressId, UserAddressDTO addressDTO) {
Long userId = SecurityUtils.getCurrentUserId();
UserAddress address = addressMapper.selectById(addressId);
if (address == null || !address.getUserId().equals(userId)) {
throw new BusinessException("地址不存在");
}
address.setName(addressDTO.getName());
address.setDetail(addressDTO.getDetail());
address.setUpdateTime(new Date());
addressMapper.updateById(address);
}
@Override
@Transactional
public void deleteUserAddress(Long addressId) {
Long userId = SecurityUtils.getCurrentUserId();
UserAddress address = addressMapper.selectById(addressId);
if (address == null || !address.getUserId().equals(userId)) {
throw new BusinessException("地址不存在");
}
addressMapper.deleteById(addressId);
}
@Override
public NotificationSettings getNotificationSettings() {
Long userId = SecurityUtils.getCurrentUserId();
return userMapper.selectNotificationSettings(userId);
}
@Override
@Transactional
public void updateNotificationSettings(NotificationSettingsDTO settingsDTO) {
Long userId = SecurityUtils.getCurrentUserId();
NotificationSettings settings = new NotificationSettings();
settings.setUserId(userId);
settings.setSystemNotice(settingsDTO.getSystemNotice());
settings.setProgressNotice(settingsDTO.getProgressNotice());
settings.setFeedbackReminder(settingsDTO.getFeedbackReminder());
settings.setUpdateTime(new Date());
userMapper.updateNotificationSettings(settings);
}
}
这样就完成了个人信息管理模块的主要功能实现,包括:
- 基本信息的查看和修改
- 密码修改
- 常用地址的增删改查
- 通知设置的管理
系统采用了前后端分离的架构,前端使用 Vue + Element UI 实现友好的用户界面,后端使用 Spring Boot 提供 RESTful API。通过这些功能,用户可以方便地管理个人信息,提升使用体验。
维修人员功能模块详解
1. 接收报修任务
1.1 功能描述
- 查看待接单任务列表
- 任务详情查看
- 接单确认
- 任务分类筛选
- 任务优先级排序
1.2 核心代码实现
前端任务列表组件:
<template>
<div class="task-list">
<!-- 筛选栏 -->
<div class="filter-bar">
<el-form :inline="true" :model="filterForm">
<el-form-item label="报修类型">
<el-select v-model="filterForm.type" clearable>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="filterForm.priority" clearable>
<el-option label="普通" value="NORMAL"></el-option>
<el-option label="紧急" value="URGENT"></el-option>
<el-option label="特急" value="CRITICAL"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadTasks">查询</el-button>
</el-form-item>
</el-form>
</div>
<!-- 任务列表 -->
<el-table
:data="taskList"
v-loading="loading"
@row-click="showTaskDetail">
<el-table-column prop="orderNo" label="工单号" width="120">
</el-table-column>
<el-table-column prop="type" label="类型" width="100">
<template slot-scope="scope">
<el-tag :type="getTypeTagType(scope.row.type)">
{{ getTypeText(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="location" label="报修位置">
</el-table-column>
<el-table-column prop="createTime" label="报修时间" width="160">
<template slot-scope="scope">
{{ formatDateTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template slot-scope="scope">
<el-tag :type="getPriorityTagType(scope.row.priority)">
{{ getPriorityText(scope.row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button
type="primary"
size="small"
@click.stop="acceptTask(scope.row)">
接单
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@current-change="handlePageChange"
@size-change="handleSizeChange"
:current-page="page"
:page-sizes="[10, 20, 50]"
:page-size="size"
:total="total"
layout="total, sizes, prev, pager, next">
</el-pagination>
<!-- 任务详情对话框 -->
<el-dialog
title="报修详情"
:visible.sync="detailDialogVisible"
width="600px">
<div class="task-detail" v-if="currentTask">
<div class="detail-item">
<label>工单号:</label>
<span>{{ currentTask.orderNo }}</span>
</div>
<div class="detail-item">
<label>报修人:</label>
<span>{{ currentTask.userName }}</span>
</div>
<div class="detail-item">
<label>联系电话:</label>
<span>{{ currentTask.userPhone }}</span>
</div>
<div class="detail-item">
<label>报修位置:</label>
<span>{{ currentTask.location }}</span>
</div>
<div class="detail-item">
<label>问题描述:</label>
<p>{{ currentTask.description }}</p>
</div>
<div class="detail-item">
<label>现场图片:</label>
<div class="image-list">
<el-image
v-for="(url, index) in currentTask.images"
:key="index"
:src="url"
:preview-src-list="currentTask.images">
</el-image>
</div>
</div>
</div>
<span slot="footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
<el-button
type="primary"
@click="acceptTask(currentTask)">
接单
</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
filterForm: {
type: '',
priority: ''
},
taskList: [],
loading: false,
page: 1,
size: 10,
total: 0,
detailDialogVisible: false,
currentTask: null
}
},
created() {
this.loadTasks()
},
methods: {
// 加载任务列表
async loadTasks() {
this.loading = true
try {
const params = {
page: this.page,
size: this.size,
type: this.filterForm.type,
priority: this.filterForm.priority
}
const { data } = await this.$api.repair.getPendingTasks(params)
this.taskList = data.records
this.total = data.total
} finally {
this.loading = false
}
},
// 显示任务详情
showTaskDetail(task) {
this.currentTask = task
this.detailDialogVisible = true
},
// 接受任务
async acceptTask(task) {
try {
await this.$confirm('确认接受该报修任务?')
await this.$api.repair.acceptTask(task.id)
this.$message.success('接单成功')
this.detailDialogVisible = false
this.loadTasks()
} catch (error) {
if (error !== 'cancel') {
this.$message.error('接单失败')
}
}
},
// 格式化日期时间
formatDateTime(date) {
return this.$moment(date).format('YYYY-MM-DD HH:mm')
},
// 获取类型标签样式
getTypeTagType(type) {
const typeMap = {
WATER: 'primary',
ELECTRIC: 'success',
NETWORK: 'warning',
DEVICE: 'info'
}
return typeMap[type] || ''
},
// 获取优先级标签样式
getPriorityTagType(priority) {
const priorityMap = {
NORMAL: 'info',
URGENT: 'warning',
CRITICAL: 'danger'
}
return priorityMap[priority] || ''
}
}
}
</script>
<style lang="scss" scoped>
.task-list {
padding: 20px;
.filter-bar {
margin-bottom: 20px;
}
.task-detail {
.detail-item {
margin-bottom: 15px;
label {
font-weight: bold;
margin-right: 10px;
}
.image-list {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 10px;
.el-image {
width: 100px;
height: 100px;
border-radius: 4px;
}
}
}
}
}
</style>
后端接口实现:
@RestController
@RequestMapping("/api/repair/staff")
public class RepairStaffController {
@Autowired
private RepairTaskService repairTaskService;
@GetMapping("/tasks/pending")
public Result getPendingTasks(TaskQueryDTO queryDTO) {
IPage<RepairTaskVO> page = repairTaskService.getPendingTasks(queryDTO);
return Result.success(page);
}
@PostMapping("/tasks/{taskId}/accept")
public Result acceptTask(@PathVariable Long taskId) {
repairTaskService.acceptTask(taskId);
return Result.success();
}
}
@Service
public class RepairTaskServiceImpl implements RepairTaskService {
@Autowired
private RepairOrderMapper repairOrderMapper;
@Autowired
private StaffMapper staffMapper;
@Autowired
private MessageService messageService;
@Override
public IPage<RepairTaskVO> getPendingTasks(TaskQueryDTO queryDTO) {
Page<RepairOrder> page = new Page<>(queryDTO.getPage(), queryDTO.getSize());
// 构建查询条件
LambdaQueryWrapper<RepairOrder> wrapper = new LambdaQueryWrapper<RepairOrder>()
.eq(RepairOrder::getStatus, OrderStatus.PENDING)
.eq(StringUtils.isNotEmpty(queryDTO.getType()),
RepairOrder::getType, queryDTO.getType())
.eq(StringUtils.isNotEmpty(queryDTO.getPriority()),
RepairOrder::getPriority, queryDTO.getPriority())
.orderByDesc(RepairOrder::getPriority)
.orderByAsc(RepairOrder::getCreateTime);
// 查询数据
IPage<RepairOrder> orderPage = repairOrderMapper.selectPage(page, wrapper);
// 转换为VO
return orderPage.convert(this::convertToVO);
}
@Override
@Transactional
public void acceptTask(Long taskId) {
// 获取当前维修人员ID
Long staffId = SecurityUtils.getCurrentUserId();
// 查询工单
RepairOrder order = repairOrderMapper.selectById(taskId);
if (order == null || order.getStatus() != OrderStatus.PENDING) {
throw new BusinessException("工单不存在或已被接单");
}
// 检查维修人员状态
Staff staff = staffMapper.selectById(staffId);
if (staff.getStatus() == StaffStatus.BUSY) {
throw new BusinessException("当前有正在处理的工单,请完成后再接单");
}
// 更新工单状态
order.setStaffId(staffId);
order.setStatus(OrderStatus.PROCESSING);
order.setAcceptTime(new Date());
repairOrderMapper.updateById(order);
// 更新维修人员状态
staff.setStatus(StaffStatus.BUSY);
staff.setCurrentTaskId(taskId);
staffMapper.updateById(staff);
// 发送消息通知用户
messageService.sendMessage(MessageTemplate.TASK_ACCEPTED, order.getUserId(),
Map.of("orderNo", order.getOrderNo(),
"staffName", staff.getName(),
"staffPhone", staff.getPhone()));
}
private RepairTaskVO convertToVO(RepairOrder order) {
RepairTaskVO vo = new RepairTaskVO();
BeanUtils.copyProperties(order, vo);
// 查询用户信息
User user = userMapper.selectById(order.getUserId());
vo.setUserName(user.getRealName());
vo.setUserPhone(user.getPhone());
return vo;
}
}
2. 任务状态更新
2.1 功能描述
- 开始维修
- 更新维修进度
- 完成维修
- 上传维修照片
- 填写维修说明
2.2 核心代码实现
前端维修进度组件:
<template>
<div class="task-progress">
<!-- 工单基本信息 -->
<el-card class="task-info">
<div slot="header">
<span>工单信息</span>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="工单号">
{{ task.orderNo }}
</el-descriptions-item>
<el-descriptions-item label="报修类型">
{{ getTypeText(task.type) }}
</el-descriptions-item>
<el-descriptions-item label="报修位置">
{{ task.location }}
</el-descriptions-item>
<el-descriptions-item label="报修时间">
{{ formatDateTime(task.createTime) }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 进度更新 -->
<el-card class="progress-update">
<div slot="header">
<span>维修进度</span>
</div>
<el-steps :active="activeStep" align-center>
<el-step title="待处理"></el-step>
<el-step title="维修中"></el-step>
<el-step title="已完成"></el-step>
</el-steps>
<div class="action-area">
<!-- 开始维修 -->
<div v-if="task.status === 'ACCEPTED'">
<el-button type="primary" @click="startRepair">
开始维修
</el-button>
</div>
<!-- 维修中 -->
<div v-else-if="task.status === 'PROCESSING'">
<el-form :model="progressForm" ref="progressForm">
<el-form-item label="进度说明">
<el-input
type="textarea"
v-model="progressForm.description"
placeholder="请输入维修进度说明">
</el-input>
</el-form-item>
<el-form-item label="现场照片">
<el-upload
action="/api/upload"
list-type="picture-card"
:on-success="handleUploadSuccess"
:on-remove="handleRemove">
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="updateProgress">
更新进度
</el-button>
<el-button type="success" @click="showCompleteDialog">
完成维修
</el-button>
</el-form-item>
</el-form>
</div>
</div>
</el-card>
<!-- 维修完成对话框 -->
<el-dialog
title="完成维修"
:visible.sync="completeDialogVisible"
width="500px">
<el-form :model="completeForm" :rules="completeRules" ref="completeForm">
<el-form-item label="维修结果" prop="result">
<el-input
type="textarea"
v-model="completeForm.result"
:rows="3"
placeholder="请描述维修结果">
</el-input>
</el-form-item>
<el-form-item label="维修建议" prop="suggestion">
<el-input
type="textarea"
v-model="completeForm.suggestion"
placeholder="请输入使用建议或注意事项">
</el-input>
</el-form-item>
<el-form-item label="完工照片" prop="images">
<el-upload
action="/api/upload"
list-type="picture-card"
:on-success="handleCompleteImageUpload"
:on-remove="handleCompleteImageRemove">
<i class="el-icon-plus"></i>
</el-upload>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="completeDialogVisible = false">取消</el-button>
<el-button type="primary" @click="completeTask">确认完成</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
task: {},
activeStep: 1,
progressForm: {
description: '',
images: []
},
completeDialogVisible: false,
completeForm: {
result: '',
suggestion: '',
images: []
},
completeRules: {
result: [
{ required: true, message: '请描述维修结果', trigger: 'blur' }
],
images: [
{ required: true, message: '请上传完工照片', trigger: 'change' }
]
}
}
},
created() {
this.loadTaskDetail()
},
methods: {
// 加载任务详情
async loadTaskDetail() {
const { data } = await this.$api.repair.getTaskDetail(this.$route.params.id)
this.task = data
this.updateActiveStep()
},
// 更新步骤状态
updateActiveStep() {
const statusMap = {
'ACCEPTED': 1,
'PROCESSING': 2,
'COMPLETED': 3
}
this.activeStep = statusMap[this.task.status] || 1
},
// 开始维修
async startRepair() {
try {
await this.$api.repair.startRepair(this.task.id)
this.$message.success('已开始维修')
this.loadTaskDetail()
} catch (error) {
this.$message.error('操作失败')
}
},
// 更新进度
async updateProgress() {
try {
await this.$api.repair.updateProgress({
taskId: this.task.id,
description: this.progressForm.description,
images: this.progressForm.images
})
this.$message.success('进度更新成功')
this.progressForm.description = ''
this.progressForm.images = []
} catch (error) {
this.$message.error('更新失败')
}
},
// 完成维修
async completeTask() {
try {
await this.$refs.completeForm.validate()
await this.$api.repair.completeTask({
taskId: this.task.id,
...this.completeForm
})
this.$message.success('维修完成')
this.completeDialogVisible = false
this.loadTaskDetail()
} catch (error) {
if (error !== 'cancel') {
this.$message.error('操作失败')
}
}
}
}
}
</script>
后端实现:
@Service
public class RepairProgressServiceImpl implements RepairProgressService {
@Autowired
private RepairOrderMapper repairOrderMapper;
@Autowired
private RepairProgressMapper progressMapper;
@Autowired
private StaffMapper staffMapper;
@Autowired
private MessageService messageService;
@Override
@Transactional
public void startRepair(Long taskId) {
RepairOrder order = checkTaskStatus(taskId, OrderStatus.ACCEPTED);
// 更新工单状态
order.setStatus(OrderStatus.PROCESSING);
order.setStartTime(new Date());
repairOrderMapper.updateById(order);
// 记录进度
saveProgress(order, "开始维修", null);
// 通知用户
messageService.sendMessage(MessageTemplate.REPAIR_STARTED, order.getUserId(),
Map.of("orderNo", order.getOrderNo()));
}
@Override
@Transactional
public void updateProgress(ProgressUpdateDTO updateDTO) {
RepairOrder order = checkTaskStatus(taskId, OrderStatus.PROCESSING);
// 记录进度
saveProgress(order, updateDTO.getDescription(), updateDTO.getImages());
// 通知用户
messageService.sendMessage(MessageTemplate.PROGRESS_UPDATED, order.getUserId(),
Map.of("orderNo", order.getOrderNo(),
"progress", updateDTO.getDescription()));
}
@Override
@Transactional
public void completeTask(TaskCompleteDTO completeDTO) {
RepairOrder order = checkTaskStatus(taskId, OrderStatus.PROCESSING);
// 更新工单状态
order.setStatus(OrderStatus.COMPLETED);
order.setFinishTime(new Date());
order.setRepairResult(completeDTO.getResult());
order.setSuggestion(completeDTO.getSuggestion());
order.setCompleteImages(completeDTO.getImages());
repairOrderMapper.updateById(order);
// 更新维修人员状态
Staff staff = staffMapper.selectById(order.getStaffId());
staff.setStatus(StaffStatus.FREE);
staff.setCurrentTaskId(null);
staffMapper.updateById(staff);
// 记录进度
saveProgress(order, "维修完成: " + completeDTO.getResult(),
completeDTO.getImages());
// 通知用户
messageService.sendMessage(MessageTemplate.REPAIR_COMPLETED, order.getUserId(),
Map.of("orderNo", order.getOrderNo()));
}
private RepairOrder checkTaskStatus(Long taskId, OrderStatus expectedStatus) {
RepairOrder order = repairOrderMapper.selectById(taskId);
if (order == null) {
throw new BusinessException("工单不存在");
}
if (order.getStatus() != expectedStatus) {
throw new BusinessException("工单状态不正确");
}
// 检查是否是当前维修人员的工单
Long staffId = SecurityUtils.getCurrentUserId();
if (!order.getStaffId().equals(staffId)) {
throw new BusinessException("无权操作此工单");
}
return order;
}
private void saveProgress(RepairOrder order, String description,
List<String> images) {
RepairProgress progress = new RepairProgress();
progress.setOrderId(order.getId());
progress.setStaffId(order.getStaffId());
progress.setDescription(description);
progress.setImages(images);
progress.setCreateTime(new Date());
progressMapper.insert(progress);
}
}
3. 维修记录管理
3.1 功能描述
- 查看历史维修记录
- 维修统计分析
- 导出维修报告
- 问题分类统计
3.2 核心代码实现
前端维修记录组件:
<template>
<div class="repair-records">
<!-- 统计卡片 -->
<el-row :gutter="20" class="stat-cards">
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">今日维修</div>
<div class="stat-number">{{ stats.todayCount }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">本周维修</div>
<div class="stat-number">{{ stats.weekCount }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">本月维修</div>
<div class="stat-number">{{ stats.monthCount }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">总维修量</div>
<div class="stat-number">{{ stats.totalCount }}</div>
</el-card>
</el-col>
</el-row>
<!-- 图表展示 -->
<el-row :gutter="20" class="charts">
<el-col :span="12">
<el-card>
<div slot="header">维修类型分布</div>
<div class="chart-container">
<v-chart :options="typeChartOptions"></v-chart>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<div slot="header">维修时长分析</div>
<div class="chart-container">
<v-chart :options="timeChartOptions"></v-chart>
</div>
</el-card>
</el-col>
</el-row>
<!-- 维修记录列表 -->
<el-card class="record-list">
<div slot="header">
<span>维修记录</span>
<el-button
style="float: right"
type="primary"
@click="exportRecords">
导出报告
</el-button>
</div>
<el-table :data="recordList" v-loading="loading">
<el-table-column prop="orderNo" label="工单号" width="120">
</el-table-column>
<el-table-column prop="type" label="类型" width="100">
<template slot-scope="scope">
{{ getTypeText(scope.row.type) }}
</template>
</el-table-column>
<el-table-column prop="location" label="维修位置">
</el-table-column>
<el-table-column prop="startTime" label="开始时间" width="160">
<template slot-scope="scope">
{{ formatDateTime(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column prop="finishTime" label="完成时间" width="160">
<template slot-scope="scope">
{{ formatDateTime(scope.row.finishTime) }}
</template>
</el-table-column>
<el-table-column prop="duration" label="耗时" width="100">
<template slot-scope="scope">
{{ formatDuration(scope.row.duration) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" fixed="right">
<template slot-scope="scope">
<el-button
type="text"
@click="viewDetail(scope.row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@current-change="handlePageChange"
@size-change="handleSizeChange"
:current-page="page"
:page-sizes="[10, 20, 50]"
:page-size="size"
:total="total"
layout="total, sizes, prev, pager, next">
</el-pagination>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
stats: {
todayCount: 0,
weekCount: 0,
monthCount: 0,
totalCount: 0
},
typeChartOptions: {
title: {
text: '维修类型分布'
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
series: [{
type: 'pie',
radius: '65%',
data: []
}]
},
timeChartOptions: {
title: {
text: '维修时长分析'
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: [{
type: 'bar',
data: []
}]
},
recordList: [],
loading: false,
page: 1,
size: 10,
total: 0
}
},
created() {
this.loadStats()
this.loadChartData()
this.loadRecords()
},
methods: {
// 加载统计数据
async loadStats() {
const { data } = await this.$api.repair.getRepairStats()
this.stats = data
},
async loadChartData() {
try {
const { data } = await this.$api.repair.getChartData()
// 更新类型分布图表
this.typeChartOptions.series[0].data = data.typeStats.map(item => ({
name: this.getTypeText(item.type),
value: item.count
}))
// 更新时长分析图表
this.timeChartOptions.xAxis.data = data.timeStats.map(item => item.range)
this.timeChartOptions.series[0].data = data.timeStats.map(item => item.count)
// 触发图表更新
this.$nextTick(() => {
this.$refs.typeChart.refresh()
this.$refs.timeChart.refresh()
})
} catch (error) {
console.error('加载图表数据失败:', error)
this.$message.error('加载图表数据失败')
}
},
// 加载维修记录
async loadRecords() {
this.loading = true
try {
const params = {
page: this.page,
size: this.size
}
const { data } = await this.$api.repair.getRepairRecords(params)
this.recordList = data.records
this.total = data.total
} finally {
this.loading = false
}
},
// 导出维修报告
async exportRecords() {
try {
const blob = await this.$api.repair.exportRepairRecords()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `维修报告_${this.formatDate(new Date())}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
this.$message.error('导出失败')
}
},
// 查看详情
viewDetail(record) {
this.$router.push(`/repair/record/${record.id}`)
},
// 格式化持续时间
formatDuration(minutes) {
if (minutes < 60) {
return `${minutes}分钟`
}
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return mins > 0 ? `${hours}小时${mins}分钟` : `${hours}小时`
}
}
}
</script>
<style lang="scss" scoped>
.repair-records {
padding: 20px;
.stat-cards {
margin-bottom: 20px;
.stat-number {
font-size: 24px;
font-weight: bold;
color: #409EFF;
text-align: center;
}
}
.charts {
margin-bottom: 20px;
.chart-container {
height: 300px;
}
}
}
</style>
后端实现:
@Service
public class RepairRecordServiceImpl implements RepairRecordService {
@Autowired
private RepairOrderMapper repairOrderMapper;
@Autowired
private RepairProgressMapper progressMapper;
@Override
public RepairStats getRepairStats() {
Long staffId = SecurityUtils.getCurrentUserId();
LocalDateTime now = LocalDateTime.now();
return RepairStats.builder()
.todayCount(getCompletedCount(staffId,
now.with(LocalTime.MIN),
now.with(LocalTime.MAX)))
.weekCount(getCompletedCount(staffId,
now.with(DayOfWeek.MONDAY).with(LocalTime.MIN),
now.with(DayOfWeek.SUNDAY).with(LocalTime.MAX)))
.monthCount(getCompletedCount(staffId,
now.withDayOfMonth(1).with(LocalTime.MIN),
now.withDayOfMonth(now.getMonth().length(now.toLocalDate().isLeapYear()))
.with(LocalTime.MAX)))
.totalCount(repairOrderMapper.countByStaffId(staffId))
.build();
}
@Override
public ChartData getChartData() {
Long staffId = SecurityUtils.getCurrentUserId();
// 获取类型统计
List<TypeStat> typeStats = repairOrderMapper.getTypeStats(staffId);
// 获取时长统计
List<TimeStat> timeStats = repairOrderMapper.getTimeStats(staffId);
return ChartData.builder()
.typeStats(typeStats)
.timeStats(timeStats)
.build();
}
@Override
public IPage<RepairRecordVO> getRepairRecords(RecordQueryDTO queryDTO) {
Long staffId = SecurityUtils.getCurrentUserId();
Page<RepairOrder> page = new Page<>(queryDTO.getPage(), queryDTO.getSize());
// 构建查询条件
LambdaQueryWrapper<RepairOrder> wrapper = new LambdaQueryWrapper<RepairOrder>()
.eq(RepairOrder::getStaffId, staffId)
.eq(RepairOrder::getStatus, OrderStatus.COMPLETED)
.orderByDesc(RepairOrder::getFinishTime);
// 查询数据
IPage<RepairOrder> orderPage = repairOrderMapper.selectPage(page, wrapper);
// 转换为VO
return orderPage.convert(this::convertToRecordVO);
}
@Override
public void exportRepairRecords(HttpServletResponse response) throws IOException {
Long staffId = SecurityUtils.getCurrentUserId();
// 查询维修记录
List<RepairOrder> records = repairOrderMapper.selectList(
new LambdaQueryWrapper<RepairOrder>()
.eq(RepairOrder::getStaffId, staffId)
.eq(RepairOrder::getStatus, OrderStatus.COMPLETED)
.orderByDesc(RepairOrder::getFinishTime)
);
// 创建工作簿
SXSSFWorkbook workbook = new SXSSFWorkbook();
Sheet sheet = workbook.createSheet("维修记录");
// 创建表头
Row headerRow = sheet.createRow(0);
String[] headers = {"工单号", "报修类型", "维修位置", "开始时间",
"完成时间", "维修时长", "维修结果"};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
}
// 填充数据
int rowNum = 1;
for (RepairOrder record : records) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(record.getOrderNo());
row.createCell(1).setCellValue(record.getType().getDesc());
row.createCell(2).setCellValue(record.getLocation());
row.createCell(3).setCellValue(
DateUtil.format(record.getStartTime(), "yyyy-MM-dd HH:mm:ss"));
row.createCell(4).setCellValue(
DateUtil.format(record.getFinishTime(), "yyyy-MM-dd HH:mm:ss"));
row.createCell(5).setCellValue(
calculateDuration(record.getStartTime(), record.getFinishTime()));
row.createCell(6).setCellValue(record.getRepairResult());
}
// 设置响应头
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition",
"attachment;filename=repair_records.xlsx");
// 输出文件
workbook.write(response.getOutputStream());
workbook.close();
}
private long getCompletedCount(Long staffId, LocalDateTime start, LocalDateTime end) {
return repairOrderMapper.selectCount(
new LambdaQueryWrapper<RepairOrder>()
.eq(RepairOrder::getStaffId, staffId)
.eq(RepairOrder::getStatus, OrderStatus.COMPLETED)
.between(RepairOrder::getFinishTime,
Date.from(start.atZone(ZoneId.systemDefault()).toInstant()),
Date.from(end.atZone(ZoneId.systemDefault()).toInstant()))
);
}
private RepairRecordVO convertToRecordVO(RepairOrder order) {
RepairRecordVO vo = new RepairRecordVO();
BeanUtils.copyProperties(order, vo);
// 计算维修时长
if (order.getStartTime() != null && order.getFinishTime() != null) {
vo.setDuration(calculateDuration(order.getStartTime(), order.getFinishTime()));
}
return vo;
}
private long calculateDuration(Date startTime, Date finishTime) {
return TimeUnit.MILLISECONDS.toMinutes(finishTime.getTime() - startTime.getTime());
}
}
4. 工作量统计
4.1 功能描述
- 工作量统计分析
- 效率评估
- 工作质量评价
- 数据可视化展示
4.2 核心代码实现
前端工作量统计组件:
<template>
<div class="work-stats">
<!-- 时间范围选择 -->
<div class="filter-bar">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:picker-options="pickerOptions"
@change="handleDateChange">
</el-date-picker>
</div>
<!-- 统计指标卡片 -->
<el-row :gutter="20" class="stat-cards">
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">完成工单数</div>
<div class="stat-number">{{ stats.completedCount }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">平均处理时长</div>
<div class="stat-number">{{ formatDuration(stats.avgDuration) }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">好评率</div>
<div class="stat-number">{{ stats.satisfactionRate }}%</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div slot="header">工作效率评分</div>
<div class="stat-number">{{ stats.efficiencyScore }}</div>
</el-card>
</el-col>
</el-row>
<!-- 图表展示 -->
<el-row :gutter="20" class="charts">
<el-col :span="12">
<el-card>
<div slot="header">日工作量趋势</div>
<div class="chart-container">
<v-chart :options="workloadChartOptions"></v-chart>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<div slot="header">评价分布</div>
<div class="chart-container">
<v-chart :options="ratingChartOptions"></v-chart>
</div>
</el-card>
</el-col>
</el-row>
<!-- 工作质量分析 -->
<el-card class="quality-analysis">
<div slot="header">工作质量分析</div>
<el-table :data="qualityList" border>
<el-table-column prop="type" label="维修类型">
</el-table-column>
<el-table-column prop="count" label="完成数量">
</el-table-column>
<el-table-column prop="avgDuration" label="平均用时">
<template slot-scope="scope">
{{ formatDuration(scope.row.avgDuration) }}
</template>
</el-table-column>
<el-table-column prop="satisfactionRate" label="满意度">
<template slot-scope="scope">
{{ scope.row.satisfactionRate }}%
</template>
</el-table-column>
<el-table-column prop="score" label="综合评分">
<template slot-scope="scope">
<el-rate
v-model="scope.row.score"
disabled
show-score
text-color="#ff9900">
</el-rate>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
export default {
data() {
return {
dateRange: [],
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])
}
}]
},
stats: {
completedCount: 0,
avgDuration: 0,
satisfactionRate: 0,
efficiencyScore: 0
},
workloadChartOptions: {
title: {
text: '日工作量趋势'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
data: []
}]
},
ratingChartOptions: {
title: {
text: '评价分布'
},
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
series: [{
type: 'pie',
radius: '65%',
data: []
}]
},
qualityList: []
}
},
created() {
// 默认加载最近一个月的数据
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
this.dateRange = [start, end]
this.loadData()
},
methods: {
// 加载所有数据
async loadData() {
await Promise.all([
this.loadStats(),
this.loadChartData(),
this.loadQualityAnalysis()
])
},
// 加载统计数据
async loadStats() {
const params = {
startDate: this.dateRange[0],
endDate: this.dateRange[1]
}
const { data } = await this.$api.repair.getWorkStats(params)
this.stats = data
},
// 加载图表数据
async loadChartData() {
const params = {
startDate: this.dateRange[0],
endDate: this.dateRange[1]
}
const { data } = await this.$api.repair.getWorkChartData(params)
// 更新工作量趋势图表
this.workloadChartOptions.xAxis.data = data.workload.map(item => item.date)
this.workloadChartOptions.series[0].data = data.workload.map(item => item.count)
// 更新评价分布图表
this.ratingChartOptions.series[0].data = data.ratings.map(item => ({
name: item.level,
value: item.count
}))
},
// 加载质量分析数据
async loadQualityAnalysis() {
const params = {
startDate: this.dateRange[0],
endDate: this.dateRange[1]
}
const { data } = await this.$api.repair.getQualityAnalysis(params)
this.qualityList = data
},
// 处理日期变化
handleDateChange() {
this.loadData()
}
}
}
</script>
后端实现:
@Service
public class WorkStatsServiceImpl implements WorkStatsService {
@Autowired
private RepairOrderMapper repairOrderMapper;
@Autowired
private RepairFeedbackMapper feedbackMapper;
@Override
public WorkStats getWorkStats(Date startDate, Date endDate) {
Long staffId = SecurityUtils.getCurrentUserId();
// 查询完成工单数
long completedCount = repairOrderMapper.countCompleted(staffId, startDate, endDate);
// 计算平均处理时长
double avgDuration = repairOrderMapper.calculateAvgDuration(staffId, startDate, endDate);
// 计算好评率
double satisfactionRate = calculateSatisfactionRate(staffId, startDate, endDate);
// 计算工作效率评分
double efficiencyScore = calculateEfficiencyScore(
completedCount, avgDuration, satisfactionRate);
return WorkStats.builder()
.completedCount(completedCount)
.avgDuration(avgDuration)
.satisfactionRate(satisfactionRate)
.efficiencyScore(efficiencyScore)
.build();
}
@Override
public WorkChartData getWorkChartData(Date startDate, Date endDate) {
Long staffId = SecurityUtils.getCurrentUserId();
// 获取日工作量数据
List<DailyWorkload> workload = repairOrderMapper.getDailyWorkload(
staffId, startDate, endDate);
// 获取评价分布数据
List<RatingDistribution> ratings = feedbackMapper.getRatingDistribution(
staffId, startDate, endDate);
return WorkChartData.builder()
.workload(workload)
.ratings(ratings)
.build();
}
@Override
public List<QualityAnalysis> getQualityAnalysis(Date startDate, Date endDate) {
Long staffId = SecurityUtils.getCurrentUserId();
// 按维修类型分组统计
return repairOrderMapper.getQualityAnalysisByType(staffId, startDate, endDate);
}
private double calculateSatisfactionRate(Long staffId, Date startDate, Date endDate) {
// 获取评价总数和好评数
long totalCount = feedbackMapper.countFeedbacks(staffId, startDate, endDate);
long goodCount = feedbackMapper.countGoodFeedbacks(staffId, startDate, endDate);
return totalCount > 0 ? (goodCount * 100.0 / totalCount) : 0;
}
private double calculateEfficiencyScore(long completedCount, double avgDuration,
double satisfactionRate) {
// 根据完成数量、平均时长和满意度综合计算效率评分
double timeScore = Math.max(0, 100 - avgDuration / 60); // 超过1小时开始扣分
return (completedCount * 0.4 + timeScore * 0.3 + satisfactionRate * 0.3);
}
}
这样就完成了维修人员功能模块的主要实现,包括:
- 任务接收和处理
- 进度更新和完工确认
- 维修记录管理
- 工作量统计分析
系统通过这些功能帮助维修人员更好地管理和完成维修工作,同时也为管理层提供了工作质量和效率的评估依据。
管理员功能模块详解
1. 用户管理
1.1 功能描述
- 用户列表查询
- 用户信息维护
- 用户状态管理
- 用户权限设置
- 批量导入导出
1.2 核心代码实现
前端用户管理组件:
<template>
<div class="user-management">
<!-- 操作栏 -->
<div class="operation-bar">
<el-button type="primary" @click="showAddDialog">
新增用户
</el-button>
<el-button type="success" @click="importUsers">
批量导入
</el-button>
<el-button type="warning" @click="exportUsers">
导出用户
</el-button>
<el-upload
class="upload-btn"
action="/api/admin/user/import"
:show-file-list="false"
:on-success="handleImportSuccess"
:before-upload="beforeImport">
<el-button type="text">下载模板</el-button>
</el-upload>
</div>
<!-- 搜索栏 -->
<el-form :inline="true" :model="queryForm" class="search-form">
<el-form-item label="用户名">
<el-input v-model="queryForm.username" placeholder="请输入用户名">
</el-input>
</el-form-item>
<el-form-item label="用户类型">
<el-select v-model="queryForm.type" clearable>
<el-option label="学生" value="STUDENT"></el-option>
<el-option label="教师" value="TEACHER"></el-option>
<el-option label="职工" value="STAFF"></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryForm.status" clearable>
<el-option label="正常" value="NORMAL"></el-option>
<el-option label="禁用" value="DISABLED"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 用户列表 -->
<el-table
:data="userList"
v-loading="loading"
border>
<el-table-column type="selection" width="55">
</el-table-column>
<el-table-column prop="username" label="用户名">
</el-table-column>
<el-table-column prop="realName" label="姓名">
</el-table-column>
<el-table-column prop="type" label="用户类型">
<template slot-scope="scope">
{{ getUserType(scope.row.type) }}
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号">
</el-table-column>
<el-table-column prop="email" label="邮箱">
</el-table-column>
<el-table-column prop="status" label="状态">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 'NORMAL' ? 'success' : 'danger'">
{{ scope.row.status === 'NORMAL' ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.row)">
编辑
</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row)">
删除
</el-button>
<el-button
size="mini"
type="warning"
@click="handleResetPwd(scope.row)">
重置密码
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="page"
:page-sizes="[10, 20, 50, 100]"
:page-size="size"
:total="total"
layout="total, sizes, prev, pager, next">
</el-pagination>
<!-- 新增/编辑对话框 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="500px">
<el-form :model="userForm" :rules="rules" ref="userForm" label-width="100px">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username"
:disabled="dialogType === 'edit'">
</el-input>
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input v-model="userForm.realName"></el-input>
</el-form-item>
<el-form-item label="用户类型" prop="type">
<el-select v-model="userForm.type">
<el-option label="学生" value="STUDENT"></el-option>
<el-option label="教师" value="TEACHER"></el-option>
<el-option label="职工" value="STAFF"></el-option>
</el-select>
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="userForm.phone"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userForm.email"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-switch
v-model="userForm.status"
active-value="NORMAL"
inactive-value="DISABLED">
</el-switch>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
// 手机号验证规则
const validatePhone = (rule, value, callback) => {
if (!value) {
callback()
} else if (!/^1[3-9]\d{9}$/.test(value)) {
callback(new Error('请输入正确的手机号'))
} else {
callback()
}
}
return {
// 查询表单
queryForm: {
username: '',
type: '',
status: ''
},
// 用户列表
userList: [],
loading: false,
// 分页
page: 1,
size: 10,
total: 0,
// 对话框
dialogVisible: false,
dialogType: 'add', // add or edit
userForm: {
username: '',
realName: '',
type: '',
phone: '',
email: '',
status: 'NORMAL'
},
// 表单验证规则
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 4, max: 20, message: '长度在 4 到 20 个字符', trigger: 'blur' }
],
realName: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择用户类型', trigger: 'change' }
],
phone: [
{ validator: validatePhone, trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
]
}
}
},
computed: {
dialogTitle() {
return this.dialogType === 'add' ? '新增用户' : '编辑用户'
}
},
created() {
this.loadUserList()
},
methods: {
// 加载用户列表
async loadUserList() {
this.loading = true
try {
const params = {
...this.queryForm,
page: this.page,
size: this.size
}
const { data } = await this.$api.admin.getUserList(params)
this.userList = data.records
this.total = data.total
} finally {
this.loading = false
}
},
// 显示新增对话框
showAddDialog() {
this.dialogType = 'add'
this.userForm = {
username: '',
realName: '',
type: '',
phone: '',
email: '',
status: 'NORMAL'
}
this.dialogVisible = true
},
// 显示编辑对话框
handleEdit(row) {
this.dialogType = 'edit'
this.userForm = { ...row }
this.dialogVisible = true
},
// 提交表单
async submitForm() {
try {
await this.$refs.userForm.validate()
if (this.dialogType === 'add') {
await this.$api.admin.addUser(this.userForm)
this.$message.success('添加成功')
} else {
await this.$api.admin.updateUser(this.userForm)
this.$message.success('更新成功')
}
this.dialogVisible = false
this.loadUserList()
} catch (error) {
// 表单验证失败不处理
if (error === 'cancel') return
this.$message.error('操作失败')
}
},
// 删除用户
async handleDelete(row) {
try {
await this.$confirm('确认删除该用户吗?')
await this.$api.admin.deleteUser(row.id)
this.$message.success('删除成功')
this.loadUserList()
} catch (error) {
if (error === 'cancel') return
this.$message.error('删除失败')
}
},
// 重置密码
async handleResetPwd(row) {
try {
await this.$confirm('确认重置该用户的密码吗?')
await this.$api.admin.resetUserPassword(row.id)
this.$message.success('密码重置成功')
} catch (error) {
if (error === 'cancel') return
this.$message.error('重置失败')
}
},
// 导入用户
async handleImportSuccess(response) {
if (response.code === 0) {
this.$message.success('导入成功')
this.loadUserList()
} else {
this.$message.error(response.message)
}
},
// 导出用户
async exportUsers() {
try {
const blob = await this.$api.admin.exportUsers(this.queryForm)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '用户列表.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
this.$message.error('导出失败')
}
}
}
}
</script>
后端实现:
@RestController
@RequestMapping("/api/admin/user")
public class UserManagementController {
@Autowired
private UserManagementService userManagementService;
@GetMapping("/list")
public Result getUserList(UserQueryDTO queryDTO) {
IPage<UserVO> page = userManagementService.getUserList(queryDTO);
return Result.success(page);
}
@PostMapping
public Result addUser(@RequestBody @Validated UserDTO userDTO) {
userManagementService.addUser(userDTO);
return Result.success();
}
@PutMapping
public Result updateUser(@RequestBody @Validated UserDTO userDTO) {
userManagementService.updateUser(userDTO);
return Result.success();
}
@DeleteMapping("/{id}")
public Result deleteUser(@PathVariable Long id) {
userManagementService.deleteUser(id);
return Result.success();
}
@PostMapping("/{id}/reset-password")
public Result resetPassword(@PathVariable Long id) {
userManagementService.resetPassword(id);
return Result.success();
}
@PostMapping("/import")
public Result importUsers(MultipartFile file) {
userManagementService.importUsers(file);
return Result.success();
}
@GetMapping("/export")
public void exportUsers(UserQueryDTO queryDTO, HttpServletResponse response)
throws IOException {
userManagementService.exportUsers(queryDTO, response);
}
}
@Service
public class UserManagementServiceImpl implements UserManagementService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public IPage<UserVO> getUserList(UserQueryDTO queryDTO) {
Page<User> page = new Page<>(queryDTO.getPage(), queryDTO.getSize());
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.like(StringUtils.isNotEmpty(queryDTO.getUsername()),
User::getUsername, queryDTO.getUsername())
.eq(StringUtils.isNotEmpty(queryDTO.getType()),
User::getType, queryDTO.getType())
.eq(StringUtils.isNotEmpty(queryDTO.getStatus()),
User::getStatus, queryDTO.getStatus())
.orderByDesc(User::getCreateTime);
// 查询数据
IPage<User> userPage = userMapper.selectPage(page, wrapper);
// 转换为VO
return userPage.convert(this::convertToVO);
}
@Override
@Transactional
public void addUser(UserDTO userDTO) {
// 检查用户名是否存在
if (checkUsernameExists(userDTO.getUsername())) {
throw new BusinessException("用户名已存在");
}
User user = new User();
BeanUtils.copyProperties(userDTO, user);
// 设置默认密码
user.setPassword(passwordEncoder.encode("123456"));
user.setCreateTime(new Date());
userMapper.insert(user);
}
@Override
@Transactional
public void updateUser(UserDTO userDTO) {
User user = userMapper.selectById(userDTO.getId());
if (user == null) {
throw new BusinessException("用户不存在");
}
BeanUtils.copyProperties(userDTO, user);
user.setUpdateTime(new Date());
userMapper.updateById(user);
}
@Override
@Transactional
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
@Override
@Transactional
public void resetPassword(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException("用户不存在");
}
user.setPassword(passwordEncoder.encode("123456"));
user.setUpdateTime(new Date());
userMapper.updateById(user);
}
@Override
@Transactional
public void importUsers(MultipartFile file) {
try {
EasyExcel.read(file.getInputStream(), UserImportDTO.class, new UserImportListener(this))
.sheet()
.doRead();
} catch (IOException e) {
throw new BusinessException("导入失败");
}
}
@Override
public void exportUsers(UserQueryDTO queryDTO, HttpServletResponse response)
throws IOException {
// 查询数据
List<User> userList = userMapper.selectList(
new LambdaQueryWrapper<User>()
.like(StringUtils.isNotEmpty(queryDTO.getUsername()),
User::getUsername, queryDTO.getUsername())
.eq(StringUtils.isNotEmpty(queryDTO.getType()),
User::getType, queryDTO.getType())
.eq(StringUtils.isNotEmpty(queryDTO.getStatus()),
User::getStatus, queryDTO.getStatus())
);
// 转换为导出DTO
List<UserExportDTO> exportList = userList.stream()
.map(this::convertToExportDTO)
.collect(Collectors.toList());
// 导出Excel
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=users.xlsx");
EasyExcel.write(response.getOutputStream(), UserExportDTO.class)
.sheet("用户列表")
.doWrite(exportList);
}
private boolean checkUsernameExists(String username) {
return userMapper.selectCount(
new LambdaQueryWrapper<User>()
.eq(User::getUsername, username)
) > 0;
}
private UserVO convertToVO(User user) {
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
return vo;
}
private UserExportDTO convertToExportDTO(User user) {
UserExportDTO dto = new UserExportDTO();
BeanUtils.copyProperties(user, dto);
return dto;
}
}
这样就完成了用户管理模块的主要功能实现,包括:
- 用户的增删改查
- 用户状态管理
- 密码重置
- 批量导入导出
系统采用了分页查询和条件筛选,支持灵活的用户管理操作。同时通过 EasyExcel 实现了用户数据的批量导入导出功能。