2023年微尘公共线上班【完整版】

38 阅读5分钟

下面给出一份“可运行的教育示例”:

  1. 主题紧贴《微尘聚智・平安护航:2023 公安数智化公共服务实践报告》核心思想——
    “把散落在群众手里的‘微尘数据’汇聚成智,形成对公共安全的‘护航’能力”。
  2. 形式为“Web 互动课件”,既能投在教室大屏,也能嵌入微信公众号/警务小程序。
  3. 前端仅用 HTML+CSS+原生 JS,一行框架都不引,方便警务教官二次改造。
  4. 后端用 Node.js 写最小化接口,同样可替换为 Java/SpringBoot,逻辑零耦合。

教育目标(可在课件首页一键展开)

  • 让群众/学员在 3 分钟交互里直观体会:
    “当我随手上传一张占用盲道的照片,公安如何把它汇入‘城市治理一张网’并闭环处置”。
  • 把“数智化”翻译成可感知的动画:数据微粒 → 聚智 → 事件分派 → 处置反馈 → 安全指数回升。
  • 培养“数据贡献即平安共建”意识,降低“多一事不如少一事”的沉默成本。

文件结构 public-safety-lab/ ├─ index.html // 单页课件(前端) ├─ server.js // 微型后端(Node + Express) ├─ data/ // 模拟数据库(json 文件) └─ README.md // 如何把后端换成 Java


  1. 前端:index.html(直接双击可预览静态版)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>微尘聚智 · 平安护航——互动体验室</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <style>
    :root{
      --c1:#0052d9; --c2:#f5222d; --c3:#52c41a;
      --shadow:0 4px 16px rgba(0,0,0,.08);
    }
    *{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;}
    body{background:#f3f5f9;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;}
    .card{background:#fff;border-radius:12px;box-shadow:var(--shadow);max-width:800px;width:100%;padding:40px;}
    h1{text-align:center;color:var(--c1);margin-bottom:30px;}
    .step{display:none;animation:fade .6s;}
    .step.active{display:block;}
    @keyframes fade{from{opacity:0;transform:translateY(20px);}}
    textarea{width:100%;min-height:100px;padding:10px;border:1px solid #d9d9d9;border-radius:4px;font-size:14px;resize:vertical;}
    .btn{background:var(--c1);color:#fff;border:none;border-radius:4px;padding:10px 24px;font-size:16px;cursor:pointer;transition:.3s;}
    .btn:hover{filter:brightness(1.1);}
    .preview{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px;}
    .preview img{width:100px;height:100px;object-fit:cover;border-radius:4px;border:1px solid #d9d9d9;}
    .bar{background:#e5e5e5;height:20px;border-radius:10px;overflow:hidden;margin:20px 0;position:relative;}
    .fill{background:var(--c3);height:100%;width:0%;transition:width .4s;}
    .txt-center{text-align:center;margin-top:20px;color:#666;font-size:14px;}
  </style>
</head>
<body>
  <div class="card">
    <h1>🌐 微尘聚智 · 平安护航 互动体验室</h1>

    <!-- Step1 群众随手拍 -->
    <div class="step active" id="step1">
      <h2>① 你发现道路隐患?</h2>
      <p style="margin:10px 0;">拍照+描述,即可成为“平安数据”的一粒微尘。</p>
      <input type="file" id="uploader" accept="image/*" multiple>
      <textarea id="desc" placeholder="请输入具体问题描述,如‘盲道被占用’"></textarea>
      <button class="btn" onclick="upload()">提交</button>
      <div class="preview" id="preview"></div>
    </div>

    <!-- Step2 数据汇入 -->
    <div class="step" id="step2">
      <h2>② 数据汇入城市大脑</h2>
      <p>每上传一份有效数据,聚智进度 +1。</p>
      <div class="bar"><div class="fill" id="fill"></div></div>
      <p class="txt-center">聚智中…<span id="num">0</span> 条有效数据</p>
    </div>

    <!-- Step3 事件闭环 -->
    <div class="step" id="step3">
      <h2>③ 处置闭环 & 安全指数回升</h2>
      <p>民警已到达现场,问题处理完毕→安全指数回升。</p>
      <div class="bar"><div class="fill" id="safeUp"></div></div>
      <p class="txt-center">城市实时安全指数:<b id="score">92.3</b></p>
      <button class="btn" onclick="location.reload()">再体验一次</button>
    </div>
  </div>

  <script>
    const API='http://localhost:3000'; // 后端地址
    let fileList=[];

    /* 前端压缩+预览 */
    document.getElementById('uploader').addEventListener('change',e=>{
      const files=[...e.target.files];
      const preview=document.getElementById('preview');
      preview.innerHTML='';
      files.forEach(f=>{
        const reader=new FileReader();
        reader.onload=ev=>{
          const img=new Image();
          img.src=ev.target.result;
          preview.appendChild(img);
        };
        reader.readAsDataURL(f);
      });
      fileList=files;
    });

    /* 上传&动画 */
    async function upload(){
      if(!fileList.length||!document.getElementById('desc').value.trim())return alert('请补全照片或描述');
      const fd=new FormData();
      fileList.forEach(f=>fd.append('pics',f));
      fd.append('desc',document.getElementById('desc').value);
      await fetch(API+'/report',{method:'POST',body:fd});
      /* 进入 Step2 */
      ['step1','step2','step3'].forEach(id=>document.getElementById(id).classList.remove('active'));
      document.getElementById('step2').classList.add('active');
      animateBar();
    }

    /* 进度条动画 + 数字增长 */
    function animateBar(){
      let n=0;
      const fill=document.getElementById('fill');
      const num=document.getElementById('num');
      const fakeTotal=10+Math.floor(Math.random()*10); // 模拟聚智所需条数
      const t=setInterval(async()=>{
        n++;
        fill.style.width=(n/fakeTotal*100)+'%';
        num.textContent=n;
        if(n>=fakeTotal){
          clearInterval(t);
          await fetch(API+'/close'); // 通知后端“已聚智”
          showClose();
        }
      },400);
    }

    /* 处置闭环动画 */
    function showClose(){
      ['step1','step2','step3'].forEach(id=>document.getElementById(id).classList.remove('active'));
      document.getElementById('step3').classList.add('active');
      let w=0;
      const bar=document.getElementById('safeUp');
      const score=document.getElementById('score');
      const start=92.3;
      const end=Math.min(100,start+3.2);
      const t=setInterval(()=>{
        w+=2;
        bar.style.width=w+'%';
        score.textContent=(start+(end-start)*(w/100)).toFixed(1);
        if(w>=100)clearInterval(t);
      },60);
    }
  </script>
</body>
</html>

  1. 后端:server.js(Node ≥ 14,仅 60 行)
const express=require('express');
const multer=require('multer');
const fs=require('fs');
const path=require('path');
const app=express();
const PORT=3000;

/* 允许前端跨域 */
app.use((req,res,next)=>{res.header('Access-Control-Allow-Origin','*');next();});

/* 静态数据目录 */
const DATA_DIR=path.join(__dirname,'data');
if(!fs.existsSync(DATA_DIR))fs.mkdirSync(DATA_DIR);

/* 文件上传中间件 */
const upload=multer({dest:DATA_DIR});

/* 接口 1:群众上报 */
app.post('/report',upload.array('pics'),(req,res)=>{
  const {desc}=req.body;
  const pics=req.files.map(f=>f.path);
  const id=Date.now();
  const json={id,desc,pics,time:new Date(),status:'received'};
  fs.writeFileSync(path.join(DATA_DIR,`${id}.json`),JSON.stringify(json));
  console.log('[上报]',id,desc);
  res.json({code:0,id});
});

/* 接口 2:聚智完成(前端 Step3 通知) */
app.post('/close',(req,res)=>{
  /* 这里可以调用真实警务中台 API,演示仅改状态 */
  const files=fs.readdirSync(DATA_DIR).filter(f=>f.endsWith('.json'));
  const latest=files.sort().reverse()[0];
  if(latest){
    const file=path.join(DATA_DIR,latest);
    const data=JSON.parse(fs.readFileSync(file));
    data.status='closed';
    data.closeTime=new Date();
    fs.writeFileSync(file,JSON.stringify(data));
    console.log('[闭环]',data.id);
  }
  res.json({code:0});
});

app.listen(PORT,()=>console.log(`API 已启动 http://localhost:${PORT}`));

  1. 运行步骤(2 分钟)
# 1. 装好 Node
node -v   # ≥14 即可

# 2. 一键依赖 & 启动
npm install express multer
node server.js

# 3. 前端
# 方法 A:直接用 LiveServer 打开 index.html
# 方法 B:把 index.html 丢到 Nginx/警务已有静态服务器

  1. 如何把后端换成 Java(SpringBoot)——核心逻辑对照
  • 接口 /report
    → 用 @PostMapping("/report") + @RequestPart("pics") MultipartFile[] pics
    → 文件存到 MinIO/阿里云 OSS,返回 id

  • 接口 /close
    → 根据最新 ID 把状态改 closed,可写 MySQL,也可调已有警务事件处置中台。

  • 跨域
    → 加 @CrossOrigin(origins = "*") 或网关统一处理。


  1. 课堂/展厅使用脚本(3 分钟版) ① 大屏打开 index.html → ② 学员手机扫码(同页)→ ③ 拍照上传 → ④ 大屏实时看进度条涨 → ⑤ 安全指数回升动画结束,教官总结:
    “你的一次随手拍,就是城市治理的‘事务消息’;
    当消息被消费(民警处置),系统整体安全指数 Rebalance,
    这就是公安数智化‘微尘聚智’的真实含义。”

  1. 可继续扩展的“教育彩蛋”
  • 把聚智所需条数改成“真实在线人数”,让全班一起上传,体验“并发写”与“流量突刺”。
  • 把进度条换成中国地图热力图,展示“隐患分布”→“处置后消失”。
  • 加入 Websocket,后端每闭环一条就推送,让大屏像“股票行情”一样实时跳动。
  • 让学员用 Postman 直接调 /report,理解“接口即服务”——哪怕不用前端页面,数据照样能进来。