《市政物联中台:基于SpringBoot与YOLOv5的智能井盖管理系统 - 高并发,高性能,高可用 架构设计与GIS定位实战》

1 项目背景​

        据住建部《2023年城市基础设施监测报告》显示,我国城市井盖数量已突破 ​​3.2亿个​​,年均增长率达4.5%。但传统管理模式存在三大致命缺陷:

  • ​巡检效率低下​​:人工巡检覆盖率不足40%,平均响应时间超72小时
  • ​安全隐患突出​​:2022年全国井盖相关事故达 ​​12.7万起​​,直接经济损失超18亿元
  • ​数据孤岛严重​​:市政、水务、电力等多部门数据割裂,协同效率低

        2021年国家发改委《新型城镇化建设重点任务》明确提出:​​“2025年前实现城市公共设施智能化监控覆盖率不低于60%”​​,标志着井盖管理已从“人防时代”迈入“技防时代”。

        随着城市化进程的加速,井盖作为城市基础设施的重要组成部分,其管理效率与安全性面临严峻挑战。传统管理模式依赖人工巡检,存在​​响应延迟、定位模糊、流程不透明​​等问题,导致事故频发、维护成本高昂。

2 项目介绍

        本项目旨在开发一套完整的井盖管理系统,主要功能包括井盖信息的录入、智能检测、维修工单的生成与管理、用户信息管理与推送等。系统采用前后端分离的设计模式,后端使用Spring Boot框架,前端使用Vue.js框架,并结合了MyBatis、MySQL、RabbitMQ、Redis等技术,确保系统的稳定性和高效性。

3 技术选型

  • 后端技术:
    Spring Boot: 构建RESTful API,简化开发和部署过程。
    Redis: 分布式缓存系统,提高数据读取速度和系统性能。
    Spring Security: 实现细粒度的访问控制和身份验证。
    MyBatis: 作为ORM框架,用于数据库操作。
    MySQL: 关系型数据库,存储系统数据。
    RabbitMQ: 消息队列,用于异步任务处理和高并发场景下的消息传递。

  • 前端技术:
    Vue.js: 构建用户界面,支持组件化和数据绑定。
    UniApp: 跨平台移动应用开发框架,实现iOS和Android应用的统一开发。

  • 其他技术:
    YOLOv5: 目标检测算法,用于自动识别井盖的损坏情况。
    WebSocket: 实现实时的全双工通信。
    GeoJSON: 地理空间数据格式,接入高德地图API实现精准定位

4 核心功能模块

        4.1 用户管理:

                用户注册与登录
                权限控制(管理员、检修员、维修员、普通用户)

        4.2井盖信息管理:

                井盖位置的上报与记录
                YOLOv5模型对井盖状态的智能检测

        4.3维修工单管理:

                自动生成维修工单
                工单分配与状态跟踪

        4.4实时通知与推送:

                通过WebSocket实现实时通信
                用户附近井盖状态的推送提醒

        4.5日志与监控:

                系统操作的详细日志记录
                异常情况的实时监控与报警

5 成果展示

5.1 数据看板(所有角色可见)
动态展示当前城市井盖管辖区下井盖档案

5.2 井盖档案(系统角色为检修员可见)
通过无人机或者巡逻车动态上传街边井盖信息

5.3  数据上传(系统角色为检修员可见)
接入高德地图api实现井盖“米级”定位

5.4 工单管理(普通管理员可见)
后台系统调用python脚本 根据井盖破损情况生成预工单,交由派单人员

5.4 派发工单(普通管理员可见)
按井盖周围就近施工人员进行推荐派单

5.5 维修人员接单(uniapp端)

维修人员接单完成后

5.6当然这个系统还有很多有趣的部分~~~


在线用户(浏览系统当前在线用户情况)

定时任务(不用写代码就可以发布定时任务,告别cron表达式和springTask)

数据监控(检测sql的执行情况,更快定位慢sql)

服务监控(查看自己的电脑配置等信息)‘

缓存监控(模拟redis客户端,无需再开一个redis的客户端工具,更快查看redis内部信息)

还有很多很多,当然这些就留给大家去下载代码后探索了

6 技术亮点(高性能,高并发,高可用)

高性能:

6.1 使用GEO数据类型,引入高德地图辅助定位,实现井盖“米级”定位

        使用MySQL的decimal类型存储井盖坐标:

create table manhole_cover
(
    id           bigint auto_increment comment '井盖唯一ID,主键'
        primary key,
    city_id      char(6)              not null comment '所属城市代码',
    image        varchar(255)         not null comment '图片地址',
    latitude     decimal(9, 6)        not null comment '纬度坐标,精度到米级',
    longitude    decimal(9, 6)        not null comment '经度坐标,精度到米级',
    ...
    // Other definition
    ...
)
    comment '井盖档案表';

        接入高德API应用​​:

<template>
    <div id="container" style="width: 100%; height: 100vh;">
      <button class="refresh-button" @click="refresh">刷新</button>
    </div>
  </template>
  
  <script setup>
  import { onMounted, ref } from 'vue';
  import { listCover, getCover, delCover, addCover, updateCover } from "@/api/managent/cover";

    const apiKey = ref('${你的API_KEY}');
    const coverList = ref([]);
    let map;

    onMounted(() => {
    const script = document.createElement('script');
    script.src = `https://webapi.amap.com/maps?v=1.4.15&key=${apiKey.value}`;
    script.onload = () => {
      getCoverList();
      initMap();
    };
    document.head.appendChild(script);
  });
</script>

  

6.2 WebSocket全双工通信​

        连接管理​:使用 ConcurrentHashMap 维护在线会话;心跳包检测(30秒无响应自动断开)  

@OnMessage  
public void onMessage(String message, Session session) {  
    if ("HEARTBEAT".equals(message)) {  
        session.getAsyncRemote().sendText("PONG");  
    }  
}  

        消息广播​​:管理员派单后触发全局通知,STOMP协议订阅/发布模式:

// Uniapp端订阅工单通知  
stompClient.subscribe('/topic/order', function(msg) {  
    console.log("新工单:" + msg.body);  
});  

6.3 使用redis+本地缓存,构建多级缓存,达到信息毫秒级相应

​​        缓存策略​​:

                一级缓存:Caffeine(最大10000条,过期时间5分钟)
                二级缓存:Redis Cluster(过期时间30分钟)

@Cacheable(value = "user", key = "#userId", cacheManager = "multiCache")  
public User getUser(String userId) {  
    return userMapper.selectById(userId);  
}  

6.4 使用canal实现缓存同步,保证修改数据库的同时保证用户信息时效性,准确性

        基于Canal+RabbitMQ的缓存同步架构设计​

                通过MySQL Binlog实时监听+消息队列分发,实现数据库与缓存的最终一致性

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.34</version>
</dependency>

                消息反序列化

public class CanalMessageDeserializer implements MessageConverter {
    @Override
    public Message fromMessage(org.springframework.amqp.core.Message message, String s) {
        String json = new String(message.getBody(), StandardCharsets.UTF_8);
        return JSON.parseObject(json, CanalMessage.class);
    }
}

                消费者逻辑

@Component
public class UserCacheConsumer {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @RabbitListener(queues = "cache.user")
    public void handleUserUpdate(CanalMessage message) {
        // 1. 仅处理UPDATE/INSERT事件
        if (!Arrays.asList("INSERT", "UPDATE").contains(message.getType())) return;

        // 2. 遍历变更数据
        message.getData().forEach(userData -> {
            String userId = userData.getString("id");
            
            // 3. 逻辑删除检查
            if ("1".equals(userData.getString("is_deleted"))) {
                redisTemplate.delete("user:" + userId);
            } else {
                // 4. 构造缓存Key并更新
                String cacheKey = "user:" + userId;
                redisTemplate.opsForValue().set(cacheKey, userData, 30, TimeUnit.MINUTES);
            }
        });
    }
}

高并发

6.5 RabbitMQ削峰填谷​

        消息队列配置

 spring:
  rabbitmq:
    host: 192.168.0.5
    port: 5672
    username: liwei
    password: 123456
    publisher-returns: true
    publisher-confirm-type: correlated
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 1

        消费者处理未ACK数据,实现削峰填谷

    public static final String EXCHANGE_DIRECT = "exchange.direct.cover";
    public static final String ROUTING_KEY = "cover";
    public static final String QUEUE_NAME = "queue.cover";

    ExecutorService TASK_SERVICE = Executors.newFixedThreadPool(10);

    @RabbitListener(queues = QUEUE_NAME)
    public void processMessage(String dataString, Message message, Channel channel) throws Exception {
        try{
            handleMessage(dataString);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            Boolean redelivered = message.getMessageProperties().getRedelivered();

            if(redelivered){
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            }else{
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
            throw new RuntimeException(e);
        }
    }

6.6 mysql读写分离 + 冷热数据分离,迁移过时数据

        读写分离:使用Sharding-JDBC路由查询,写操作主库,读操作从库

spring:  
  shardingsphere:  
    datasource:  
      names: master,slave  
      master:  
        url: jdbc:mysql://master:3306/manhole  
      slave:  
        url: jdbc:mysql://slave:3306/manhole  
    rules:  
      replica-query:  
        load-balancers:  
          type: ROUND_ROBIN  

​        ​冷热分离​:
                热数据:近一个月的操作日志
                冷数据:一个月前的操作日志

   @Scheduled(cron = "0 0 0 30 * ?")
    public void monthlyTask(){
        List<SysOperLog> list = opeLogMapper.selectOperLogList(new SysOperLog());
        opeLogMapper.insert(list);
        Long[] ll = new Long[list.size()];
        for(int i=0;i<list.size();i++){
            ll[i] = list.get(i).getOperId();
        }
        opeLogMapper.deleteOperLogByIds(ll);
    }

6.7 使用redission分布式锁保障系统资源互斥,基于lua脚本实现系统原子性奖励扣减

        锁设计:

                锁Key按stockID分片:lock:reward:${stockId}+看门狗机制自动续期

RLock lock = redissonClient.getLock("lock:reward:" + stockId);  
lock.lock(30, TimeUnit.SECONDS); // 自动续期直到释放  

            Lua原子操作​​:保证奖励发放的原子性

    local key = KEYS[1]  
    local delta = tonumber(ARGV[1])  
    if redis.call('GET', key) >= delta then  
        return redis.call('DECRBY', key, delta)  
    else  
        return 0  
    end  

    高可用

    6.8 基于Spring Security的RABC权限控制,全面安全认证和授权支持

            动态权限模型:

                    角色层级:管理员​、检修员、维修员,普通用户 — 权限粒度细化到按钮级别

    -- 权限表结构  
    CREATE TABLE sys_role (  
        role_id   BIGINT PRIMARY KEY COMMENT '角色ID',  
        role_name VARCHAR(50) NOT NULL COMMENT '角色名称'  
    );  
    CREATE TABLE sys_menu (  
        menu_id   BIGINT PRIMARY KEY COMMENT '菜单ID',  
        menu_name VARCHAR(50) COMMENT '菜单名称',  
        perms     VARCHAR(100) COMMENT '权限标识(如sys:user:add)'  
    );  

            安全拦截:

                    自定义AccessDeniedHandler处理无权限访问异常
                    使用@PreAuthorize注解动态控制接口权限:

        /**
         * 查询井盖档案列表
         */
        @PreAuthorize("@ss.hasPermi('managent:cover:list')")
        @GetMapping("/list")
        public TableDataInfo list(Cover cover)
        {
            startPage();
            List<Cover> list = coverService.selectCoverList(cover);
            return getDataTable(list);
        }
    
        /**
         * 导出井盖档案列表
         */
        @PreAuthorize("@ss.hasPermi('managent:cover:export')")
        @Log(title = "井盖档案", businessType = BusinessType.EXPORT)
        @PostMapping("/export")
        public void export(HttpServletResponse response, Cover cover)
        {
            List<Cover> list = coverService.selectCoverList(cover);
            ExcelUtil<Cover> util = new ExcelUtil<Cover>(Cover.class);
            util.exportExcel(response, list, "井盖档案数据");
        }

      6.9 @Scheduled + 操作日志记录 + 切面注解,及时响应异常情况

              操作日志切面​​:

                      自定义注解@Log
                      记录操作耗时与异常堆栈

      protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
          {
              try
              {
                  // 获取当前的用户
                  LoginUser loginUser = SecurityUtils.getLoginUser();
      
                  // *========数据库日志=========*//
                  SysOperLog operLog = new SysOperLog();
                  operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
                  // 请求的地址
                  String ip = IpUtils.getIpAddr();
                  operLog.setOperIp(ip);
                  operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
                  if (loginUser != null)
                  {
                      operLog.setOperName(loginUser.getUserId().toString());
                      SysUser currentUser = loginUser.getUser();
                      if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept()))
                      {
                          operLog.setDeptName(currentUser.getDept().getDeptName());
                      }
                  }
      
                  if (e != null)
                  {
                      operLog.setStatus(BusinessStatus.FAIL.ordinal());
                      operLog.setErrorMsg(StringUtils.substring(Convert.toStr(e.getMessage(), ExceptionUtil.getExceptionMessage(e)), 0, 2000));
                  }
                  // 设置方法名称
                  String className = joinPoint.getTarget().getClass().getName();
                  String methodName = joinPoint.getSignature().getName();
                  operLog.setMethod(className + "." + methodName + "()");
                  // 设置请求方式
                  operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
                  // 处理设置注解上的参数
                  getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
                  // 设置消耗时间
                  operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
                  // 保存数据库
                  AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
              }
              catch (Exception exp)
              {
                  // 记录本地异常日志
                  log.error("异常信息:{}", exp.getMessage());
                  exp.printStackTrace();
              }
              finally
              {
                  TIME_THREADLOCAL.remove();
              }
          }

              定时任务​​:

          @Scheduled(cron = "0 0 * * * ?")
          public void dailyTask() {
              List<String> count = opeLogMapper.selectPersonalCount(COUNT_MAX);
              for(String i:count){
                  Long id = Long.valueOf(i);
                  SysUser user = new SysUser();
                  user.setUserId(id);
                  user.setStatus("1");
                  sysUserMapper.updateUser(user);
              }
          }

      7 项目地址

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值