From 074bcb8394fab66ce531c219e1e7de7c142ff2d5 Mon Sep 17 00:00:00 2001
From: doum <doum>
Date: 星期五, 29 五月 2026 11:07:10 +0800
Subject: [PATCH] 新增智能电表、空调管理

---
 admin/src/components/common/Menu.vue                                                                               |   49 
 admin/src/views/business/components/YwElectricalRemote.vue                                                         |  239 ++++
 admin/public/template/yw_device.xlsx                                                                               |    0 
 server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/ConditionerUtil.java                        |   25 
 admin/src/components/business/DailyEnergyTrendPanel.vue                                                            |  240 ++++
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java               |   15 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java   |    3 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java      |  228 +++
 admin/src/views/business/ywcustomerrecharge.vue                                                                    |   12 
 admin/src/components/business/OperaDeviceImportWindow.vue                                                          |   86 +
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwCustomerRechargeCloudController.java            |    3 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java |  293 +++-
 server/db/business.yw_device.permissions.sql                                                                       |    1 
 admin/src/components/business/ElectricalWarningWorkbench.vue                                                       |  310 +++++
 admin/src/views/operation/device.vue                                                                               |    7 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwWorkDeskCloutController.java                    |   36 
 admin/src/components/business/commonFunctions.vue                                                                  |   25 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwDeviceCloudController.java                      |   15 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/admin/request/DeviceImport.java                         |   64 +
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwDeviceService.java                       |    6 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalWarningServiceImpl.java   |   33 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/DailyEnergyStatVO.java                      |   24 
 admin/src/utils/menu.js                                                                                            |   68 +
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalWarningService.java            |    3 
 admin/src/views/index.vue                                                                                          | 1002 ++++++++++------
 admin/src/api/Inspection/device.js                                                                                 |    5 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalActions.java                 |    2 
 admin/src/api/ywWorkDesk.js                                                                                        |   22 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/WarningTypeStatVO.java                      |   22 
 admin/src/views/business/ywelectricalactions.vue                                                                   |    4 
 server/db/business.yw_device.importExcel.grant.sql                                                                 |   12 
 admin/src/views/business/components/YwCustomerConditionerTab.vue                                                   |  245 +++-
 server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleControlApiRequest.java         |    2 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwDeviceServiceImpl.java              |  332 +++++
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java       |    8 
 35 files changed, 2,849 insertions(+), 592 deletions(-)

diff --git a/admin/public/template/yw_device.xlsx b/admin/public/template/yw_device.xlsx
new file mode 100644
index 0000000..4dda1d0
--- /dev/null
+++ b/admin/public/template/yw_device.xlsx
Binary files differ
diff --git a/admin/src/api/Inspection/device.js b/admin/src/api/Inspection/device.js
index d565e90..9cd6a09 100644
--- a/admin/src/api/Inspection/device.js
+++ b/admin/src/api/Inspection/device.js
@@ -52,3 +52,8 @@
 export function getDeviceCateData (data) {
   return request.post('/visitsAdmin/cloudService/business/ywDevice/getDeviceCateData', data)
 }
+
+// 鎵归噺瀵煎叆
+export function importExcel (data) {
+  return request.post('/visitsAdmin/cloudService/business/ywDevice/importExcel', data)
+}
diff --git a/admin/src/api/ywWorkDesk.js b/admin/src/api/ywWorkDesk.js
index 11b291f..51d7bec 100644
--- a/admin/src/api/ywWorkDesk.js
+++ b/admin/src/api/ywWorkDesk.js
@@ -24,3 +24,25 @@
 export function workDeskData () {
   return request.get('/visitsAdmin/cloudService/business/ywWorkDesk/workDeskData')
 }
+
+// 鐢佃〃鎶ヨ绫诲瀷缁熻
+export function electricalWarningStats () {
+  return request.get('/visitsAdmin/cloudService/business/ywWorkDesk/electricalWarningStats')
+}
+
+// 鐢佃〃鎶ヨ鍒嗛〉
+export function electricalWarningPage (data) {
+  return request.post('/visitsAdmin/cloudService/business/ywWorkDesk/electricalWarningPage', data, {
+    trim: true
+  })
+}
+
+// 杩�30澶╂櫤鑳界數琛ㄦ瘡鏃ョ數閲�/鐢佃垂
+export function electricalDailyEnergyStats () {
+  return request.get('/visitsAdmin/cloudService/business/ywWorkDesk/electricalDailyEnergyStats')
+}
+
+// 杩�30澶╃┖璋冨鑱旀満姣忔棩鐢甸噺/鐢佃垂
+export function conditionerDailyEnergyStats () {
+  return request.get('/visitsAdmin/cloudService/business/ywWorkDesk/conditionerDailyEnergyStats')
+}
diff --git a/admin/src/components/business/DailyEnergyTrendPanel.vue b/admin/src/components/business/DailyEnergyTrendPanel.vue
new file mode 100644
index 0000000..211e067
--- /dev/null
+++ b/admin/src/components/business/DailyEnergyTrendPanel.vue
@@ -0,0 +1,240 @@
+<template>
+  <div class="daily-energy-panel">
+    <div class="panel-header">
+      <h3 class="panel-header__title">{{ title }}</h3>
+    </div>
+    <div class="panel_body" v-loading="loading">
+      <div class="chart_box">
+        <div ref="mixChart" class="chart_item"></div>
+        <div v-if="!loading && !hasData" class="chart_empty">鏆傛棤鏁版嵁</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+export default {
+  name: 'DailyEnergyTrendPanel',
+  props: {
+    title: {
+      type: String,
+      required: true
+    },
+    loadData: {
+      type: Function,
+      required: true
+    }
+  },
+  data () {
+    return {
+      loading: false,
+      statList: [],
+      chartInstance: null
+    }
+  },
+  computed: {
+    hasData () {
+      return this.statList.some(item => Number(item.totalKwh) > 0 || Number(item.totalFee) > 0)
+    }
+  },
+  mounted () {
+    this.fetchStats()
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy () {
+    window.removeEventListener('resize', this.handleResize)
+    this.disposeChart()
+  },
+  methods: {
+    fetchStats () {
+      this.loading = true
+      this.loadData()
+        .then(res => {
+          this.statList = res || []
+          this.$nextTick(() => {
+            this.renderChart()
+          })
+        })
+        .finally(() => {
+          this.loading = false
+        })
+    },
+    disposeChart () {
+      if (this.chartInstance) {
+        this.chartInstance.dispose()
+        this.chartInstance = null
+      }
+    },
+    renderChart () {
+      const chartDom = this.$refs.mixChart
+      if (!chartDom) {
+        return
+      }
+      if (!this.chartInstance) {
+        this.chartInstance = echarts.init(chartDom)
+      }
+      const dates = this.statList.map(item => item.statDate.substring(5))
+      const kwhData = this.statList.map(item => Number(item.totalKwh || 0))
+      const feeData = this.statList.map(item => Number(item.totalFee || 0))
+      this.chartInstance.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'cross' },
+          backgroundColor: 'rgba(255,255,255,0.96)',
+          borderColor: '#EEF2F8',
+          borderWidth: 1,
+          textStyle: { color: '#333', fontSize: 12 },
+          extraCssText: 'border-radius: 6px;'
+        },
+        legend: {
+          data: ['鎬荤數閲�(kWh)', '鎬荤數璐�(鍏�)'],
+          top: 0,
+          right: 0,
+          textStyle: { fontSize: 12, color: '#666' }
+        },
+        grid: {
+          left: 50,
+          right: 50,
+          top: 36,
+          bottom: 28
+        },
+        xAxis: {
+          type: 'category',
+          data: dates,
+          axisLabel: {
+            fontSize: 11,
+            color: '#999',
+            interval: 4
+          },
+          axisLine: { lineStyle: { color: '#E8E8E8' } }
+        },
+        yAxis: [
+          {
+            type: 'value',
+            name: 'kWh',
+            nameTextStyle: { color: '#999', fontSize: 11 },
+            axisLabel: { fontSize: 11, color: '#999' },
+            splitLine: { lineStyle: { color: '#F0F0F0' } }
+          },
+          {
+            type: 'value',
+            name: '鍏�',
+            nameTextStyle: { color: '#999', fontSize: 11 },
+            axisLabel: { fontSize: 11, color: '#999' },
+            splitLine: { show: false }
+          }
+        ],
+        series: [
+          {
+            name: '鎬荤數閲�(kWh)',
+            type: 'bar',
+            barMaxWidth: 24,
+            itemStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  { offset: 0, color: '#5B9CF5' },
+                  { offset: 1, color: '#3E80EF' }
+                ]
+              },
+              borderRadius: [3, 3, 0, 0]
+            },
+            data: kwhData
+          },
+          {
+            name: '鎬荤數璐�(鍏�)',
+            type: 'line',
+            smooth: true,
+            yAxisIndex: 1,
+            symbol: 'circle',
+            symbolSize: 5,
+            itemStyle: { color: '#FFB020' },
+            lineStyle: { width: 2, color: '#FFB020' },
+            areaStyle: {
+              color: {
+                type: 'linear',
+                x: 0,
+                y: 0,
+                x2: 0,
+                y2: 1,
+                colorStops: [
+                  { offset: 0, color: 'rgba(255, 176, 32, 0.25)' },
+                  { offset: 1, color: 'rgba(255, 176, 32, 0.02)' }
+                ]
+              }
+            },
+            data: feeData
+          }
+        ]
+      }, true)
+    },
+    handleResize () {
+      if (this.chartInstance) {
+        this.chartInstance.resize()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.daily-energy-panel {
+  flex: 1;
+  min-width: 0;
+  background: #FFFFFF;
+  border-radius: 10px;
+  padding: 20px;
+  box-sizing: border-box;
+
+  .panel-header {
+    margin-bottom: 16px;
+
+    &__title {
+      margin: 0;
+      padding-left: 10px;
+      font-weight: 600;
+      font-size: 18px;
+      color: #222222;
+      line-height: 1.4;
+      border-left: 3px solid #3E80EF;
+    }
+  }
+
+  .panel_body {
+    min-height: 280px;
+  }
+
+  .chart_box {
+    position: relative;
+    width: 100%;
+    padding: 12px;
+    background: #FAFBFE;
+    border-radius: 8px;
+    border: 1px solid #EEF2F8;
+    box-sizing: border-box;
+
+    .chart_item {
+      width: 100%;
+      height: 280px;
+    }
+
+    .chart_empty {
+      position: absolute;
+      inset: 12px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #999;
+      font-size: 14px;
+      background: #FAFBFE;
+      border-radius: 6px;
+    }
+  }
+}
+</style>
diff --git a/admin/src/components/business/ElectricalWarningWorkbench.vue b/admin/src/components/business/ElectricalWarningWorkbench.vue
new file mode 100644
index 0000000..353e8cd
--- /dev/null
+++ b/admin/src/components/business/ElectricalWarningWorkbench.vue
@@ -0,0 +1,310 @@
+<template>
+  <div class="electrical-warning-panel">
+    <div class="panel-header">
+      <h3 class="panel-header__title">鏃ュ父鐢甸噺绠$悊鎶ヨ淇℃伅</h3>
+    </div>
+    <div class="panel_body">
+      <div class="chart_box" v-loading="chartLoading">
+        <div ref="warningChart" class="warning_chart"></div>
+        <div v-if="!chartLoading && !chartData.length" class="chart_empty">鏆傛棤鎶ヨ鏁版嵁</div>
+      </div>
+      <div class="list_box">
+        <el-table
+          v-loading="listLoading"
+          :data="warningList"
+          stripe
+          highlight-current-row
+          class="warning_table"
+          @row-click="handleRowClick"
+        >
+          <el-table-column label="搴忓彿" width="60" align="center">
+            <template slot-scope="{ $index }">
+              {{ (pagination.page - 1) * pagination.pageSize + $index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="electricalName" label="鐢佃〃鍚嶇О" min-width="120" align="center" show-overflow-tooltip />
+          <el-table-column prop="deviceAddress" label="鐢佃〃鍦板潃" min-width="120" align="center" show-overflow-tooltip />
+          <el-table-column label="鎶ヨ椤�" min-width="160" align="center">
+            <template slot-scope="{ row }">
+              <span v-if="row.warningName" class="warn-tag">{{ row.warningName }}</span>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="鎶ヨ鏃堕棿" min-width="150" align="center">
+            <template slot-scope="{ row }">
+              {{ formatDateTime(row.startTime) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="msg" label="鎶ヨ璇︽儏" min-width="160" align="center" show-overflow-tooltip />
+        </el-table>
+        <div class="pagination_wrap">
+          <Pagination
+            @size-change="handleSizeChange"
+            @current-change="loadWarningList"
+            :pagination="pagination"
+          />
+        </div>
+      </div>
+    </div>
+    <YwElectricalRemote ref="remoteWindow" />
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import dayjs from 'dayjs'
+import Pagination from '@/components/common/Pagination'
+import { electricalWarningStats, electricalWarningPage } from '@/api/ywWorkDesk'
+import YwElectricalRemote from '@/views/business/components/YwElectricalRemote'
+
+const CHART_COLORS = ['#8EC5FF', '#FF8C69', '#6B5BFF', '#52C41A', '#FFB020', '#9254DE', '#36CFC9', '#F759AB']
+
+export default {
+  name: 'ElectricalWarningWorkbench',
+  components: { Pagination, YwElectricalRemote },
+  data () {
+    return {
+      chartLoading: false,
+      listLoading: false,
+      chartData: [],
+      warningList: [],
+      chartInstance: null,
+      pagination: {
+        page: 1,
+        pageSize: 10,
+        total: 0
+      }
+    }
+  },
+  mounted () {
+    this.loadStats()
+    this.loadWarningList(1)
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy () {
+    window.removeEventListener('resize', this.handleResize)
+    if (this.chartInstance) {
+      this.chartInstance.dispose()
+      this.chartInstance = null
+    }
+  },
+  methods: {
+    loadStats () {
+      this.chartLoading = true
+      electricalWarningStats()
+        .then(res => {
+          this.chartData = (res || []).map((item, index) => ({
+            name: `銆愮數鑳借〃銆�${item.warningName}(${item.count})`,
+            value: item.count,
+            itemStyle: { color: CHART_COLORS[index % CHART_COLORS.length] }
+          }))
+          this.$nextTick(() => {
+            this.renderChart()
+          })
+        })
+        .finally(() => {
+          this.chartLoading = false
+        })
+    },
+    loadWarningList (page) {
+      this.listLoading = true
+      if (page) {
+        this.pagination.page = page
+      }
+      electricalWarningPage({
+        page: this.pagination.page,
+        capacity: this.pagination.pageSize,
+        model: {}
+      })
+        .then(res => {
+          this.warningList = res.records || []
+          this.pagination.total = res.total || 0
+        })
+        .finally(() => {
+          this.listLoading = false
+        })
+    },
+    handleSizeChange (size) {
+      this.pagination.pageSize = size
+      this.loadWarningList(1)
+    },
+    renderChart () {
+      const chartDom = this.$refs.warningChart
+      if (!chartDom) {
+        return
+      }
+      if (!this.chartInstance) {
+        this.chartInstance = echarts.init(chartDom)
+      }
+      this.chartInstance.setOption({
+        tooltip: {
+          trigger: 'item',
+          formatter: '{b}: {c} ({d}%)',
+          backgroundColor: 'rgba(255,255,255,0.96)',
+          borderColor: '#EEF2F8',
+          borderWidth: 1,
+          textStyle: { color: '#333', fontSize: 12 }
+        },
+        legend: {
+          type: 'scroll',
+          orient: 'vertical',
+          bottom: 8,
+          left: 'center',
+          height: 80,
+          pageButtonItemGap: 6,
+          pageIconSize: 10,
+          itemWidth: 12,
+          itemHeight: 12,
+          itemGap: 8,
+          textStyle: {
+            fontSize: 12,
+            color: '#666'
+          }
+        },
+        series: [
+          {
+            type: 'pie',
+            radius: ['45%', '62%'],
+            center: ['50%', '38%'],
+            avoidLabelOverlap: true,
+            label: {
+              show: false
+            },
+            labelLine: {
+              show: false
+            },
+            data: this.chartData
+          }
+        ]
+      }, true)
+    },
+    handleResize () {
+      if (this.chartInstance) {
+        this.chartInstance.resize()
+      }
+    },
+    handleRowClick (row) {
+      if (!row.electricalId) {
+        this.$message.warning('璇ユ姤璀︽湭鍏宠仈鐢佃〃锛屾棤娉曟墦寮�杩滅▼鎺у埗')
+        return
+      }
+      this.$refs.remoteWindow.open({ id: row.electricalId }, 'basic')
+    },
+    formatDateTime (value) {
+      if (!value) {
+        return '-'
+      }
+      return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.electrical-warning-panel {
+  width: 100%;
+  background: #FFFFFF;
+  border-radius: 10px;
+  padding: 20px;
+  box-sizing: border-box;
+
+  .panel-header {
+    margin-bottom: 16px;
+
+    &__title {
+      margin: 0;
+      padding-left: 10px;
+      font-weight: 600;
+      font-size: 18px;
+      color: #222222;
+      line-height: 1.4;
+      border-left: 3px solid #3E80EF;
+    }
+  }
+
+  .panel_body {
+    display: flex;
+    align-items: stretch;
+    gap: 20px;
+  }
+
+  .chart_box {
+    position: relative;
+    width: 400px;
+    flex-shrink: 0;
+    min-height: 340px;
+    padding: 12px;
+    background: #FAFBFE;
+    border-radius: 8px;
+    border: 1px solid #EEF2F8;
+    box-sizing: border-box;
+
+    .warning_chart {
+      width: 100%;
+      height: 316px;
+    }
+
+    .chart_empty {
+      position: absolute;
+      inset: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #999;
+      font-size: 14px;
+    }
+  }
+
+  .list_box {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    padding-left: 4px;
+    border-left: 1px solid #EEF2F8;
+  }
+
+  .warning_table {
+    width: 100%;
+
+    ::v-deep tbody tr {
+      cursor: pointer;
+    }
+  }
+
+  .pagination_wrap {
+    margin-top: 12px;
+  }
+
+  .warn-tag {
+    display: inline-block;
+    color: #f56c6c;
+    border: 1px solid #f56c6c;
+    border-radius: 4px;
+    padding: 0 6px;
+    margin: 2px;
+    font-size: 12px;
+    line-height: 20px;
+    white-space: nowrap;
+  }
+}
+
+@media (max-width: 1200px) {
+  .electrical-warning-panel {
+    .panel_body {
+      flex-direction: column;
+    }
+
+    .chart_box {
+      width: 100%;
+    }
+
+    .list_box {
+      padding-left: 0;
+      border-left: none;
+      padding-top: 4px;
+      border-top: 1px solid #EEF2F8;
+    }
+  }
+}
+</style>
diff --git a/admin/src/components/business/OperaDeviceImportWindow.vue b/admin/src/components/business/OperaDeviceImportWindow.vue
new file mode 100644
index 0000000..58f0daa
--- /dev/null
+++ b/admin/src/components/business/OperaDeviceImportWindow.vue
@@ -0,0 +1,86 @@
+<template>
+  <el-dialog
+      class="center-title"
+      :title="title"
+      width="500px"
+      top="30vh"
+      :visible.sync="visible"
+      :confirm-working="isWorking"
+      @confirm="confirm"
+  >
+    <p class="tip-warn"><i class="el-icon-warning"></i>瀵煎叆璇存槑锛�<br>
+      1.璇峰厛涓嬭浇鏂囦欢妯℃澘锛屽苟鎸夌収妯℃澘瑕佹眰濉啓琛ㄦ牸鍐呭;<br>
+      2.甯�*涓哄繀濉」锛屽叧鑱旀埧婧愭牸寮忎负锛氭ゼ瀹�/妤煎眰/鎴挎簮;<br>
+      3.璁惧鍒嗙被鏍煎紡涓猴細涓�绾у垎绫�/浜岀骇鍒嗙被;<br>
+    </p>
+    <el-form class="demo-form-inline">
+      <el-form-item label="璁惧鍚嶅崟" required>
+        <div style="width: 100%;display: flex;align-items: center;">
+          <el-button type="primary" @click="clickRef">鐐瑰嚮涓婁紶</el-button>
+          <el-button type="text" @click="exportTemplate">鐐瑰嚮涓嬭浇妯$増.EXCEL</el-button>
+        </div>
+        <div style="font-size: 14px; color: black;" v-if="fileName">{{ fileName }}</div>
+      </el-form-item>
+    </el-form>
+    <input type="file" style="position: fixed; left: 0; top: -50px;" accept=".xlsx" ref="fileExcel" @change="result" />
+    <template v-slot:footer>
+      <el-button @click="visible=false">杩斿洖</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+import BaseOpera from '@/components/base/BaseOpera'
+import GlobalWindow from '@/components/common/GlobalWindow'
+import { importExcel } from '@/api/Inspection/device'
+export default {
+  name: 'OperaDeviceImportWindow',
+  extends: BaseOpera,
+  // eslint-disable-next-line vue/no-unused-components
+  components: { GlobalWindow },
+  data () {
+    return {
+      importing: false,
+      fileName: ''
+    }
+  },
+  methods: {
+    open (title) {
+      this.title = title
+      this.fileName = ''
+      this.visible = true
+    },
+    exportTemplate () {
+      window.open('/template/yw_device.xlsx')
+    },
+    clickRef () {
+      this.$refs.fileExcel.click()
+    },
+    result (e) {
+      const file = e.target.files[0]
+      if (!file) {
+        return
+      }
+      this.fileName = file.name
+      const data = new FormData()
+      data.append('file', file)
+      importExcel(data)
+        .then(res => {
+          this.$message.success(res || '鎿嶄綔鎴愬姛')
+          this.$emit('success')
+          this.visible = false
+        })
+        .catch(() => {
+          this.fileName = ''
+        })
+        .finally(() => {
+          this.$refs.fileExcel.value = null
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/admin/src/components/business/commonFunctions.vue b/admin/src/components/business/commonFunctions.vue
index 15ce0d7..b3eb492 100644
--- a/admin/src/components/business/commonFunctions.vue
+++ b/admin/src/components/business/commonFunctions.vue
@@ -9,7 +9,7 @@
             <span>璇锋牴鎹娇鐢ㄤ範鎯嚜瀹氫箟甯哥敤鍔熻兘锛屾敮鎸佹嫋鍔ㄦ帓</span>
             <draggable v-model="filterList" chosenClass="chosen" forceFallback="true" group="people" animation="1000">
                 <transition-group class="dra">
-                    <div class="list" v-for="(item, index) in filterList" :key="index">
+                    <div class="list" v-for="item in filterList" :key="item.path || item.id">
                         <div class="list_checkbox">
                             <el-checkbox v-model="item.checked"> </el-checkbox>
                         </div>
@@ -25,7 +25,7 @@
 <script>
   import BaseOpera from '@/components/base/BaseOpera'
   import GlobalWindow from '@/components/common/GlobalWindow'
-  import { updMyYwQuickModel, getDefaultYwQuickList } from '@/api/ywWorkDesk'
+  import { updMyYwQuickModel, getDefaultYwQuickList, getYwQuickList } from '@/api/ywWorkDesk'
   import draggable from 'vuedraggable'
   export default {
     name: 'commonFunctions',
@@ -39,14 +39,25 @@
     methods: {
       open (title) {
         this.title = title
-        getDefaultYwQuickList({})
-          .then(res => {
-            res.forEach(item => {
-              item.checked = false
+        Promise.all([getDefaultYwQuickList(), getYwQuickList()])
+          .then(([allList, myList]) => {
+            const selectedPaths = (myList || []).map(item => item.path)
+            const pathOrder = new Map(selectedPaths.map((path, index) => [path, index]))
+            const selected = []
+            const unselected = []
+            ;(allList || []).forEach(item => {
+              const entry = { ...item, checked: selectedPaths.includes(item.path) }
+              if (entry.checked) {
+                selected.push(entry)
+              } else {
+                unselected.push(entry)
+              }
             })
-            this.filterList = res
+            selected.sort((a, b) => (pathOrder.get(a.path) || 0) - (pathOrder.get(b.path) || 0))
+            this.filterList = [...selected, ...unselected]
             this.visible = true
           })
+          .catch(e => this.$tip.apiFailed(e))
       },
       confirm () {
         this.isWorking = true
diff --git a/admin/src/components/common/Menu.vue b/admin/src/components/common/Menu.vue
index 75a48d1..6690926 100644
--- a/admin/src/components/common/Menu.vue
+++ b/admin/src/components/common/Menu.vue
@@ -27,33 +27,50 @@
 import { mapState } from 'vuex'
 import MenuItems from './MenuItems'
 import Scrollbar from './Scrollbar'
+import { findMenuByUrl, getOpenMenuIndexes } from '@/utils/menu'
 export default {
   name: 'Menu',
   components: { Scrollbar, MenuItems },
   computed: {
     ...mapState(['menuData']),
     // 閫変腑鐨勮彍鍗昳ndex
-    activeIndex() {
-      let path = this.$route.path
-      if (path.endsWith('/')) {
-        path = path.substring(0, path.length - 1)
-      }
-      const menuConfig = this.__getMenuConfig(path, 'index', this.menuData.list)
-      if (menuConfig == null) {
-        return null
-      } else {
-        this.$store.commit('pushtags', menuConfig)
-      }
-      return menuConfig.index
+    activeIndex () {
+      const menuConfig = findMenuByUrl(this.$route.path, this.menuData.list)
+      return menuConfig ? menuConfig.index : null
     },
     // 榛樿灞曞紑鐨勮彍鍗昳ndex
-    defaultOpeneds() {
-      // return this.menuData.list.map(menu => menu.index)
-      
-      return [this.menuData.list[0].index]
+    defaultOpeneds () {
+      const openeds = getOpenMenuIndexes(this.$route.path, this.menuData.list)
+      if (openeds.length > 0) {
+        return openeds
+      }
+      return this.menuData.list.length > 0 ? [this.menuData.list[0].index] : []
+    }
+  },
+  watch: {
+    '$route': {
+      handler () {
+        this.syncMenuState()
+      },
+      immediate: true
     }
   },
   methods: {
+    syncMenuState () {
+      const menuConfig = findMenuByUrl(this.$route.path, this.menuData.list)
+      if (menuConfig) {
+        this.$store.commit('pushtags', menuConfig)
+      }
+      this.$nextTick(() => {
+        const menuRef = this.$refs.menu
+        if (!menuRef) {
+          return
+        }
+        getOpenMenuIndexes(this.$route.path, this.menuData.list).forEach(index => {
+          menuRef.open(index)
+        })
+      })
+    },
     // 澶勭悊鑿滃崟閫変腑
     handleSelect(menuIndex) {
       const menuConfig = this.__getMenuConfig(menuIndex, 'index', this.menuData.list)
diff --git a/admin/src/utils/menu.js b/admin/src/utils/menu.js
new file mode 100644
index 0000000..6c69abb
--- /dev/null
+++ b/admin/src/utils/menu.js
@@ -0,0 +1,68 @@
+function normalizePath (path) {
+  if (!path) {
+    return ''
+  }
+  let normalized = path
+  if (normalized.endsWith('/')) {
+    normalized = normalized.substring(0, normalized.length - 1)
+  }
+  return normalized
+}
+
+export function findMenuByUrl (url, menus) {
+  const target = normalizePath(url)
+  for (const menu of menus || []) {
+    if (menu.url && normalizePath(menu.url) === target) {
+      return menu
+    }
+    if (menu.children && menu.children.length > 0) {
+      const child = findMenuByUrl(target, menu.children)
+      if (child) {
+        return child
+      }
+    }
+  }
+  return null
+}
+
+export function findMenuPathByUrl (url, menus, ancestors = []) {
+  const target = normalizePath(url)
+  for (const menu of menus || []) {
+    const chain = [...ancestors, menu]
+    if (menu.url && normalizePath(menu.url) === target) {
+      return chain
+    }
+    if (menu.children && menu.children.length > 0) {
+      const childChain = findMenuPathByUrl(target, menu.children, chain)
+      if (childChain) {
+        return childChain
+      }
+    }
+  }
+  return null
+}
+
+export function getOpenMenuIndexes (url, menus) {
+  const chain = findMenuPathByUrl(url, menus)
+  if (!chain || chain.length <= 1) {
+    return []
+  }
+  return chain.slice(0, -1).map(menu => menu.index)
+}
+
+export function navigateByMenu (router, store, path, menus) {
+  const menuConfig = findMenuByUrl(path, menus)
+  if (menuConfig) {
+    if (menuConfig.params) {
+      router.push({
+        path: menuConfig.url,
+        query: { index: menuConfig.index, param: menuConfig.params }
+      })
+    } else {
+      router.push(menuConfig.url)
+    }
+    store.commit('pushtags', menuConfig)
+    return
+  }
+  router.push({ path })
+}
diff --git a/admin/src/views/business/components/YwCustomerConditionerTab.vue b/admin/src/views/business/components/YwCustomerConditionerTab.vue
index e731bc2..7a1e404 100644
--- a/admin/src/views/business/components/YwCustomerConditionerTab.vue
+++ b/admin/src/views/business/components/YwCustomerConditionerTab.vue
@@ -1,24 +1,34 @@
 <template>
   <div v-loading="loading" class="conditioner-tab">
-    <section class="config-section">
-      <div class="section-title">璁¤垂閰嶇疆</div>
-      <el-form label-width="150px" size="small" class="config-form">
-        <el-row :gutter="24">
+    <el-form ref="configForm" :model="form" :rules="rules" label-width="150px" size="small" class="config-form">
+      <section class="config-section">
+        <div class="section-title">璁¤垂閰嶇疆</div>
+        <el-row :gutter="24" class="config-first-row">
           <el-col :span="12">
-            <el-form-item label="璁¤垂寮�鍏�">
-              <el-switch v-model="form.isPwr" :active-value="1" :inactive-value="0" active-text="寮�鍚�" inactive-text="鍏抽棴"/>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="18:00-09:00 涓嶅仠鏈�">
-              <el-switch v-model="form.isRestStop" :active-value="1" :inactive-value="0" active-text="鏄�" inactive-text="鍚�"/>
+            <el-form-item label="娆犺垂棰濆害" prop="stopMoney" class="stop-money-item">
+              <div class="el-input-group stop-money-group">
+                <el-input-number
+                  v-model="form.stopMoney"
+                  :min="0"
+                  :precision="2"
+                  :step="10"
+                  controls-position="right"
+                  class="stop-money-input"
+                />
+                <div class="el-input-group__append">鍏�</div>
+              </div>
             </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="24">
           <el-col :span="12">
-            <el-form-item label="娆犺垂棰濆害锛堝厓锛�">
-              <el-input-number v-model="form.stopMoney" :min="0" :precision="2" :step="10" controls-position="right" style="width: 100%"/>
+            <el-form-item label="璁¤垂寮�鍏�" prop="isPwr">
+              <el-switch v-model="form.isPwr" :active-value="1" :inactive-value="0" active-text="寮�鍚�" inactive-text="鍏抽棴"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="18:00-09:00 涓嶅仠鏈�" prop="isRestStop">
+              <el-switch v-model="form.isRestStop" :active-value="1" :inactive-value="0" active-text="鏄�" inactive-text="鍚�"/>
             </el-form-item>
           </el-col>
         </el-row>
@@ -32,36 +42,38 @@
             placeholder="璇疯緭鍏ュ娉紙閫夊~锛�"
           />
         </el-form-item>
-      </el-form>
-    </section>
+      </section>
 
-    <section class="config-section">
-      <div class="section-header">
-        <span class="section-title">鍏宠仈鍐呮満</span>
-        <el-button type="primary" size="small" icon="el-icon-plus" @click="openSelector">娣诲姞鍐呮満</el-button>
-      </div>
-      <el-table :data="form.conditioners" stripe size="small" class="device-table" empty-text="鏆傛湭鍏宠仈鍐呮満锛岃鐐瑰嚮娣诲姞">
-        <el-table-column label="璁惧" min-width="200" align="left" show-overflow-tooltip>
-          <template slot-scope="{ row }">{{ deviceLabel(row) }}</template>
-        </el-table-column>
-        <el-table-column prop="platformDevId" label="骞冲彴璁惧ID" min-width="110" align="center"/>
-        <el-table-column label="鍦ㄧ嚎" min-width="80" align="center">
-          <template slot-scope="{ row }">
-            <span :class="row.online === '鍦ㄧ嚎' ? 'green' : 'red'">{{ row.online || '-' }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="鐢佃垂鍗犳瘮%" min-width="130" align="center">
-          <template slot-scope="{ row }">
-            <el-input-number v-model="row.devRatio" :min="1" :max="100" size="small" controls-position="right"/>
-          </template>
-        </el-table-column>
-        <el-table-column label="鎿嶄綔" width="80" align="center" fixed="right">
-          <template slot-scope="{ $index }">
-            <el-button type="text" class="red" @click="form.conditioners.splice($index, 1)">绉婚櫎</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </section>
+      <section class="config-section">
+        <div class="section-header">
+          <span class="section-title required-title">鍏宠仈鍐呮満</span>
+          <el-button type="primary" size="small" icon="el-icon-plus" @click="openSelector">娣诲姞鍐呮満</el-button>
+        </div>
+        <el-form-item prop="conditioners" label-width="0" class="conditioners-form-item">
+          <el-table :data="form.conditioners" stripe size="small" class="device-table" empty-text="鏆傛湭鍏宠仈鍐呮満锛岃鐐瑰嚮娣诲姞">
+            <el-table-column label="璁惧" min-width="200" align="left" show-overflow-tooltip>
+              <template slot-scope="{ row }">{{ deviceLabel(row) }}</template>
+            </el-table-column>
+            <el-table-column prop="platformDevId" label="骞冲彴璁惧ID" min-width="110" align="center"/>
+            <el-table-column label="鍦ㄧ嚎" min-width="80" align="center">
+              <template slot-scope="{ row }">
+                <span :class="row.online === '鍦ㄧ嚎' ? 'green' : 'red'">{{ row.online || '-' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鐢佃垂鍗犳瘮%" min-width="130" align="center">
+              <template slot-scope="{ row }">
+                <el-input-number v-model="row.devRatio" :min="1" :max="100" size="small" controls-position="right"/>
+              </template>
+            </el-table-column>
+            <el-table-column label="鎿嶄綔" width="80" align="center" fixed="right">
+              <template slot-scope="{ $index }">
+                <el-button type="text" class="red" @click="removeConditioner($index)">绉婚櫎</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-form-item>
+      </section>
+    </el-form>
 
     <div class="footer-btns">
       <el-button type="primary" :loading="saving" v-permissions="['business:ywcustomerrecharge:bindDevice']" @click="save">淇濆瓨閰嶇疆</el-button>
@@ -105,11 +117,46 @@
       loading: false,
       saving: false,
       form: {
-        isPwr: 1,
-        isRestStop: 0,
-        stopMoney: 0,
+        isPwr: null,
+        isRestStop: null,
+        stopMoney: undefined,
         gsBz: '',
         conditioners: []
+      },
+      rules: {
+        stopMoney: [{
+          validator: (rule, value, callback) => {
+            if (value === null || value === undefined || value === '') {
+              callback(new Error('璇疯緭鍏ユ瑺璐归搴�'))
+            } else if (Number(value) < 0) {
+              callback(new Error('娆犺垂棰濆害涓嶈兘灏忎簬0'))
+            } else {
+              callback()
+            }
+          },
+          trigger: 'blur'
+        }],
+        isPwr: [{
+          validator: (rule, value, callback) => {
+            if (value === 0 || value === 1) callback()
+            else callback(new Error('璇烽�夋嫨璁¤垂寮�鍏�'))
+          },
+          trigger: 'change'
+        }],
+        isRestStop: [{
+          validator: (rule, value, callback) => {
+            if (value === 0 || value === 1) callback()
+            else callback(new Error('璇烽�夋嫨鏄惁鍋滄満'))
+          },
+          trigger: 'change'
+        }],
+        conditioners: [{
+          validator: (rule, value, callback) => {
+            if (value && value.length) callback()
+            else callback(new Error('璇疯嚦灏戝叧鑱斾竴鍙板唴鏈�'))
+          },
+          trigger: 'change'
+        }]
       },
       selectorVisible: false,
       selectorLoading: false,
@@ -143,14 +190,14 @@
         rechargeApi.conditionerPage(this.customerId, { page: 1, capacity: 500, model: {} })
       ]).then(([gs, page]) => {
         if (gs) {
-          this.form.isPwr = gs.isPwr != null ? gs.isPwr : 1
-          this.form.isRestStop = gs.isRestStop != null ? gs.isRestStop : 0
-          this.form.stopMoney = gs.stopMoney != null ? gs.stopMoney : 0
+          this.form.isPwr = gs.isPwr != null ? gs.isPwr : null
+          this.form.isRestStop = gs.isRestStop != null ? gs.isRestStop : null
+          this.form.stopMoney = gs.stopMoney != null ? gs.stopMoney : undefined
           this.form.gsBz = gs.gsBz || ''
         } else {
-          this.form.isPwr = 1
-          this.form.isRestStop = 0
-          this.form.stopMoney = 0
+          this.form.isPwr = null
+          this.form.isRestStop = null
+          this.form.stopMoney = undefined
           this.form.gsBz = ''
         }
         this.form.conditioners = (page.records || []).map(c => ({
@@ -162,29 +209,41 @@
           online: c.online,
           devRatio: c.devRatio != null ? c.devRatio : 100
         }))
+        this.$nextTick(() => {
+          if (this.$refs.configForm) {
+            this.$refs.configForm.clearValidate()
+          }
+        })
       }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.loading = false })
     },
     save () {
-      if (!this.form.conditioners.length) {
-        this.$tip.warning('璇疯嚦灏戝叧鑱斾竴鍙板唴鏈�')
-        return
-      }
-      this.saving = true
-      rechargeApi.saveGsConfig({
-        customerId: this.customerId,
-        isPwr: this.form.isPwr,
-        isRestStop: this.form.isRestStop,
-        stopMoney: this.form.stopMoney,
-        gsBz: this.form.gsBz,
-        conditioners: this.form.conditioners.map(c => ({
-          conditionerId: c.conditionerId,
-          devRatio: c.devRatio
-        }))
-      }).then(() => {
-        this.$tip.success('淇濆瓨鎴愬姛')
-        this.loadConfig()
-        this.$emit('success')
-      }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.saving = false })
+      this.$refs.configForm.validate(valid => {
+        if (!valid) return
+        this.saving = true
+        rechargeApi.saveGsConfig({
+          customerId: this.customerId,
+          isPwr: this.form.isPwr,
+          isRestStop: this.form.isRestStop,
+          stopMoney: this.form.stopMoney,
+          gsBz: this.form.gsBz,
+          conditioners: this.form.conditioners.map(c => ({
+            conditionerId: c.conditionerId,
+            devRatio: c.devRatio
+          }))
+        }).then(() => {
+          this.$tip.success('淇濆瓨鎴愬姛')
+          this.loadConfig()
+          this.$emit('success')
+        }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.saving = false })
+      })
+    },
+    removeConditioner (index) {
+      this.form.conditioners.splice(index, 1)
+      this.$nextTick(() => {
+        if (this.$refs.configForm) {
+          this.$refs.configForm.validateField('conditioners')
+        }
+      })
     },
     openSelector () {
       this.selectorVisible = true
@@ -226,6 +285,11 @@
         })
       })
       this.selectorVisible = false
+      this.$nextTick(() => {
+        if (this.$refs.configForm) {
+          this.$refs.configForm.validateField('conditioners')
+        }
+      })
     }
   }
 }
@@ -265,6 +329,31 @@
 .config-form {
   margin-bottom: 0;
 }
+.config-first-row {
+  margin-bottom: 4px;
+  padding-bottom: 4px;
+  border-bottom: 1px dashed #ebeef5;
+}
+.config-form ::v-deep .stop-money-item {
+  margin-bottom: 8px;
+}
+.stop-money-group {
+  display: inline-flex;
+  width: 100%;
+  max-width: 280px;
+  vertical-align: middle;
+}
+.stop-money-group ::v-deep .stop-money-input {
+  flex: 1;
+  width: auto;
+}
+.stop-money-group ::v-deep .el-input-number {
+  width: 100%;
+}
+.stop-money-group ::v-deep .el-input__inner {
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
 .config-form ::v-deep .el-form-item {
   margin-bottom: 12px;
 }
@@ -277,6 +366,20 @@
 .config-form ::v-deep .el-switch {
   width: auto;
 }
+.required-title::before {
+  content: '*';
+  color: #f56c6c;
+  margin-right: 4px;
+}
+.conditioners-form-item {
+  margin-bottom: 0;
+}
+.conditioners-form-item ::v-deep .el-form-item__content {
+  margin-left: 0 !important;
+}
+.conditioners-form-item ::v-deep .el-form-item__error {
+  padding-top: 4px;
+}
 .device-table {
   width: 100%;
 }
diff --git a/admin/src/views/business/components/YwElectricalRemote.vue b/admin/src/views/business/components/YwElectricalRemote.vue
index 6261d0e..571c9e1 100644
--- a/admin/src/views/business/components/YwElectricalRemote.vue
+++ b/admin/src/views/business/components/YwElectricalRemote.vue
@@ -1,6 +1,6 @@
 <template>
   <GlobalWindow title="鐢佃〃杩滅▼鎺у埗" :visible.sync="visible" width="780px" :show-confirm="false">
-    <el-tabs v-model="activeTab">
+    <el-tabs v-model="activeTab" @tab-click="onTabClick">
       <el-tab-pane label="鍩烘湰鎿嶄綔" name="basic">
         <div class="info-block">
           <p>閲囬泦鍣ㄥ彿锛歿{ info.collectorId }} <span :class="info.online === 1 ? 'green' : 'red'">{{ info.online === 1 ? '鍦ㄧ嚎' : '绂荤嚎' }}</span></p>
@@ -9,11 +9,23 @@
           <p>鐢佃〃绫诲瀷锛歿{ info.type }}</p>
           <p>鍏宠仈鎴块棿锛歿{ info.roomNames }}</p>
         </div>
+        <div class="meter-data-block" v-loading="infoLoading">
+          <div class="meter-data-block__title">鏈�鏂版妱琛ㄦ暟鎹�</div>
+          <p>褰撳墠鎬荤數閲忥細{{ totalPower }}</p>
+          <p>鐢甸噺鍚屾鏃堕棿锛歿{ latest && latest.addTime ? latest.addTime : '-' }}</p>
+          <p>褰撳墠鍓╀綑閲戦(鍏�)锛歿{ latest && latest.ye != null ? latest.ye : (info.balance != null ? info.balance : '0.0000') }}</p>
+          <p>浣欓鍚屾鏃堕棿锛歿{ balanceSyncTime }}</p>
+        </div>
         <div class="btn-row">
           <el-button type="primary" :loading="isOperating" @click="doOperate('resetPrepay', '纭娓呴浂骞跺垏鎹㈠埌棰勪粯璐规ā寮忓悧锛�')">娓呴浂骞跺垏鎹㈠埌棰勪粯璐规ā寮�</el-button>
           <el-button type="primary" :loading="isOperating" @click="doOperate('resetPostpay', '纭娓呴浂骞跺垏鎹㈠埌鍚庝粯璐规ā寮忓悧锛�')">娓呴浂骞跺垏鎹㈠埌鍚庝粯璐规ā寮�</el-button>
-          <el-button type="primary" :loading="isOperating" @click="doOperate('trip', '纭鎷夐椄鍚楋紵')">鎷夐椄</el-button>
-          <el-button type="primary" :loading="isOperating" @click="doOperate('close', '纭鍚堥椄鍚楋紵')">鍚堥椄</el-button>
+        </div>
+        <div class="btn-row btn-row--relay">
+          <span class="btn-row__label">鎷夊悎闂告搷浣�</span>
+          <el-button type="danger" :loading="isOperating" @click="doRelayOperate('trip')">鎷夐椄</el-button>
+          <el-button type="primary" :loading="isOperating" @click="doRelayOperate('close')">鍚堥椄</el-button>
+          <el-button type="warning" :loading="isOperating" @click="doRelayOperate('powerProtect')">淇濈數</el-button>
+          <el-button type="danger" plain :loading="isOperating" @click="doRelayOperate('powerProtectRelease')">瑙i櫎淇濈數</el-button>
         </div>
       </el-tab-pane>
       <el-tab-pane label="寮�鎴�" name="open">
@@ -39,18 +51,86 @@
           @confirm="doOperate('recharge', '纭鍏呭�煎悧锛�')"
         />
       </el-tab-pane>
+      <el-tab-pane label="鎺у埗璁板綍" name="records">
+        <el-table v-loading="recordsLoading" :data="recordsList" stripe size="small" class="records-table">
+          <el-table-column label="鎿嶄綔绫诲瀷" min-width="100" align="center">
+            <template slot-scope="{ row }">{{ formatActionType(row.actionType) }}</template>
+          </el-table-column>
+          <el-table-column prop="oprId" label="鎿嶄綔ID" min-width="150" align="center" show-overflow-tooltip />
+          <el-table-column label="鐘舵��" min-width="80" align="center">
+            <template slot-scope="{ row }">
+              <span :class="statusClass(row.status)">{{ formatStatus(row.status) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="resultMsg" label="鎵ц缁撴灉" min-width="140" align="center" show-overflow-tooltip>
+            <template slot-scope="{ row }">{{ row.resultMsg || '-' }}</template>
+          </el-table-column>
+          <el-table-column prop="createDate" label="鎿嶄綔鏃堕棿" min-width="150" align="center" />
+          <el-table-column label="鎶ユ枃" min-width="120" align="center">
+            <template slot-scope="{ row }">
+              <el-button type="text" :disabled="!row.requestBody" @click="openJson('璇锋眰鎶ユ枃', row.requestBody)">璇锋眰</el-button>
+              <el-button type="text" :disabled="!row.responseBody" @click="openJson('鍝嶅簲鎶ユ枃', row.responseBody)">鍝嶅簲</el-button>
+            </template>
+          </el-table-column>
+          <el-table-column label="鎿嶄綔" min-width="100" align="center" fixed="right">
+            <template slot-scope="{ row }">
+              <el-button
+                v-if="row.status === 0 && row.oprId"
+                type="text"
+                :loading="queryLoadingId === row.id"
+                v-permissions="['business:ywelectricalactions:queryResult']"
+                @click="handleQueryResult(row)"
+              >鏌ヨ缁撴灉</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination
+          small
+          @size-change="handleRecordsSizeChange"
+          @current-change="loadActionRecords"
+          :pagination="recordsPagination"
+        />
+      </el-tab-pane>
     </el-tabs>
+    <OperaInterfaceLogWindow ref="jsonWindow" />
+    <template slot="footer">
+      <el-button type="primary" icon="el-icon-refresh" :loading="isRefreshing" @click="refreshMeterData">鍒锋柊鐢佃〃鏁版嵁</el-button>
+      <el-button @click="visible = false">杩斿洖</el-button>
+    </template>
   </GlobalWindow>
 </template>
 
 <script>
 import GlobalWindow from '@/components/common/GlobalWindow'
+import Pagination from '@/components/common/Pagination'
+import OperaInterfaceLogWindow from '@/components/business/OperaInterfaceLogWindow'
 import { getRemoteInfo, operate } from '@/api/business/ywelectrical'
+import * as actionsApi from '@/api/business/ywelectricalactions'
 import AccountRechargePanel from './AccountRechargePanel'
+
+const RELAY_ACTIONS = {
+  trip: { label: '鎷夐椄', message: '纭瀵硅鐢佃〃鎵ц鎷夐椄鎿嶄綔鍚楋紵' },
+  close: { label: '鍚堥椄', message: '纭瀵硅鐢佃〃鎵ц鍚堥椄鎿嶄綔鍚楋紵' },
+  powerProtect: { label: '淇濈數', message: '纭瀵硅鐢佃〃鎵ц淇濈數鎿嶄綔鍚楋紵' },
+  powerProtectRelease: { label: '瑙i櫎淇濈數', message: '纭瀵硅鐢佃〃鎵ц瑙i櫎淇濈數鎿嶄綔鍚楋紵' }
+}
+
+const ACTION_TYPE_MAP = {
+  1: '棰勪粯璐规竻闆�',
+  2: '鍚庝粯璐规竻闆�',
+  3: '杩滅▼閿�鎴�',
+  4: '鎷夐椄',
+  5: '鍚堥椄',
+  6: '寮�鎴�',
+  7: '鍏呭��',
+  8: '鎶勮〃',
+  9: '淇濈數',
+  10: '瑙i櫎淇濈數'
+}
 
 export default {
   name: 'YwElectricalRemote',
-  components: { GlobalWindow, AccountRechargePanel },
+  components: { GlobalWindow, AccountRechargePanel, Pagination, OperaInterfaceLogWindow },
   data () {
     return {
       visible: false,
@@ -60,7 +140,23 @@
       latest: null,
       purchaseCount: '0',
       form: { money: 0, remark: '' },
-      isOperating: false
+      isOperating: false,
+      isRefreshing: false,
+      infoLoading: false,
+      recordsLoading: false,
+      recordsList: [],
+      recordsPagination: { pageIndex: 1, pageSize: 10, total: 0 },
+      queryLoadingId: null
+    }
+  },
+  computed: {
+    totalPower () {
+      if (!this.latest) return '0.00kWh'
+      const v = this.latest.zhygzdl || '0'
+      return String(v).toLowerCase().indexOf('kwh') >= 0 ? v : v + 'kWh'
+    },
+    balanceSyncTime () {
+      return (this.latest && this.latest.addTime) ? this.latest.addTime : (this.info.balanceTime || '-')
     }
   },
   methods: {
@@ -68,21 +164,86 @@
       this.electricalId = row.id
       this.activeTab = tab || 'basic'
       this.form = { money: 0, remark: '' }
+      this.recordsPagination.pageIndex = 1
+      this.recordsList = []
       this.visible = true
       this.loadInfo()
+      if (this.activeTab === 'records') {
+        this.loadActionRecords(1)
+      }
+    },
+    onTabClick (tab) {
+      if (tab.name === 'records' && this.recordsList.length === 0) {
+        this.loadActionRecords(1)
+      }
     },
     loadInfo () {
+      if (!this.electricalId) return
+      this.infoLoading = true
       getRemoteInfo(this.electricalId).then(res => {
         this.info = res.electrical || {}
         this.latest = res.latestData
         this.purchaseCount = res.purchaseCount || '0'
-      }).catch(e => this.$tip.apiFailed(e))
+      }).catch(e => this.$tip.apiFailed(e)).finally(() => { this.infoLoading = false })
+    },
+    refreshMeterData () {
+      if (!this.electricalId) return
+      this.isRefreshing = true
+      operate({
+        electricalId: this.electricalId,
+        action: 'readMeter'
+      })
+        .then(res => {
+          this.$tip.apiSuccess(res || '鎶勮〃璇锋眰宸叉彁浜わ紝姝e湪鍒锋柊鏁版嵁')
+          this.loadInfo()
+          this.$emit('success')
+          if (this.activeTab === 'records') {
+            this.loadActionRecords(this.recordsPagination.pageIndex)
+          }
+        })
+        .catch(e => this.$tip.apiFailed(e))
+        .finally(() => { this.isRefreshing = false })
+    },
+    loadActionRecords (page) {
+      if (!this.electricalId) return
+      if (page) {
+        this.recordsPagination.pageIndex = page
+      }
+      this.recordsLoading = true
+      actionsApi.fetchList({
+        page: this.recordsPagination.pageIndex,
+        capacity: this.recordsPagination.pageSize,
+        model: { electricalId: this.electricalId }
+      })
+        .then(data => {
+          this.recordsList = data.records || []
+          this.recordsPagination.total = data.total || 0
+        })
+        .catch(e => this.$tip.apiFailed(e))
+        .finally(() => { this.recordsLoading = false })
+    },
+    handleRecordsSizeChange (size) {
+      this.recordsPagination.pageSize = size
+      this.loadActionRecords(1)
     },
     readMeter () {
       this.submitOperate('readMeter', true)
     },
     doOperate (action, msg) {
       this.$dialog.actionConfirm(msg, '鎿嶄綔纭')
+        .then(() => this.submitOperate(action, false))
+        .catch(() => {})
+    },
+    relayMeterTip () {
+      const name = this.info.name || '-'
+      const address = this.info.address || '-'
+      return `琛ㄥ悕绉帮細${name}锛岃〃鍦板潃锛�${address}`
+    },
+    doRelayOperate (action) {
+      const cfg = RELAY_ACTIONS[action]
+      if (!cfg) return
+      const meterTip = this.relayMeterTip()
+      this.$dialog.actionConfirm(`${cfg.message}锛�${meterTip}锛塦, '鎿嶄綔纭')
         .then(() => this.submitOperate(action, false))
         .catch(() => {})
     },
@@ -98,15 +259,79 @@
           this.$tip.apiSuccess(res || (silent ? '鎶勮〃璇锋眰宸叉彁浜�' : '鎻愪氦鎴愬姛锛岃鍦ㄣ�愭棩甯哥敤鐢电鐞�-鍏呭�艰褰曘�戜腑鏌ョ湅鍏呭�肩粨鏋�'))
           this.loadInfo()
           this.$emit('success')
+          if (this.activeTab === 'records') {
+            this.loadActionRecords(this.recordsPagination.pageIndex)
+          }
         })
         .catch(e => this.$tip.apiFailed(e))
         .finally(() => { this.isOperating = false })
+    },
+    formatActionType (val) {
+      return ACTION_TYPE_MAP[val] || val || '-'
+    },
+    formatStatus (val) {
+      if (val === 0 || val === '0') return '澶勭悊涓�'
+      if (val === 1 || val === '1') return '鎴愬姛'
+      if (val === 2 || val === '2') return '澶辫触'
+      return val == null || val === '' ? '-' : String(val)
+    },
+    statusClass (val) {
+      if (val === 1 || val === '1') return 'green'
+      if (val === 2 || val === '2') return 'red'
+      if (val === 0 || val === '0') return 'orange'
+      return ''
+    },
+    openJson (title, content) {
+      if (!content) return
+      this.$refs.jsonWindow.open(title, { content })
+    },
+    handleQueryResult (row) {
+      this.queryLoadingId = row.id
+      actionsApi.queryResult(row.id)
+        .then(msg => {
+          this.$tip.success(msg || '鏌ヨ瀹屾垚')
+          this.loadActionRecords(this.recordsPagination.pageIndex)
+        })
+        .catch(e => this.$tip.apiFailed(e))
+        .finally(() => { this.queryLoadingId = null })
     }
   }
 }
 </script>
 
 <style scoped>
-.info-block { margin-bottom: 16px; line-height: 28px; color: #303133; }
+.info-block { margin-bottom: 12px; line-height: 28px; color: #303133; }
+.meter-data-block {
+  margin-bottom: 16px;
+  padding: 12px 14px;
+  background: #fafbfe;
+  border: 1px solid #eef2f8;
+  border-radius: 8px;
+  line-height: 26px;
+  color: #303133;
+}
+.meter-data-block__title {
+  font-weight: 600;
+  font-size: 14px;
+  margin-bottom: 6px;
+  color: #222;
+}
+.meter-data-block p { margin: 0; }
 .btn-row .el-button { margin: 0 8px 8px 0; }
+.btn-row--relay {
+  margin-top: 4px;
+  padding-top: 12px;
+  border-top: 1px dashed #ebeef5;
+}
+.btn-row__label {
+  display: block;
+  width: 100%;
+  margin-bottom: 8px;
+  font-size: 13px;
+  color: #909399;
+}
+.records-table { width: 100%; margin-top: 4px; }
+.green { color: #67c23a; }
+.red { color: #f56c6c; }
+.orange { color: #e6a23c; }
 </style>
diff --git a/admin/src/views/business/ywcustomerrecharge.vue b/admin/src/views/business/ywcustomerrecharge.vue
index c5c0e8a..ecc712b 100644
--- a/admin/src/views/business/ywcustomerrecharge.vue
+++ b/admin/src/views/business/ywcustomerrecharge.vue
@@ -113,14 +113,20 @@
         capacity: this.tableData.pagination.pageSize,
         model: this.buildSearchModel()
       }).then(data => {
-        this.tableData.list = data.records
-        this.tableData.pagination.total = data.total
-      }).catch(() => {}).finally(() => { this.isWorking.search = false })
+        this.tableData.list = (data && data.records) || []
+        this.tableData.pagination.total = (data && data.total) || 0
+      }).catch(e => {
+        this.$tip.apiFailed(e)
+      }).finally(() => { this.isWorking.search = false })
     },
     search () {
       this.tableData.pagination.pageIndex = 1
       this.loadList()
     },
+    handleSizeChange (size) {
+      this.tableData.pagination.pageSize = size
+      this.loadList()
+    },
     buildSearchModel () {
       const model = {}
       if (this.searchForm.nameKeyword) model.nameKeyword = this.searchForm.nameKeyword
diff --git a/admin/src/views/business/ywelectricalactions.vue b/admin/src/views/business/ywelectricalactions.vue
index 82bae2c..b233f26 100644
--- a/admin/src/views/business/ywelectricalactions.vue
+++ b/admin/src/views/business/ywelectricalactions.vue
@@ -96,7 +96,9 @@
   5: '鍚堥椄',
   6: '寮�鎴�',
   7: '鍏呭��',
-  8: '鎶勮〃'
+  8: '鎶勮〃',
+  9: '淇濈數',
+  10: '瑙i櫎淇濈數'
 }
 
 export default {
diff --git a/admin/src/views/index.vue b/admin/src/views/index.vue
index 51f20e9..6df214f 100644
--- a/admin/src/views/index.vue
+++ b/admin/src/views/index.vue
@@ -1,109 +1,139 @@
 <template>
-  <div class="main">
-    <div class="main_left">
-      <div class="main_left_head">
-        <div class="head_item">
-          <div class="head_item_left">
-            <span>寰呭姙宸ュ崟</span>
-            <span>{{obj.waitDealWorkOrderSize || 0}}</span>
-            <span @click="jump(1)">鏌ョ湅鏇村</span>
-          </div>
-          <img class="head_item_icon" src="@/assets/indexIcon/ic_daibangongdan@2x.png" />
-        </div>
-        <div class="head_item">
-          <div class="head_item_left">
-            <span>寰呭贰妫�</span>
-            <span>{{obj.waitTaskSize || 0}}</span>
-            <span @click="jump(2)">鏌ョ湅鏇村</span>
-          </div>
-          <img class="head_item_icon" src="@/assets/indexIcon/ic_daixuncha@2x.png" />
-        </div>
-        <div class="head_item">
-          <div class="head_item_left">
-            <span>寰呯洏鐐�</span>
-            <span>{{obj.stocktakingSize || 0}}</span>
-            <span @click="jump(3)">鏌ョ湅鏇村</span>
-          </div>
-          <img class="head_item_icon" src="@/assets/indexIcon/ic_daipandian@2x.png" />
-        </div>
-      </div>
-      <div class="main_left_menu">
-        <div class="main_left_menu_title">
-          <span>甯哥敤鍔熻兘</span>
-          <div class="main_left_menu_title_edit" @click="$refs.commonFunctions.open('甯哥敤鍔熻兘绠$悊')">
-            <img src="@/assets/indexIcon/ic_daibangongdan@2x.png" />
-            <span>鑷畾涔夊姛鑳�</span>
-          </div>
-        </div>
-        <div class="main_left_menu_list">
-          <div class="list_item" v-for="(item, index) in list" :key="index" @click="jump1(item.path)">
-            <div class="list_item_left">
-              <img :src="item.icoPath" />
-              <span>{{item.name}}</span>
+  <div class="dashboard">
+    <section class="dashboard-top">
+      <div class="dashboard-top__left">
+        <div class="stat-cards card">
+          <div class="stat-cards__item head_item--workorder" @click="jump(1)">
+            <div class="stat-cards__content">
+              <span class="stat-cards__label">寰呭姙宸ュ崟</span>
+              <span class="stat-cards__value">{{ obj.waitDealWorkOrderSize || 0 }}</span>
+              <span class="stat-cards__link">鏌ョ湅鏇村</span>
             </div>
-            <i class="el-icon-arrow-right"></i>
+            <img class="stat-cards__icon" src="@/assets/indexIcon/ic_daibangongdan@2x.png" alt="" />
           </div>
-          <div class="list_item1"></div>
-          <div class="list_item1"></div>
-          <div class="list_item1"></div>
+          <div class="stat-cards__item head_item--inspect" @click="jump(2)">
+            <div class="stat-cards__content">
+              <span class="stat-cards__label">寰呭贰妫�</span>
+              <span class="stat-cards__value">{{ obj.waitTaskSize || 0 }}</span>
+              <span class="stat-cards__link">鏌ョ湅鏇村</span>
+            </div>
+            <img class="stat-cards__icon" src="@/assets/indexIcon/ic_daixuncha@2x.png" alt="" />
+          </div>
+          <div class="stat-cards__item head_item--stock" @click="jump(3)">
+            <div class="stat-cards__content">
+              <span class="stat-cards__label">寰呯洏鐐�</span>
+              <span class="stat-cards__value">{{ obj.stocktakingSize || 0 }}</span>
+              <span class="stat-cards__link">鏌ョ湅鏇村</span>
+            </div>
+            <img class="stat-cards__icon" src="@/assets/indexIcon/ic_daipandian@2x.png" alt="" />
+          </div>
         </div>
-      </div>
-    </div>
-    <div class="main_right">
-      <div class="main_right_title">鏃ョ▼</div>
-      <div class="main_right_search">
-        <el-select v-model="nian" style="width: 150px; margin-right: 15px;" @change="getMonthNoticess" placeholder="璇烽�夋嫨">
-          <el-option
-            v-for="item in yearList"
-            :key="item.val"
-            :label="item.name"
-            :value="item.val">
-          </el-option>
-        </el-select>
-        <el-select v-model="yue" style="width: 150px;" @change="getMonthNoticess" placeholder="璇烽�夋嫨">
-          <el-option
-            v-for="(item, index) in 12"
-            :key="index"
-            :label="item + '鏈�'"
-            :value="item > 9 ? item : `0${item}`">
-          </el-option>
-        </el-select>
-      </div>
-      <div class="main_right_date">
-        <Calendar
-          ref="Calendar"
-          v-on:choseDay="clickDay"
-          :markDateMore="markDateMore"
-        ></Calendar>
-      </div>
-      <div class="main_right_rc">
-        <div class="main_right_rc_title">褰撴棩鏃ョ▼锛坽{dataList.length}}锛�</div>
-        <div class="main_right_rc_list" v-loading="loading">
-          <div class="main_right_rc_list_row" v-for="(item, index) in dataList" :key="index">
-            <div class="top">
-              <div class="top_left">
-                <div class="top_left_dian"></div>
-                <div class="top_left_title">{{item.title}}</div>
+
+        <div class="quick-menu card">
+          <div class="section-header">
+            <h3 class="section-header__title">甯哥敤鍔熻兘</h3>
+            <div class="section-header__action" @click="$refs.commonFunctions.open('甯哥敤鍔熻兘绠$悊')">
+              <i class="el-icon-setting"></i>
+              <span>鑷畾涔夊姛鑳�</span>
+            </div>
+          </div>
+          <div class="quick-menu__grid">
+            <div
+              class="quick-menu__item"
+              v-for="(item, index) in list"
+              :key="index"
+              @click="jump1(item.path)"
+            >
+              <div class="quick-menu__item-left">
+                <img :src="item.icoPath" alt="" />
+                <span>{{ item.name }}</span>
               </div>
-              <div class="top_date">{{item.param1}}</div>
-            </div>
-            <div class="bottom">
-              {{item.content}}
+              <i class="el-icon-arrow-right quick-menu__arrow"></i>
             </div>
           </div>
         </div>
       </div>
-    </div>
+
+      <aside class="schedule-panel card">
+        <div class="section-header">
+          <h3 class="section-header__title">鏃ョ▼</h3>
+        </div>
+        <div class="schedule-panel__filters">
+          <el-select v-model="nian" class="schedule-panel__select" @change="getMonthNoticess" placeholder="璇烽�夋嫨">
+            <el-option
+              v-for="item in yearList"
+              :key="item.val"
+              :label="item.name"
+              :value="item.val"
+            />
+          </el-select>
+          <el-select v-model="yue" class="schedule-panel__select" @change="getMonthNoticess" placeholder="璇烽�夋嫨">
+            <el-option
+              v-for="(item, index) in 12"
+              :key="index"
+              :label="item + '鏈�'"
+              :value="item > 9 ? item : `0${item}`"
+            />
+          </el-select>
+        </div>
+        <div class="schedule-panel__calendar">
+          <Calendar
+            ref="Calendar"
+            v-on:choseDay="clickDay"
+            :markDateMore="markDateMore"
+          />
+        </div>
+        <div class="schedule-panel__list-wrap">
+          <div class="schedule-panel__list-title">褰撴棩鏃ョ▼锛坽{ dataList.length }}锛�</div>
+          <div class="schedule-panel__list" v-loading="loading">
+            <template v-if="dataList.length">
+              <div class="schedule-item" v-for="(item, index) in dataList" :key="index">
+                <div class="schedule-item__top">
+                  <div class="schedule-item__title-wrap">
+                    <div class="schedule-item__dot"></div>
+                    <div class="schedule-item__title">{{ item.title }}</div>
+                  </div>
+                  <div class="schedule-item__time">{{ item.param1 }}</div>
+                </div>
+                <div class="schedule-item__content">{{ item.content }}</div>
+              </div>
+            </template>
+            <div v-else class="schedule-empty">褰撴棩鏆傛棤鏃ョ▼</div>
+          </div>
+        </div>
+      </aside>
+    </section>
+
+    <section class="dashboard-warning">
+      <ElectricalWarningWorkbench />
+    </section>
+
+    <section class="dashboard-energy">
+      <DailyEnergyTrendPanel
+        title="杩�30澶╂櫤鑳界數琛ㄦ瘡鏃ユ�荤數閲�/鎬荤數璐�"
+        :load-data="electricalDailyEnergyStats"
+      />
+      <DailyEnergyTrendPanel
+        title="杩�30澶╃┖璋冨鑱旀満姣忔棩鎬荤數閲�/鎬荤數璐�"
+        :load-data="conditionerDailyEnergyStats"
+      />
+    </section>
+
     <CommonFunctions ref="commonFunctions" @success="getYwQuickLists" />
   </div>
 </template>
 
 <script>
 import CommonFunctions from '@/components/business/commonFunctions'
-import { getYwQuickList, getMonthNotices, workDeskData } from '@/api/ywWorkDesk'
+import ElectricalWarningWorkbench from '@/components/business/ElectricalWarningWorkbench'
+import DailyEnergyTrendPanel from '@/components/business/DailyEnergyTrendPanel'
+import { getYwQuickList, getMonthNotices, workDeskData, electricalDailyEnergyStats, conditionerDailyEnergyStats } from '@/api/ywWorkDesk'
+import { navigateByMenu } from '@/utils/menu'
+import { mapState } from 'vuex'
 import Calendar from 'vue-calendar-component'
+
 export default {
-  data() {
+  data () {
     return {
       list: [],
       value: new Date(),
@@ -116,16 +146,21 @@
       loading: false
     }
   },
-  components: { CommonFunctions, Calendar },
+  components: { CommonFunctions, Calendar, ElectricalWarningWorkbench, DailyEnergyTrendPanel },
+  computed: {
+    ...mapState(['menuData'])
+  },
   created () {
     this.getWorkDeskData()
     this.getYwQuickLists()
     this.getYear()
   },
   methods: {
-    getYear() {
-      let currentYear = new Date().getFullYear();
-      let currentMonth = new Date().getMonth() + 1;
+    electricalDailyEnergyStats,
+    conditionerDailyEnergyStats,
+    getYear () {
+      const currentYear = new Date().getFullYear()
+      const currentMonth = new Date().getMonth() + 1
       this.nian = currentYear
       this.yue = currentMonth > 9 ? currentMonth : `0${currentMonth}`
       for (let i = currentYear - 10; i <= currentYear; i++) {
@@ -133,40 +168,27 @@
       }
       this.getMonthNoticess()
     },
-    // 鑾峰彇浠e姙
-    getWorkDeskData() {
-      workDeskData({})
-        .then(res => {
-          this.obj = res
-        })
-    },
-    clickDay(e) {
-      this.loading = true
-      let date = e.replace("/\\//g", "-")
-      let arr = this.markDateMore.filter(item => {
-        if (item.date === date) {
-          return item
-        }
+    getWorkDeskData () {
+      workDeskData({}).then(res => {
+        this.obj = res
       })
-      if (arr.length > 0) {
-        this.dataList = arr[0].noticeList
-      } else {
-        this.dataList = []
-      }
+    },
+    clickDay (e) {
+      this.loading = true
+      const date = e.replace('/\\//g', '-')
+      const arr = this.markDateMore.filter(item => item.date === date)
+      this.dataList = arr.length > 0 ? arr[0].noticeList : []
       this.loading = false
     },
-    // 鑾峰彇鏃ョ▼
-    getMonthNoticess() {
+    getMonthNoticess () {
       getMonthNotices(this.nian + '-' + this.yue).then(res => {
-        let arr = res.filter(item => {
-          if (item.noticeList && item.noticeList.length > 0) {
-            return item
-          }
-        })
-        this.markDateMore = arr.map(item => {
-          return { date: item.monthDate.replace("/-0/g", "-"), className: 'markRed', noticeList: item.noticeList }
-        })
-        let toDay = this.getDay()
+        const arr = res.filter(item => item.noticeList && item.noticeList.length > 0)
+        this.markDateMore = arr.map(item => ({
+          date: item.monthDate.replace('/-0/g', '-'),
+          className: 'markRed',
+          noticeList: item.noticeList
+        }))
+        const toDay = this.getDay()
         this.markDateMore.forEach(item => {
           if (item.date === toDay) {
             this.dataList = item.noticeList
@@ -175,301 +197,477 @@
         this.$refs.Calendar.ChoseMonth(`${this.nian}-${this.yue}`, false)
       })
     },
-    getYwQuickLists() {
-      getYwQuickList({})
-        .then(res => {
-          this.list = res
-        })
+    getYwQuickLists () {
+      getYwQuickList({}).then(res => {
+        this.list = res
+      })
     },
-    getDay() {
-      let date = new Date();
-      let year = date.getFullYear();  // 鑾峰彇骞翠唤
-      let month = String(date.getMonth() + 1).padStart(2, '0');  // 鑾峰彇鏈堜唤锛屽苟琛ラ浂
-      let day = String(date.getDate()).padStart(2, '0');  // 鑾峰彇鏃ユ湡锛屽苟琛ラ浂
-  
-      return `${year}-${month}-${day}`;
+    getDay () {
+      const date = new Date()
+      const year = date.getFullYear()
+      const month = String(date.getMonth() + 1).padStart(2, '0')
+      const day = String(date.getDate()).padStart(2, '0')
+      return `${year}-${month}-${day}`
     },
-    jump(type) {
-      if (type === 1) {
-        this.$router.push({ path: '/workorder/workorderList' })
-      } else if (type === 2) {
-        this.$router.push({ path: '/Inspection/task' })
-      } else if (type === 3) {
-        this.$router.push({ path: '/stock/check' })
+    jump (type) {
+      const pathMap = {
+        1: '/workorder/workorderList',
+        2: '/Inspection/task',
+        3: '/stock/check'
       }
+      this.navigateToMenu(pathMap[type])
     },
-    jump1(path) {
-      this.$router.push({ path })
+    jump1 (path) {
+      this.navigateToMenu(path)
+    },
+    navigateToMenu (path) {
+      navigateByMenu(this.$router, this.$store, path, this.menuData.list)
     }
   }
 }
 </script>
+
 <style lang="scss" scoped>
-  .main {
-    width: 100%;
-    padding: 15px;
-    box-sizing: border-box;
-    background: #F4F7FC;
+.dashboard {
+  --dash-primary: #3E80EF;
+  --dash-primary-light: rgba(62, 128, 239, 0.08);
+  --dash-text: #222222;
+  --dash-text-secondary: #666666;
+  --dash-text-muted: #999999;
+  --dash-bg: #F4F7FC;
+  --dash-card-bg: #FFFFFF;
+  --dash-border: #EEF2F8;
+  --dash-radius: 10px;
+  --dash-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
+  --dash-gap: 16px;
+
+  width: 100%;
+  min-height: 100%;
+  padding: var(--dash-gap);
+  box-sizing: border-box;
+  background: var(--dash-bg);
+  display: flex;
+  flex-direction: column;
+  gap: var(--dash-gap);
+}
+
+.card {
+  background: var(--dash-card-bg);
+  border-radius: var(--dash-radius);
+  border: 1px solid var(--dash-border);
+  box-shadow: var(--dash-shadow);
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+
+  &__title {
+    margin: 0;
+    padding-left: 10px;
+    font-weight: 600;
+    font-size: 18px;
+    color: var(--dash-text);
+    line-height: 1.4;
+    border-left: 3px solid var(--dash-primary);
+  }
+
+  &__action {
+    cursor: pointer;
     display: flex;
-    align-items: self-start;
+    align-items: center;
+    gap: 4px;
+    font-size: 14px;
+    color: var(--dash-primary);
+    transition: opacity 0.2s;
+
+    &:hover {
+      opacity: 0.8;
+    }
+
+    i {
+      font-size: 15px;
+    }
+  }
+}
+
+.dashboard-top {
+  display: flex;
+  align-items: flex-start;
+  gap: var(--dash-gap);
+
+  &__left {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    gap: var(--dash-gap);
+  }
+}
+
+.stat-cards {
+  display: flex;
+  align-items: stretch;
+  gap: 12px;
+  padding: 20px;
+
+  &__item {
+    flex: 1;
+    display: flex;
+    align-items: center;
     justify-content: space-between;
-    .main_left {
-      flex: 1;
-      height: 100%;
-      display: flex;
-      flex-direction: column;
-      .main_left_head {
-        width: 100%;
-        height: 199px;
-        background: #FFFFFF;
-        border-radius: 8px;
-        display: flex;
-        align-items: center;
-        padding: 20px;
-        box-sizing: border-box;
-        .head_item {
-          flex: 1;
-          height: 100%;
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          background: #F1F5FF;
-          border-radius: 8px;
-          margin-right: 15px;
-          padding: 28px 20px;
-          box-sizing: border-box;
-          &:last-child {
-            margin: 0 !important;
-          }
-          .head_item_left {
-            flex: 1;
-            display: flex;
-            flex-direction: column;
-            span {
-              &:nth-child(1) {
-                font-weight: 600;
-                font-size: 17px;
-                color: #222222;
-              }
-              &:nth-child(2) {
-                font-weight: 600;
-                font-size: 30px;
-                color: #111111;
-                margin-top: 6px;
-                margin-bottom: 14px;
-              }
-              &:nth-child(3) {
-                font-weight: 400;
-                font-size: 13px;
-                color: #999999;
-                cursor: pointer;
-              }
-            }
-          }
-          .head_item_icon {
-            width: 60px;
-            height: 60px;
-          }
-        }
-      }
-      .main_left_menu {
-        width: 100%;
-        height: calc(100% - 214px);
-        background: #FFFFFF;
-        border-radius: 8px;
-        margin-top: 15px;
-        padding: 20px;
-        box-sizing: border-box;
-        .main_left_menu_title {
-          width: 100%;
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          span {
-            font-weight: 600;
-            font-size: 18px;
-            color: #222222;
-          }
-          .main_left_menu_title_edit {
-            cursor: pointer;
-            display: flex;
-            align-items: center;
-            img {
-              width: 15px;
-              height: 15px;
-            }
-            span {
-              font-weight: 400;
-              font-size: 14px;
-              color: #3E80EF;
-              margin-left: 5px;
-            }
-          }
-        }
-        .main_left_menu_list {
-          width: 100%;
-          margin-top: 20px;
-          display: flex;
-          align-items: center;
-          flex-wrap: wrap;
-          justify-content: space-between;
-          .list_item1 {
-            width: 24%;
-            height: 0;
-          }
-          .list_item {
-            cursor: pointer;
-            width: 24%;
-            height: 70px;
-            background: #FFFFFF;
-            border-radius: 8px;
-            border: 1px solid #EEEEEE;
-            padding: 0 16px;
-            box-sizing: border-box;
-            margin-bottom: 15px;
-            display: flex;
-            align-items: center;
-            justify-content: space-between;
-            .list_item_left {
-              display: flex;
-              align-items: center;
-              img {
-                width: 40px;
-                height: 40px;
-                margin-right: 5px;
-              }
-              span {
-                font-weight: 500;
-                font-size: 15px;
-                color: #222222;
-              }
-            }
-          }
-        }
+    padding: 24px 20px;
+    border-radius: 8px;
+    cursor: pointer;
+    transition: transform 0.2s, box-shadow 0.2s;
+
+    &:hover {
+      transform: translateY(-2px);
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+
+      .stat-cards__icon {
+        transform: scale(1.05);
       }
     }
-    .main_right {
-      flex-shrink: 0;
-      width: 500px;
-      height: 100%;
-      margin-left: 15px;
-      background: #FEFEFF;
-      border-radius: 8px;
-      padding: 20px;
-      box-sizing: border-box;
-      .main_right_title {
-        font-weight: 600;
-        font-size: 18px;
-        color: #222222;
-        margin-bottom: 12px;
+
+    &.head_item--workorder {
+      background: linear-gradient(135deg, #EEF4FF 0%, #F5F8FF 100%);
+
+      .stat-cards__value {
+        color: #3E80EF;
       }
-      .main_right_rc {
-        width: 100%;
-        display: flex;
-        flex-direction: column;
-        .main_right_rc_title {
-          font-weight: 500;
-          font-size: 16px;
-          color: #222222;
-          margin-bottom: 15px;
-        }
-        .main_right_rc_list {
-          width: 100%;
-          height: 300px;
-          overflow-y: scroll;
-          .main_right_rc_list_row {
-            width: 100%;
-            padding: 15px;
-            box-sizing: border-box;
-            background: #F4F7FC;
-            border-radius: 2px;
-            margin-bottom: 10px;
-            .top {
-              width: 100%;
-              display: flex;
-              align-items: center;
-              justify-content: space-between;
-              .top_left {
-                display: flex;
-                align-items: center;
-                .top_left_dian {
-                  width: 10px;
-                  height: 10px;
-                  border-radius: 50%;
-                  background: #FF9E00;
-                  margin-right: 10px;
-                }
-                .top_left_title {
-                  font-weight: 500;
-                  font-size: 15px;
-                  color: #222222;
-                }
-              }
-              .top_date {
-                font-weight: 400;
-                font-size: 12px;
-                color: #999999;
-              }
-            }
-            .bottom {
-              font-weight: 400;
-              font-size: 13px;
-              color: #666666;
-              margin-top: 8px;
-            }
-          }
-        }
+
+      .stat-cards__link:hover {
+        color: #3E80EF;
       }
-      .main_right_date {
-        width: 100%;
-        padding-top: 15px;
-        box-sizing: border-box;
-        ::v-deep .wh_content_item {
-          height: 50px;
-          color: #222222;
-          font-weight: 400;
-          font-size: 15px;
-        }
-        ::v-deep .wh_item_date {
-          width: 30px;
-          height: 30px;
-          font-size: 15px;
-        }
-        ::v-deep .wh_content_all {
-          background-color: #ffffff;
-        }
-        ::v-deep .wh_top_changge {
-          display: none;
-        }
-        ::v-deep .wh_container {
-          max-width: 100%;
-        }
-        ::v-deep .wh_content_item .wh_isToday {
-          background-color: rgba(62, 128, 239, 0.47);
-          color: #ffffff;
-        }
-        ::v-deep .wh_item_date:hover {
-          background-color: #3E80EF;
-          border-radius: 50%;
-          color: #ffffff;
-        }
-        ::v-deep .wh_content_item .wh_chose_day {
-          background: #3E80EF;
-          color: #fff;
-        }
-        ::v-deep .markRed {
-          position: relative;
-        }
-        ::v-deep .markRed::after {
-          content: '鈼�';
-          color: #FF9E00;
-          font-size: 11px;
-          position: absolute;
-          bottom: -30px;
-          left: 50%;
-          transform: translate(-50%, 0);
-        }
+    }
+
+    &.head_item--inspect {
+      background: linear-gradient(135deg, #E8FAF0 0%, #F2FBF6 100%);
+
+      .stat-cards__value {
+        color: #36B37E;
+      }
+
+      .stat-cards__link:hover {
+        color: #36B37E;
+      }
+    }
+
+    &.head_item--stock {
+      background: linear-gradient(135deg, #FFF6E8 0%, #FFFAF2 100%);
+
+      .stat-cards__value {
+        color: #FF9E00;
+      }
+
+      .stat-cards__link:hover {
+        color: #FF9E00;
       }
     }
   }
+
+  &__content {
+    display: flex;
+    flex-direction: column;
+  }
+
+  &__label {
+    font-weight: 600;
+    font-size: 16px;
+    color: var(--dash-text);
+  }
+
+  &__value {
+    font-weight: 700;
+    font-size: 32px;
+    line-height: 1.2;
+    margin: 6px 0 12px;
+  }
+
+  &__link {
+    font-size: 13px;
+    color: var(--dash-text-muted);
+    transition: color 0.2s;
+  }
+
+  &__icon {
+    width: 56px;
+    height: 56px;
+    flex-shrink: 0;
+    transition: transform 0.2s;
+  }
+}
+
+.quick-menu {
+  padding: 20px;
+
+  &__grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 12px;
+  }
+
+  &__item {
+    cursor: pointer;
+    height: 72px;
+    background: var(--dash-card-bg);
+    border-radius: 8px;
+    border: 1px solid #EEEEEE;
+    padding: 0 16px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
+
+    &:hover {
+      border-color: var(--dash-primary);
+      box-shadow: 0 2px 8px rgba(62, 128, 239, 0.12);
+      transform: translateY(-1px);
+
+      .quick-menu__arrow {
+        color: var(--dash-primary);
+      }
+    }
+  }
+
+  &__item-left {
+    display: flex;
+    align-items: center;
+    min-width: 0;
+
+    img {
+      width: 40px;
+      height: 40px;
+      margin-right: 8px;
+      flex-shrink: 0;
+    }
+
+    span {
+      font-weight: 500;
+      font-size: 14px;
+      color: var(--dash-text);
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  &__arrow {
+    color: #C0C4CC;
+    font-size: 14px;
+    flex-shrink: 0;
+    transition: color 0.2s;
+  }
+}
+
+.schedule-panel {
+  flex-shrink: 0;
+  width: 480px;
+  padding: 20px;
+
+  &__filters {
+    display: flex;
+    gap: 12px;
+    margin-bottom: 12px;
+  }
+
+  &__select {
+    width: 140px;
+  }
+
+  &__calendar {
+    padding: 12px;
+    background: #FAFBFE;
+    border-radius: 8px;
+    border: 1px solid var(--dash-border);
+  }
+
+  &__list-wrap {
+    margin-top: 16px;
+  }
+
+  &__list-title {
+    font-weight: 500;
+    font-size: 15px;
+    color: var(--dash-text);
+    margin-bottom: 12px;
+  }
+
+  &__list {
+    height: 280px;
+    overflow-y: auto;
+    padding-right: 4px;
+
+    &::-webkit-scrollbar {
+      width: 5px;
+    }
+
+    &::-webkit-scrollbar-thumb {
+      background: #D8DCE6;
+      border-radius: 3px;
+    }
+  }
+}
+
+.schedule-item {
+  padding: 14px 14px 14px 12px;
+  background: #FAFBFE;
+  border-radius: 8px;
+  border: 1px solid var(--dash-border);
+  border-left: 3px solid #FF9E00;
+  margin-bottom: 10px;
+
+  &__top {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 8px;
+  }
+
+  &__title-wrap {
+    display: flex;
+    align-items: center;
+    min-width: 0;
+  }
+
+  &__dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background: #FF9E00;
+    margin-right: 8px;
+    flex-shrink: 0;
+  }
+
+  &__title {
+    font-weight: 500;
+    font-size: 14px;
+    color: var(--dash-text);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  &__time {
+    font-size: 12px;
+    color: var(--dash-text-muted);
+    flex-shrink: 0;
+  }
+
+  &__content {
+    font-size: 13px;
+    color: var(--dash-text-secondary);
+    margin-top: 8px;
+    line-height: 1.5;
+  }
+}
+
+.schedule-empty {
+  height: 120px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--dash-text-muted);
+  font-size: 14px;
+}
+
+.dashboard-warning,
+.dashboard-energy {
+  display: flex;
+  flex-direction: column;
+  gap: var(--dash-gap);
+}
+
+::v-deep .electrical-warning-panel,
+::v-deep .daily-energy-panel {
+  margin-top: 0 !important;
+  border: 1px solid var(--dash-border);
+  box-shadow: var(--dash-shadow);
+  border-radius: var(--dash-radius);
+}
+
+.schedule-panel__calendar {
+  ::v-deep .wh_content_item {
+    height: 50px;
+    color: var(--dash-text);
+    font-size: 14px;
+  }
+
+  ::v-deep .wh_item_date {
+    width: 30px;
+    height: 30px;
+    font-size: 14px;
+  }
+
+  ::v-deep .wh_content_all {
+    background-color: transparent;
+  }
+
+  ::v-deep .wh_top_changge {
+    display: none;
+  }
+
+  ::v-deep .wh_container {
+    max-width: 100%;
+  }
+
+  ::v-deep .wh_content_item .wh_isToday {
+    background-color: rgba(62, 128, 239, 0.47);
+    color: #ffffff;
+  }
+
+  ::v-deep .wh_item_date:hover {
+    background-color: var(--dash-primary);
+    border-radius: 50%;
+    color: #ffffff;
+  }
+
+  ::v-deep .wh_content_item .wh_chose_day {
+    background: var(--dash-primary);
+    color: #fff;
+  }
+
+  ::v-deep .markRed {
+    position: relative;
+  }
+
+  ::v-deep .markRed::after {
+    content: '鈼�';
+    color: #FF9E00;
+    font-size: 11px;
+    position: absolute;
+    bottom: -30px;
+    left: 50%;
+    transform: translate(-50%, 0);
+  }
+}
+
+@media (max-width: 1400px) {
+  .dashboard-top {
+    flex-direction: column;
+  }
+
+  .schedule-panel {
+    width: 100%;
+  }
+}
+
+@media (max-width: 1200px) {
+  .quick-menu__grid {
+    grid-template-columns: repeat(3, 1fr);
+  }
+
+  .stat-cards {
+    flex-direction: column;
+  }
+}
+
+@media (max-width: 768px) {
+  .quick-menu__grid {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
 </style>
diff --git a/admin/src/views/operation/device.vue b/admin/src/views/operation/device.vue
index 43a7339..767a196 100644
--- a/admin/src/views/operation/device.vue
+++ b/admin/src/views/operation/device.vue
@@ -4,6 +4,8 @@
     <div class="mt20">
       <el-button type="primary" @click="handleEdit()" icon="el-icon-plus"
         v-permissions="['business:ywpatrolline:create']">鏂板缓</el-button>
+      <el-button type="primary" v-permissions="['business:ywdevice:importExcel']"
+        @click="$refs.deviceImport.open('璁惧鎵归噺瀵煎叆')">鎵归噺瀵煎叆</el-button>
     </div>
     <el-table v-loading="loading" :data="list" stripe>
       <el-table-column prop="code" label="璁惧缂栧彿" min-width="100" show-overflow-tooltip />
@@ -35,6 +37,7 @@
     </div>
     <Edit v-if="showEdit" ref="EditRef" @success="getList" @close="showEdit = false" />
     <Detail ref="DetailRef" />
+    <OperaDeviceImportWindow ref="deviceImport" @success="getList(1)" />
   </div>
 </template>
 
@@ -43,13 +46,15 @@
 import QueryForm from '@/components/common/QueryForm'
 import Edit from './components/deviceEdit'
 import Detail from './components/deviceDetail'
+import OperaDeviceImportWindow from '@/components/business/OperaDeviceImportWindow'
 import { fetchList, deleteById } from '@/api/Inspection/device'
 export default {
   components: {
     Pagination,
     QueryForm,
     Edit,
-    Detail
+    Detail,
+    OperaDeviceImportWindow
   },
   data() {
     return {
diff --git a/server/db/business.yw_device.importExcel.grant.sql b/server/db/business.yw_device.importExcel.grant.sql
new file mode 100644
index 0000000..a768adc
--- /dev/null
+++ b/server/db/business.yw_device.importExcel.grant.sql
@@ -0,0 +1,12 @@
+-- 璁惧绠$悊鎵归噺瀵煎叆锛氫负瓒呯骇绠$悊鍛樿鑹茶ˉ鍏� importExcel 鏉冮檺锛堝彲閲嶅鎵ц锛�
+-- 鎵ц鍚庤閲嶆柊鐧诲綍浠ュ埛鏂� Redis 涓殑鏉冮檺缂撳瓨
+
+INSERT INTO `SYSTEM_ROLE_PERMISSION` (`ROLE_ID`, `PERMISSION_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
+SELECT r.`ID`, p.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
+FROM `SYSTEM_ROLE` r
+INNER JOIN `SYSTEM_PERMISSION` p ON p.`CODE` = 'business:ywdevice:importExcel' AND p.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` IN ('瓒呯骇绠$悊鍛�', '绠$悊鍛�'))
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_ROLE_PERMISSION` rp
+    WHERE rp.`ROLE_ID` = r.`ID` AND rp.`PERMISSION_ID` = p.`ID` AND rp.`DELETED` = 0
+  );
diff --git a/server/db/business.yw_device.permissions.sql b/server/db/business.yw_device.permissions.sql
index c5f64e5..8767c92 100644
--- a/server/db/business.yw_device.permissions.sql
+++ b/server/db/business.yw_device.permissions.sql
@@ -3,4 +3,5 @@
 INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywdevice:update', '淇敼杩愮淮璁惧淇℃伅琛�', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
 INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywdevice:query', '鏌ヨ杩愮淮璁惧淇℃伅琛�', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
 INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywdevice:exportExcel', '瀵煎嚭杩愮淮璁惧淇℃伅琛�(Excel)', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
+INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywdevice:importExcel', '瀵煎叆杩愮淮璁惧淇℃伅琛�(Excel)', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
 
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwCustomerRechargeCloudController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwCustomerRechargeCloudController.java
index da8fe3d..8d2aec4 100644
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwCustomerRechargeCloudController.java
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwCustomerRechargeCloudController.java
@@ -34,6 +34,9 @@
     public ApiResponse<PageData<YwCustomerRechargeMerchantVO>> merchantPage(
             @RequestBody PageWrap<YwCustomerRechargeQueryDTO> pageWrap,
             @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        if (pageWrap == null) {
+            pageWrap = new PageWrap<>();
+        }
         return ApiResponse.success(ywCustomerRechargeBizService.findMerchantPage(pageWrap));
     }
 
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwDeviceCloudController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwDeviceCloudController.java
index db601da..adf98ad 100644
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwDeviceCloudController.java
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwDeviceCloudController.java
@@ -13,12 +13,17 @@
 import com.doumee.dao.business.vo.YwDeviceStatusDataVO;
 import com.doumee.service.business.YwDeviceService;
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
 import com.doumee.config.annotation.CloudRequiredPermission;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletResponse;
+import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -90,6 +95,16 @@
         ExcelExporter.build(YwDevice.class).export(ywDeviceService.findPage(pageWrap).getRecords(), "杩愮淮璁惧淇℃伅琛�", response);
     }
 
+    @ApiOperation("璁惧鎵归噺瀵煎叆")
+    @PostMapping("/importExcel")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "file", value = "file", required = true, paramType = "query", dataType = "file", dataTypeClass = File.class),
+    })
+    @CloudRequiredPermission("business:ywdevice:importExcel")
+    public ApiResponse<String> importExcel(@ApiParam(value = "file") MultipartFile file, @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywDeviceService.importBatch(file, this.getLoginUser(token)));
+    }
+
     @ApiOperation("鏍规嵁ID鏌ヨ")
     @GetMapping("/{id}")
     @CloudRequiredPermission("business:ywdevice:query")
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwWorkDeskCloutController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwWorkDeskCloutController.java
index e7c3479..4dcee74 100644
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwWorkDeskCloutController.java
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwWorkDeskCloutController.java
@@ -10,10 +10,15 @@
 import com.doumee.core.utils.Constants;
 import com.doumee.dao.business.model.YwPatrolTaskRecord;
 import com.doumee.dao.business.model.YwQuickModel;
+import com.doumee.dao.business.model.YwElectricalWarning;
+import com.doumee.dao.business.vo.DailyEnergyStatVO;
 import com.doumee.dao.business.vo.MonthDataResponse;
+import com.doumee.dao.business.vo.WarningTypeStatVO;
 import com.doumee.dao.business.vo.WorkDeskDataResponse;
 import com.doumee.dao.system.model.Notices;
 import com.doumee.service.business.WorkbenchesService;
+import com.doumee.service.business.YwElectricalWarningService;
+import com.doumee.service.business.YwWorkDeskEnergyService;
 import com.doumee.service.business.YwPatrolTaskRecordService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -36,6 +41,12 @@
 
     @Autowired
     private WorkbenchesService workbenchesService;
+
+    @Autowired
+    private YwElectricalWarningService ywElectricalWarningService;
+
+    @Autowired
+    private YwWorkDeskEnergyService ywWorkDeskEnergyService;
 
     @ApiOperation("鑾峰彇蹇嵎鑿滃崟妯″潡淇℃伅")
     @GetMapping("/getYwQuickList")
@@ -75,4 +86,29 @@
         return ApiResponse.success(workbenchesService.getMyNotices(pageWrap,getLoginUser(token)));
     }
 
+    @ApiOperation("宸ヤ綔鍙�-鐢佃〃鎶ヨ绫诲瀷缁熻")
+    @GetMapping("/electricalWarningStats")
+    public ApiResponse<List<WarningTypeStatVO>> electricalWarningStats(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywElectricalWarningService.warningTypeStats());
+    }
+
+    @ApiOperation("宸ヤ綔鍙�-鐢佃〃鎶ヨ鍒嗛〉")
+    @PostMapping("/electricalWarningPage")
+    public ApiResponse<PageData<YwElectricalWarning>> electricalWarningPage(@RequestBody PageWrap<YwElectricalWarning> pageWrap,
+                                                                            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywElectricalWarningService.findPage(pageWrap));
+    }
+
+    @ApiOperation("宸ヤ綔鍙�-杩�30澶╂櫤鑳界數琛ㄦ瘡鏃ョ數閲�/鐢佃垂")
+    @GetMapping("/electricalDailyEnergyStats")
+    public ApiResponse<List<DailyEnergyStatVO>> electricalDailyEnergyStats(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywWorkDeskEnergyService.electricalDailyStats());
+    }
+
+    @ApiOperation("宸ヤ綔鍙�-杩�30澶╃┖璋冨鑱旀満姣忔棩鐢甸噺/鐢佃垂")
+    @GetMapping("/conditionerDailyEnergyStats")
+    public ApiResponse<List<DailyEnergyStatVO>> conditionerDailyEnergyStats(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywWorkDeskEnergyService.conditionerDailyStats());
+    }
+
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/ConditionerUtil.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/ConditionerUtil.java
index 0872b5e..45f4b3f 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/ConditionerUtil.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/conditoner/ConditionerUtil.java
@@ -18,6 +18,8 @@
 
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.math.BigDecimal;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -290,12 +292,8 @@
         JSONObject body = new JSONObject(true);
         putSessionFields(body, req);
         body.put("is_pwr", req.getIs_pwr() != null ? req.getIs_pwr() : 1);
-        if (req.getLi_dev() != null && !req.getLi_dev().isEmpty()) {
-            body.put("li_dev", req.getLi_dev());
-        }
-        if (req.getD_dev() != null && !req.getD_dev().isEmpty()) {
-            body.put("d_dev", req.getD_dev());
-        }
+        body.put("li_dev", req.getLi_dev() != null ? req.getLi_dev() : Collections.emptyList());
+        body.put("d_dev", req.getD_dev() != null ? req.getD_dev() : Collections.emptyMap());
         body.put("gs_name", req.getGs_name());
         body.put("is_rest_stop", req.getIs_rest_stop() != null ? req.getIs_rest_stop() : 0);
         body.put("gs_bz", StringUtils.defaultString(req.getGs_bz()));
@@ -309,6 +307,7 @@
         }
         req.fillSessionDefaults();
         JSONObject body = new JSONObject(true);
+        putSessionFields(body, req);
         body.put("id", req.getId());
         body.put("is_pwr", req.getIs_pwr() != null ? req.getIs_pwr() : 1);
         body.put("is_rest_stop", req.getIs_rest_stop() != null ? req.getIs_rest_stop() : 0);
@@ -317,16 +316,22 @@
             body.put("left_money", req.getLeft_money());
         }
         body.put("is_stop", req.getIs_stop() != null ? req.getIs_stop() : 0);
-        body.put("li_dev", req.getLi_dev());
-        body.put("d_dev", req.getD_dev());
+        body.put("li_dev", req.getLi_dev() != null ? req.getLi_dev() : Collections.emptyList());
+        body.put("d_dev", req.getD_dev() != null ? req.getD_dev() : Collections.emptyMap());
         body.put("gs_bz", StringUtils.defaultString(req.getGs_bz()));
         putStopMoney(body, req.getStop_money());
         return postJsonBody("/changeGs", body, Object.class);
     }
 
     private static void putStopMoney(JSONObject body, Object stopMoney) {
-        if (stopMoney != null) {
-            body.put("stop_money", stopMoney);
+        if (stopMoney == null) {
+            body.put("stop_money", "0");
+            return;
+        }
+        if (stopMoney instanceof BigDecimal) {
+            body.put("stop_money", ((BigDecimal) stopMoney).toPlainString());
+        } else {
+            body.put("stop_money", String.valueOf(stopMoney));
         }
     }
 
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleControlApiRequest.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleControlApiRequest.java
index 7876274..02cb5c2 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleControlApiRequest.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleControlApiRequest.java
@@ -11,6 +11,6 @@
 @EqualsAndHashCode(callSuper = true)
 public class EleControlApiRequest extends OpenAccountRequest {
 
-    @ApiModelProperty("10鎷夐椄 11鍚堥椄")
+    @ApiModelProperty("10鎷夐椄 11鍚堥椄 63淇濈數 220瑙i櫎淇濈數")
     private int type;
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/admin/request/DeviceImport.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/admin/request/DeviceImport.java
new file mode 100644
index 0000000..f97c65b
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/admin/request/DeviceImport.java
@@ -0,0 +1,64 @@
+package com.doumee.dao.admin.request;
+
+import com.doumee.core.annotation.excel.ExcelColumn;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+/**
+ * 杩愮淮璁惧鎵归噺瀵煎叆
+ */
+@Data
+@ApiModel("杩愮淮璁惧瀵煎叆")
+public class DeviceImport {
+
+    @ExcelColumn(name = "*璁惧缂栧彿", value = "code", index = 0)
+    private String code;
+
+    @ExcelColumn(name = "*璁惧鍚嶇О", value = "name", index = 1)
+    private String name;
+
+    @ExcelColumn(name = "*璁惧鍒嗙被", value = "categoryPath", index = 2)
+    private String categoryPath;
+
+    @ExcelColumn(name = "璁惧鍨嬪彿", value = "modelNo", index = 3)
+    private String modelNo;
+
+    @ExcelColumn(name = "璁惧绠$悊鍛�", value = "adminUserName", index = 4)
+    private String adminUserName;
+
+    @ExcelColumn(name = "鎵�鍦ㄤ綅缃�", value = "addr", index = 5)
+    private String addr;
+
+    @ExcelColumn(name = "*鎵�灞為」鐩�", value = "projectName", index = 6)
+    private String projectName;
+
+    @ExcelColumn(name = "*鍏宠仈鎴挎簮", value = "roomPath", index = 7)
+    private String roomPath;
+
+    @ExcelColumn(name = "璐叆鏃堕棿", value = "buyDate", index = 8)
+    private String buyDate;
+
+    @ExcelColumn(name = "璁惧鐘舵��", value = "statusText", index = 9)
+    private String statusText;
+
+    @ExcelColumn(name = "杩愮淮鍐呭", value = "content", index = 10)
+    private String content;
+
+    @ExcelColumn(name = "璁惧渚涘簲鍟�", value = "supplier", index = 11)
+    private String supplier;
+
+    @ExcelColumn(name = "渚涘簲鍟嗚仈绯讳汉", value = "supplierLinker", index = 12)
+    private String supplierLinker;
+
+    @ExcelColumn(name = "渚涘簲鍟嗚仈绯绘柟寮�", value = "supplierPhone", index = 13)
+    private String supplierPhone;
+
+    @ExcelColumn(name = "缁翠繚璐熻矗浜�", value = "maintenanceUserName", index = 14)
+    private String maintenanceUserName;
+
+    @ExcelColumn(name = "缁翠繚鍒版湡鏃�", value = "maintenanceOverDate", index = 15)
+    private String maintenanceOverDate;
+
+    @ExcelColumn(name = "缁翠繚璇存槑", value = "maintenanceContent", index = 16)
+    private String maintenanceContent;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalActions.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalActions.java
index f6ba30f..9c0c736 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalActions.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalActions.java
@@ -28,7 +28,7 @@
     private String remark;
     @ApiModelProperty("鐢佃〃ID")
     private Integer electricalId;
-    @ApiModelProperty("鎿嶄綔绫诲瀷 1棰勪粯璐规竻闆� 2鍚庝粯璐规竻闆� 3杩滅▼閿�鎴� 4鎷夐椄 5鍚堥椄 6寮�鎴� 7鍏呭�� 8鎶勮〃")
+    @ApiModelProperty("鎿嶄綔绫诲瀷 1棰勪粯璐规竻闆� 2鍚庝粯璐规竻闆� 3杩滅▼閿�鎴� 4鎷夐椄 5鍚堥椄 6寮�鎴� 7鍏呭�� 8鎶勮〃 9淇濈數 10瑙i櫎淇濈數")
     private Integer actionType;
     private String oprId;
     private String requestBody;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/DailyEnergyStatVO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/DailyEnergyStatVO.java
new file mode 100644
index 0000000..2ceaf72
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/DailyEnergyStatVO.java
@@ -0,0 +1,24 @@
+package com.doumee.dao.business.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 鎸夋棩姹囨�荤數閲�/鐢佃垂
+ */
+@Data
+@ApiModel("鎸夋棩姹囨�荤數閲�/鐢佃垂")
+public class DailyEnergyStatVO {
+
+    @ApiModelProperty("鏃ユ湡 yyyy-MM-dd")
+    private String statDate;
+
+    @ApiModelProperty("鎬荤數閲� kWh")
+    private BigDecimal totalKwh;
+
+    @ApiModelProperty("鎬荤數璐� 鍏�")
+    private BigDecimal totalFee;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/WarningTypeStatVO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/WarningTypeStatVO.java
new file mode 100644
index 0000000..e9edd7b
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/vo/WarningTypeStatVO.java
@@ -0,0 +1,22 @@
+package com.doumee.dao.business.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 鐢佃〃鎶ヨ绫诲瀷缁熻
+ */
+@Data
+@ApiModel("鐢佃〃鎶ヨ绫诲瀷缁熻")
+public class WarningTypeStatVO {
+
+    @ApiModelProperty("鎶ヨ浜嬩欢id")
+    private Integer warningDefId;
+
+    @ApiModelProperty("鎶ヨ椤瑰悕绉�")
+    private String warningName;
+
+    @ApiModelProperty("鏁伴噺")
+    private Long count;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwDeviceService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwDeviceService.java
index ef46e18..e5c5808 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwDeviceService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwDeviceService.java
@@ -8,6 +8,7 @@
 import com.doumee.dao.business.vo.YwDeviceDataVO;
 import com.doumee.dao.business.vo.YwDeviceParentCateDataVO;
 import com.doumee.dao.business.vo.YwDeviceStatusDataVO;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
 import java.util.Set;
@@ -112,4 +113,9 @@
     Set<YwDeviceParentCateDataVO> getDeviceCateData(YwDevice model);
 
     YwDeviceStatusDataVO getDeviceStatus(YwDevice model);
+
+    /**
+     * 鎵归噺瀵煎叆璁惧
+     */
+    String importBatch(MultipartFile file, LoginUserInfo user);
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalWarningService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalWarningService.java
index ab39583..0fce338 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalWarningService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalWarningService.java
@@ -4,6 +4,7 @@
 import com.doumee.core.model.PageWrap;
 import com.doumee.dao.business.dto.WarningDefOptionDTO;
 import com.doumee.dao.business.model.YwElectricalWarning;
+import com.doumee.dao.business.vo.WarningTypeStatVO;
 
 import java.util.List;
 
@@ -19,4 +20,6 @@
     String syncFromPlatform();
 
     List<WarningDefOptionDTO> listWarningDefOptions();
+
+    List<WarningTypeStatVO> warningTypeStats();
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java
new file mode 100644
index 0000000..dd64e4c
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java
@@ -0,0 +1,15 @@
+package com.doumee.service.business;
+
+import com.doumee.dao.business.vo.DailyEnergyStatVO;
+
+import java.util.List;
+
+/**
+ * 宸ヤ綔鍙扮數閲�/鐢佃垂缁熻
+ */
+public interface YwWorkDeskEnergyService {
+
+    List<DailyEnergyStatVO> electricalDailyStats();
+
+    List<DailyEnergyStatVO> conditionerDailyStats();
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java
index 8cfe1f1..66dc3c4 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java
@@ -27,6 +27,7 @@
 import com.doumee.service.business.YwCustomerRechargeBizService;
 import com.doumee.service.business.YwElectricalBizService;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -40,14 +41,13 @@
 import java.util.stream.Collectors;
 
 @Service
+@Slf4j
 public class YwCustomerRechargeBizServiceImpl implements YwCustomerRechargeBizService {
 
     private static final String ONLINE_TEXT = "鍦ㄧ嚎";
 
     @Autowired
     private YwCustomerMapper ywCustomerMapper;
-    @Autowired
-    private MemberMapper memberMapper;
     @Autowired
     private YwCustomerGsMapper ywCustomerGsMapper;
     @Autowired
@@ -69,14 +69,14 @@
 
     @Override
     public PageData<YwCustomerRechargeMerchantVO> findMerchantPage(PageWrap<YwCustomerRechargeQueryDTO> pageWrap) {
+        if (pageWrap == null) {
+            pageWrap = new PageWrap<>();
+        }
         YwCustomerRechargeQueryDTO query = pageWrap.getModel() != null ? pageWrap.getModel() : new YwCustomerRechargeQueryDTO();
         boolean hasDeviceFilter = query.getElectricalStatusFilter() != null || query.getConditionerStatusFilter() != null;
 
         if (hasDeviceFilter) {
-            List<YwCustomer> all = ywCustomerMapper.selectList(new QueryWrapper<YwCustomer>().lambda()
-                    .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                    .like(StringUtils.isNotBlank(query.getNameKeyword()), YwCustomer::getName, query.getNameKeyword())
-                    .orderByDesc(YwCustomer::getCreateDate));
+            List<YwCustomer> all = ywCustomerMapper.selectJoinList(YwCustomer.class, buildMerchantCustomerWrapper(query));
             List<YwCustomerRechargeMerchantVO> enriched = enrichMerchantList(all);
             List<YwCustomerRechargeMerchantVO> filtered = enriched.stream()
                     .filter(vo -> matchDeviceFilter(vo, query))
@@ -85,10 +85,7 @@
         }
 
         IPage<YwCustomer> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
-        IPage<YwCustomer> result = ywCustomerMapper.selectPage(page, new QueryWrapper<YwCustomer>().lambda()
-                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                .like(StringUtils.isNotBlank(query.getNameKeyword()), YwCustomer::getName, query.getNameKeyword())
-                .orderByDesc(YwCustomer::getCreateDate));
+        IPage<YwCustomer> result = ywCustomerMapper.selectJoinPage(page, YwCustomer.class, buildMerchantCustomerWrapper(query));
         List<YwCustomerRechargeMerchantVO> list = enrichMerchantList(result.getRecords());
         PageData<YwCustomerRechargeMerchantVO> data = new PageData<>();
         data.setRecords(list);
@@ -96,6 +93,17 @@
         data.setPage(result.getCurrent());
         data.setCapacity(result.getSize());
         return data;
+    }
+
+    private MPJLambdaWrapper<YwCustomer> buildMerchantCustomerWrapper(YwCustomerRechargeQueryDTO query) {
+        return new MPJLambdaWrapper<YwCustomer>()
+                .selectAll(YwCustomer.class)
+                .selectAs(Member::getName, YwCustomer::getMemberName)
+                .selectAs(Member::getPhone, YwCustomer::getMemberPhone)
+                .leftJoin(Member.class, Member::getId, YwCustomer::getMemberId)
+                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
+                .like(StringUtils.isNotBlank(query.getNameKeyword()), YwCustomer::getName, query.getNameKeyword())
+                .orderByDesc(YwCustomer::getCreateDate);
     }
 
     private PageData<YwCustomerRechargeMerchantVO> manualPage(List<YwCustomerRechargeMerchantVO> list, long page, long capacity) {
@@ -146,45 +154,23 @@
         }
         List<Integer> customerIds = customers.stream().map(YwCustomer::getId).collect(Collectors.toList());
 
-        Map<Integer, YwCustomerGs> gsMap = ywCustomerGsMapper.selectList(new QueryWrapper<YwCustomerGs>().lambda()
-                        .eq(YwCustomerGs::getIsdeleted, Constants.ZERO)
-                        .in(YwCustomerGs::getCustomerId, customerIds))
-                .stream().collect(Collectors.toMap(YwCustomerGs::getCustomerId, g -> g, (a, b) -> a));
+        Map<Integer, YwCustomerGs> gsMap = loadGsMap(customerIds);
 
-        List<YwCustomerElectrical> relE = ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
-                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
-                .in(YwCustomerElectrical::getCustomerId, customerIds));
+        List<YwCustomerElectrical> relE = loadCustomerElectricalRels(customerIds);
         Map<Integer, List<Integer>> customerElectricalIds = relE.stream()
                 .collect(Collectors.groupingBy(YwCustomerElectrical::getCustomerId,
                         Collectors.mapping(YwCustomerElectrical::getElectricalId, Collectors.toList())));
 
-        List<YwCustomerConditioner> relC = ywCustomerConditionerMapper.selectList(new QueryWrapper<YwCustomerConditioner>().lambda()
-                .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO)
-                .in(YwCustomerConditioner::getCustomerId, customerIds));
+        List<YwCustomerConditioner> relC = loadCustomerConditionerRels(customerIds);
         Map<Integer, List<Integer>> customerConditionerIds = relC.stream()
                 .collect(Collectors.groupingBy(YwCustomerConditioner::getCustomerId,
                         Collectors.mapping(YwCustomerConditioner::getConditionerId, Collectors.toList())));
 
         Set<Integer> allElectricalIds = relE.stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toSet());
-        Map<Integer, YwElectrical> electricalMap = allElectricalIds.isEmpty() ? Collections.emptyMap()
-                : ywElectricalMapper.selectBatchIds(allElectricalIds).stream()
-                .filter(e -> !Objects.equals(e.getIsdeleted(), Constants.ONE))
-                .collect(Collectors.toMap(YwElectrical::getId, e -> e, (a, b) -> a));
+        Map<Integer, YwElectrical> electricalMap = loadElectricalMap(allElectricalIds);
 
         Set<Integer> allConditionerIds = relC.stream().map(YwCustomerConditioner::getConditionerId).collect(Collectors.toSet());
-        Map<Integer, YwConditioner> conditionerMap = allConditionerIds.isEmpty() ? Collections.emptyMap()
-                : ywConditionerMapper.selectBatchIds(allConditionerIds).stream()
-                .filter(c -> !Objects.equals(c.getIsdeleted(), Constants.ONE))
-                .collect(Collectors.toMap(YwConditioner::getId, c -> c, (a, b) -> a));
-
-        Set<Integer> memberIds = customers.stream()
-                .map(YwCustomer::getMemberId)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
-        Map<Integer, Member> memberMap = memberIds.isEmpty() ? Collections.emptyMap()
-                : memberMapper.selectBatchIds(memberIds).stream()
-                .filter(m -> !Objects.equals(m.getIsdeleted(), Constants.ONE))
-                .collect(Collectors.toMap(Member::getId, m -> m, (a, b) -> a));
+        Map<Integer, YwConditioner> conditionerMap = loadConditionerMap(allConditionerIds);
 
         List<YwCustomerRechargeMerchantVO> list = new ArrayList<>();
         for (YwCustomer c : customers) {
@@ -194,11 +180,8 @@
             vo.setName(c.getName());
             vo.setPhone(c.getPhone());
             vo.setCreateDate(c.getCreateDate());
-            Member member = c.getMemberId() != null ? memberMap.get(c.getMemberId()) : null;
-            if (member != null) {
-                vo.setMemberName(member.getName());
-                vo.setMemberPhone(member.getPhone());
-            }
+            vo.setMemberName(c.getMemberName());
+            vo.setMemberPhone(StringUtils.defaultIfBlank(c.getMemberPhone(), c.getPhone()));
 
             List<Integer> eIds = customerElectricalIds.getOrDefault(c.getId(), Collections.emptyList());
             vo.setElectricalCount(eIds.size());
@@ -258,6 +241,86 @@
             list.add(vo);
         }
         return list;
+    }
+
+    private Map<Integer, YwCustomerGs> loadGsMap(List<Integer> customerIds) {
+        if (CollectionUtils.isEmpty(customerIds)) {
+            return Collections.emptyMap();
+        }
+        try {
+            return ywCustomerGsMapper.selectList(new QueryWrapper<YwCustomerGs>().lambda()
+                            .select(YwCustomerGs::getId, YwCustomerGs::getCustomerId,
+                                    YwCustomerGs::getLeftMoney, YwCustomerGs::getSyncDate, YwCustomerGs::getPlatformGsId)
+                            .eq(YwCustomerGs::getIsdeleted, Constants.ZERO)
+                            .in(YwCustomerGs::getCustomerId, customerIds))
+                    .stream().collect(Collectors.toMap(YwCustomerGs::getCustomerId, g -> g, (a, b) -> a));
+        } catch (Exception e) {
+            log.warn("load yw_customer_gs failed, skip gs stats: {}", e.getMessage());
+            return Collections.emptyMap();
+        }
+    }
+
+    private List<YwCustomerElectrical> loadCustomerElectricalRels(List<Integer> customerIds) {
+        if (CollectionUtils.isEmpty(customerIds)) {
+            return Collections.emptyList();
+        }
+        try {
+            return ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
+                    .select(YwCustomerElectrical::getCustomerId, YwCustomerElectrical::getElectricalId)
+                    .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
+                    .in(YwCustomerElectrical::getCustomerId, customerIds));
+        } catch (Exception e) {
+            log.warn("load yw_customer_electrical failed, skip electrical stats: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
+    private List<YwCustomerConditioner> loadCustomerConditionerRels(List<Integer> customerIds) {
+        if (CollectionUtils.isEmpty(customerIds)) {
+            return Collections.emptyList();
+        }
+        try {
+            return ywCustomerConditionerMapper.selectList(new QueryWrapper<YwCustomerConditioner>().lambda()
+                    .select(YwCustomerConditioner::getCustomerId, YwCustomerConditioner::getConditionerId)
+                    .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO)
+                    .in(YwCustomerConditioner::getCustomerId, customerIds));
+        } catch (Exception e) {
+            log.warn("load yw_customer_conditioner failed, skip conditioner stats: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
+    private Map<Integer, YwElectrical> loadElectricalMap(Set<Integer> electricalIds) {
+        if (CollectionUtils.isEmpty(electricalIds)) {
+            return Collections.emptyMap();
+        }
+        try {
+            return ywElectricalMapper.selectList(new QueryWrapper<YwElectrical>().lambda()
+                            .select(YwElectrical::getId, YwElectrical::getName, YwElectrical::getAddress,
+                                    YwElectrical::getBalance, YwElectrical::getOnline, YwElectrical::getIsdeleted)
+                            .in(YwElectrical::getId, electricalIds)
+                            .eq(YwElectrical::getIsdeleted, Constants.ZERO))
+                    .stream().collect(Collectors.toMap(YwElectrical::getId, e -> e, (a, b) -> a));
+        } catch (Exception e) {
+            log.warn("load electrical for merchant page failed: {}", e.getMessage());
+            return Collections.emptyMap();
+        }
+    }
+
+    private Map<Integer, YwConditioner> loadConditionerMap(Set<Integer> conditionerIds) {
+        if (CollectionUtils.isEmpty(conditionerIds)) {
+            return Collections.emptyMap();
+        }
+        try {
+            return ywConditionerMapper.selectList(new QueryWrapper<YwConditioner>().lambda()
+                            .select(YwConditioner::getId, YwConditioner::getOnline, YwConditioner::getIsdeleted)
+                            .in(YwConditioner::getId, conditionerIds)
+                            .eq(YwConditioner::getIsdeleted, Constants.ZERO))
+                    .stream().collect(Collectors.toMap(YwConditioner::getId, c -> c, (a, b) -> a));
+        } catch (Exception e) {
+            log.warn("load conditioner for merchant page failed: {}", e.getMessage());
+            return Collections.emptyMap();
+        }
     }
 
     @Override
@@ -367,19 +430,30 @@
 
     @Override
     public YwCustomerGs getCustomerGsConfig(Integer customerId) {
-        return ywCustomerGsMapper.selectOne(new QueryWrapper<YwCustomerGs>().lambda()
-                .eq(YwCustomerGs::getCustomerId, customerId)
-                .eq(YwCustomerGs::getIsdeleted, Constants.ZERO)
-                .last("limit 1"));
+        try {
+            return ywCustomerGsMapper.selectOne(new QueryWrapper<YwCustomerGs>().lambda()
+                    .eq(YwCustomerGs::getCustomerId, customerId)
+                    .eq(YwCustomerGs::getIsdeleted, Constants.ZERO)
+                    .last("limit 1"));
+        } catch (Exception e) {
+            log.warn("load yw_customer_gs failed, retry without stop_money: {}", e.getMessage());
+            return ywCustomerGsMapper.selectOne(new QueryWrapper<YwCustomerGs>().lambda()
+                    .select(YwCustomerGs::getId, YwCustomerGs::getCustomerId, YwCustomerGs::getPlatformGsId,
+                            YwCustomerGs::getIsPwr, YwCustomerGs::getIsRestStop, YwCustomerGs::getGsBz,
+                            YwCustomerGs::getLeftMoney, YwCustomerGs::getSyncDate,
+                            YwCustomerGs::getCreator, YwCustomerGs::getCreateDate,
+                            YwCustomerGs::getEditor, YwCustomerGs::getEditDate, YwCustomerGs::getIsdeleted)
+                    .eq(YwCustomerGs::getCustomerId, customerId)
+                    .eq(YwCustomerGs::getIsdeleted, Constants.ZERO)
+                    .last("limit 1"));
+        }
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void saveCustomerGsConfig(YwCustomerGsConfigDTO dto, LoginUserInfo user) {
         YwCustomer customer = requireCustomer(dto.getCustomerId());
-        if (CollectionUtils.isEmpty(dto.getConditioners())) {
-            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "璇疯嚦灏戝叧鑱斾竴鍙扮┖璋冨唴鏈�");
-        }
+        validateGsConfigRequired(dto);
         conditionerBizService.ensureLogin();
 
         List<Integer> conditionerIds = dto.getConditioners().stream()
@@ -413,10 +487,10 @@
         }
         gs.setEditor(user.getId());
         gs.setEditDate(new Date());
-        gs.setIsPwr(dto.getIsPwr() != null ? dto.getIsPwr() : Constants.ONE);
-        gs.setIsRestStop(dto.getIsRestStop() != null ? dto.getIsRestStop() : Constants.ZERO);
+        gs.setIsPwr(dto.getIsPwr());
+        gs.setIsRestStop(dto.getIsRestStop());
         gs.setGsBz(StringUtils.defaultString(dto.getGsBz()));
-        gs.setStopMoney(dto.getStopMoney() != null ? dto.getStopMoney() : BigDecimal.ZERO);
+        gs.setStopMoney(dto.getStopMoney());
 
         if (StringUtils.isBlank(customer.getName())) {
             throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀹㈡埛鍚嶇О涓嶈兘涓虹┖");
@@ -432,6 +506,8 @@
         companyReq.setIs_rest_stop(gs.getIsRestStop());
         companyReq.setGs_bz(gs.getGsBz());
         companyReq.setStop_money(gs.getStopMoney());
+        companyReq.setLi_dev(liDev);
+        companyReq.setD_dev(dDev);
 
         if (gs.getPlatformGsId() == null) {
             ConditionerBaseResponse<Object> addResp = ConditionerUtil.addGs(companyReq);
@@ -464,29 +540,88 @@
 
         refreshGsLeftMoney(gs);
         if (gs.getId() == null) {
-            ywCustomerGsMapper.insert(gs);
+            try {
+                ywCustomerGsMapper.insert(gs);
+            } catch (Exception e) {
+                if (gs.getStopMoney() != null) {
+                    log.warn("insert yw_customer_gs with stop_money failed, retry without stop_money: {}", e.getMessage());
+                    gs.setStopMoney(null);
+                    ywCustomerGsMapper.insert(gs);
+                } else {
+                    throw e;
+                }
+            }
         } else {
-            ywCustomerGsMapper.updateById(gs);
+            try {
+                ywCustomerGsMapper.updateById(gs);
+            } catch (Exception e) {
+                if (gs.getStopMoney() != null) {
+                    log.warn("update yw_customer_gs with stop_money failed, retry without stop_money: {}", e.getMessage());
+                    BigDecimal stopMoney = gs.getStopMoney();
+                    gs.setStopMoney(null);
+                    ywCustomerGsMapper.updateById(gs);
+                    gs.setStopMoney(stopMoney);
+                } else {
+                    throw e;
+                }
+            }
         }
 
-        ywCustomerConditionerMapper.update(null, new UpdateWrapper<YwCustomerConditioner>().lambda()
-                .set(YwCustomerConditioner::getIsdeleted, Constants.ONE)
-                .set(YwCustomerConditioner::getEditDate, new Date())
-                .set(YwCustomerConditioner::getEditor, user.getId())
-                .eq(YwCustomerConditioner::getCustomerId, dto.getCustomerId())
-                .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO));
+        saveCustomerConditionerRels(dto, user);
+    }
+
+    /**
+     * 鍏宠仈鍐呮満 upsert锛氳〃涓婃湁 uk(customer_id, conditioner_id)锛屼笉鑳借蒋鍒犲悗閲嶅 insert銆�
+     */
+    private void saveCustomerConditionerRels(YwCustomerGsConfigDTO dto, LoginUserInfo user) {
+        List<YwCustomerConditioner> existingRels = ywCustomerConditionerMapper.selectList(
+                new QueryWrapper<YwCustomerConditioner>().lambda()
+                        .eq(YwCustomerConditioner::getCustomerId, dto.getCustomerId()));
+        Map<Integer, YwCustomerConditioner> relByCondId = existingRels.stream()
+                .collect(Collectors.toMap(YwCustomerConditioner::getConditionerId, r -> r, (a, b) -> a));
+
+        Set<Integer> targetIds = dto.getConditioners().stream()
+                .map(YwCustomerGsConfigDTO.ConditionerItem::getConditionerId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        Date now = new Date();
+        for (YwCustomerConditioner rel : existingRels) {
+            if (!targetIds.contains(rel.getConditionerId())
+                    && Objects.equals(rel.getIsdeleted(), Constants.ZERO)) {
+                ywCustomerConditionerMapper.update(null, new UpdateWrapper<YwCustomerConditioner>().lambda()
+                        .set(YwCustomerConditioner::getIsdeleted, Constants.ONE)
+                        .set(YwCustomerConditioner::getEditDate, now)
+                        .set(YwCustomerConditioner::getEditor, user.getId())
+                        .eq(YwCustomerConditioner::getId, rel.getId()));
+            }
+        }
 
         for (YwCustomerGsConfigDTO.ConditionerItem item : dto.getConditioners()) {
-            YwCustomerConditioner rel = new YwCustomerConditioner();
-            rel.setCreator(user.getId());
-            rel.setCreateDate(new Date());
-            rel.setEditor(user.getId());
-            rel.setEditDate(new Date());
-            rel.setIsdeleted(Constants.ZERO);
-            rel.setCustomerId(dto.getCustomerId());
-            rel.setConditionerId(item.getConditionerId());
-            rel.setDevRatio(item.getDevRatio() != null ? item.getDevRatio() : 100);
-            ywCustomerConditionerMapper.insert(rel);
+            if (item.getConditionerId() == null) {
+                continue;
+            }
+            int ratio = item.getDevRatio() != null ? item.getDevRatio() : 100;
+            YwCustomerConditioner rel = relByCondId.get(item.getConditionerId());
+            if (rel != null) {
+                rel.setIsdeleted(Constants.ZERO);
+                rel.setDevRatio(ratio);
+                rel.setEditor(user.getId());
+                rel.setEditDate(now);
+                ywCustomerConditionerMapper.updateById(rel);
+            } else {
+                YwCustomerConditioner created = new YwCustomerConditioner();
+                created.setCreator(user.getId());
+                created.setCreateDate(now);
+                created.setEditor(user.getId());
+                created.setEditDate(now);
+                created.setIsdeleted(Constants.ZERO);
+                created.setCustomerId(dto.getCustomerId());
+                created.setConditionerId(item.getConditionerId());
+                created.setDevRatio(ratio);
+                ywCustomerConditionerMapper.insert(created);
+                relByCondId.put(item.getConditionerId(), created);
+            }
         }
     }
 
@@ -838,6 +973,24 @@
         }
     }
 
+    private void validateGsConfigRequired(YwCustomerGsConfigDTO dto) {
+        if (dto.getStopMoney() == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "娆犺垂棰濆害涓嶈兘涓虹┖");
+        }
+        if (dto.getStopMoney().compareTo(BigDecimal.ZERO) < 0) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "娆犺垂棰濆害涓嶈兘灏忎簬0");
+        }
+        if (dto.getIsPwr() == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "璁¤垂寮�鍏充笉鑳戒负绌�");
+        }
+        if (dto.getIsRestStop() == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鏄惁鍋滄満涓嶈兘涓虹┖");
+        }
+        if (CollectionUtils.isEmpty(dto.getConditioners())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "璇疯嚦灏戝叧鑱斾竴鍙扮┖璋冨唴鏈�");
+        }
+    }
+
     private String apiMsg(ConditionerBaseResponse<?> resp, String def) {
         return resp != null && StringUtils.isNotBlank(resp.getMessage()) ? resp.getMessage() : def;
     }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwDeviceServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwDeviceServiceImpl.java
index c308645..4754f5a 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwDeviceServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwDeviceServiceImpl.java
@@ -9,14 +9,19 @@
 import com.doumee.core.utils.Constants;
 import com.doumee.core.utils.DateUtil;
 import com.doumee.core.utils.Utils;
+import com.doumee.core.annotation.excel.ExcelImporter;
+import com.doumee.dao.admin.request.DeviceImport;
+import com.doumee.dao.business.*;
 import com.doumee.dao.business.YwDeviceMapper;
 import com.doumee.dao.business.YwDeviceRecordMapper;
+import com.doumee.dao.business.join.MemberJoinMapper;
 import com.doumee.dao.business.model.*;
 import com.doumee.dao.business.vo.YwDeviceCateDataVO;
 import com.doumee.dao.business.vo.YwDeviceDataVO;
 import com.doumee.dao.business.vo.YwDeviceParentCateDataVO;
 import com.doumee.dao.business.vo.YwDeviceStatusDataVO;
 import com.doumee.dao.system.MultifileMapper;
+import com.doumee.dao.system.SystemUserMapper;
 import com.doumee.dao.system.model.Multifile;
 import com.doumee.dao.system.model.SystemUser;
 import com.doumee.service.business.YwDeviceRecordService;
@@ -31,8 +36,11 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
+import org.springframework.web.multipart.MultipartFile;
 
+import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -57,6 +65,22 @@
     @Autowired
     private RedisTemplate<String,Object> redisTemplate;
 
+    @Autowired
+    private CategoryMapper categoryMapper;
+    @Autowired
+    private YwProjectMapper ywProjectMapper;
+    @Autowired
+    private YwBuildingMapper ywBuildingMapper;
+    @Autowired
+    private YwFloorMapper ywFloorMapper;
+    @Autowired
+    private YwRoomMapper ywRoomMapper;
+    @Autowired
+    private MemberJoinMapper memberJoinMapper;
+    @Autowired
+    private SystemUserMapper systemUserMapper;
+
+    private static final int IMPORT_EXCEL_ROW_OFFSET = 3;
 
     @Override
     public Integer create(YwDevice ywDevice) {
@@ -434,7 +458,313 @@
         return ywDeviceStatusDataVO;
 
     }
-  
+
+    @Override
+    @Transactional(rollbackFor = {BusinessException.class, Exception.class})
+    public String importBatch(MultipartFile file, LoginUserInfo loginUserInfo) {
+        List<DeviceImport> dataList;
+        try {
+            ExcelImporter ie = new ExcelImporter(file, 1, 0);
+            dataList = ie.getDataList(DeviceImport.class, null);
+        } catch (Exception e) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝瀵煎叆鏂囦欢瑙f瀽澶辫触锛岃妫�鏌ヨ〃鏍兼牸寮忥紒");
+        }
+        if (CollectionUtils.isEmpty(dataList)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝褰曞叆鏁版嵁涓虹┖锛�");
+        }
+        DeviceImportCache cache = buildImportCache();
+        List<YwDevice> insertList = new ArrayList<>();
+        List<YwDevice> updateList = new ArrayList<>();
+        Set<String> fileCodes = new HashSet<>();
+        Date now = new Date();
+        for (int i = 0; i < dataList.size(); i++) {
+            DeviceImport row = dataList.get(i);
+            if (isImportBlankRow(row)) {
+                continue;
+            }
+            YwDevice device = checkImportRow(row, i, cache, fileCodes, loginUserInfo, now);
+            if (device.getId() != null) {
+                updateList.add(device);
+            } else {
+                insertList.add(device);
+            }
+        }
+        if (CollectionUtils.isEmpty(insertList) && CollectionUtils.isEmpty(updateList)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝褰曞叆鏈夋晥鏁版嵁涓虹┖锛�");
+        }
+        if (!CollectionUtils.isEmpty(insertList)) {
+            ywDeviceMapper.insert(insertList);
+        }
+        for (YwDevice device : updateList) {
+            ywDeviceMapper.updateById(device);
+        }
+        return String.format("鎿嶄綔鎴愬姛锛屾湰娆″鍏� 鏂板%d涓澶囷紝鏇存柊%d涓澶�", insertList.size(), updateList.size());
+    }
+
+    private boolean isImportBlankRow(DeviceImport row) {
+        return StringUtils.isBlank(row.getCode())
+                && StringUtils.isBlank(row.getName())
+                && StringUtils.isBlank(row.getProjectName())
+                && StringUtils.isBlank(row.getRoomPath());
+    }
+
+    private DeviceImportCache buildImportCache() {
+        DeviceImportCache cache = new DeviceImportCache();
+        List<Category> categories = categoryMapper.selectList(new QueryWrapper<Category>().lambda()
+                .eq(Category::getIsdeleted, Constants.ZERO)
+                .eq(Category::getType, Constants.FIVE));
+        Map<Integer, Category> categoryById = categories.stream()
+                .collect(Collectors.toMap(Category::getId, c -> c, (a, b) -> a));
+        for (Category category : categories) {
+            if (category.getParentId() == null || category.getParentId() <= 0) {
+                continue;
+            }
+            Category parent = categoryById.get(category.getParentId());
+            if (parent == null || StringUtils.isBlank(parent.getName()) || StringUtils.isBlank(category.getName())) {
+                continue;
+            }
+            cache.categoryPathMap.put(parent.getName() + "/" + category.getName(), category.getId());
+        }
+        List<YwProject> projects = ywProjectMapper.selectList(new QueryWrapper<YwProject>().lambda()
+                .eq(YwProject::getIsdeleted, Constants.ZERO));
+        for (YwProject project : projects) {
+            if (StringUtils.isNotBlank(project.getName())) {
+                cache.projectNameMap.put(project.getName(), project.getId());
+            }
+        }
+        List<YwBuilding> buildings = ywBuildingMapper.selectList(new QueryWrapper<YwBuilding>().lambda()
+                .eq(YwBuilding::getIsdeleted, Constants.ZERO));
+        for (YwBuilding building : buildings) {
+            if (building.getProjectId() != null && StringUtils.isNotBlank(building.getName())) {
+                cache.buildingMap.put(building.getProjectId() + "|" + building.getName(), building.getId());
+            }
+        }
+        List<YwFloor> floors = ywFloorMapper.selectList(new QueryWrapper<YwFloor>().lambda()
+                .eq(YwFloor::getIsdeleted, Constants.ZERO));
+        for (YwFloor floor : floors) {
+            if (floor.getBuildingId() != null && StringUtils.isNotBlank(floor.getName())) {
+                cache.floorMap.put(floor.getBuildingId() + "|" + floor.getName(), floor.getId());
+            }
+        }
+        List<YwRoom> rooms = ywRoomMapper.selectList(new QueryWrapper<YwRoom>().lambda()
+                .eq(YwRoom::getIsdeleted, Constants.ZERO));
+        for (YwRoom room : rooms) {
+            if (room.getProjectId() == null || room.getBuildingId() == null || room.getFloor() == null) {
+                continue;
+            }
+            if (StringUtils.isNotBlank(room.getCode())) {
+                cache.roomMap.put(roomKey(room.getProjectId(), room.getBuildingId(), room.getFloor(), room.getCode()), room.getId());
+            }
+            if (StringUtils.isNotBlank(room.getRoomNum())) {
+                cache.roomMap.put(roomKey(room.getProjectId(), room.getBuildingId(), room.getFloor(), room.getRoomNum()), room.getId());
+            }
+        }
+        List<YwDevice> devices = ywDeviceMapper.selectList(new QueryWrapper<YwDevice>().lambda()
+                .eq(YwDevice::getIsdeleted, Constants.ZERO)
+                .select(YwDevice::getId, YwDevice::getCode));
+        for (YwDevice device : devices) {
+            if (StringUtils.isNotBlank(device.getCode())) {
+                cache.existingCodeIdMap.put(device.getCode(), device.getId());
+            }
+        }
+        List<Member> members = memberJoinMapper.selectJoinList(Member.class, new MPJLambdaWrapper<Member>()
+                .select(Member::getId, Member::getName)
+                .leftJoin(Company.class, Company::getId, Member::getCompanyId)
+                .eq(Member::getIsdeleted, Constants.ZERO)
+                .eq(Company::getType, Constants.ONE));
+        if (!CollectionUtils.isEmpty(members)) {
+            List<Integer> memberIds = members.stream().map(Member::getId).filter(Objects::nonNull).collect(Collectors.toList());
+            if (!CollectionUtils.isEmpty(memberIds)) {
+                List<SystemUser> systemUsers = systemUserMapper.selectList(new QueryWrapper<SystemUser>().lambda()
+                        .eq(SystemUser::getDeleted, Boolean.FALSE)
+                        .in(SystemUser::getMemberId, memberIds));
+                Map<Integer, Integer> memberUserMap = systemUsers.stream()
+                        .filter(u -> u.getMemberId() != null)
+                        .collect(Collectors.toMap(SystemUser::getMemberId, SystemUser::getId, (a, b) -> a));
+                for (Member member : members) {
+                    if (StringUtils.isBlank(member.getName())) {
+                        continue;
+                    }
+                    Integer userId = memberUserMap.get(member.getId());
+                    if (userId == null) {
+                        continue;
+                    }
+                    if (cache.internalUserNameMap.containsKey(member.getName())) {
+                        cache.duplicateInternalUserNames.add(member.getName());
+                    }
+                    cache.internalUserNameMap.put(member.getName(), userId);
+                }
+            }
+        }
+        return cache;
+    }
+
+    private String roomKey(Integer projectId, Integer buildingId, Integer floorId, String roomName) {
+        return projectId + "|" + buildingId + "|" + floorId + "|" + roomName;
+    }
+
+    private YwDevice checkImportRow(DeviceImport row, int index, DeviceImportCache cache, Set<String> fileCodes,
+                                    LoginUserInfo loginUserInfo, Date now) {
+        int rowNum = index + IMPORT_EXCEL_ROW_OFFSET;
+        if (StringUtils.isBlank(row.getCode())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛岃澶囩紪鍙蜂笉鑳戒负绌猴紝璇锋鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        if (StringUtils.isBlank(row.getName())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛岃澶囧悕绉颁笉鑳戒负绌猴紝璇锋鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        if (StringUtils.isBlank(row.getProjectName())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛屾墍灞為」鐩笉鑳戒负绌猴紝璇锋鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        if (StringUtils.isBlank(row.getRoomPath())) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛屽叧鑱旀埧婧愪笉鑳戒负绌猴紝璇锋鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        String code = row.getCode().trim();
+        if (fileCodes.contains(code)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛岃澶囩紪鍙枫��" + code + "銆戦噸澶嶏紝璇锋鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        fileCodes.add(code);
+        Integer existingId = cache.existingCodeIdMap.get(code);
+
+        Integer cateId = null;
+        if (StringUtils.isNotBlank(row.getCategoryPath())) {
+            String categoryPath = row.getCategoryPath().trim();
+            String[] cateParts = categoryPath.split("/");
+            if (cateParts.length < 2 || StringUtils.isBlank(cateParts[1])) {
+                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛岃澶囧垎绫汇��" + categoryPath + "銆戞牸寮忎笉姝g‘锛岃浣跨敤涓�绾�/浜岀骇鏍煎紡锛�");
+            }
+            cateId = cache.categoryPathMap.get(categoryPath);
+            if (cateId == null) {
+                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛岃澶囧垎绫汇��" + categoryPath + "銆戞湭鎵惧埌锛岃妫�鏌ヨ〃鏍煎唴瀹癸紒");
+            }
+        }
+
+        Integer projectId = cache.projectNameMap.get(row.getProjectName().trim());
+        if (projectId == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛屾墍灞為」鐩��" + row.getProjectName() + "銆戞湭鎵惧埌锛岃妫�鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+
+        String[] roomParts = row.getRoomPath().trim().split("/");
+        if (roomParts.length < 3 || StringUtils.isAnyBlank(roomParts[0], roomParts[1], roomParts[2])) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛屽叧鑱旀埧婧愩��" + row.getRoomPath() + "銆戞牸寮忎笉姝g‘锛岃浣跨敤妤煎畤/妤煎眰/鎴挎簮鏍煎紡锛�");
+        }
+        Integer buildingId = cache.buildingMap.get(projectId + "|" + roomParts[0].trim());
+        if (buildingId == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛屽叧鑱旀埧婧愩��" + row.getRoomPath() + "銆戞湭鎵惧埌锛岃妫�鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        Integer floorId = cache.floorMap.get(buildingId + "|" + roomParts[1].trim());
+        if (floorId == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛屽叧鑱旀埧婧愩��" + row.getRoomPath() + "銆戞湭鎵惧埌锛岃妫�鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        Integer roomId = cache.roomMap.get(roomKey(projectId, buildingId, floorId, roomParts[2].trim()));
+        if (roomId == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛屽叧鑱旀埧婧愩��" + row.getRoomPath() + "銆戞湭鎵惧埌锛岃妫�鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+
+        Integer status = parseImportStatus(row.getStatusText(), rowNum);
+
+        Integer userId = resolveInternalUserId(row.getAdminUserName(), rowNum, "璁惧绠$悊鍛�", cache);
+        Integer maintenanceUserId = resolveInternalUserId(row.getMaintenanceUserName(), rowNum, "缁翠繚璐熻矗浜�", cache);
+
+        YwDevice device = new YwDevice();
+        device.setCode(code);
+        device.setName(row.getName().trim());
+        device.setCateId(cateId);
+        device.setModelNo(StringUtils.trimToNull(row.getModelNo()));
+        device.setUserId(userId);
+        device.setAddr(StringUtils.trimToNull(row.getAddr()));
+        device.setProjectId(projectId);
+        device.setBuildingId(buildingId);
+        device.setFloorId(floorId);
+        device.setRoomId(roomId);
+        device.setBuyDate(parseImportDate(row.getBuyDate(), rowNum, "璐叆鏃堕棿"));
+        device.setStatus(status);
+        device.setContent(StringUtils.trimToNull(row.getContent()));
+        device.setSupplier(StringUtils.trimToNull(row.getSupplier()));
+        device.setSupplierLinker(StringUtils.trimToNull(row.getSupplierLinker()));
+        device.setSupplierPhone(StringUtils.trimToNull(row.getSupplierPhone()));
+        device.setMaintenanceUserId(maintenanceUserId);
+        device.setMaintenanceOverDate(parseImportDate(row.getMaintenanceOverDate(), rowNum, "缁翠繚鍒版湡鏃�"));
+        device.setMaintenanceContent(StringUtils.trimToNull(row.getMaintenanceContent()));
+        if (existingId != null) {
+            device.setId(existingId);
+            device.setEditor(loginUserInfo.getId());
+            device.setEditDate(now);
+        } else {
+            device.setCreator(loginUserInfo.getId());
+            device.setCreateDate(now);
+            device.setIsdeleted(Constants.ZERO);
+        }
+        return device;
+    }
+
+    private Integer resolveInternalUserId(String userName, int rowNum, String fieldLabel, DeviceImportCache cache) {
+        if (StringUtils.isBlank(userName)) {
+            return null;
+        }
+        String name = userName.trim();
+        if (cache.duplicateInternalUserNames.contains(name)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛�" + fieldLabel + "銆�" + name + "銆戝瓨鍦ㄩ噸鍚嶏紝璇锋鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        Integer userId = cache.internalUserNameMap.get(name);
+        if (userId == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛�" + fieldLabel + "銆�" + name + "銆戞湭鎵惧埌锛岃妫�鏌ヨ〃鏍煎唴瀹癸紒");
+        }
+        return userId;
+    }
+
+    private Integer parseImportStatus(String statusText, int rowNum) {
+        if (StringUtils.isBlank(statusText)) {
+            return Constants.ZERO;
+        }
+        String text = statusText.trim();
+        if ("姝e父".equals(text)) {
+            return Constants.ZERO;
+        }
+        if ("鎹熷潖".equals(text)) {
+            return Constants.ONE;
+        }
+        if ("鎶ュ簾".equals(text)) {
+            return Constants.TWO;
+        }
+        throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "瀵逛笉璧凤紝绗�" + rowNum + "琛岃澶囩姸鎬併��" + statusText + "銆戜笉姝g‘锛屼粎鏀寔姝e父/鎹熷潖/鎶ュ簾锛�");
+    }
+
+    private Date parseImportDate(String dateText, int rowNum, String fieldLabel) {
+        if (StringUtils.isBlank(dateText)) {
+            return null;
+        }
+        String text = dateText.trim();
+        if (StringUtils.endsWith(text, ".0")) {
+            text = StringUtils.substringBefore(text, ".0");
+        }
+        Date date = DateUtil.parseFromFormats(text);
+        if (date != null) {
+            return date;
+        }
+        try {
+            return new SimpleDateFormat("yyyy-MM-dd").parse(text);
+        } catch (Exception ignored) {
+            // try excel serial number below
+        }
+        try {
+            return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(Double.parseDouble(text));
+        } catch (Exception ignored) {
+            // fall through to error
+        }
+        throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),
+                "瀵逛笉璧凤紝绗�" + rowNum + "琛�" + fieldLabel + "銆�" + dateText + "銆戞牸寮忎笉姝g‘锛岃浣跨敤yyyy-MM-dd鏍煎紡锛�");
+    }
+
+    private static class DeviceImportCache {
+        private final Map<String, Integer> categoryPathMap = new HashMap<>();
+        private final Map<String, Integer> projectNameMap = new HashMap<>();
+        private final Map<String, Integer> buildingMap = new HashMap<>();
+        private final Map<String, Integer> floorMap = new HashMap<>();
+        private final Map<String, Integer> roomMap = new HashMap<>();
+        private final Map<String, Integer> existingCodeIdMap = new HashMap<>();
+        private final Map<String, Integer> internalUserNameMap = new HashMap<>();
+        private final Set<String> duplicateInternalUserNames = new HashSet<>();
+    }
 
 }
 
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java
index 3eb58fd..bcfd258 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java
@@ -47,6 +47,9 @@
         if (model.getActionType() != null) {
             queryWrapper.eq(YwElectricalActions::getActionType, model.getActionType());
         }
+        if (model.getElectricalId() != null) {
+            queryWrapper.eq(YwElectricalActions::getElectricalId, model.getElectricalId());
+        }
         if (model.getOperateTimeBegin() != null) {
             queryWrapper.ge(YwElectricalActions::getCreateDate, Utils.Date.getStart(model.getOperateTimeBegin()));
         }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
index c9911dd..8ab9565 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
@@ -52,6 +52,8 @@
     public static final int ACTION_OPEN = 6;
     public static final int ACTION_RECHARGE = 7;
     public static final int ACTION_READ = 8;
+    public static final int ACTION_POWER_PROTECT = 9;
+    public static final int ACTION_POWER_PROTECT_RELEASE = 10;
 
     private static final long FIRST_STATUS_QUERY_DELAY_MS = 30_000L;
     private static final long STATUS_QUERY_MIN_INTERVAL_MS = 3_600_000L;
@@ -164,6 +166,10 @@
                 return doEleControl(e, 10, ACTION_TRIP, user);
             case "close":
                 return doEleControl(e, 11, ACTION_CLOSE, user);
+            case "powerProtect":
+                return doEleControl(e, 63, ACTION_POWER_PROTECT, user);
+            case "powerProtectRelease":
+                return doEleControl(e, 220, ACTION_POWER_PROTECT_RELEASE, user);
             case "openAccount":
                 return doOpenAccount(e, dto, user);
             case "recharge":
@@ -209,7 +215,7 @@
         List<OpenAccountRequest> list = new ArrayList<>();
         list.add(req);
         ElectronicBaseResponse resp = ElectronicToolUtil.eleControl(list);
-        return finishAsync(e, actionType, oprId, "/Api_v2/ele_security/ele_control", reqJson, resp, user);
+        return finishAsync(e, actionType, oprId, "/Api_v2/ele_control", reqJson, resp, user);
     }
 
     private String doOpenAccount(YwElectrical e, YwElectricalOperateDTO dto, LoginUserInfo user) {
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalWarningServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalWarningServiceImpl.java
index 8a06b32..1f12943 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalWarningServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalWarningServiceImpl.java
@@ -22,6 +22,7 @@
 import com.doumee.dao.business.model.YwElectricalRoom;
 import com.doumee.dao.business.model.YwElectricalWarning;
 import com.doumee.dao.business.model.YwRoom;
+import com.doumee.dao.business.vo.WarningTypeStatVO;
 import com.doumee.service.business.YwElectricalWarningService;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.apache.commons.lang3.StringUtils;
@@ -159,6 +160,38 @@
         return list;
     }
 
+    @Override
+    public List<WarningTypeStatVO> warningTypeStats() {
+        QueryWrapper<YwElectricalWarning> wrapper = new QueryWrapper<>();
+        wrapper.select("warning_def_id", "count(1) as cnt")
+                .eq("isdeleted", Constants.ZERO)
+                .eq("device_type", ELECTRICAL_DEVICE_TYPE)
+                .isNotNull("warning_def_id")
+                .groupBy("warning_def_id")
+                .orderByDesc("cnt");
+        List<Map<String, Object>> rows = ywElectricalWarningMapper.selectMaps(wrapper);
+        List<WarningTypeStatVO> list = new ArrayList<>();
+        if (CollectionUtils.isEmpty(rows)) {
+            return list;
+        }
+        for (Map<String, Object> row : rows) {
+            Object defIdObj = row.get("warning_def_id");
+            if (defIdObj == null) {
+                continue;
+            }
+            Integer warningDefId = Integer.parseInt(String.valueOf(defIdObj));
+            Object cntObj = row.get("cnt");
+            long count = cntObj == null ? 0L : Long.parseLong(String.valueOf(cntObj));
+            WarningTypeStatVO stat = new WarningTypeStatVO();
+            stat.setWarningDefId(warningDefId);
+            stat.setCount(count);
+            ElectronicConstant.warningDefId def = ElectronicConstant.warningDefId.getByKey(warningDefId);
+            stat.setWarningName(def != null ? def.getName() : "鏈煡鎶ヨ");
+            list.add(stat);
+        }
+        return list;
+    }
+
     private MPJLambdaWrapper<YwElectricalWarning> buildPageQuery(PageWrap<YwElectricalWarning> pageWrap) {
         MPJLambdaWrapper<YwElectricalWarning> queryWrapper = new MPJLambdaWrapper<>();
         YwElectricalWarning model = pageWrap.getModel() == null ? new YwElectricalWarning() : pageWrap.getModel();
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java
new file mode 100644
index 0000000..aa2d11c
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java
@@ -0,0 +1,228 @@
+package com.doumee.service.business.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.doumee.core.utils.Constants;
+import com.doumee.dao.business.YwConditionerUsageMapper;
+import com.doumee.dao.business.YwElectricalDataMapper;
+import com.doumee.dao.business.model.YwConditionerUsage;
+import com.doumee.dao.business.model.YwElectricalData;
+import com.doumee.dao.business.vo.DailyEnergyStatVO;
+import com.doumee.service.business.YwWorkDeskEnergyService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class YwWorkDeskEnergyServiceImpl implements YwWorkDeskEnergyService {
+
+    private static final int DAYS = 30;
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final String READ_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
+
+    @Autowired
+    private YwElectricalDataMapper ywElectricalDataMapper;
+
+    @Autowired
+    private YwConditionerUsageMapper ywConditionerUsageMapper;
+
+    @Override
+    public List<DailyEnergyStatVO> electricalDailyStats() {
+        LocalDate end = LocalDate.now();
+        LocalDate start = end.minusDays(DAYS - 1L);
+        LocalDate queryStart = start.minusDays(1L);
+        Map<String, DailyEnergyStatVO> bucket = initDailyBucket(start, end);
+
+        List<YwElectricalData> rows = ywElectricalDataMapper.selectList(new QueryWrapper<YwElectricalData>().lambda()
+                .eq(YwElectricalData::getIsdeleted, Constants.ZERO)
+                .ge(YwElectricalData::getCreateDate, java.sql.Date.valueOf(queryStart))
+                .le(YwElectricalData::getCreateDate, java.sql.Date.valueOf(end.plusDays(1))));
+
+        SimpleDateFormat dayFmt = new SimpleDateFormat("yyyy-MM-dd");
+        SimpleDateFormat readTimeFmt = new SimpleDateFormat(READ_TIME_PATTERN);
+        Map<String, Map<String, MeterDayReading>> meterDayLatest = new HashMap<>();
+
+        for (YwElectricalData row : rows) {
+            String meterKey = resolveMeterKey(row);
+            if (StringUtils.isBlank(meterKey)) {
+                continue;
+            }
+            String dayKey = resolveElectricalDayKey(row, dayFmt);
+            if (StringUtils.isBlank(dayKey)) {
+                continue;
+            }
+            BigDecimal totalEnergy = resolveTotalEnergy(row);
+            if (totalEnergy.compareTo(BigDecimal.ZERO) < 0) {
+                continue;
+            }
+            long readingTime = resolveReadingTime(row, readTimeFmt);
+            MeterDayReading snapshot = new MeterDayReading(totalEnergy, parseDecimal(row.getDqdj()), readingTime);
+            upsertLatestReading(meterDayLatest, meterKey, dayKey, snapshot);
+        }
+
+        for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) {
+            String todayKey = day.format(DATE_FMT);
+            String yesterdayKey = day.minusDays(1).format(DATE_FMT);
+            DailyEnergyStatVO stat = bucket.get(todayKey);
+            for (Map<String, MeterDayReading> dayReadings : meterDayLatest.values()) {
+                MeterDayReading todayReading = dayReadings.get(todayKey);
+                MeterDayReading yesterdayReading = dayReadings.get(yesterdayKey);
+                if (todayReading == null || yesterdayReading == null) {
+                    continue;
+                }
+                BigDecimal usage = todayReading.totalEnergy.subtract(yesterdayReading.totalEnergy);
+                if (usage.compareTo(BigDecimal.ZERO) <= 0) {
+                    continue;
+                }
+                stat.setTotalKwh(stat.getTotalKwh().add(usage));
+                stat.setTotalFee(stat.getTotalFee().add(usage.multiply(todayReading.price)));
+            }
+        }
+        return normalizeBucket(bucket);
+    }
+
+    @Override
+    public List<DailyEnergyStatVO> conditionerDailyStats() {
+        LocalDate end = LocalDate.now();
+        LocalDate start = end.minusDays(DAYS - 1L);
+        Map<String, DailyEnergyStatVO> bucket = initDailyBucket(start, end);
+
+        List<YwConditionerUsage> rows = ywConditionerUsageMapper.selectList(new QueryWrapper<YwConditionerUsage>().lambda()
+                .eq(YwConditionerUsage::getIsdeleted, Constants.ZERO)
+                .ge(YwConditionerUsage::getUsageDate, java.sql.Date.valueOf(start))
+                .le(YwConditionerUsage::getUsageDate, java.sql.Date.valueOf(end)));
+
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        for (YwConditionerUsage row : rows) {
+            if (row.getUsageDate() == null) {
+                continue;
+            }
+            String dayKey = sdf.format(row.getUsageDate());
+            if (!bucket.containsKey(dayKey)) {
+                continue;
+            }
+            DailyEnergyStatVO stat = bucket.get(dayKey);
+            stat.setTotalKwh(stat.getTotalKwh().add(nullToZero(row.getSumDl())));
+            stat.setTotalFee(stat.getTotalFee().add(nullToZero(row.getSumDf())));
+        }
+        return normalizeBucket(bucket);
+    }
+
+    private static void upsertLatestReading(Map<String, Map<String, MeterDayReading>> meterDayLatest,
+                                            String meterKey,
+                                            String dayKey,
+                                            MeterDayReading snapshot) {
+        Map<String, MeterDayReading> dayMap = meterDayLatest.computeIfAbsent(meterKey, key -> new HashMap<>());
+        MeterDayReading existing = dayMap.get(dayKey);
+        if (existing == null || snapshot.readingTime >= existing.readingTime) {
+            dayMap.put(dayKey, snapshot);
+        }
+    }
+
+    private static String resolveMeterKey(YwElectricalData row) {
+        if (StringUtils.isNotBlank(row.getAddress())) {
+            return row.getAddress().trim();
+        }
+        if (StringUtils.isNotBlank(row.getDeviceId())) {
+            return "dev:" + row.getDeviceId().trim();
+        }
+        if (StringUtils.isNotBlank(row.getMid())) {
+            return "mid:" + row.getMid().trim();
+        }
+        return null;
+    }
+
+    private static BigDecimal resolveTotalEnergy(YwElectricalData row) {
+        BigDecimal total = parseDecimal(row.getZhygzdl());
+        if (total.compareTo(BigDecimal.ZERO) <= 0) {
+            total = parseDecimal(row.getZyje());
+        }
+        return total;
+    }
+
+    private static long resolveReadingTime(YwElectricalData row, SimpleDateFormat readTimeFmt) {
+        if (StringUtils.isNotBlank(row.getAddTime())) {
+            String addTime = row.getAddTime().trim();
+            if (addTime.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
+                try {
+                    return readTimeFmt.parse(addTime).getTime();
+                } catch (ParseException ignored) {
+                    // fallback to createDate
+                }
+            }
+        }
+        Date createDate = row.getCreateDate();
+        return createDate == null ? 0L : createDate.getTime();
+    }
+
+    private static List<DailyEnergyStatVO> normalizeBucket(Map<String, DailyEnergyStatVO> bucket) {
+        List<DailyEnergyStatVO> list = new ArrayList<>(bucket.values());
+        for (DailyEnergyStatVO stat : list) {
+            stat.setTotalKwh(stat.getTotalKwh().setScale(2, RoundingMode.HALF_UP));
+            stat.setTotalFee(stat.getTotalFee().setScale(2, RoundingMode.HALF_UP));
+        }
+        return list;
+    }
+
+    private static Map<String, DailyEnergyStatVO> initDailyBucket(LocalDate start, LocalDate end) {
+        Map<String, DailyEnergyStatVO> bucket = new LinkedHashMap<>();
+        for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
+            DailyEnergyStatVO vo = new DailyEnergyStatVO();
+            vo.setStatDate(d.format(DATE_FMT));
+            vo.setTotalKwh(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
+            vo.setTotalFee(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
+            bucket.put(vo.getStatDate(), vo);
+        }
+        return bucket;
+    }
+
+    private static String resolveElectricalDayKey(YwElectricalData row, SimpleDateFormat sdf) {
+        if (StringUtils.isNotBlank(row.getAddTime())) {
+            String addTime = row.getAddTime().trim();
+            if (addTime.length() >= 10) {
+                return addTime.substring(0, 10);
+            }
+        }
+        Date createDate = row.getCreateDate();
+        return createDate == null ? null : sdf.format(createDate);
+    }
+
+    private static BigDecimal parseDecimal(String val) {
+        if (StringUtils.isBlank(val)) {
+            return BigDecimal.ZERO;
+        }
+        try {
+            return new BigDecimal(val.trim());
+        } catch (NumberFormatException e) {
+            return BigDecimal.ZERO;
+        }
+    }
+
+    private static BigDecimal nullToZero(BigDecimal val) {
+        return val == null ? BigDecimal.ZERO : val;
+    }
+
+    private static class MeterDayReading {
+        private final BigDecimal totalEnergy;
+        private final BigDecimal price;
+        private final long readingTime;
+
+        private MeterDayReading(BigDecimal totalEnergy, BigDecimal price, long readingTime) {
+            this.totalEnergy = totalEnergy;
+            this.price = price;
+            this.readingTime = readingTime;
+        }
+    }
+}

--
Gitblit v1.9.3