当前位置: 首页 > article >正文

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
);

项目亮点

  1. 智能分配系统:采用工作量均衡算法,实现维修任务的智能分配

  2. 实时通知:基于 WebSocket 实现工单状态实时推送

  3. 评价反馈:引入评分机制,促进服务质量提升

  4. 数据可视化:使用 ECharts 实现维修数据的图表展示

性能优化

  1. 缓存优化
@Cacheable(value = "repair", key = "#orderId")
public RepairOrder getOrderDetail(Long orderId) {
    return repairOrderMapper.selectById(orderId);
}
  1. 分页查询优化
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")
    );
}

项目总结

通过本项目的开发,不仅实现了校园报修流程的信息化管理,也积累了全栈开发的实战经验。项目中的难点包括:

  1. 工单分配算法的优化
  2. 实时消息推送的实现
  3. 高并发场景下的性能优化

未来计划继续优化以下方面:

  • 引入微服务架构
  • 添加移动端应用
  • 集成智能预警系统

项目收获

  1. 掌握了前后端分离项目的完整开发流程
  2. 深入理解了 Spring Boot 企业级应用开发
  3. 提升了代码设计和架构能力
  4. 积累了项目管理和团队协作经验

通过这个项目,不仅提供了一个实用的校园服务平台,也为后续类似项目的开发积累了宝贵经验。

校园报修系统用户端功能详解

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);
    }
}

这样就完成了个人信息管理模块的主要功能实现,包括:

  1. 基本信息的查看和修改
  2. 密码修改
  3. 常用地址的增删改查
  4. 通知设置的管理

系统采用了前后端分离的架构,前端使用 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. 任务接收和处理
  2. 进度更新和完工确认
  3. 维修记录管理
  4. 工作量统计分析

系统通过这些功能帮助维修人员更好地管理和完成维修工作,同时也为管理层提供了工作质量和效率的评估依据。

管理员功能模块详解

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;
    }
}

这样就完成了用户管理模块的主要功能实现,包括:

  1. 用户的增删改查
  2. 用户状态管理
  3. 密码重置
  4. 批量导入导出

系统采用了分页查询和条件筛选,支持灵活的用户管理操作。同时通过 EasyExcel 实现了用户数据的批量导入导出功能。


http://www.kler.cn/a/456180.html

相关文章:

  • 深入解析Spring Boot中的@ConfigurationProperties注解
  • ​Python数据序列化模块pickle使用
  • Pandas03
  • Grok 2.0:马斯克的大模型挑战ChatGPT,AI竞争再升级
  • Git如何添加子仓库
  • C#WPF基础介绍/第一个WPF程序
  • STM32F103RCT6学习之五:ADC
  • Element Plus 日期时间选择器大于当天时间置灰
  • QT应用单例——qtsingleapplication
  • 设计模式之模板方法模式:咖啡,茶,和代码
  • 经典问题——华测
  • OpenCV相机标定与3D重建(37)计算两幅图像之间单应性矩阵(Homography Matrix)的函数findHomography()的使用
  • 【Unity3D】ECS入门学习(十一)ComponentSystem、JobComponentSystem
  • information_schema是什么?
  • Python小括号( )、中括号[ ]和大括号{}代表什么
  • 仓颉语言实战——2.名字、作用域、变量、修饰符
  • 在C#中实现事件的订阅和解除订阅
  • C++ OCR 文字识别
  • Redis——数据淘汰策略
  • 关于启动vue项目,出现:Error [ERR_MODULE_NOT_FOUND]: Cannot find module ‘xxx‘此类错误
  • Java与SQL Server数据库连接的实践与要点
  • web服务器之云主机、物理机租用、服务器托管的区别
  • sql server index
  • SQL 实战:字符串处理函数 – 数据清洗与文本格式化
  • CSS系列(41)-- Logical Properties详解
  • 数据结构课程设计/校园导游程序及通信线路设计 #3