From 74190ebc24e6e850d418ad0ce041fd91b795c23e Mon Sep 17 00:00:00 2001
From: doum <doum>
Date: 星期一, 25 五月 2026 18:32:21 +0800
Subject: [PATCH] 新增智能电表、空调管理

---
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalData.java                  |   25 
 admin/src/views/business/components/YwElectricalRemote.vue                                                       |  112 +
 server/db/business.yw_electrical_data.menu.sql                                                                   |   34 
 server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicConstant.java                 |   25 
 admin/src/views/business/ywelectrical.vue                                                                        |   65 
 server/db/business.yw_electrical.menu.sql                                                                        |   79 +
 server/db/business.yw_electrical_charge.permissions.sql                                                          |    2 
 server/visits/dmvisit_service/src/main/java/com/doumee/core/device/ElectronicToolUtil.java                       |   93 +
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalActionsService.java          |   13 
 admin/src/views/business/ywelectricalcharge.vue                                                                  |  114 +
 admin/src/views/business/ywelectricaldata.vue                                                                    |  238 +++
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectrical.java                      |    5 
 server/system_timer/src/main/java/com/doumee/jobs/fegin/VisitServiceFegin.java                                   |    6 
 server/db/business.yw_electrical_actions.menu.sql                                                                |   30 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java             |    5 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java            |   10 
 server/db/business.yw_electrical_actions.permissions.sql                                                         |    1 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalActions.java               |   57 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalEditDTO.java                 |   14 
 server/visits/admin_timer/src/main/java/com/doumee/job/ElectricalScheduleJob.java                                |   48 
 admin/src/api/business/ywelectrical.js                                                                           |    2 
 admin/src/views/business/ywelectricalactions.vue                                                                 |  176 ++
 server/db/business.yw_electrical_data.permissions.sql                                                            |    3 
 server/db/business.yw_electrical_param.permissions.sql                                                           |    5 
 server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleControlApiRequest.java       |   16 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalActionsCloudController.java         |   37 
 server/system_service/src/main/java/com/doumee/core/utils/Constants.java                                         |    7 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/ElectronicConfigService.java        |   52 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwElectricalActionsMapper.java               |    9 
 server/db/yw_electrical.electrical_param_id.sql                                                                  |    3 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java |   53 
 server/system_service/src/main/java/com/doumee/core/utils/ImageBase64Util.java                                   |    6 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalOperateDTO.java              |   15 
 admin/src/views/business/components/YwElectricalParamEdit.vue                                                    |  598 +++++++++
 server/db/yw_electrical_actions.sql                                                                              |   21 
 admin/src/api/business/ywelectricaldata.js                                                                       |   15 
 server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicNotifyStatus.java             |   55 
 admin/src/api/business/ywelectricalcharge.js                                                                     |   11 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java              |   34 
 server/db/business.yw_electrical_param.menu.sql                                                                  |   36 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalServiceImpl.java        |    8 
 server/db/electrical_param.dict.sql                                                                              |   49 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalWarningServiceImpl.java |   40 
 admin/src/views/business/components/YwElectricalEdit.vue                                                         |  199 +++
 admin/src/api/business/ywelectricalparam.js                                                                      |   31 
 admin/src/views/business/components/AccountRechargePanel.vue                                                     |   82 +
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java    |  150 +-
 server/system_service/src/main/java/com/doumee/core/utils/FtpUtil.java                                           |    1 
 admin/src/api/business/ywelectricalactions.js                                                                    |    7 
 server/db/ELECTRICAL_INTEGRATION.md                                                                              |  172 ++
 server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleReadRequest.java             |    2 
 admin/src/views/business/ywelectricalparam.vue                                                                   |  136 ++
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java     |  939 ++++++++++++++
 53 files changed, 3,819 insertions(+), 127 deletions(-)

diff --git a/admin/src/api/business/ywelectrical.js b/admin/src/api/business/ywelectrical.js
index f51e909..4456f9e 100644
--- a/admin/src/api/business/ywelectrical.js
+++ b/admin/src/api/business/ywelectrical.js
@@ -34,6 +34,6 @@
   return request.post('/visitsAdmin/cloudService/business/ywElectricalParam/page', data || {
     page: 1,
     capacity: 500,
-    model: { isdeleted: 0 }
+    model: {}
   }, { trim: true })
 }
diff --git a/admin/src/api/business/ywelectricalactions.js b/admin/src/api/business/ywelectricalactions.js
new file mode 100644
index 0000000..b1c430c
--- /dev/null
+++ b/admin/src/api/business/ywelectricalactions.js
@@ -0,0 +1,7 @@
+import request from '../../utils/request'
+
+const base = '/visitsAdmin/cloudService/business/ywElectricalActions'
+
+export function fetchList (data) {
+  return request.post(base + '/page', data, { trim: true })
+}
diff --git a/admin/src/api/business/ywelectricalcharge.js b/admin/src/api/business/ywelectricalcharge.js
new file mode 100644
index 0000000..04229fa
--- /dev/null
+++ b/admin/src/api/business/ywelectricalcharge.js
@@ -0,0 +1,11 @@
+import request from '../../utils/request'
+
+const base = '/visitsAdmin/cloudService/business/ywElectricalCharge'
+
+export function fetchList (data) {
+  return request.post(base + '/page', data, { trim: true })
+}
+
+export function exportExcel (data) {
+  return request.post(base + '/exportExcel', data, { trim: true, download: true })
+}
diff --git a/admin/src/api/business/ywelectricaldata.js b/admin/src/api/business/ywelectricaldata.js
new file mode 100644
index 0000000..93bde78
--- /dev/null
+++ b/admin/src/api/business/ywelectricaldata.js
@@ -0,0 +1,15 @@
+import request from '../../utils/request'
+
+const base = '/visitsAdmin/cloudService/business/ywElectricalData'
+
+export function fetchList (data) {
+  return request.post(base + '/page', data, { trim: true })
+}
+
+export function exportExcel (data) {
+  return request.post(base + '/exportExcel', data, { trim: true, download: true })
+}
+
+export function syncAll (data) {
+  return request.post(base + '/syncAll', data || {})
+}
diff --git a/admin/src/api/business/ywelectricalparam.js b/admin/src/api/business/ywelectricalparam.js
new file mode 100644
index 0000000..4f8bab4
--- /dev/null
+++ b/admin/src/api/business/ywelectricalparam.js
@@ -0,0 +1,31 @@
+import request from '../../utils/request'
+
+const base = '/visitsAdmin/cloudService/business/ywElectricalParam'
+
+export function fetchList (data) {
+  return request.post(base + '/page', data, { trim: true })
+}
+
+export function exportExcel (data) {
+  return request.post(base + '/exportExcel', data, { trim: true, download: true })
+}
+
+export function create (data) {
+  return request.post(base + '/create', data)
+}
+
+export function getInfoById (id) {
+  return request.get(base + '/' + id)
+}
+
+export function updateById (data) {
+  return request.post(base + '/updateById', data)
+}
+
+export function deleteById (id) {
+  return request.get(base + '/delete/' + id)
+}
+
+export function deleteByIdInBatch (ids) {
+  return request.get(base + '/delete/batch', { params: { ids } })
+}
diff --git a/admin/src/views/business/components/AccountRechargePanel.vue b/admin/src/views/business/components/AccountRechargePanel.vue
new file mode 100644
index 0000000..262f11d
--- /dev/null
+++ b/admin/src/views/business/components/AccountRechargePanel.vue
@@ -0,0 +1,82 @@
+<template>
+  <div>
+    <div class="info-block">
+      <p>閲囬泦鍣ㄥ彿锛歿{ info.collectorId }} <span :class="info.online === 1 ? 'green' : 'red'">{{ info.online === 1 ? '鍦ㄧ嚎' : '绂荤嚎' }}</span></p>
+      <p>琛ㄥ湴鍧�锛歿{ info.address }}</p>
+      <p>琛ㄥ悕绉帮細{{ info.name }}</p>
+      <p>鐢佃〃绫诲瀷锛歿{ info.type }}</p>
+      <p>鍏宠仈鎴块棿锛歿{ info.roomNames }}</p>
+      <p>鐢佃〃鐘舵�侊細{{ info.accountStatus === 1 ? '宸插紑鎴�' : '鏈紑鎴�' }}</p>
+      <p v-if="mode === 'recharge'">璐數娆℃暟锛歿{ purchaseCount }}</p>
+    </div>
+    <el-form label-width="150px">
+      <el-form-item :label="mode === 'open' ? '寮�鎴烽噾棰�' : '鍏呭�奸噾棰�'">
+        <el-input-number v-model="form.money" :min="0" :precision="4" style="width: 200px"/>
+      </el-form-item>
+      <el-form-item :label="mode === 'open' ? '寮�鎴峰娉�' : '鍏呭�煎娉�'">
+        <el-input v-model="form.remark" :placeholder="mode === 'open' ? '寮�鎴峰娉紝鏈�澶�50涓瓧绗�' : '鍏呭�煎娉紝鏈�澶�300涓瓧绗�'" :maxlength="mode === 'open' ? 50 : 300" style="width: 400px"/>
+      </el-form-item>
+      <el-form-item label="褰撳墠鎬荤數閲�">
+        <span class="meter-read-row">
+          <span class="meter-read-value">{{ totalPower }}</span>
+          <el-button type="primary" size="small" class="read-meter-btn" :loading="isOperating" @click="$emit('read')">绔嬪嵆鎶勮〃骞舵洿鏂扮敤閲忎綑棰�</el-button>
+        </span>
+      </el-form-item>
+      <el-form-item label="鐢甸噺鍚屾鏃堕棿">{{ latest && latest.addTime ? latest.addTime : '-' }}</el-form-item>
+      <el-form-item label="褰撳墠鍓╀綑閲戦(鍏�)">
+        <span class="meter-read-row">
+          <span class="meter-read-value">{{ latest && latest.ye != null ? latest.ye : '0.0000' }}</span>
+          <el-button type="primary" size="small" class="read-meter-btn" :loading="isOperating" @click="$emit('read')">绔嬪嵆鎶勮〃骞舵洿鏂扮敤閲忎綑棰�</el-button>
+        </span>
+      </el-form-item>
+      <el-form-item label="鍓╀綑閲戦鍚屾鏃堕棿">{{ syncTime }}</el-form-item>
+    </el-form>
+    <div class="footer-btns">
+      <el-button type="primary" :loading="isOperating" @click="$emit('confirm')">{{ mode === 'open' ? '纭寮�鎴�' : '纭鍏呭��' }}</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'AccountRechargePanel',
+  props: {
+    info: { type: Object, default: () => ({}) },
+    latest: { type: Object, default: null },
+    form: { type: Object, required: true },
+    isOperating: Boolean,
+    mode: { type: String, default: 'open' },
+    purchaseCount: { type: String, default: '0' }
+  },
+  computed: {
+    totalPower () {
+      if (!this.latest) return '0.00kWh'
+      const v = this.latest.zhygzdl || '0'
+      return String(v).toLowerCase().indexOf('kwh') >= 0 ? v : v + 'kWh'
+    },
+    syncTime () {
+      return (this.latest && this.latest.addTime) ? this.latest.addTime : (this.info.balanceTime || '-')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.info-block p { margin: 4px 0; line-height: 26px; }
+.green { color: #67c23a; }
+.red { color: #f56c6c; }
+.footer-btns { text-align: right; margin-top: 16px; }
+.meter-read-row {
+  display: inline-flex;
+  align-items: center;
+  flex-wrap: nowrap;
+  white-space: nowrap;
+  max-width: 100%;
+}
+.meter-read-value { margin-right: 12px; flex-shrink: 0; }
+.read-meter-btn {
+  flex-shrink: 0;
+  width: auto;
+  white-space: nowrap;
+}
+</style>
diff --git a/admin/src/views/business/components/YwElectricalEdit.vue b/admin/src/views/business/components/YwElectricalEdit.vue
new file mode 100644
index 0000000..8522326
--- /dev/null
+++ b/admin/src/views/business/components/YwElectricalEdit.vue
@@ -0,0 +1,199 @@
+<template>
+  <GlobalWindow title="缂栬緫鐢佃〃" :visible.sync="visible" width="920px" :confirm-working="isWorking" @confirm="confirm">
+    <el-form ref="form" :model="form" :rules="rules" label-width="120px" class="electrical-edit-form">
+      <el-form-item label="閲囬泦鍣ㄥ彿">
+        <span>{{ form.collectorId }}</span>
+        <span :class="form.online === 1 ? 'green' : 'red'" style="margin-left: 12px">{{ form.online === 1 ? '鍦ㄧ嚎' : '绂荤嚎' }}</span>
+      </el-form-item>
+      <el-form-item label="琛ㄥ湴鍧�">{{ form.address }}</el-form-item>
+      <el-form-item label="鐢佃〃绫诲瀷">{{ form.type || '-' }}</el-form-item>
+      <el-form-item label="缁х數鍣�">
+        <span v-if="form.relayStatus === '0' || form.relayStatus === 0" class="red">鏂紑</span>
+        <span v-else-if="form.relayStatus === '1' || form.relayStatus === 1" class="green">闂悎</span>
+        <span v-else>-</span>
+      </el-form-item>
+      <el-form-item label="鐢佃〃鍚嶇О">
+        <span>{{ form.name || '-' }}</span>
+      </el-form-item>
+      <el-form-item label="鐢佃〃鍙傛暟" prop="electricalParamId">
+        <el-select v-model="form.electricalParamId" clearable placeholder="璇烽�夋嫨鐢佃〃鍙傛暟" style="width: 400px">
+          <el-option v-for="p in paramList" :key="p.id" :label="p.name" :value="p.id"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鐢佃〃鍊嶇巼" prop="rate">
+        <el-input-number v-model="form.rate" :min="0.01" :precision="2" :step="0.1" style="width: 400px"/>
+        <div class="form-tip">閽堝浜掓劅琛ㄩ渶瑕侀厤缃紝鍏跺畠鍗曠浉琛ㄩ粯璁ら厤缃�1鍗冲彲</div>
+      </el-form-item>
+      <el-form-item label="璇烽�夋嫨鎴挎簮">
+        <el-tree
+          ref="roomTree"
+          :data="houseList"
+          show-checkbox
+          node-key="idd"
+          :props="{ children: 'projectDataVOList', label: 'name' }"
+          :default-checked-keys="checkedKeys"
+          @check="onRoomCheck"
+          style="max-height: 320px; overflow: auto; width: 400px; border: 1px solid #dcdfe6; padding: 8px;"
+        />
+      </el-form-item>
+      <el-form-item label="宸查�夋埧婧�">
+        <div class="selected-room-list">
+          <template v-if="selectedRooms.length">
+            <el-tag
+              v-for="room in selectedRooms"
+              :key="room.idd"
+              closable
+              class="selected-room-tag"
+              @close="removeSelectedRoom(room)"
+            >{{ room.roomPath }}</el-tag>
+          </template>
+          <span v-else class="selected-room-empty">-</span>
+        </div>
+      </el-form-item>
+    </el-form>
+  </GlobalWindow>
+</template>
+
+<script>
+import GlobalWindow from '@/components/common/GlobalWindow'
+import { tree } from '@/api/project/ywProject'
+import { getDetail, saveDetail, fetchParamList } from '@/api/business/ywelectrical'
+
+export default {
+  name: 'YwElectricalEdit',
+  components: { GlobalWindow },
+  data () {
+    return {
+      visible: false,
+      isWorking: false,
+      form: {},
+      paramList: [],
+      houseList: [],
+      checkedKeys: [],
+      selectedRooms: [],
+      rules: {
+      }
+    }
+  },
+  methods: {
+    open (row) {
+      this.visible = true
+      this.checkedKeys = []
+      this.selectedRooms = []
+      this.loadParams()
+      this.loadHouseTree()
+      getDetail(row.id).then(data => {
+        this.form = { ...data, rate: data.rate != null ? Number(data.rate) : 1 }
+        if (data.roomIds && data.roomIds.length) {
+          this.checkedKeys = data.roomIds.map(id => '3-' + id)
+          this.$nextTick(() => {
+            if (this.$refs.roomTree) {
+              this.$refs.roomTree.setCheckedKeys(this.checkedKeys)
+              this.syncSelectedRoomsFromTree()
+            }
+          })
+        }
+      }).catch(e => this.$tip.apiFailed(e))
+    },
+    loadParams () {
+      fetchParamList().then(res => {
+        this.paramList = (res && res.records) ? res.records : []
+      }).catch(() => {})
+    },
+    loadHouseTree () {
+      tree({}).then(res => {
+        this.markTreeNodes(res || [])
+        this.houseList = res || []
+        this.$nextTick(() => {
+          if (this.checkedKeys.length && this.$refs.roomTree) {
+            this.$refs.roomTree.setCheckedKeys(this.checkedKeys)
+            this.syncSelectedRoomsFromTree()
+          }
+        })
+      })
+    },
+    markTreeNodes (arr, ancestors) {
+      if (!arr) return
+      arr.forEach(node => {
+        node.idd = node.lv + '-' + node.id
+        node.disabled = node.lv !== 3
+        const names = (ancestors || []).concat(node.name)
+        if (node.lv === 3) {
+          node.roomPath = names.length >= 3 ? names.slice(-3).join('/') : names.join('/')
+        }
+        if (node.projectDataVOList && node.projectDataVOList.length) {
+          this.markTreeNodes(node.projectDataVOList, names)
+        }
+      })
+    },
+    syncSelectedRoomsFromTree () {
+      const tree = this.$refs.roomTree
+      if (!tree) {
+        this.selectedRooms = []
+        return
+      }
+      const nodes = tree.getCheckedNodes(true).filter(n => n.lv === 3)
+      this.selectedRooms = nodes.map(n => ({
+        id: n.id,
+        idd: n.idd,
+        roomPath: n.roomPath || n.name
+      }))
+      this.form.roomIds = nodes.map(n => n.id)
+      this.checkedKeys = nodes.map(n => n.idd)
+    },
+    removeSelectedRoom (room) {
+      const tree = this.$refs.roomTree
+      if (!tree || !room) return
+      tree.setChecked(room.idd, false, true)
+      this.syncSelectedRoomsFromTree()
+    },
+    onRoomCheck () {
+      this.syncSelectedRoomsFromTree()
+    },
+    confirm () {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.isWorking = true
+        saveDetail({
+          id: this.form.id,
+          electricalParamId: this.form.electricalParamId,
+          rate: this.form.rate,
+          roomIds: this.form.roomIds || []
+        })
+          .then(() => {
+            this.$tip.success('淇濆瓨鎴愬姛')
+            this.visible = false
+            this.$emit('success')
+          })
+          .catch(e => this.$tip.apiFailed(e))
+          .finally(() => { this.isWorking = false })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.green { color: #67c23a; }
+.red { color: #f56c6c; }
+.form-tip { font-size: 12px; color: #909399; margin-top: 4px; }
+.selected-room-list {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  max-width: 560px;
+  min-height: 32px;
+  gap: 8px;
+}
+.selected-room-tag {
+  max-width: 100%;
+  white-space: normal;
+  height: auto;
+  line-height: 1.5;
+  padding-top: 4px;
+  padding-bottom: 4px;
+}
+.selected-room-empty {
+  color: #909399;
+}
+</style>
diff --git a/admin/src/views/business/components/YwElectricalParamEdit.vue b/admin/src/views/business/components/YwElectricalParamEdit.vue
new file mode 100644
index 0000000..642df5f
--- /dev/null
+++ b/admin/src/views/business/components/YwElectricalParamEdit.vue
@@ -0,0 +1,598 @@
+<template>
+  <GlobalWindow
+    :title="title"
+    :visible.sync="visible"
+    :confirm-working="isWorking"
+    width="920px"
+    @confirm="confirm"
+    @close="onClose"
+  >
+    <el-form
+      v-loading="detailLoading"
+      :model="form"
+      ref="form"
+      :rules="rules"
+      label-width="150px"
+      class="electrical-param-form"
+    >
+      <el-form-item label="閰嶇疆鍚嶇О" prop="name">
+        <el-input
+          v-model="form.name"
+          class="param-input"
+          maxlength="100"
+          placeholder="璇疯緭鍏ラ厤缃悕绉�"
+          clearable
+          v-trim
+        />
+      </el-form-item>
+
+      <el-form-item label="閫忔敮閲戦" prop="tzMoney">
+        <div class="param-field">
+          <div class="param-field__row">
+            <el-input-number
+              v-model="form.tzMoney"
+              class="param-input-number param-input-number--wide"
+              :min="0"
+              :max="190000"
+              :precision="2"
+              controls-position="right"
+              placeholder="0 - 190000"
+            />
+            <span class="param-field__unit">鍏�</span>
+          </div>
+          <p class="param-field__tip">鐢佃〃鏈�澶у彲<span class="param-tip-highlight">閫忔敮</span>閲戦锛岃缃悗鍏佽璐熸暟鍓╀綑閲戦</p>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="涓�绾ф姤璀﹂噾棰�" prop="yjbjMoney">
+        <div class="param-field">
+          <div class="param-field__row">
+            <el-input-number
+              v-model="form.yjbjMoney"
+              class="param-input-number"
+              :min="0"
+              :precision="2"
+              controls-position="right"
+              placeholder="0"
+            />
+            <span class="param-field__unit">鍏�</span>
+          </div>
+          <p class="param-field__tip">浣庝簬璁剧疆鍊煎睆骞曟樉绀烘彁绀�</p>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="浜岀骇鎶ヨ閲戦" prop="ejbjMoney">
+        <div class="param-field">
+          <div class="param-field__row">
+            <el-input-number
+              v-model="form.ejbjMoney"
+              class="param-input-number"
+              :min="0"
+              :precision="2"
+              controls-position="right"
+              placeholder="0"
+            />
+            <span class="param-field__unit">鍏�</span>
+          </div>
+          <p class="param-field__tip">浣庝簬璁剧疆鍊奸棯鐏厜鎶ヨ鎻愮ず</p>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="璐熻嵎闄愬埗鍔熺巼" prop="limitFhgl">
+        <div class="param-field">
+          <div class="param-field__row">
+            <el-input-number
+              v-model="form.limitFhgl"
+              class="param-input-number"
+              :min="0"
+              :max="80"
+              :precision="4"
+              controls-position="right"
+              placeholder="0 - 80"
+            />
+            <span class="param-field__unit param-field__unit--suffix">(4浣嶅皬鏁�) KW</span>
+          </div>
+          <div class="param-field__info">
+            <p class="param-field__info-title">鍚勭瑙勬牸琛ㄦ渶澶у彲璁剧疆鐨勫姛鐜囷細</p>
+            <ul class="param-field__info-list">
+              <li>
+                <strong>鍗曠浉琛�</strong>锛氭寜瑙勬牸鏈�澶у畨鍩规暟 脳 鐢靛帇銆備緥濡� 10(40)A 琛ㄥ彲璁剧疆鏈�澶у姛鐜囦负 40A 脳 220V = <em>8.8KW</em>
+              </li>
+              <li>
+                <strong>涓夌浉琛�</strong>锛氭寜鏈�澶ц鏍肩數娴� 脳 220V 脳 3銆備緥濡� 10(40)A 琛ㄥ彲璁剧疆鏈�澶у姛鐜囦负 40A 脳 220V 脳 3 = <em>26.4KW</em>
+              </li>
+              <li>
+                <strong>涓夌浉浜掓劅寮忚〃</strong>锛氭棤闇�鍐嶄箻浜掓劅姣斻�備緥濡傛敮鎸佹渶澶х數娴� 5A锛屽彲璁剧疆鏈�澶у姛鐜囦负 5A 脳 220V 脳 3 = <em>3.3KW</em>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="瓒呰礋鑽峰欢鏃堕棿" prop="limitFhTime">
+        <div class="param-field__row">
+          <el-input-number
+            v-model="form.limitFhTime"
+            class="param-input-number"
+            :min="0"
+            :max="99"
+            :precision="0"
+            controls-position="right"
+            placeholder="0 - 99"
+          />
+          <span class="param-field__unit">鍒嗛挓</span>
+        </div>
+      </el-form-item>
+
+      <!-- 鐢典环锛欰PP 灞曠ず涓鸿绠楃粨鏋滐紝闈炲繀濉緭鍏� -->
+      <el-form-item label="鐢典环" class="price-section">
+        <template slot="label">
+          <span>鐢典环</span>
+          <el-tooltip content="鐢典环 = 鍩虹浠� + 闄勫姞鍗曚环锛屼繚瀛樺悗鍐欏叆鍙傛暟妗f" placement="top">
+            <i class="el-icon-question price-section__help"/>
+          </el-tooltip>
+        </template>
+        <div class="param-field">
+          <div class="price-display">
+            <span class="price-display__value">{{ displayPriceText }}</span>
+            <span class="param-field__unit param-field__unit--suffix">(4浣嶅皬鏁�) 鍏�/KWH</span>
+            <span class="price-display__tag">APP绔睍绀虹數浠�</span>
+          </div>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="" prop="extraPrice" class="price-formula-item">
+        <div class="price-formula">
+          <span class="price-formula__eq">= 鍩虹浠�*</span>
+          <el-input-number
+            v-model="form.basePrice"
+            class="param-input-number param-input-number--formula"
+            :min="0"
+            :precision="4"
+            controls-position="right"
+            placeholder="0"
+            @change="onPricePartChange"
+          />
+          <span class="price-formula__unit">鍏�/KWH + 闄勫姞鍗曚环*</span>
+          <el-input-number
+            v-model="form.extraPrice"
+            class="param-input-number param-input-number--formula"
+            :min="0"
+            :precision="4"
+            controls-position="right"
+            placeholder="0"
+            @change="onPricePartChange"
+          />
+          <span class="price-formula__unit">鍏�/KWH</span>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="闄勫姞浠锋牸缁勬垚璇存槑" prop="remark">
+        <el-input
+          v-model="form.remark"
+          class="param-textarea"
+          type="textarea"
+          :rows="4"
+          maxlength="500"
+          placeholder="闄勫姞浠锋牸缁勬垚璇存槑"
+        />
+      </el-form-item>
+    </el-form>
+  </GlobalWindow>
+</template>
+
+<script>
+import BaseOpera from '@/components/base/BaseOpera'
+import GlobalWindow from '@/components/common/GlobalWindow'
+import * as paramApi from '@/api/business/ywelectricalparam'
+
+const numRequired = (label) => [
+  {
+    validator (rule, value, callback) {
+      if (value === undefined || value === null || value === '') {
+        callback(new Error('璇峰~鍐�' + label))
+      } else {
+        callback()
+      }
+    },
+    trigger: ['blur', 'change']
+  }
+]
+
+const PARAM_FORM_DEFAULTS = {
+  id: null,
+  name: '',
+  tzMoney: 0,
+  yjbjMoney: 0,
+  ejbjMoney: 0,
+  limitFhgl: 0,
+  limitFhTime: 0,
+  basePrice: 1,
+  extraPrice: 0,
+  price: 1,
+  status: 0,
+  remark: ''
+}
+
+const PARAM_NUMERIC_KEYS = ['tzMoney', 'yjbjMoney', 'ejbjMoney', 'limitFhgl', 'limitFhTime', 'basePrice', 'extraPrice', 'price']
+
+const hasValue = (v) => v !== null && v !== undefined && v !== ''
+
+const toNumber = (v) => {
+  if (!hasValue(v)) return null
+  const n = Number(v)
+  return isNaN(n) ? null : n
+}
+
+export default {
+  name: 'YwElectricalParamEdit',
+  extends: BaseOpera,
+  components: { GlobalWindow },
+  data () {
+    return {
+      title: '',
+      visible: false,
+      api: paramApi,
+      configData: {
+        'field.id': 'id'
+      },
+      isWorking: false,
+      detailLoading: false,
+      form: { ...PARAM_FORM_DEFAULTS },
+      rules: {
+        name: [{ required: true, message: '璇疯緭鍏ュ弬鏁拌缃悕绉�', trigger: 'blur' }],
+        tzMoney: numRequired('閫忔敮閲戦'),
+        yjbjMoney: numRequired('涓�绾ф姤璀﹂噾棰�'),
+        ejbjMoney: numRequired('浜岀骇鎶ヨ閲戦'),
+        limitFhgl: numRequired('璐熻嵎闄愬埗鍔熺巼'),
+        limitFhTime: numRequired('瓒呰礋鑽峰欢鏃堕棿')
+      }
+    }
+  },
+  computed: {
+    displayPrice () {
+      const base = Number(this.form.basePrice)
+      const extra = Number(this.form.extraPrice)
+      if (isNaN(base) || isNaN(extra)) {
+        return ''
+      }
+      return (base + extra).toFixed(4)
+    },
+    displayPriceText () {
+      return this.displayPrice === '' ? '-' : this.displayPrice
+    }
+  },
+  created () {
+    this.config({
+      api: '/business/ywelectricalparam',
+      'field.id': 'id'
+    })
+    if (!this.api) {
+      this.api = paramApi
+    }
+    this.$set(this.rules, 'extraPrice', [{ validator: this.validatePriceParts, trigger: ['blur', 'change'] }])
+  },
+  methods: {
+    /** 灏嗗垪琛ㄨ/鎺ュ彛鏁版嵁鏄犲皠涓鸿〃鍗曪紙缂栬緫鏃朵繚鐣欏師濮嬩笟鍔″�硷級 */
+    mapApiToForm (raw, { isNew = false } = {}) {
+      if (isNew || raw == null) {
+        return { ...PARAM_FORM_DEFAULTS }
+      }
+      const merged = { ...PARAM_FORM_DEFAULTS }
+      Object.keys(PARAM_FORM_DEFAULTS).forEach((key) => {
+        if (Object.prototype.hasOwnProperty.call(raw, key)) {
+          merged[key] = raw[key]
+        }
+      })
+
+      PARAM_NUMERIC_KEYS.forEach((key) => {
+        const n = toNumber(merged[key])
+        if (n !== null) {
+          merged[key] = key === 'limitFhTime' ? Math.round(n) : n
+        }
+      })
+
+      ;['tzMoney', 'yjbjMoney', 'ejbjMoney', 'limitFhgl', 'limitFhTime', 'extraPrice'].forEach((key) => {
+        if (!hasValue(merged[key])) {
+          merged[key] = 0
+        }
+      })
+
+      const priceNum = toNumber(raw.price) != null ? toNumber(raw.price) : toNumber(merged.price)
+      if (!hasValue(merged.basePrice) && !hasValue(merged.extraPrice) && priceNum != null) {
+        merged.basePrice = priceNum
+        merged.extraPrice = 0
+      } else {
+        if (!hasValue(merged.extraPrice)) {
+          merged.extraPrice = 0
+        }
+        if (!hasValue(merged.basePrice)) {
+          if (priceNum != null && hasValue(merged.extraPrice)) {
+            merged.basePrice = Number((priceNum - Number(merged.extraPrice)).toFixed(4))
+          } else if (priceNum != null) {
+            merged.basePrice = priceNum
+            merged.extraPrice = 0
+          } else {
+            merged.basePrice = 0
+          }
+        }
+      }
+
+      if (!hasValue(merged.name)) merged.name = raw.name || ''
+      if (merged.remark == null) merged.remark = raw.remark || ''
+      if (merged.status == null || merged.status === '') merged.status = raw.status != null ? raw.status : 0
+
+      if (toNumber(raw.price) != null) {
+        merged.price = toNumber(raw.price)
+      } else {
+        const base = Number(merged.basePrice) || 0
+        const extra = Number(merged.extraPrice) || 0
+        merged.price = Number((base + extra).toFixed(4))
+      }
+      return merged
+    },
+    applyForm (formData) {
+      this.form = formData
+      this.$nextTick(() => {
+        if (this.$refs.form) {
+          this.$refs.form.clearValidate()
+        }
+      })
+    },
+    validatePriceParts (rule, value, callback) {
+      const base = this.form.basePrice
+      const extra = this.form.extraPrice
+      if (base === undefined || base === null || base === '') {
+        callback(new Error('璇峰~鍐欏熀纭�浠�'))
+      } else if (extra === undefined || extra === null || extra === '') {
+        callback(new Error('璇峰~鍐欓檮鍔犲崟浠�'))
+      } else {
+        callback()
+      }
+    },
+    onPricePartChange () {
+      this.syncPrice()
+      this.$nextTick(() => {
+        if (this.$refs.form) {
+          this.$refs.form.validateField('extraPrice')
+        }
+      })
+    },
+    syncPrice () {
+      const base = Number(this.form.basePrice) || 0
+      const extra = Number(this.form.extraPrice) || 0
+      this.form.price = Number((base + extra).toFixed(4))
+    },
+    confirm () {
+      this.syncPrice()
+      this.form.status = this.form.status != null ? this.form.status : 0
+      if (this.form.id == null || this.form.id === '') {
+        this.__confirmCreate()
+        return
+      }
+      this.__confirmEdit()
+    },
+    onClose () {
+      this.isWorking = false
+      this.detailLoading = false
+      this.visible = false
+      this.$emit('close')
+    },
+    open (title, target) {
+      this.title = title
+      this.isWorking = false
+      this.detailLoading = false
+      if (target == null) {
+        this.visible = true
+        this.applyForm(this.mapApiToForm(null, { isNew: true }))
+        return
+      }
+      this.visible = true
+      // 浼樺厛鐢ㄥ垪琛ㄩ�変腑琛屽洖鏄撅紝涓庤〃鏍煎睍绀轰竴鑷�
+      this.applyForm(this.mapApiToForm(target, { isNew: false }))
+      const id = target.id
+      if (id == null || id === '') {
+        return
+      }
+      this.detailLoading = true
+      paramApi.getInfoById(id)
+        .then(data => {
+          if (data == null) {
+            return
+          }
+          // 鍒楄〃琛屽瓧娈典紭鍏堬紝閬垮厤鎺ュ彛缂哄瓧娈垫椂琚粯璁ゅ�艰鐩�
+          this.applyForm(this.mapApiToForm(Object.assign({}, data, target), { isNew: false }))
+        })
+        .catch(() => {})
+        .finally(() => { this.detailLoading = false })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.electrical-param-form {
+  padding: 8px 0 16px;
+  max-width: 680px;
+}
+
+.electrical-param-form ::v-deep .el-form-item {
+  margin-bottom: 20px;
+}
+
+.electrical-param-form ::v-deep .el-form-item__label {
+  color: #303133;
+  font-weight: 500;
+  line-height: 32px;
+}
+
+.electrical-param-form ::v-deep .el-form-item__content {
+  line-height: normal;
+}
+
+.electrical-param-form ::v-deep .el-form-item__content > .param-input,
+.electrical-param-form ::v-deep .el-form-item__content > .param-field,
+.electrical-param-form ::v-deep .el-form-item__content > .param-field__row,
+.electrical-param-form ::v-deep .el-form-item__content > .param-textarea,
+.electrical-param-form ::v-deep .el-form-item__content > .price-formula {
+  width: 100% !important;
+  max-width: 520px;
+}
+
+.param-input {
+  width: 400px !important;
+  max-width: 100%;
+}
+
+.param-input ::v-deep .el-input__inner {
+  height: 32px;
+  line-height: 32px;
+}
+
+.param-field__row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.param-input-number {
+  width: 160px !important;
+  flex-shrink: 0;
+}
+
+.param-input-number--wide {
+  width: 200px !important;
+}
+
+.param-input-number--formula {
+  width: 120px !important;
+}
+
+.param-input-number ::v-deep .el-input {
+  width: 100% !important;
+}
+
+.param-input-number ::v-deep .el-input__inner {
+  height: 32px;
+  line-height: 32px;
+  text-align: left;
+}
+
+.param-textarea {
+  width: 400px !important;
+  max-width: 100%;
+}
+
+.param-textarea ::v-deep .el-textarea__inner {
+  min-height: 88px;
+  padding: 8px 12px;
+}
+
+.param-field__unit {
+  margin-left: 8px;
+  color: #606266;
+  font-size: 14px;
+  white-space: nowrap;
+}
+
+.param-field__unit--suffix {
+  margin-left: 6px;
+  color: #909399;
+  font-size: 12px;
+}
+
+.param-field__tip {
+  margin: 6px 0 0;
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.6;
+  width: 100%;
+}
+
+.param-tip-highlight {
+  color: #f56c6c;
+  font-weight: 600;
+}
+
+.param-field__info {
+  margin-top: 10px;
+  padding: 10px 12px;
+  background: #f4f8fc;
+  border: 1px solid #e4ebf3;
+  border-radius: 4px;
+  max-width: 100%;
+}
+
+.param-field__info-title {
+  margin: 0 0 8px;
+  font-size: 12px;
+  color: #606266;
+  font-weight: 600;
+}
+
+.param-field__info-list {
+  margin: 0;
+  padding-left: 18px;
+  font-size: 12px;
+  color: #606266;
+  line-height: 1.75;
+}
+
+.param-field__info-list em {
+  font-style: normal;
+  color: #409eff;
+  font-weight: 600;
+}
+
+.price-section__help {
+  margin-left: 4px;
+  color: #909399;
+  cursor: pointer;
+}
+
+.price-display {
+  display: flex;
+  align-items: center;
+  flex-wrap: nowrap;
+}
+
+.price-display__value {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+  line-height: 32px;
+  white-space: nowrap;
+}
+
+.price-display__tag {
+  margin-left: 12px;
+  font-size: 12px;
+  color: #909399;
+}
+
+.price-formula-item ::v-deep .el-form-item__content {
+  margin-left: 150px !important;
+}
+
+.price-formula {
+  display: flex;
+  align-items: center;
+  flex-wrap: nowrap;
+  gap: 6px 8px;
+  font-size: 14px;
+  color: #303133;
+  white-space: nowrap;
+  overflow-x: auto;
+}
+
+.price-formula__eq,
+.price-formula__unit {
+  flex-shrink: 0;
+  white-space: nowrap;
+  line-height: 32px;
+}
+
+.price-formula ::v-deep .param-input-number--formula {
+  flex-shrink: 0;
+}
+</style>
diff --git a/admin/src/views/business/components/YwElectricalRemote.vue b/admin/src/views/business/components/YwElectricalRemote.vue
new file mode 100644
index 0000000..6261d0e
--- /dev/null
+++ b/admin/src/views/business/components/YwElectricalRemote.vue
@@ -0,0 +1,112 @@
+<template>
+  <GlobalWindow title="鐢佃〃杩滅▼鎺у埗" :visible.sync="visible" width="780px" :show-confirm="false">
+    <el-tabs v-model="activeTab">
+      <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>
+          <p>琛ㄥ湴鍧�锛歿{ info.address }}</p>
+          <p>琛ㄥ悕绉帮細{{ info.name }}</p>
+          <p>鐢佃〃绫诲瀷锛歿{ info.type }}</p>
+          <p>鍏宠仈鎴块棿锛歿{ info.roomNames }}</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>
+      </el-tab-pane>
+      <el-tab-pane label="寮�鎴�" name="open">
+        <account-recharge-panel
+          :info="info"
+          :latest="latest"
+          :form="form"
+          :is-operating="isOperating"
+          mode="open"
+          @read="readMeter"
+          @confirm="doOperate('openAccount', '纭寮�鎴峰悧锛�')"
+        />
+      </el-tab-pane>
+      <el-tab-pane label="鍏呭��" name="recharge">
+        <account-recharge-panel
+          :info="info"
+          :latest="latest"
+          :form="form"
+          :is-operating="isOperating"
+          :purchase-count="purchaseCount"
+          mode="recharge"
+          @read="readMeter"
+          @confirm="doOperate('recharge', '纭鍏呭�煎悧锛�')"
+        />
+      </el-tab-pane>
+    </el-tabs>
+  </GlobalWindow>
+</template>
+
+<script>
+import GlobalWindow from '@/components/common/GlobalWindow'
+import { getRemoteInfo, operate } from '@/api/business/ywelectrical'
+import AccountRechargePanel from './AccountRechargePanel'
+
+export default {
+  name: 'YwElectricalRemote',
+  components: { GlobalWindow, AccountRechargePanel },
+  data () {
+    return {
+      visible: false,
+      activeTab: 'basic',
+      electricalId: null,
+      info: {},
+      latest: null,
+      purchaseCount: '0',
+      form: { money: 0, remark: '' },
+      isOperating: false
+    }
+  },
+  methods: {
+    open (row, tab) {
+      this.electricalId = row.id
+      this.activeTab = tab || 'basic'
+      this.form = { money: 0, remark: '' }
+      this.visible = true
+      this.loadInfo()
+    },
+    loadInfo () {
+      getRemoteInfo(this.electricalId).then(res => {
+        this.info = res.electrical || {}
+        this.latest = res.latestData
+        this.purchaseCount = res.purchaseCount || '0'
+      }).catch(e => this.$tip.apiFailed(e))
+    },
+    readMeter () {
+      this.submitOperate('readMeter', true)
+    },
+    doOperate (action, msg) {
+      this.$dialog.actionConfirm(msg, '鎿嶄綔纭')
+        .then(() => this.submitOperate(action, false))
+        .catch(() => {})
+    },
+    submitOperate (action, silent) {
+      this.isOperating = true
+      operate({
+        electricalId: this.electricalId,
+        action,
+        money: this.form.money,
+        remark: this.form.remark
+      })
+        .then(res => {
+          this.$tip.apiSuccess(res || (silent ? '鎶勮〃璇锋眰宸叉彁浜�' : '鎻愪氦鎴愬姛锛岃鍦ㄣ�愭棩甯哥敤鐢电鐞�-鍏呭�艰褰曘�戜腑鏌ョ湅鍏呭�肩粨鏋�'))
+          this.loadInfo()
+          this.$emit('success')
+        })
+        .catch(e => this.$tip.apiFailed(e))
+        .finally(() => { this.isOperating = false })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.info-block { margin-bottom: 16px; line-height: 28px; color: #303133; }
+.btn-row .el-button { margin: 0 8px 8px 0; }
+</style>
diff --git a/admin/src/views/business/ywelectrical.vue b/admin/src/views/business/ywelectrical.vue
index 96658fb..6e4aa45 100644
--- a/admin/src/views/business/ywelectrical.vue
+++ b/admin/src/views/business/ywelectrical.vue
@@ -32,11 +32,26 @@
         <el-table-column prop="name" label="鐢佃〃鍚嶇О" fixed min-width="140" align="center" show-overflow-tooltip></el-table-column>
         <el-table-column prop="address" label="鐢佃〃鍦板潃" min-width="130" align="center" show-overflow-tooltip></el-table-column>
         <el-table-column prop="accountId" label="寮�鎴峰彿" min-width="100" align="center" show-overflow-tooltip></el-table-column>
+        <el-table-column label="寮�鎴风姸鎬�" min-width="100" align="center">
+          <template slot-scope="{ row }">
+            <span :class="row.accountStatus === 1 ? 'green' : 'red'">{{ formatAccountStatus(row.accountStatus) }}</span>
+          </template>
+        </el-table-column>
         <el-table-column prop="roomNames" label="缁戝畾鎴块棿" min-width="160" align="center" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="paramName" label="鐢佃〃鍙傛暟鍚�" min-width="140" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.paramName || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="鐢佃〃鍊嶇巼" min-width="100" align="center">
+          <template slot-scope="{ row }">{{ formatRate(row.rate) }}</template>
+        </el-table-column>
         <el-table-column prop="balanceBattery" label="绱鐢ㄧ數閲�" min-width="120" align="center">
           <template slot-scope="{ row }">{{ formatBattery(row.balanceBattery) }}</template>
         </el-table-column>
-        <el-table-column prop="balance" label="璐︽埛浣欓" min-width="100" align="center"></el-table-column>
+        <el-table-column label="璐︽埛浣欓" min-width="110" align="center">
+          <template slot-scope="{ row }">
+            <span :class="{ red: isBalanceLow(row.balance) }">{{ formatBalance(row.balance) }}</span>
+          </template>
+        </el-table-column>
         <el-table-column prop="createDate" label="鍒涘缓鏃堕棿" min-width="160" align="center"></el-table-column>
         <el-table-column label="鍦ㄧ嚎鐘舵��" min-width="90" align="center">
           <template slot-scope="{ row }">
@@ -50,9 +65,16 @@
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column label="棰勮鎯呭喌" min-width="160" align="center" show-overflow-tooltip>
+        <el-table-column label="棰勮鎯呭喌" min-width="200" align="center">
           <template slot-scope="{ row }">
-            <span class="red">{{ row.warnTypeName || row.warnType || '-' }}</span>
+            <template v-if="warnTypeLabels(row).length">
+              <span
+                v-for="(label, index) in warnTypeLabels(row)"
+                :key="index"
+                class="warn-tag"
+              >{{ label }}</span>
+            </template>
+            <span v-else>-</span>
           </template>
         </el-table-column>
         <el-table-column label="鎿嶄綔" align="center" min-width="220" fixed="right">
@@ -135,6 +157,32 @@
       if (!val) return '-'
       return String(val).indexOf('kwh') >= 0 || String(val).indexOf('kWh') >= 0 ? val : val + 'kwh'
     },
+    formatRate (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      const n = Number(val)
+      return isNaN(n) ? val : n.toFixed(2)
+    },
+    formatBalance (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      const n = Number(val)
+      if (isNaN(n)) return val
+      return `${n.toFixed(2)}鍏僠
+    },
+    isBalanceLow (val) {
+      if (val === null || val === undefined || val === '') return false
+      const n = Number(val)
+      return !isNaN(n) && n <= 0
+    },
+    warnTypeLabels (row) {
+      const text = row.warnTypeName || row.warnType || ''
+      if (!text) return []
+      return text.split(',').map(item => item.trim()).filter(Boolean)
+    },
+    formatAccountStatus (val) {
+      if (val === 1 || val === '1') return '宸插紑鎴�'
+      if (val === 0 || val === '0') return '鏈紑鎴�'
+      return val == null || val === '' ? '-' : '鏈紑鎴�'
+    },
     handleSync () {
       this.$dialog.actionConfirm('纭浠庝笁鏂瑰钩鍙板悓姝ュ叏閮ㄧ數琛ㄦ暟鎹悧锛�', '鍚屾鐢佃〃')
         .then(() => {
@@ -162,4 +210,15 @@
 <style scoped>
 .green { color: #67c23a; }
 .red { color: #f56c6c; }
+.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;
+}
 </style>
diff --git a/admin/src/views/business/ywelectricalactions.vue b/admin/src/views/business/ywelectricalactions.vue
new file mode 100644
index 0000000..d1ec309
--- /dev/null
+++ b/admin/src/views/business/ywelectricalactions.vue
@@ -0,0 +1,176 @@
+<template>
+  <TableLayout :permissions="['business:ywelectricalactions:query']">
+    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
+      <el-form-item label="鎿嶄綔绫诲瀷" prop="actionType">
+        <el-select v-model="searchForm.actionType" clearable placeholder="鍏ㄩ儴" style="min-width: 160px">
+          <el-option
+            v-for="item in actionTypeOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鎿嶄綔鏃堕棿" prop="operateTimeRange">
+        <el-date-picker
+          v-model="searchForm.operateTimeRange"
+          type="datetimerange"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          range-separator="-"
+          start-placeholder="寮�濮嬫椂闂�"
+          end-placeholder="缁撴潫鏃堕棿"
+          style="width: 360px"
+        />
+      </el-form-item>
+      <section>
+        <el-button type="primary" icon="el-icon-search" @click="search">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh" @click="reset">閲嶇疆</el-button>
+      </section>
+    </el-form>
+    <template v-slot:table-wrap>
+      <el-table v-loading="isWorking.search" :data="tableData.list" stripe>
+        <el-table-column prop="id" label="ID" min-width="80" align="center" />
+        <el-table-column prop="electricalName" label="鐢佃〃鍚嶇О" min-width="140" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.electricalName || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="electricalAddress" label="鐢佃〃鍦板潃" min-width="140" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.electricalAddress || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="鎿嶄綔绫诲瀷" min-width="120" align="center">
+          <template slot-scope="{ row }">{{ formatActionType(row.actionType) }}</template>
+        </el-table-column>
+        <el-table-column prop="oprId" label="鎿嶄綔ID" min-width="180" align="center" show-overflow-tooltip />
+        <el-table-column label="鐘舵��" min-width="90" 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="160" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.resultMsg || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="createDate" label="鎿嶄綔鏃堕棿" min-width="160" align="center" />
+        <el-table-column label="璇锋眰鎶ユ枃" min-width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-button type="text" :disabled="!row.requestBody" @click="openJson('璇锋眰鎶ユ枃', row.requestBody)">鏌ョ湅</el-button>
+          </template>
+        </el-table-column>
+        <el-table-column label="鍝嶅簲鎶ユ枃" min-width="100" align="center">
+          <template slot-scope="{ row }">
+            <el-button type="text" :disabled="!row.responseBody" @click="openJson('鍝嶅簲鎶ユ枃', row.responseBody)">鏌ョ湅</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        @size-change="handleSizeChange"
+        @current-change="handlePageChange"
+        :pagination="tableData.pagination"
+      />
+    </template>
+    <OperaInterfaceLogWindow ref="jsonWindow" />
+  </TableLayout>
+</template>
+
+<script>
+import BaseTable from '@/components/base/BaseTable'
+import TableLayout from '@/layouts/TableLayout'
+import Pagination from '@/components/common/Pagination'
+import OperaInterfaceLogWindow from '@/components/business/OperaInterfaceLogWindow'
+import * as actionsApi from '@/api/business/ywelectricalactions'
+
+const ACTION_TYPE_MAP = {
+  1: '棰勪粯璐规竻闆�',
+  2: '鍚庝粯璐规竻闆�',
+  3: '杩滅▼閿�鎴�',
+  4: '鎷夐椄',
+  5: '鍚堥椄',
+  6: '寮�鎴�',
+  7: '鍏呭��',
+  8: '鎶勮〃'
+}
+
+export default {
+  name: 'YwElectricalActions',
+  extends: BaseTable,
+  components: { TableLayout, Pagination, OperaInterfaceLogWindow },
+  data () {
+    return {
+      searchForm: {
+        actionType: null,
+        operateTimeRange: []
+      },
+      actionTypeOptions: Object.keys(ACTION_TYPE_MAP).map(key => ({
+        value: Number(key),
+        label: ACTION_TYPE_MAP[key]
+      }))
+    }
+  },
+  created () {
+    this.api = actionsApi
+    this.module = '鐢佃〃杩滅▼鎿嶄綔璁板綍'
+    this.configData['field.id'] = 'id'
+    this.configData['field.main'] = 'id'
+    this.search()
+  },
+  methods: {
+    buildSearchModel () {
+      const model = {}
+      if (this.searchForm.actionType !== null && this.searchForm.actionType !== '' && this.searchForm.actionType !== undefined) {
+        model.actionType = this.searchForm.actionType
+      }
+      if (this.searchForm.operateTimeRange && this.searchForm.operateTimeRange.length === 2) {
+        model.operateTimeBegin = this.searchForm.operateTimeRange[0]
+        model.operateTimeEnd = this.searchForm.operateTimeRange[1]
+      }
+      return model
+    },
+    handlePageChange (pageIndex) {
+      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
+      this.isWorking.search = true
+      actionsApi.fetchList({
+        page: this.tableData.pagination.pageIndex,
+        capacity: this.tableData.pagination.pageSize,
+        model: this.buildSearchModel(),
+        sorts: this.tableData.sorts
+      })
+        .then(data => {
+          this.tableData.list = data.records
+          this.tableData.pagination.total = data.total
+        })
+        .catch(() => {})
+        .finally(() => { this.isWorking.search = false })
+    },
+    reset () {
+      this.searchForm = { actionType: null, operateTimeRange: [] }
+      if (this.$refs.searchForm) {
+        this.$refs.searchForm.resetFields()
+      }
+      this.search()
+    },
+    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 })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.green { color: #67c23a; }
+.red { color: #f56c6c; }
+.orange { color: #e6a23c; }
+</style>
diff --git a/admin/src/views/business/ywelectricalcharge.vue b/admin/src/views/business/ywelectricalcharge.vue
new file mode 100644
index 0000000..7eac02a
--- /dev/null
+++ b/admin/src/views/business/ywelectricalcharge.vue
@@ -0,0 +1,114 @@
+<template>
+  <TableLayout :permissions="['business:ywelectricalcharge:query']">
+    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
+      <el-form-item label="鐢佃〃淇℃伅" prop="meterKeyword">
+        <el-input v-model="searchForm.meterKeyword" placeholder="琛ㄥ悕绉�/琛ㄥ湴鍧�" clearable @keypress.enter.native="search"/>
+      </el-form-item>
+      <el-form-item label="鍏呭�肩姸鎬�" prop="status">
+        <el-select v-model="searchForm.status" clearable placeholder="鍏ㄩ儴" style="min-width: 120px">
+          <el-option label="鍏呭�间腑" :value="0"/>
+          <el-option label="鍏呭�兼垚鍔�" :value="1"/>
+          <el-option label="鍏呭�煎け璐�" :value="2"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="浠诲姟ID" prop="oprId">
+        <el-input v-model="searchForm.oprId" placeholder="opr_id" clearable/>
+      </el-form-item>
+      <section>
+        <el-button type="primary" icon="el-icon-search" @click="search">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh" @click="reset">閲嶇疆</el-button>
+      </section>
+    </el-form>
+    <template v-slot:table-wrap>
+      <ul class="toolbar">
+        <li>
+          <el-button @click="exportExcel" :loading="isWorking.export" v-permissions="['business:ywelectricalcharge:exportExcel']">瀵煎嚭</el-button>
+        </li>
+      </ul>
+      <el-table v-loading="isWorking.search" :data="tableData.list" stripe>
+        <el-table-column prop="name" label="鐢佃〃鍚嶇О" min-width="130" align="center" show-overflow-tooltip/>
+        <el-table-column prop="address" label="琛ㄥ湴鍧�" min-width="130" align="center" show-overflow-tooltip/>
+        <el-table-column prop="roomNames" label="缁戝畾鎴块棿" min-width="150" align="center" show-overflow-tooltip/>
+        <el-table-column prop="money" label="鍏呭�奸噾棰�(鍏�)" min-width="110" align="center"/>
+        <el-table-column prop="banlance" label="鍏呭�煎墠浣欓" min-width="110" align="center"/>
+        <el-table-column label="鐘舵��" min-width="100" align="center">
+          <template slot-scope="{ row }">
+            <span v-if="row.status === 0">鍏呭�间腑</span>
+            <span v-else-if="row.status === 1" class="green">鍏呭�兼垚鍔�</span>
+            <span v-else-if="row.status === 2" class="red">鍏呭�煎け璐�</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="oprId" label="浠诲姟ID" min-width="200" align="center" show-overflow-tooltip/>
+        <el-table-column prop="remark" label="澶囨敞" min-width="120" align="center" show-overflow-tooltip/>
+        <el-table-column prop="statusInfo" label="鐘舵�佽鏄�" min-width="140" align="center" show-overflow-tooltip/>
+        <el-table-column prop="createDate" label="鎻愪氦鏃堕棿" min-width="160" align="center"/>
+        <el-table-column prop="statusTime" label="鐘舵�佹洿鏂版椂闂�" min-width="160" align="center"/>
+      </el-table>
+      <pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination"/>
+    </template>
+  </TableLayout>
+</template>
+
+<script>
+import BaseTable from '@/components/base/BaseTable'
+import TableLayout from '@/layouts/TableLayout'
+import Pagination from '@/components/common/Pagination'
+import * as chargeApi from '@/api/business/ywelectricalcharge'
+
+export default {
+  name: 'YwElectricalCharge',
+  extends: BaseTable,
+  components: { TableLayout, Pagination },
+  data () {
+    return {
+      searchForm: {
+        meterKeyword: '',
+        status: '',
+        oprId: ''
+      }
+    }
+  },
+  created () {
+    this.api = chargeApi
+    this.module = '鐢佃〃鍏呭�艰褰�'
+    this.configData['field.id'] = 'id'
+    this.configData['field.main'] = 'name'
+    this.search()
+  },
+  methods: {
+    handlePageChange (pageIndex) {
+      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
+      this.isWorking.search = true
+      chargeApi.fetchList({
+        page: this.tableData.pagination.pageIndex,
+        capacity: this.tableData.pagination.pageSize,
+        model: this.buildSearchModel(),
+        sorts: this.tableData.sorts
+      })
+        .then(data => {
+          this.tableData.list = data.records
+          this.tableData.pagination.total = data.total
+        })
+        .catch(() => {})
+        .finally(() => { this.isWorking.search = false })
+    },
+    buildSearchModel () {
+      const model = { type: 0 }
+      if (this.searchForm.meterKeyword) model.meterKeyword = this.searchForm.meterKeyword
+      if (this.searchForm.status !== '' && this.searchForm.status !== null) model.status = this.searchForm.status
+      if (this.searchForm.oprId) model.oprId = this.searchForm.oprId
+      return model
+    },
+    reset () {
+      this.searchForm = { meterKeyword: '', status: '', oprId: '' }
+      this.search()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.green { color: #67c23a; }
+.red { color: #f56c6c; }
+</style>
diff --git a/admin/src/views/business/ywelectricaldata.vue b/admin/src/views/business/ywelectricaldata.vue
new file mode 100644
index 0000000..7dc1ab5
--- /dev/null
+++ b/admin/src/views/business/ywelectricaldata.vue
@@ -0,0 +1,238 @@
+<template>
+  <TableLayout :permissions="['business:ywelectricaldata:query']">
+    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
+      <el-form-item label="鐢佃〃淇℃伅" prop="meterKeyword">
+        <el-input
+          v-model="searchForm.meterKeyword"
+          placeholder="璇疯緭鍏ョ數琛ㄥ悕绉�/琛ㄥ彿"
+          clearable
+          @keypress.enter.native="search"
+        />
+      </el-form-item>
+      <el-form-item label="閫夋嫨鎴块棿" prop="roomId">
+        <el-select v-model="searchForm.roomId" clearable filterable placeholder="鍏ㄩ儴" style="min-width: 200px">
+          <el-option v-for="item in roomOptions" :key="item.id" :label="item.label" :value="item.id"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鎶勮〃鏃堕棿" prop="readTimeRange">
+        <el-date-picker
+          v-model="searchForm.readTimeRange"
+          type="datetimerange"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          range-separator="-"
+          start-placeholder="寮�濮嬫椂闂�"
+          end-placeholder="缁撴潫鏃堕棿"
+          style="width: 360px"
+        />
+      </el-form-item>
+      <section>
+        <el-button type="primary" icon="el-icon-search" @click="search">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh" @click="reset">閲嶇疆</el-button>
+      </section>
+    </el-form>
+    <template v-slot:table-wrap>
+      <ul class="toolbar">
+        <li>
+          <el-button
+            type="primary"
+            @click="handleReadNow"
+            :loading="isReading"
+            v-permissions="['business:ywelectricaldata:sync']"
+          >绔嬪嵆鎶勮〃</el-button>
+        </li>
+        <li>
+          <el-button
+            @click="exportExcel"
+            :loading="isWorking.export"
+            v-permissions="['business:ywelectricaldata:exportExcel']"
+          >瀵煎嚭</el-button>
+        </li>
+      </ul>
+      <el-table v-loading="isWorking.search" :data="tableData.list" stripe>
+        <el-table-column type="selection" width="55"/>
+        <el-table-column prop="address" label="鐢佃〃鍦板潃" min-width="140" align="center" show-overflow-tooltip/>
+        <el-table-column label="鐢佃〃鍚嶇О" min-width="140" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.electricalName || row.name || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="roomNames" label="缁戝畾鎴块棿" min-width="150" align="center" show-overflow-tooltip/>
+        <el-table-column label="缁撶畻鏂瑰紡" min-width="100" align="center">
+          <template slot-scope="{ row }">{{ formatJsfs(row.jsfs) }}</template>
+        </el-table-column>
+        <el-table-column label="缁勫悎鏈夊姛鎬荤數閲�" min-width="140" align="center">
+          <template slot-scope="{ row }">{{ formatEnergy(row.zhygzdl) }}</template>
+        </el-table-column>
+        <el-table-column prop="ye" label="璐︽埛浣欓" min-width="100" align="center"/>
+        <el-table-column prop="countnum" label="璐數娆℃暟" min-width="90" align="center"/>
+        <el-table-column label="鎶勮〃鏃堕棿" min-width="160" align="center">
+          <template slot-scope="{ row }">
+            <span :title="formatDateTime(readTimeValue(row))">{{ formatRelativeTime(readTimeValue(row)) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="鐘舵��" min-width="90" align="center">
+          <template>
+            <span class="red">宸茶Е鍙�</span>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination
+        @size-change="handleSizeChange"
+        @current-change="handlePageChange"
+        :pagination="tableData.pagination"
+      />
+    </template>
+  </TableLayout>
+</template>
+
+<script>
+import BaseTable from '@/components/base/BaseTable'
+import TableLayout from '@/layouts/TableLayout'
+import Pagination from '@/components/common/Pagination'
+import * as dataApi from '@/api/business/ywelectricaldata'
+import { tree } from '@/api/project/ywProject'
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import 'dayjs/locale/zh-cn'
+
+dayjs.extend(relativeTime)
+dayjs.locale('zh-cn')
+
+export default {
+  name: 'YwElectricalData',
+  extends: BaseTable,
+  components: { TableLayout, Pagination },
+  data () {
+    return {
+      searchForm: {
+        meterKeyword: '',
+        roomId: null,
+        readTimeRange: []
+      },
+      roomOptions: [],
+      isReading: false
+    }
+  },
+  created () {
+    this.api = dataApi
+    this.module = '鎶勮〃璁板綍'
+    this.configData['field.id'] = 'id'
+    this.configData['field.main'] = 'address'
+    this.loadRoomOptions()
+    this.search()
+  },
+  methods: {
+    loadRoomOptions () {
+      tree({}).then(res => {
+        this.roomOptions = this.flattenRooms(res || [])
+      }).catch(() => {})
+    },
+    flattenRooms (nodes, prefix) {
+      if (!nodes || !nodes.length) return []
+      let list = []
+      nodes.forEach(node => {
+        const path = prefix ? prefix + '/' + node.name : node.name
+        if (node.lv === 3) {
+          list.push({ id: node.id, label: path })
+        }
+        if (node.projectDataVOList && node.projectDataVOList.length) {
+          list = list.concat(this.flattenRooms(node.projectDataVOList, path))
+        }
+      })
+      return list
+    },
+    buildSearchModel () {
+      const model = {}
+      if (this.searchForm.meterKeyword) {
+        model.meterKeyword = this.searchForm.meterKeyword
+      }
+      if (this.searchForm.roomId != null && this.searchForm.roomId !== '') {
+        model.roomId = this.searchForm.roomId
+      }
+      if (this.searchForm.readTimeRange && this.searchForm.readTimeRange.length === 2) {
+        model.readTimeBegin = this.searchForm.readTimeRange[0]
+        model.readTimeEnd = this.searchForm.readTimeRange[1]
+      }
+      return model
+    },
+    handlePageChange (pageIndex) {
+      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
+      this.isWorking.search = true
+      dataApi.fetchList({
+        page: this.tableData.pagination.pageIndex,
+        capacity: this.tableData.pagination.pageSize,
+        model: this.buildSearchModel(),
+        sorts: this.tableData.sorts
+      })
+        .then(data => {
+          this.tableData.list = data.records
+          this.tableData.pagination.total = data.total
+        })
+        .catch(() => {})
+        .finally(() => { this.isWorking.search = false })
+    },
+    reset () {
+      this.searchForm = {
+        meterKeyword: '',
+        roomId: null,
+        readTimeRange: []
+      }
+      if (this.$refs.searchForm) {
+        this.$refs.searchForm.resetFields()
+      }
+      this.search()
+    },
+    handleReadNow () {
+      this.$dialog.actionConfirm('纭绔嬪嵆浠庣涓夋柟骞冲彴鍚屾鎶勮〃鏁版嵁鍚楋紵', '鎿嶄綔纭鎻愰啋')
+        .then(() => {
+          this.isReading = true
+          dataApi.syncAll({})
+            .then(res => {
+              this.$tip.apiSuccess(res || '鎶勮〃鍚屾鎴愬姛')
+              this.search()
+            })
+            .catch(e => {
+              this.$tip.apiFailed(e)
+            })
+            .finally(() => {
+              this.isReading = false
+            })
+        })
+        .catch(() => {})
+    },
+    formatJsfs (val) {
+      if (val == null || val === '') return '-'
+      const text = String(val).trim().toLowerCase()
+      if (text === 'true' || text === '1') return '棰勪粯璐�'
+      if (text === 'false' || text === '0') return '鍚庝粯璐�'
+      return val
+    },
+    formatEnergy (val) {
+      if (val == null || val === '') return '-'
+      const text = String(val)
+      if (text.toLowerCase().indexOf('kwh') >= 0) return text
+      return text + 'kWh'
+    },
+    readTimeValue (row) {
+      return row.addTime || row.createDate
+    },
+    formatDateTime (value) {
+      if (!value) return '-'
+      const d = dayjs(value)
+      return d.isValid() ? d.format('YYYY-MM-DD HH:mm:ss') : value
+    },
+    formatRelativeTime (value) {
+      if (!value) return '-'
+      const d = dayjs(value)
+      if (!d.isValid()) return value
+      const diffHours = dayjs().diff(d, 'hour')
+      if (diffHours >= 24) {
+        return d.format('YYYY-MM-DD HH:mm:ss')
+      }
+      return d.fromNow()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.red { color: #f56c6c; }
+</style>
diff --git a/admin/src/views/business/ywelectricalparam.vue b/admin/src/views/business/ywelectricalparam.vue
new file mode 100644
index 0000000..aa2034f
--- /dev/null
+++ b/admin/src/views/business/ywelectricalparam.vue
@@ -0,0 +1,136 @@
+<template>
+  <TableLayout :permissions="['business:ywelectricalparam:query']">
+    <el-form ref="searchForm" slot="search-form" :model="searchForm" label-width="100px" inline>
+      <el-form-item label="閰嶇疆鍚嶇О" prop="nameKeyword">
+        <el-input v-model="searchForm.nameKeyword" placeholder="璇疯緭鍏ラ厤缃悕绉�" clearable @keypress.enter.native="search"/>
+      </el-form-item>
+      <section>
+        <el-button type="primary" icon="el-icon-search" @click="search">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh" @click="reset">閲嶇疆</el-button>
+      </section>
+    </el-form>
+    <template v-slot:table-wrap>
+      <ul class="toolbar">
+        <li>
+          <el-button type="primary" icon="el-icon-plus" @click="$refs.editWindow.open('鏂板缓鐢佃〃鍙傛暟')" v-permissions="['business:ywelectricalparam:create']">鏂板缓</el-button>
+        </li>
+        <li>
+          <el-button @click="exportExcel" :loading="isWorking.export" v-permissions="['business:ywelectricalparam:exportExcel']">瀵煎嚭</el-button>
+        </li>
+      </ul>
+      <el-table v-loading="isWorking.search" :data="tableData.list" stripe @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55"/>
+        <el-table-column prop="name" label="閰嶇疆鍚嶇О" min-width="180" align="center" show-overflow-tooltip/>
+        <el-table-column label="鐢典环" min-width="120" align="center">
+          <template slot-scope="{ row }">{{ formatPrice(row.price) }}</template>
+        </el-table-column>
+        <el-table-column label="閫忔敮閲戦" min-width="100" align="center">
+          <template slot-scope="{ row }">{{ formatYuan(row.tzMoney) }}</template>
+        </el-table-column>
+        <el-table-column label="涓�绾ф姤璀﹂噾棰�" min-width="120" align="center">
+          <template slot-scope="{ row }">{{ formatYuan(row.yjbjMoney) }}</template>
+        </el-table-column>
+        <el-table-column label="浜岀骇鎶ヨ閲戦" min-width="120" align="center">
+          <template slot-scope="{ row }">{{ formatYuan(row.ejbjMoney) }}</template>
+        </el-table-column>
+        <el-table-column label="璐熻嵎闄愬埗鍔熺巼" min-width="120" align="center">
+          <template slot-scope="{ row }">{{ formatPower(row.limitFhgl) }}</template>
+        </el-table-column>
+        <el-table-column label="瓒呰礋鑽锋椂闂�" min-width="110" align="center">
+          <template slot-scope="{ row }">{{ formatMinutes(row.limitFhTime) }}</template>
+        </el-table-column>
+        <el-table-column prop="editDate" label="鏇存柊鏃堕棿" min-width="160" align="center"/>
+        <el-table-column label="鎿嶄綔" align="center" min-width="140" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="text" @click="$refs.editWindow.open('缂栬緫鐢佃〃鍙傛暟', row)" v-permissions="['business:ywelectricalparam:update']">缂栬緫</el-button>
+            <el-button type="text" class="red" @click="deleteById(row)" v-permissions="['business:ywelectricalparam:delete']">鍒犻櫎</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination"/>
+    </template>
+    <YwElectricalParamEdit ref="editWindow" @success="handlePageChange"/>
+  </TableLayout>
+</template>
+
+<script>
+import BaseTable from '@/components/base/BaseTable'
+import TableLayout from '@/layouts/TableLayout'
+import Pagination from '@/components/common/Pagination'
+import YwElectricalParamEdit from './components/YwElectricalParamEdit'
+
+export default {
+  name: 'YwElectricalParam',
+  extends: BaseTable,
+  components: { TableLayout, Pagination, YwElectricalParamEdit },
+  data () {
+    return {
+      searchForm: {
+        nameKeyword: ''
+      }
+    }
+  },
+  created () {
+    this.config({
+      module: '鐢佃〃鍙傛暟璁剧疆',
+      api: '/business/ywelectricalparam',
+      'field.id': 'id',
+      'field.main': 'name'
+    })
+    this.search()
+  },
+  methods: {
+    formatYuan (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      const n = Number(val)
+      if (isNaN(n)) return val
+      return `${n.toFixed(2)}鍏僠
+    },
+    formatPrice (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      const n = Number(val)
+      if (isNaN(n)) return val
+      return `${n.toFixed(2)} 鍏�/KWH`
+    },
+    formatPower (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      const n = Number(val)
+      if (isNaN(n)) return val
+      return `${n.toFixed(2)}KW`
+    },
+    formatMinutes (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      return `${val}鍒嗛挓`
+    },
+    buildSearchModel () {
+      const model = {}
+      if (this.searchForm.nameKeyword) model.nameKeyword = this.searchForm.nameKeyword
+      return model
+    },
+    handlePageChange (pageIndex) {
+      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
+      this.isWorking.search = true
+      this.api.fetchList({
+        page: this.tableData.pagination.pageIndex,
+        capacity: this.tableData.pagination.pageSize,
+        model: this.buildSearchModel(),
+        sorts: this.tableData.sorts
+      })
+        .then(data => {
+          this.tableData.list = data.records
+          this.tableData.pagination.total = data.total
+        })
+        .catch(() => {})
+        .finally(() => { this.isWorking.search = false })
+    },
+    reset () {
+      this.searchForm = { nameKeyword: '' }
+      this.search()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.red { color: #f56c6c; }
+</style>
diff --git a/server/db/ELECTRICAL_INTEGRATION.md b/server/db/ELECTRICAL_INTEGRATION.md
new file mode 100644
index 0000000..d206bdf
--- /dev/null
+++ b/server/db/ELECTRICAL_INTEGRATION.md
@@ -0,0 +1,172 @@
+# 鏅鸿兘鐢佃〃 鈥� 鑱旇皟楠岀涓庡厖鍊艰褰曞鎺ヨ鏄�
+
+## 涓�銆佹暟鎹簱鑴氭湰鎵ц椤哄簭
+
+鍦ㄤ笟鍔″簱锛堝 `funingyunwei`锛変緷娆℃墽琛岋細
+
+| 椤哄簭 | 鑴氭湰 | 璇存槑 |
+|------|------|------|
+| 1 | `yw_electrical_actions.sql` | 寮傛鎿嶄綔璁板綍琛� |
+| 2 | `business.yw_electrical.permissions.sql` | 鍚� `business:ywelectrical:device` |
+| 3 | `business.yw_electrical_charge.permissions.sql` | 鍏呭�艰褰曟煡璇�/瀵煎嚭 |
+| 4 | `business.yw_electrical_param.permissions.sql` | 鐢佃〃鍙傛暟锛堣嫢鏈墽琛岃繃锛� |
+| 5 | `business.yw_electrical.menu.sql` | 鏅鸿兘鐢佃〃銆佸厖鍊艰褰曘�佸弬鏁拌缃彍鍗� + admin 鎺堟潈 |
+| 6 | `business.yw_electrical_warning.*` | 鎶ヨ璁板綍锛堣嫢鏈墽琛岃繃锛� |
+
+鎵ц鍚庤 **閲嶆柊鐧诲綍** 鎴栧埛鏂版潈闄愮紦瀛橈紝渚ф爮鎵嶄細鍑虹幇鏂拌彍鍗曘��
+
+---
+
+## 浜屻�佸紓姝ュ洖璋� `electricalNotify` 鑱旇皟楠岀
+
+### 2.1 鍥炶皟鍦板潃
+
+- **璺緞**锛歚POST {鏈嶅姟鏍瑰湴鍧�}/electronic/electricalNotify`
+- **Content-Type**锛歚application/x-www-form-urlencoded`锛堣〃鍗曞瓧娈碉級
+- **瀛楁**锛歚response_content`銆乣timestamp`銆乣sign`
+- **閰嶇疆**锛歚ElectronicConstant.notify_url` 蹇呴』涓庝笁鏂瑰钩鍙板悗鍙伴厤缃殑 **瀹屾暣鍏綉 URL** 涓�鑷达紙鍚矾寰勶級銆�
+
+绀轰緥锛�
+
+```text
+https://your-domain.com/electronic/electricalNotify
+```
+
+### 2.2 楠岀绠楁硶锛堜笌鍑虹珯 `ElectronicToolUtil.getSign` 涓�鑷达級
+
+1. 鍙栧弬涓庣鍚嶅瓧娈碉紝缁勬垚 `Map<String,String>`锛�**鎺ㄨ崘**锛夛細
+   - `auth_code` = `ElectronicConstant.auth_code`
+   - `response_content` = 鍘熷 JSON 瀛楃涓诧紙涓� POST 鍙傛暟瀹屽叏涓�鑷达紝涓嶈浜屾鏍煎紡鍖栵級
+   - `timestamp` = 骞冲彴 POST 鐨� timestamp 瀛楃涓�
+2. 灏� Map 鐨� **key 鎸夊瓧鍏稿簭鎺掑簭**锛屼緷娆℃嫾鎺ュ悇 key 瀵瑰簲鐨� **value**锛堝彧鎷� value锛屼笉鍚� key 鍚嶏級銆�
+3. 鍦ㄦ嫾鎺ョ粨鏋滄湯灏捐拷鍔� `ElectronicConstant.nonce`銆�
+4. 瀵规渶缁堝瓧绗︿覆鍋� **UTF-8 MD5**锛屽緱鍒� 32 浣嶅皬鍐欏崄鍏繘鍒讹紙姣旇緝鏃跺拷鐣ュぇ灏忓啓锛夈��
+
+鑻ヤ笂杩伴獙绛惧け璐ワ紝浠g爜浼� **鍐嶅皾璇�** 浠呬娇鐢� `response_content + timestamp` 涓や釜瀛楁锛堜笉鍚� `auth_code`锛夛紝浠ュ吋瀹归儴鍒嗗钩鍙扮増鏈��
+
+### 2.3 鏈湴鑷祴 sign锛圝ava锛�
+
+```java
+Map<String, String> data = new HashMap<>();
+data.put("auth_code", ElectronicConstant.auth_code);
+data.put("response_content", responseContent); // 涓� POST 涓�鑷�
+data.put("timestamp", timestamp);
+String sign = ElectronicToolUtil.getSign(data);
+```
+
+### 2.4 鍝嶅簲绾﹀畾
+
+| 鍦烘櫙 | HTTP 鐘舵�� | 鍝嶅簲浣� | 骞冲彴琛屼负 |
+|------|-----------|--------|----------|
+| 楠岀閫氳繃涓斿鐞嗗畬鎴� | 200 | `SUCCESS` | 鍋滄閲嶈瘯 |
+| 楠岀澶辫触 | 400 | `FAIL` | 鎸夌瓥鐣ラ噸璇� |
+| 闈� 200 鎴� `FAIL` | - | - | 闂撮殧閲嶈瘯鐩磋嚦鎴愬姛鎴栬揪涓婇檺 |
+
+### 2.5 `response_content` 鍗曟潯缁撴瀯
+
+```json
+{
+  "opr_id": "涓庢彁浜ゅ紓姝ヤ换鍔℃椂涓�鑷�",
+  "status": "SUCCESS",
+  "cid": "閲囬泦鍣ㄥ彿",
+  "address": "琛ㄥ湴鍧�",
+  "error_msg": "澶辫触鏃剁殑閿欒鐮佹垨璇存槑"
+}
+```
+
+**status 涓庢湰鍦� `yw_electrical_actions.status` 鏄犲皠锛�**
+
+| 骞冲彴 status | actions.status | 璇存槑 |
+|-------------|----------------|------|
+| ACCEPTED / QUEUE / PROCESSING | 0 | 澶勭悊涓� |
+| SUCCESS / DELIVERED | 1 | 鎴愬姛 |
+| FAIL / TIMEOUT / NOTSUPPORT / CANCELED / RESPONSE_TIMEOUT / RESPONSE_FAIL / NOTFOUND | 2 | 澶辫触 |
+
+### 2.6 鑱旇皟妫�鏌ユ竻鍗�
+
+- [ ] `dmvisit_admin` 宸插惎鍔紝Shiro 宸叉斁琛� `/electronic/electricalNotify`
+- [ ] 鍏綉/NAT 鑳借闂洖璋� URL锛堝唴缃戦渶绔彛鏄犲皠鎴栫┛閫忥級
+- [ ] `auth_code`銆乣nonce` 涓庝笁鏂瑰悗鍙颁竴鑷�
+- [ ] 鎻愪氦杩滅▼鎿嶄綔鍚� `yw_electrical_actions` 鏈� `opr_id` 涓� `status=0`
+- [ ] 妯℃嫙 POST 鍥炶皟鍚� `status` 鏇存柊锛屾媺闂�/鍚堥椄/寮�鎴�/鍏呭�煎壇浣滅敤姝g‘
+- [ ] `yw_electrical_log` 鏈� `type=1` 鐨勬帹閫佽褰�
+
+### 2.7 甯歌闂
+
+**楠岀涓�鐩村け璐�**
+
+- 鏍稿 `response_content` 鏄惁涓庡钩鍙板彂閫佸瓧鑺傜骇涓�鑷达紙URL 缂栫爜銆佺┖鏍笺�佽浆涔夛級銆�
+- 纭 timestamp 涓哄瓧绗︿覆鑰岄潪甯﹀皬鏁般��
+- 鐢ㄥ钩鍙版枃妗g‘璁ゆ槸鍚﹀寘鍚� `auth_code` 鍙備笌绛惧悕銆�
+
+**鍥炶皟鎴愬姛浣� actions 涓嶆洿鏂�**
+
+- 鏌� `opr_id` 鏄惁涓� `yw_electrical_actions.opr_id` 瀹屽叏涓�鑷达紙鎻愪氦鏃舵湰鍦扮敓鎴愮殑 UUID锛夈��
+
+---
+
+## 涓夈�佸厖鍊艰褰曢〉瀵规帴
+
+### 3.1 鏁版嵁娴�
+
+```text
+鐢ㄦ埛銆岀‘璁ゅ厖鍊笺��
+  鈫� operate(recharge) 鈫� 涓夋柟 recharger锛堝紓姝ワ級
+  鈫� 鍐欏叆 yw_electrical_charge锛坰tatus=0 鍏呭�间腑锛宱pr_id=浠诲姟ID锛�
+  鈫� 鍐欏叆 yw_electrical_actions + yw_electrical_log
+
+骞冲彴鍥炶皟 electricalNotify锛坰tatus=SUCCESS锛�
+  鈫� 鏇存柊 yw_electrical_actions.status=1
+  鈫� 鎸� opr_id 鏇存柊 yw_electrical_charge.status=1锛宻tatusTime/statusInfo
+  鈫� 鍒锋柊 yw_electrical 浣欓锛坮efreshBalanceFromData锛�
+
+鍥炶皟 FAIL
+  鈫� actions.status=2锛宑harge.status=2
+```
+
+### 3.2 琛ㄥ瓧娈靛搴�
+
+| yw_electrical_charge | 鏉ユ簮 |
+|----------------------|------|
+| obj_id | 鐢佃〃 id |
+| address / name / c_id | 鐢佃〃妗f |
+| money | 鍏呭�奸噾棰� |
+| remark | 鍏呭�煎娉� |
+| opr_id | 寮傛浠诲姟 opr_id |
+| status | 0鍏呭�间腑 / 1鎴愬姛 / 2澶辫触 |
+| banlance | 鍏呭�煎墠璐︽埛浣欓锛堢數琛� balance锛� |
+| room_names | 缁戝畾鎴块棿灞曠ず鍚� |
+
+### 3.3 鍓嶇椤甸潰
+
+- **鑿滃崟璺緞**锛歚/business/ywelectricalcharge`
+- **椤甸潰**锛歚admin/src/views/business/ywelectricalcharge.vue`
+- **鎺ュ彛**锛歚/visitsAdmin/cloudService/business/ywElectricalCharge/page`
+- **鏉冮檺**锛歚business:ywelectricalcharge:query`
+
+鐢ㄦ埛鍦ㄦ櫤鑳界數琛ㄨ繙绋嬫帶鍒舵彁浜ゅ厖鍊煎悗锛屾彁绀恒�岃鍒板厖鍊艰褰曟煡鐪嬬粨鏋溿�嶏紝鍗冲湪鍒楄〃鎸夎〃鍚�/鍦板潃銆乣opr_id`銆佺姸鎬佺瓫閫夋煡鐪嬨��
+
+### 3.4 涓庛�屽晢鎴峰厖鍊笺�嶅尯鍒�
+
+渚ф爮銆屽晢鎴峰厖鍊� / 鍏呭�艰褰曘�嶈嫢涓� `yw_top_up_log` 绛変笟鍔★紝涓� **鐢佃〃鍏呭�艰褰�**锛坄yw_electrical_charge`锛変负涓嶅悓鏁版嵁婧愶紱鏃ュ父鐢ㄧ數涓嬨�屽厖鍊艰褰曘�嶈彍鍗曟寚鍚戠數琛ㄤ笓鐢ㄩ〉闈€��
+
+---
+
+## 鍥涖�佺浉鍏抽厤缃」姹囨��
+
+| 閰嶇疆椤� | 浣嶇疆 | 璇存槑 |
+|--------|------|------|
+| auth_code | ElectronicConstant | 涓夋柟鎺堟潈鐮� |
+| nonce | ElectronicConstant | 绛惧悕闅忔満涓� |
+| notify_url | ElectronicConstant | 寮傛閫氱煡瀹屾暣 URL |
+| api_url / api2_url | ElectronicConstant | 涓夋柟 API 鏍瑰湴鍧� |
+
+---
+
+## 浜斻�佸畾鏃朵换鍔★紙鍙�� HTTP 瑙﹀彂锛�
+
+| 浠诲姟 | Cron | HTTP锛坅dmin_timer锛� |
+|------|------|---------------------|
+| 閲囬泦鍣ㄧ姸鎬� | `0 */5 * * * ?` | GET `/timer/yw/getElectricalStatus` |
+| 鎵归噺鎶勮〃 | `30 0 * * * ?` | GET `/timer/yw/syncElectricalMeterData` |
+| 鏃ュ織娓呯悊 | `0 30 2 * * ?` | GET `/timer/yw/cleanElectricalLog` |
diff --git a/server/db/business.yw_electrical.menu.sql b/server/db/business.yw_electrical.menu.sql
new file mode 100644
index 0000000..a02e5d6
--- /dev/null
+++ b/server/db/business.yw_electrical.menu.sql
@@ -0,0 +1,79 @@
+-- 鏃ュ父鐢ㄧ數绠$悊锛氭櫤鑳界數琛ㄣ�佸厖鍊艰褰曘�佺數琛ㄥ弬鏁拌缃紙闇�鍦� dev 搴撴墽琛岋級
+-- 鐖惰彍鍗曟寜銆屾棩甯哥敤鐢点�嶅尮閰嶏紝鑻ュ悕绉颁笉鍚岃鏀� LIKE 鏉′欢
+
+-- 鏅鸿兘鐢佃〃
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT m.`ID`, '鏅鸿兘鐢佃〃', '/business/ywelectrical', '鏅鸿兘鐢佃〃绠$悊', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = m.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` m
+WHERE m.`DELETED` = 0 AND m.`NAME` LIKE '%鏃ュ父鐢ㄧ數%'
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywelectrical')
+LIMIT 1;
+
+-- 鍏呭�艰褰曪紙鐢佃〃锛�
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT m.`ID`, '鍏呭�艰褰�', '/business/ywelectricalcharge', '鐢佃〃鍏呭�艰褰�', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = m.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` m
+WHERE m.`DELETED` = 0 AND m.`NAME` LIKE '%鏃ュ父鐢ㄧ數%'
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywelectricalcharge')
+LIMIT 1;
+
+-- 鐢佃〃鍙傛暟璁剧疆
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT m.`ID`, '鐢佃〃鍙傛暟璁剧疆', '/business/ywelectricalparam', '鐢佃〃鍙傛暟妗f', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = m.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` m
+WHERE m.`DELETED` = 0 AND m.`NAME` LIKE '%鏃ュ父鐢ㄧ數%'
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywelectricalparam')
+LIMIT 1;
+
+-- 鎶勮〃璁板綍
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT m.`ID`, '鎶勮〃璁板綍', '/business/ywelectricaldata', '鐢佃〃鎶勮〃璁板綍', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = m.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` m
+WHERE m.`DELETED` = 0 AND m.`NAME` LIKE '%鏃ュ父鐢ㄧ數%'
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywelectricaldata')
+LIMIT 1;
+
+-- admin 瑙掕壊锛氳彍鍗�
+INSERT INTO `SYSTEM_ROLE_MENU` (`ROLE_ID`, `MENU_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
+SELECT r.`ID`, menu.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
+FROM `SYSTEM_ROLE` r
+INNER JOIN `SYSTEM_MENU` menu ON menu.`PATH` IN ('/business/ywelectrical', '/business/ywelectricalcharge', '/business/ywelectricalparam', '/business/ywelectricaldata') AND menu.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_ROLE_MENU` rm
+    WHERE rm.`ROLE_ID` = r.`ID` AND rm.`MENU_ID` = menu.`ID` AND rm.`DELETED` = 0
+  );
+
+-- admin 瑙掕壊锛氭櫤鑳界數琛ㄦ寜閽潈闄�
+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` IN (
+    'business:ywelectrical:query',
+    'business:ywelectrical:update',
+    'business:ywelectrical:device',
+    'business:ywelectrical:exportExcel',
+    'business:ywelectricalcharge:query',
+    'business:ywelectricalcharge:exportExcel',
+    'business:ywelectricalparam:query',
+    'business:ywelectricalparam:create',
+    'business:ywelectricalparam:update',
+    'business:ywelectricalparam:delete',
+    'business:ywelectricalparam:exportExcel',
+    'business:ywelectricaldata:query',
+    'business:ywelectricaldata:exportExcel',
+    'business:ywelectricaldata:sync'
+) AND p.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  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_electrical_actions.menu.sql b/server/db/business.yw_electrical_actions.menu.sql
new file mode 100644
index 0000000..58793c5
--- /dev/null
+++ b/server/db/business.yw_electrical_actions.menu.sql
@@ -0,0 +1,30 @@
+-- 鑿滃崟锛氭棩甯哥敤鐢电鐞� -> 鎿嶄綔璁板綍锛堥渶鍦� dev 搴撴墽琛岋級
+
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT m.`ID`, '鎿嶄綔璁板綍', '/business/ywelectricalactions', '鐢佃〃杩滅▼鎿嶄綔璁板綍', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = m.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` m
+WHERE m.`DELETED` = 0 AND m.`NAME` LIKE '%鏃ュ父鐢ㄧ數%'
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywelectricalactions')
+LIMIT 1;
+
+INSERT INTO `SYSTEM_ROLE_MENU` (`ROLE_ID`, `MENU_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
+SELECT r.`ID`, menu.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
+FROM `SYSTEM_ROLE` r
+INNER JOIN `SYSTEM_MENU` menu ON menu.`PATH` = '/business/ywelectricalactions' AND menu.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_ROLE_MENU` rm
+    WHERE rm.`ROLE_ID` = r.`ID` AND rm.`MENU_ID` = menu.`ID` AND rm.`DELETED` = 0
+  );
+
+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:ywelectricalactions:query' AND p.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  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_electrical_actions.permissions.sql b/server/db/business.yw_electrical_actions.permissions.sql
new file mode 100644
index 0000000..0c1995c
--- /dev/null
+++ b/server/db/business.yw_electrical_actions.permissions.sql
@@ -0,0 +1 @@
+INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalactions:query', '鏌ヨ鐢佃〃杩滅▼鎿嶄綔璁板綍', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
diff --git a/server/db/business.yw_electrical_charge.permissions.sql b/server/db/business.yw_electrical_charge.permissions.sql
new file mode 100644
index 0000000..2c01ffa
--- /dev/null
+++ b/server/db/business.yw_electrical_charge.permissions.sql
@@ -0,0 +1,2 @@
+INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalcharge: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:ywelectricalcharge:exportExcel', '瀵煎嚭鐢佃〃鍏呭�艰褰�(Excel)', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
diff --git a/server/db/business.yw_electrical_data.menu.sql b/server/db/business.yw_electrical_data.menu.sql
new file mode 100644
index 0000000..94dac14
--- /dev/null
+++ b/server/db/business.yw_electrical_data.menu.sql
@@ -0,0 +1,34 @@
+-- 鎶勮〃璁板綍鑿滃崟涓� admin 鎺堟潈锛堥渶鍦� dev 搴撴墽琛岋紝鐖惰彍鍗曚负銆屾棩甯哥敤鐢电鐞嗐�嶏級
+
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT m.`ID`, '鎶勮〃璁板綍', '/business/ywelectricaldata', '鐢佃〃鎶勮〃璁板綍', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = m.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` m
+WHERE m.`DELETED` = 0 AND m.`NAME` LIKE '%鏃ュ父鐢ㄧ數%'
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywelectricaldata')
+LIMIT 1;
+
+INSERT INTO `SYSTEM_ROLE_MENU` (`ROLE_ID`, `MENU_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
+SELECT r.`ID`, menu.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
+FROM `SYSTEM_ROLE` r
+INNER JOIN `SYSTEM_MENU` menu ON menu.`PATH` = '/business/ywelectricaldata' AND menu.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_ROLE_MENU` rm
+    WHERE rm.`ROLE_ID` = r.`ID` AND rm.`MENU_ID` = menu.`ID` AND rm.`DELETED` = 0
+  );
+
+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` IN (
+    'business:ywelectricaldata:query',
+    'business:ywelectricaldata:exportExcel',
+    'business:ywelectricaldata:sync'
+) AND p.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  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_electrical_data.permissions.sql b/server/db/business.yw_electrical_data.permissions.sql
new file mode 100644
index 0000000..92ebce2
--- /dev/null
+++ b/server/db/business.yw_electrical_data.permissions.sql
@@ -0,0 +1,3 @@
+INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricaldata: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:ywelectricaldata: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:ywelectricaldata:sync', '绔嬪嵆鎶勮〃', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
diff --git a/server/db/business.yw_electrical_param.menu.sql b/server/db/business.yw_electrical_param.menu.sql
new file mode 100644
index 0000000..211c947
--- /dev/null
+++ b/server/db/business.yw_electrical_param.menu.sql
@@ -0,0 +1,36 @@
+-- 鑿滃崟锛氭棩甯哥敤鐢电鐞� -> 鐢佃〃鍙傛暟璁剧疆
+
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT m.`ID`, '鐢佃〃鍙傛暟璁剧疆', '/business/ywelectricalparam', '鐢佃〃鍙傛暟妗f缁存姢', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = m.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` m
+WHERE m.`DELETED` = 0 AND m.`NAME` LIKE '%鏃ュ父鐢ㄧ數%'
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywelectricalparam')
+LIMIT 1;
+
+INSERT INTO `SYSTEM_ROLE_MENU` (`ROLE_ID`, `MENU_ID`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`)
+SELECT r.`ID`, menu.`ID`, CURRENT_TIMESTAMP, NULL, 1, NULL, 0
+FROM `SYSTEM_ROLE` r
+INNER JOIN `SYSTEM_MENU` menu ON menu.`PATH` = '/business/ywelectricalparam' AND menu.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_ROLE_MENU` rm
+    WHERE rm.`ROLE_ID` = r.`ID` AND rm.`MENU_ID` = menu.`ID` AND rm.`DELETED` = 0
+  );
+
+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` IN (
+    'business:ywelectricalparam:query',
+    'business:ywelectricalparam:create',
+    'business:ywelectricalparam:update',
+    'business:ywelectricalparam:delete',
+    'business:ywelectricalparam:exportExcel'
+) AND p.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` = '绠$悊鍛�')
+  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_electrical_param.permissions.sql b/server/db/business.yw_electrical_param.permissions.sql
new file mode 100644
index 0000000..482f32b
--- /dev/null
+++ b/server/db/business.yw_electrical_param.permissions.sql
@@ -0,0 +1,5 @@
+INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ywelectricalparam:create', '鏂板缓鐢佃〃鍙傛暟', '', 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:ywelectricalparam:delete', '鍒犻櫎鐢佃〃鍙傛暟', '', 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:ywelectricalparam: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:ywelectricalparam: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:ywelectricalparam:exportExcel', '瀵煎嚭鐢佃〃鍙傛暟(Excel)', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
diff --git a/server/db/electrical_param.dict.sql b/server/db/electrical_param.dict.sql
new file mode 100644
index 0000000..afb7e77
--- /dev/null
+++ b/server/db/electrical_param.dict.sql
@@ -0,0 +1,49 @@
+-- 鏅鸿兘鐢佃〃绗笁鏂瑰钩鍙板弬鏁帮紙SYSTEM_DICT / SYSTEM_DICT_DATA锛�
+-- 瀛楀吀绫诲瀷锛欵LECTRICAL_PARAM
+-- 瀛楀吀鏁版嵁锛歭abel=鍙傛暟閿紝code=鍙傛暟鍊�
+
+INSERT INTO `SYSTEM_DICT` (`CODE`, `NAME`, `REMARK`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT 'ELECTRICAL_PARAM', '鐢佃〃涓夋柟骞冲彴鍙傛暟', '鏅鸿兘鐢佃〃绗笁鏂� API锛歛uth_code銆乶once銆乶otify_url銆乤pi_url銆乤pi_url2', 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+WHERE NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_DICT` WHERE `CODE` = 'ELECTRICAL_PARAM' AND `DELETED` = 0
+);
+
+INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT d.`ID`, '1f29d378fc6792d5d2b735877993ffb7', 'auth_code', '涓夋柟鎺堟潈鐮�', 1, 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+FROM `SYSTEM_DICT` d
+WHERE d.`CODE` = 'ELECTRICAL_PARAM' AND d.`DELETED` = 0
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_DICT_DATA` x WHERE x.`DICT_ID` = d.`ID` AND x.`LABEL` = 'auth_code' AND x.`DELETED` = 0
+  );
+
+INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT d.`ID`, 'lvyOY7fcun719WkxF8ToVYatStgt', 'nonce', '绛惧悕闅忔満涓�', 2, 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+FROM `SYSTEM_DICT` d
+WHERE d.`CODE` = 'ELECTRICAL_PARAM' AND d.`DELETED` = 0
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_DICT_DATA` x WHERE x.`DICT_ID` = d.`ID` AND x.`LABEL` = 'nonce' AND x.`DELETED` = 0
+  );
+
+INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT d.`ID`, 'http://115.221.15.8:8055/electronic/electricalNotify', 'notify_url', '寮傛鍥炶皟瀹屾暣 URL', 3, 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+FROM `SYSTEM_DICT` d
+WHERE d.`CODE` = 'ELECTRICAL_PARAM' AND d.`DELETED` = 0
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_DICT_DATA` x WHERE x.`DICT_ID` = d.`ID` AND x.`LABEL` = 'notify_url' AND x.`DELETED` = 0
+  );
+
+INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT d.`ID`, 'http://api1.tqdianbiao.com', 'api_url', 'Api v1 鏍瑰湴鍧�', 4, 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+FROM `SYSTEM_DICT` d
+WHERE d.`CODE` = 'ELECTRICAL_PARAM' AND d.`DELETED` = 0
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_DICT_DATA` x WHERE x.`DICT_ID` = d.`ID` AND x.`LABEL` = 'api_url' AND x.`DELETED` = 0
+  );
+
+INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
+SELECT d.`ID`, 'http://api2.tqdianbiao.com', 'api_url2', 'Api v2 鏍瑰湴鍧�', 5, 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
+FROM `SYSTEM_DICT` d
+WHERE d.`CODE` = 'ELECTRICAL_PARAM' AND d.`DELETED` = 0
+  AND NOT EXISTS (
+    SELECT 1 FROM `SYSTEM_DICT_DATA` x WHERE x.`DICT_ID` = d.`ID` AND x.`LABEL` = 'api_url2' AND x.`DELETED` = 0
+  );
diff --git a/server/db/yw_electrical.electrical_param_id.sql b/server/db/yw_electrical.electrical_param_id.sql
new file mode 100644
index 0000000..6e00652
--- /dev/null
+++ b/server/db/yw_electrical.electrical_param_id.sql
@@ -0,0 +1,3 @@
+-- 鐢佃〃琛ㄦ柊澧炴湰鍦板弬鏁版。妗堝叧鑱斿瓧娈碉紙鍏宠仈 yw_electrical_param.id锛�
+ALTER TABLE `yw_electrical`
+    ADD COLUMN `electrical_param_id` int NULL DEFAULT NULL COMMENT '鐢佃〃鍙傛暟ID锛堝叧鑱� yw_electrical_param.id锛�' AFTER `param_id`;
diff --git a/server/db/yw_electrical_actions.sql b/server/db/yw_electrical_actions.sql
new file mode 100644
index 0000000..2735fb9
--- /dev/null
+++ b/server/db/yw_electrical_actions.sql
@@ -0,0 +1,21 @@
+-- 鐢佃〃杩滅▼鎿嶄綔璁板綍锛堝紓姝ヤ换鍔¤窡韪級
+CREATE TABLE IF NOT EXISTS `yw_electrical_actions` (
+  `id` int NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+  `creator` int DEFAULT NULL COMMENT '鍒涘缓浜�',
+  `create_date` datetime DEFAULT NULL COMMENT '鍒涘缓鏃堕棿',
+  `editor` int DEFAULT NULL COMMENT '鏇存柊浜�',
+  `edit_date` datetime DEFAULT NULL COMMENT '鏇存柊鏃堕棿',
+  `isdeleted` int DEFAULT 0 COMMENT '0鍚� 1鏄�',
+  `remark` varchar(500) DEFAULT NULL COMMENT '澶囨敞',
+  `electrical_id` int NOT NULL COMMENT '鐢佃〃ID yw_electrical.id',
+  `action_type` int NOT NULL COMMENT '1棰勪粯璐规竻闆� 2鍚庝粯璐规竻闆� 3杩滅▼閿�鎴� 4鎷夐椄 5鍚堥椄 6寮�鎴� 7鍏呭�� 8鎶勮〃',
+  `opr_id` varchar(64) DEFAULT NULL COMMENT '绗笁鏂规搷浣淚D',
+  `request_body` text COMMENT '璇锋眰鎶ユ枃',
+  `response_body` text COMMENT '鍝嶅簲鎶ユ枃',
+  `status` int DEFAULT 0 COMMENT '0澶勭悊涓� 1鎴愬姛 2澶辫触',
+  `result_msg` varchar(500) DEFAULT NULL COMMENT '鎵ц缁撴灉璇存槑',
+  PRIMARY KEY (`id`),
+  KEY `idx_electrical_id` (`electrical_id`),
+  KEY `idx_opr_id` (`opr_id`),
+  KEY `idx_create_date` (`create_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鐢佃〃杩滅▼鎿嶄綔璁板綍';
diff --git a/server/system_service/src/main/java/com/doumee/core/utils/Constants.java b/server/system_service/src/main/java/com/doumee/core/utils/Constants.java
index 179b273..8fe6c89 100644
--- a/server/system_service/src/main/java/com/doumee/core/utils/Constants.java
+++ b/server/system_service/src/main/java/com/doumee/core/utils/Constants.java
@@ -162,6 +162,13 @@
     public static final String WMS_OUTBOUND_PLATFROM_URL ="WMS_OUTBOUND_PLATFROM_URL" ;
     public static final String TSM_PARAM ="TSM_PARAM" ;
     public static final String TMS_ORDER_LIST_URL ="TMS_ORDER_LIST_URL" ;
+    /** 鏅鸿兘鐢佃〃绗笁鏂瑰钩鍙板弬鏁帮紙SYSTEM_DICT.code锛� */
+    public static final String ELECTRICAL_PARAM = "ELECTRICAL_PARAM";
+    public static final String ELECTRICAL_AUTH_CODE = "auth_code";
+    public static final String ELECTRICAL_NONCE = "nonce";
+    public static final String ELECTRICAL_NOTIFY_URL = "notify_url";
+    public static final String ELECTRICAL_API_URL = "api_url";
+    public static final String ELECTRICAL_API2_URL = "api_url2";
     public static final String TMS_ORDER_DETAIL_URL ="TMS_ORDER_DETAIL_URL" ;
     public static final String TMS_LOCK_STATUS_URL ="TMS_LOCK_STATUS_URL" ;
     public static final String TMS_INTERFACE_URL_PREFIX ="TMS_INTERFACE_URL_PREFIX" ;
diff --git a/server/system_service/src/main/java/com/doumee/core/utils/FtpUtil.java b/server/system_service/src/main/java/com/doumee/core/utils/FtpUtil.java
index 9c009bb..696381c 100644
--- a/server/system_service/src/main/java/com/doumee/core/utils/FtpUtil.java
+++ b/server/system_service/src/main/java/com/doumee/core/utils/FtpUtil.java
@@ -18,7 +18,6 @@
 import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.util.EntityUtils;
 import org.apache.xpath.operations.Bool;
-import sun.misc.BASE64Encoder;
 
 /** */
 /**
diff --git a/server/system_service/src/main/java/com/doumee/core/utils/ImageBase64Util.java b/server/system_service/src/main/java/com/doumee/core/utils/ImageBase64Util.java
index 4595df5..c899b3c 100644
--- a/server/system_service/src/main/java/com/doumee/core/utils/ImageBase64Util.java
+++ b/server/system_service/src/main/java/com/doumee/core/utils/ImageBase64Util.java
@@ -1,8 +1,6 @@
 package com.doumee.core.utils;
 
 import org.apache.commons.codec.binary.Base64;
-import sun.misc.BASE64Decoder;
-import sun.misc.BASE64Encoder;
 
 import java.io.*;
 import java.net.HttpURLConnection;
@@ -42,10 +40,8 @@
         //
         if (imgStr == null) // 鍥惧儚鏁版嵁涓虹┖
             return false;
-        BASE64Decoder decoder = new BASE64Decoder();
         try {
-            // Base64瑙g爜
-            byte[] b = decoder.decodeBuffer(imgStr);
+            byte[] b = Base64.decodeBase64(imgStr);
             for (int i = 0; i < b.length; ++i) {
                 if (b[i] < 0) {// 璋冩暣寮傚父鏁版嵁
                     b[i] += 256;
diff --git a/server/system_timer/src/main/java/com/doumee/jobs/fegin/VisitServiceFegin.java b/server/system_timer/src/main/java/com/doumee/jobs/fegin/VisitServiceFegin.java
index 3a31274..b9de78e 100644
--- a/server/system_timer/src/main/java/com/doumee/jobs/fegin/VisitServiceFegin.java
+++ b/server/system_timer/src/main/java/com/doumee/jobs/fegin/VisitServiceFegin.java
@@ -89,6 +89,12 @@
     @ApiOperation("銆愰槣瀹佽繍缁淬�戝畾鏃跺鐞嗗悎鍚岃处鍗曠紪鐮�")
     @GetMapping("/timer/yw/ywDealContractBillCodeTimer")
     ApiResponse ywDealContractBillCodeTimer();
+    @ApiOperation("銆愰槣瀹佽繍缁淬�戝畾鏃舵煡璇㈡櫤鎱х數琛ㄨ澶囩姸鎬�")
+    @GetMapping("/timer/yw/getElectricalStatus")
+    ApiResponse getElectricalStatus();
+    @ApiOperation("銆愰槣瀹佽繍缁淬�戞瘡灏忔椂杩涜涓�娆℃妱琛ㄤ换鍔�")
+    @GetMapping("/timer/yw/syncElectricalMeterData")
+    ApiResponse syncElectricalMeterData();
 
     @ApiOperation("銆愰槣瀹佽繍缁淬�戝畾鏃跺鐞嗗悎鍚岃繃鏈�")
     @GetMapping("/timer/yw/ywDealContractTimeOutTimer")
diff --git a/server/visits/admin_timer/src/main/java/com/doumee/job/ElectricalScheduleJob.java b/server/visits/admin_timer/src/main/java/com/doumee/job/ElectricalScheduleJob.java
new file mode 100644
index 0000000..42dbca7
--- /dev/null
+++ b/server/visits/admin_timer/src/main/java/com/doumee/job/ElectricalScheduleJob.java
@@ -0,0 +1,48 @@
+package com.doumee.job;
+
+import com.doumee.service.business.YwElectricalBizService;
+import com.doumee.service.business.YwElectricalService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class ElectricalScheduleJob {
+
+    @Autowired
+    private YwElectricalService ywElectricalService;
+    @Autowired
+    private YwElectricalBizService ywElectricalBizService;
+
+    /** 姣�5鍒嗛挓鍚屾閲囬泦鍣ㄥ湪绾跨姸鎬� */
+    @Scheduled(cron = "0 */5 * * * ?")
+    public void syncElectricalStatus() {
+        try {
+            ywElectricalService.getElectricalStatus();
+        } catch (Exception e) {
+            log.error("syncElectricalStatus failed", e);
+        }
+    }
+
+    /** 姣忓皬鏃�0鍒�30绉掓壒閲忔妱琛� */
+    @Scheduled(cron = "30 0 * * * ?")
+    public void syncMeterData() {
+        try {
+            ywElectricalBizService.syncMeterDataScheduled();
+        } catch (Exception e) {
+            log.error("syncMeterData failed", e);
+        }
+    }
+
+    /** 姣忔棩娓呯悊3涓湀鍓嶇殑鎺ュ彛鏃ュ織 */
+    @Scheduled(cron = "0 30 2 * * ?")
+    public void cleanElectricalLog() {
+        try {
+            ywElectricalBizService.cleanLogBeforeThreeMonths();
+        } catch (Exception e) {
+            log.error("cleanElectricalLog failed", e);
+        }
+    }
+}
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalActionsCloudController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalActionsCloudController.java
new file mode 100644
index 0000000..d2b16b8
--- /dev/null
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalActionsCloudController.java
@@ -0,0 +1,37 @@
+package com.doumee.cloud.admin;
+
+import com.doumee.api.BaseController;
+import com.doumee.config.annotation.CloudRequiredPermission;
+import com.doumee.core.model.ApiResponse;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.core.utils.Constants;
+import com.doumee.dao.business.model.YwElectricalActions;
+import com.doumee.service.business.YwElectricalActionsService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 鐢佃〃杩滅▼鎿嶄綔璁板綍
+ */
+@Api(tags = "yw_electrical_actions鎺ュ彛")
+@RestController
+@RequestMapping(Constants.CLOUD_SERVICE_URL_INDEX + "/business/ywElectricalActions")
+public class YwElectricalActionsCloudController extends BaseController {
+
+    @Autowired
+    private YwElectricalActionsService ywElectricalActionsService;
+
+    @ApiOperation("鍒嗛〉鏌ヨ")
+    @PostMapping("/page")
+    @CloudRequiredPermission("business:ywelectricalactions:query")
+    public ApiResponse<PageData<YwElectricalActions>> findPage(@RequestBody PageWrap<YwElectricalActions> pageWrap,
+                                                               @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        if (pageWrap.getModel() != null) {
+            pageWrap.getModel().setLoginUserInfo(this.getLoginUser(token));
+        }
+        return ApiResponse.success(ywElectricalActionsService.findPage(pageWrap));
+    }
+}
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java
index 5c06865..babb5eb 100644
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java
@@ -80,7 +80,7 @@
     @CloudRequiredPermission("business:ywelectricaldata:exportExcel")
     public void exportExcel (@RequestBody PageWrap<YwElectricalData> pageWrap, HttpServletResponse response, @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
          pageWrap.getModel().setLoginUserInfo(this.getLoginUser(token));
-        ExcelExporter.build(YwElectricalData.class).export(ywElectricalDataService.findPage(pageWrap).getRecords(), "鐢佃〃鎶勮〃鏁版嵁", response);
+        ExcelExporter.build(YwElectricalData.class).export(ywElectricalDataService.findPage(pageWrap).getRecords(), "鎶勮〃璁板綍", response);
     }
 
     @ApiOperation("鏍规嵁ID鏌ヨ")
@@ -89,4 +89,12 @@
     public ApiResponse findById(@PathVariable Integer id, @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
         return ApiResponse.success(ywElectricalDataService.findById(id));
     }
+
+    @PreventRepeat
+    @ApiOperation("绔嬪嵆鎶勮〃锛堝悓姝ョ涓夋柟鎶勮〃鏁版嵁锛�")
+    @PostMapping("/syncAll")
+    @CloudRequiredPermission("business:ywelectricaldata:sync")
+    public ApiResponse<String> syncAll(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywElectricalDataService.syncFromPlatform());
+    }
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/ElectronicToolUtil.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/ElectronicToolUtil.java
index 9e14c73..065155a 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/ElectronicToolUtil.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/ElectronicToolUtil.java
@@ -155,8 +155,8 @@
     }
     public static void main(String[] args) {
         QueryDataRequest param = new QueryDataRequest();
-        param.setStart_time("2026-05-19 10:00::00");
-        param.setEnd_time("2026-05-19 12:24:25");
+        param.setStart_time("2026-05-25 10:00::00");
+        param.setEnd_time("2026-05-25 14:24:25");
         param.setLimit(1);
         param.setFunctionids("253");
         param.setOffset(0);
@@ -178,10 +178,7 @@
         try {
             String request_content = JSON.toJSONString(param);
             String r = requestAsync(url, request_content);
-            TypeReference typeReference =
-                    new TypeReference< ElectronicBaseResponse<List<MeterDealResponse>> >(){};
-            ElectronicBaseResponse  result = JSONObject.parseObject(r, typeReference.getType());
-            return  result;
+            return parseAsyncMeterResponse(r);
         }catch (Exception e){
             log.error("鐢佃〃==============寮�鎴�",e);
         }
@@ -199,10 +196,7 @@
         try {
             String request_content = JSON.toJSONString(param);
             String r = requestAsync(url, request_content);
-            TypeReference typeReference =
-                    new TypeReference< ElectronicBaseResponse<List<MeterDealResponse>> >(){};
-            ElectronicBaseResponse  result = JSONObject.parseObject(r, typeReference.getType());
-            return  result;
+            return parseAsyncMeterResponse(r);
         }catch (Exception e){
             log.error("鐢佃〃==============寮�鎴�",e);
         }
@@ -220,10 +214,7 @@
         try {
             String request_content = JSON.toJSONString(param);
             String r = requestAsync(url, request_content);
-            TypeReference typeReference =
-                    new TypeReference< ElectronicBaseResponse<List<MeterDealResponse>> >(){};
-            ElectronicBaseResponse  result = JSONObject.parseObject(r, typeReference.getType());
-            return  result;
+            return parseAsyncMeterResponse(r);
         }catch (Exception e){
             log.error("鐢佃〃==============寮�鎴�",e);
         }
@@ -241,17 +232,14 @@
         try {
             String request_content = JSON.toJSONString(param);
             String r = requestAsync(url, request_content);
-            TypeReference typeReference =
-                    new TypeReference< ElectronicBaseResponse<List<MeterDealResponse>> >(){};
-            ElectronicBaseResponse  result = JSONObject.parseObject(r, typeReference.getType());
-            return  result;
+            return parseAsyncMeterResponse(r);
         }catch (Exception e){
             log.error("鐢佃〃==============鎷夊悎闂�",e);
         }
         return null;
     }
     /**
-     * 鐢佃〃_鎷夊悎闂�
+     * 鐢佃〃_绔嬪嵆鎶勮〃
      */
     public static ElectronicBaseResponse   eleRead(List<EleReadRequest> param) {
         if(param ==null || param.size()==0){
@@ -262,12 +250,9 @@
         try {
             String request_content = JSON.toJSONString(param);
             String r = requestAsync(url, request_content);
-            TypeReference typeReference =
-                    new TypeReference< ElectronicBaseResponse<List<MeterDealResponse>> >(){};
-            ElectronicBaseResponse  result = JSONObject.parseObject(r, typeReference.getType());
-            return  result;
+            return parseAsyncMeterResponse(r);
         }catch (Exception e){
-            log.error("鐢佃〃==============鎷夊悎闂�",e);
+            log.error("鐢佃〃==============绔嬪嵆鎶勮〃",e);
         }
         return null;
     }
@@ -363,6 +348,14 @@
         }
         return null;
     }
+
+    /**
+     * 鏌ヨ鍘嗗彶鏁版嵁鎺ュ彛
+     */
+    public static ElectronicDataResponse queryDataRequest() {
+        return queryDataRequest(null);
+    }
+
     /**
      * 鏌ヨ鍘嗗彶鏁版嵁鎺ュ彛
      */
@@ -474,6 +467,58 @@
     }
 
     // 鎵撳嵃鍝嶅簲鍐呭
+    /**
+     * 瑙f瀽 Api_v2 寮傛鐢佃〃鎿嶄綔鍝嶅簲銆�
+     * 骞冲彴 response_content 鍙兘鏄� JSON 鏁扮粍锛屼篃鍙兘鏄瓧绗︿覆褰㈠紡鐨� JSON 鏁扮粍銆�
+     */
+    public static ElectronicBaseResponse parseAsyncMeterResponse(String r) {
+        if (StringUtils.isBlank(r)) {
+            return null;
+        }
+        try {
+            JSONObject jsonObject = JSON.parseObject(r);
+            if (jsonObject == null) {
+                return null;
+            }
+            ElectronicBaseResponse<List<MeterDealResponse>> result = new ElectronicBaseResponse<>();
+            result.setStatus(jsonObject.getString("status"));
+            result.setTimestamp(jsonObject.getString("timestamp"));
+            result.setError_msg(jsonObject.getString("error_msg"));
+            result.setSign(jsonObject.getString("sign"));
+            result.setResponse_content(parseResponseContentList(jsonObject.get("response_content"), MeterDealResponse.class));
+            return result;
+        } catch (Exception e) {
+            log.error("parse async meter response failed, raw={}", r, e);
+            return null;
+        }
+    }
+
+    private static <T> List<T> parseResponseContentList(Object content, Class<T> clazz) {
+        if (content == null) {
+            return null;
+        }
+        if (content instanceof JSONArray) {
+            return ((JSONArray) content).toJavaList(clazz);
+        }
+        if (content instanceof JSONObject) {
+            T one = ((JSONObject) content).toJavaObject(clazz);
+            return one != null ? Collections.singletonList(one) : null;
+        }
+        String text = String.valueOf(content);
+        if (StringUtils.isBlank(text) || "null".equalsIgnoreCase(text.trim())) {
+            return null;
+        }
+        text = text.trim();
+        if (text.startsWith("[")) {
+            return JSON.parseArray(text, clazz);
+        }
+        if (text.startsWith("{")) {
+            T one = JSON.parseObject(text, clazz);
+            return one != null ? Collections.singletonList(one) : null;
+        }
+        return null;
+    }
+
     public static void printResponse(String response) {
         JSONObject jsonObject = JSON.parseObject(response);
 
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicConstant.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicConstant.java
index f9bd2c5..3b3603c 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicConstant.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicConstant.java
@@ -6,15 +6,24 @@
 import java.util.List;
 
 public class ElectronicConstant {
-    // 鎺堟潈鐮� 鐧诲綍 鍚庡彴鑾峰彇锛� 鑱旂郴浣犵殑鐢叉柟鎴栬�呮槸閿�鍞�
-    public static String auth_code = "1f29d378fc6792d5d2b735877993ffb7";
-    // 闅忔満瀛楃涓� 鍚庡彴鑾峰彇
-    public static String nonce = "lvyOY7fcun719WkxF8ToVYatStgt";
+
+    public static final String DEFAULT_AUTH_CODE = "1f29d378fc6792d5d2b735877993ffb7";
+    public static final String DEFAULT_NONCE = "lvyOY7fcun719WkxF8ToVYatStgt";
+    public static final String DEFAULT_NOTIFY_URL = "http://115.221.15.8:8055/electronic/electricalNotify";
+    public static final String DEFAULT_API_URL = "http://api1.tqdianbiao.com";
+    public static final String DEFAULT_API2_URL = "http://api2.tqdianbiao.com";
+
+    /** 鎺堟潈鐮侊紝鐢辨暟鎹瓧鍏� ELECTRICAL_PARAM 鍔犺浇 */
+    public static String auth_code = DEFAULT_AUTH_CODE;
+    /** 绛惧悕闅忔満涓诧紝鐢辨暟鎹瓧鍏� ELECTRICAL_PARAM 鍔犺浇 */
+    public static String nonce = DEFAULT_NONCE;
     public static String debug_meter = "000066660942";
-    // 寮傛閫氱煡鍦板潃锛堟湇鍔″鏋滈儴缃插湪鍐呯綉锛屽湪鍏綉鏃犳硶鐩存帴璁块棶鍒帮紝闇�瑕佸湪璺敱鍣ㄤ笂閰嶇疆绔彛鏄犲皠锛屾垨鑰呴厤缃唴缃戠┛閫忓伐鍏锋潵瀹炵幇璁块棶锛� 姝ゅ浠呬负娴嬭瘯 绀轰緥
-    public static String notify_url = "http://115.221.15.8:8055/electronic/electricalNotify";
-    public static String api_url = "http://api1.tqdianbiao.com";
-    public static String api2_url = "http://api2.tqdianbiao.com";
+    /** 寮傛閫氱煡鍦板潃锛岀敱鏁版嵁瀛楀吀 ELECTRICAL_PARAM 鍔犺浇 */
+    public static String notify_url = DEFAULT_NOTIFY_URL;
+    /** Api v1 鏍瑰湴鍧�锛岀敱鏁版嵁瀛楀吀 ELECTRICAL_PARAM 鍔犺浇 */
+    public static String api_url = DEFAULT_API_URL;
+    /** Api v2 鏍瑰湴鍧�锛岀敱鏁版嵁瀛楀吀 ELECTRICAL_PARAM 鍔犺浇 */
+    public static String api2_url = DEFAULT_API2_URL;
 
 
     /**
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicNotifyStatus.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicNotifyStatus.java
new file mode 100644
index 0000000..68164fa
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/ElectronicNotifyStatus.java
@@ -0,0 +1,55 @@
+package com.doumee.core.device.model;
+
+public enum ElectronicNotifyStatus {
+
+    NOTFOUND("NOTFOUND", "鏈壘鍒�"),
+    ACCEPTED("ACCEPTED", "宸叉帴鍙椾换鍔�"),
+    QUEUE("QUEUE", "闃熷垪涓�"),
+    PROCESSING("PROCESSING", "鎵ц涓�"),
+    DELIVERED("DELIVERED", "宸叉墽琛�"),
+    SUCCESS("SUCCESS", "鎴愬姛"),
+    NOTSUPPORT("NOTSUPPORT", "璁惧涓嶆敮鎸�"),
+    FAIL("FAIL", "澶辫触"),
+    TIMEOUT("TIMEOUT", "浠诲姟瓒呮椂"),
+    CANCELED("CANCELED", "鍙栨秷"),
+    RESPONSE_TIMEOUT("RESPONSE_TIMEOUT", "璁惧鍝嶅簲瓒呮椂"),
+    RESPONSE_FAIL("RESPONSE_FAIL", "璁惧鍝嶅簲澶辫触"),
+    UNKOWN("UNKOWN", "鏈煡");
+
+    private final String code;
+    private final String label;
+
+    ElectronicNotifyStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public static ElectronicNotifyStatus fromCode(String code) {
+        if (code == null) {
+            return UNKOWN;
+        }
+        for (ElectronicNotifyStatus s : values()) {
+            if (s.code.equalsIgnoreCase(code.trim())) {
+                return s;
+            }
+        }
+        return UNKOWN;
+    }
+
+    public boolean isTerminalSuccess() {
+        return this == SUCCESS || this == DELIVERED;
+    }
+
+    public boolean isTerminalFail() {
+        return this == FAIL || this == TIMEOUT || this == NOTSUPPORT || this == CANCELED
+                || this == RESPONSE_TIMEOUT || this == RESPONSE_FAIL || this == NOTFOUND;
+    }
+
+    public boolean isInProgress() {
+        return this == ACCEPTED || this == QUEUE || this == PROCESSING;
+    }
+}
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
new file mode 100644
index 0000000..7876274
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleControlApiRequest.java
@@ -0,0 +1,16 @@
+package com.doumee.core.device.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 鎷夊悎闂歌姹傦紙鍦ㄥ紑鎴疯姹傜粨鏋勫熀纭�涓婂鍔� type锛�
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class EleControlApiRequest extends OpenAccountRequest {
+
+    @ApiModelProperty("10鎷夐椄 11鍚堥椄")
+    private int type;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleReadRequest.java b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleReadRequest.java
index b98fdca..e5602da 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleReadRequest.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/core/device/model/request/EleReadRequest.java
@@ -25,7 +25,7 @@
     private int time_out;
     @ApiModelProperty(value = "濡傛灉浼爐rue ,鍒欑郴缁熸帴鍙椾换鍔℃椂璁惧涓嶅湪绾跨洿鎺ヨ繑鍥炲け璐�")
     private String must_online;
-    @ApiModelProperty(value = "鎿嶄綔绫诲瀷ID")
+    @ApiModelProperty(value = "鎿嶄綔绫诲瀷ID锛堟妱鐢佃〃鏁版嵁涓� 3锛屽嬁涓� queryData functionids=253 娣锋穯锛�")
     private int type;
     @ApiModelProperty(value = "閲嶈瘯娆℃暟锛�0-3锛� ,榛樿1娆�")
     private int retry_times;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwElectricalActionsMapper.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwElectricalActionsMapper.java
new file mode 100644
index 0000000..2a3f4ea
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/YwElectricalActionsMapper.java
@@ -0,0 +1,9 @@
+package com.doumee.dao.business;
+
+import com.doumee.dao.business.model.YwElectricalActions;
+import com.github.yulichang.base.MPJBaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface YwElectricalActionsMapper extends MPJBaseMapper<YwElectricalActions> {
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalEditDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalEditDTO.java
new file mode 100644
index 0000000..c133e64
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalEditDTO.java
@@ -0,0 +1,14 @@
+package com.doumee.dao.business.dto;
+
+import com.doumee.dao.business.model.YwElectrical;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class YwElectricalEditDTO extends YwElectrical {
+
+    private List<Integer> roomIds;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalOperateDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalOperateDTO.java
new file mode 100644
index 0000000..739535a
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalOperateDTO.java
@@ -0,0 +1,15 @@
+package com.doumee.dao.business.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class YwElectricalOperateDTO {
+
+    private Integer electricalId;
+    /** resetPrepay resetPostpay trip close openAccount recharge readMeter */
+    private String action;
+    private BigDecimal money;
+    private String remark;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectrical.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectrical.java
index 45621e9..f64d116 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectrical.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectrical.java
@@ -134,11 +134,16 @@
     @ApiModelProperty("鍏宠仈鍙傛暟妗fID priceid鍜宲aram_id 姣忓潡琛ㄥ彧浼氱敤鍏朵腑涓�绉�")
     @ExcelColumn(name="鍏宠仈鍙傛暟妗fID priceid鍜宲aram_id 姣忓潡琛ㄥ彧浼氱敤鍏朵腑涓�绉�",index=36 ,width=10)
     private String paramId;
+    @ApiModelProperty("鐢佃〃鍙傛暟ID锛堝叧鑱� yw_electrical_param.id锛�")
+    private Integer electricalParamId;
     @ApiModelProperty("缁戝畾鎴块棿鍚嶇О锛堝叧鑱攜w_electrical_room銆亂w_room锛�")
     @TableField(exist = false)
     private String roomNames;
 
     @TableField(exist = false)
+    private String paramName;
+
+    @TableField(exist = false)
     private String warnTypeName;
 
     @TableField(exist = false)
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
new file mode 100644
index 0000000..f6ba30f
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalActions.java
@@ -0,0 +1,57 @@
+package com.doumee.dao.business.model;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.doumee.core.model.LoginUserModel;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("yw_electrical_actions")
+@ApiModel("鐢佃〃杩滅▼鎿嶄綔璁板綍")
+public class YwElectricalActions extends LoginUserModel {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+    private Integer creator;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createDate;
+    private Integer editor;
+    private Date editDate;
+    private Integer isdeleted;
+    private String remark;
+    @ApiModelProperty("鐢佃〃ID")
+    private Integer electricalId;
+    @ApiModelProperty("鎿嶄綔绫诲瀷 1棰勪粯璐规竻闆� 2鍚庝粯璐规竻闆� 3杩滅▼閿�鎴� 4鎷夐椄 5鍚堥椄 6寮�鎴� 7鍏呭�� 8鎶勮〃")
+    private Integer actionType;
+    private String oprId;
+    private String requestBody;
+    private String responseBody;
+    @ApiModelProperty("0澶勭悊涓� 1鎴愬姛 2澶辫触")
+    private Integer status;
+    private String resultMsg;
+
+    @TableField(exist = false)
+    @ApiModelProperty("鐢佃〃鍚嶇О")
+    private String electricalName;
+
+    @TableField(exist = false)
+    @ApiModelProperty("鐢佃〃鍦板潃")
+    private String electricalAddress;
+
+    @TableField(exist = false)
+    @ApiModelProperty("鎿嶄綔寮�濮嬫椂闂�")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date operateTimeBegin;
+
+    @TableField(exist = false)
+    @ApiModelProperty("鎿嶄綔缁撴潫鏃堕棿")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date operateTimeEnd;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalData.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalData.java
index c4606ac..0b21d7b 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalData.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalData.java
@@ -185,4 +185,29 @@
     @ApiModelProperty("鍊嶇巼")
     @ExcelColumn(name="鍊嶇巼",index=54 ,width=10)
     private BigDecimal radio;
+
+    @TableField(exist = false)
+    private String meterKeyword;
+
+    @TableField(exist = false)
+    private Integer roomId;
+
+    @TableField(exist = false)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date readTimeBegin;
+
+    @TableField(exist = false)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date readTimeEnd;
+
+    @TableField(exist = false)
+    private Integer electricalId;
+
+    @TableField(exist = false)
+    @ExcelColumn(name = "鐢佃〃鍚嶇О", index = 55, width = 20)
+    private String electricalName;
+
+    @TableField(exist = false)
+    @ExcelColumn(name = "缁戝畾鎴块棿", index = 56, width = 24)
+    private String roomNames;
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalActionsService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalActionsService.java
new file mode 100644
index 0000000..4882fb9
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalActionsService.java
@@ -0,0 +1,13 @@
+package com.doumee.service.business;
+
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.dao.business.model.YwElectricalActions;
+
+/**
+ * 鐢佃〃杩滅▼鎿嶄綔璁板綍 Service
+ */
+public interface YwElectricalActionsService {
+
+    PageData<YwElectricalActions> findPage(PageWrap<YwElectricalActions> pageWrap);
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java
new file mode 100644
index 0000000..c2cf405
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java
@@ -0,0 +1,34 @@
+package com.doumee.service.business;
+
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.dao.business.dto.YwElectricalEditDTO;
+import com.doumee.dao.business.dto.YwElectricalOperateDTO;
+import com.doumee.dao.business.model.YwElectrical;
+import com.doumee.dao.business.model.YwElectricalData;
+
+import java.util.List;
+import java.util.Map;
+
+public interface YwElectricalBizService {
+
+    YwElectricalEditDTO getDetail(Integer id);
+
+    void updateDetail(YwElectricalEditDTO dto, LoginUserInfo user);
+
+    Map<String, Object> getRemoteInfo(Integer electricalId);
+
+    String operate(YwElectricalOperateDTO dto, LoginUserInfo user);
+
+    boolean handleElectricalNotify(String responseContent, String timestamp, String sign);
+
+    void syncMeterDataScheduled();
+
+    /** 鎵嬪姩浠庣涓夋柟骞冲彴鎷夊彇鎶勮〃鏁版嵁鍏ュ簱 */
+    String syncMeterDataFromPlatform();
+
+    void cleanLogBeforeThreeMonths();
+
+    void enrichList(List<YwElectrical> list);
+
+    void enrichDataList(List<YwElectricalData> list);
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java
index 96dcc0e..73a7488 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java
@@ -108,4 +108,9 @@
      * @return long
      */
     long count(YwElectricalData model);
+
+    /**
+     * 浠庣涓夋柟骞冲彴鍚屾鎶勮〃鏁版嵁
+     */
+    String syncFromPlatform();
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/ElectronicConfigService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/ElectronicConfigService.java
new file mode 100644
index 0000000..c6bc85e
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/ElectronicConfigService.java
@@ -0,0 +1,52 @@
+package com.doumee.service.business.impl;
+
+import com.doumee.biz.system.SystemDictDataBiz;
+import com.doumee.core.device.model.ElectronicConstant;
+import com.doumee.core.utils.Constants;
+import com.doumee.dao.system.model.SystemDictData;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+
+/**
+ * 浠庢暟鎹瓧鍏� ELECTRICAL_PARAM 鍔犺浇鏅鸿兘鐢佃〃绗笁鏂瑰钩鍙拌繛鎺ュ弬鏁般��
+ */
+@Slf4j
+@Service
+public class ElectronicConfigService {
+
+    @Autowired
+    private SystemDictDataBiz systemDictDataBiz;
+
+    @PostConstruct
+    public void init() {
+        refreshFromDict();
+    }
+
+    /** 閲嶆柊浠庢暟鎹瓧鍏稿姞杞介厤缃紙淇敼瀛楀吀鍚庨噸鍚湇鍔$敓鏁堬紝鎴栦富鍔ㄨ皟鐢ㄥ埛鏂帮級 */
+    public void refreshFromDict() {
+        ElectronicConstant.auth_code = read(Constants.ELECTRICAL_AUTH_CODE, ElectronicConstant.DEFAULT_AUTH_CODE);
+        ElectronicConstant.nonce = read(Constants.ELECTRICAL_NONCE, ElectronicConstant.DEFAULT_NONCE);
+        ElectronicConstant.notify_url = read(Constants.ELECTRICAL_NOTIFY_URL, ElectronicConstant.DEFAULT_NOTIFY_URL);
+        ElectronicConstant.api_url = read(Constants.ELECTRICAL_API_URL, ElectronicConstant.DEFAULT_API_URL);
+        ElectronicConstant.api2_url = read(Constants.ELECTRICAL_API2_URL, ElectronicConstant.DEFAULT_API2_URL);
+        log.info("electrical config loaded from dict {}, api_url={}, api2_url={}, notify_url={}",
+                Constants.ELECTRICAL_PARAM, ElectronicConstant.api_url, ElectronicConstant.api2_url, ElectronicConstant.notify_url);
+    }
+
+    private String read(String label, String defaultValue) {
+        try {
+            SystemDictData data = systemDictDataBiz.queryByCode(Constants.ELECTRICAL_PARAM, label);
+            if (data != null && StringUtils.isNotBlank(data.getCode())) {
+                return data.getCode().trim();
+            }
+            log.warn("electrical config [{}] empty, use default", label);
+        } catch (Exception e) {
+            log.warn("electrical config [{}] load failed, use default", label, e);
+        }
+        return defaultValue;
+    }
+}
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
new file mode 100644
index 0000000..999a070
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalActionsServiceImpl.java
@@ -0,0 +1,53 @@
+package com.doumee.service.business.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.core.utils.Constants;
+import com.doumee.core.utils.Utils;
+import com.doumee.dao.business.YwElectricalActionsMapper;
+import com.doumee.dao.business.model.YwElectrical;
+import com.doumee.dao.business.model.YwElectricalActions;
+import com.doumee.service.business.YwElectricalActionsService;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 鐢佃〃杩滅▼鎿嶄綔璁板綍 Service 瀹炵幇
+ */
+@Service
+public class YwElectricalActionsServiceImpl implements YwElectricalActionsService {
+
+    @Autowired
+    private YwElectricalActionsMapper ywElectricalActionsMapper;
+
+    @Override
+    public PageData<YwElectricalActions> findPage(PageWrap<YwElectricalActions> pageWrap) {
+        IPage<YwElectricalActions> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
+        YwElectricalActions model = pageWrap.getModel() == null ? new YwElectricalActions() : pageWrap.getModel();
+        Utils.MP.blankToNull(model);
+
+        MPJLambdaWrapper<YwElectricalActions> queryWrapper = new MPJLambdaWrapper<>();
+        queryWrapper.selectAll(YwElectricalActions.class)
+                .selectAs(YwElectrical::getName, YwElectricalActions::getElectricalName)
+                .selectAs(YwElectrical::getAddress, YwElectricalActions::getElectricalAddress)
+                .leftJoin(YwElectrical.class, YwElectrical::getId, YwElectricalActions::getElectricalId)
+                .eq(YwElectricalActions::getIsdeleted, Constants.ZERO);
+
+        if (model.getActionType() != null) {
+            queryWrapper.eq(YwElectricalActions::getActionType, model.getActionType());
+        }
+        if (model.getOperateTimeBegin() != null) {
+            queryWrapper.ge(YwElectricalActions::getCreateDate, Utils.Date.getStart(model.getOperateTimeBegin()));
+        }
+        if (model.getOperateTimeEnd() != null) {
+            queryWrapper.le(YwElectricalActions::getCreateDate, Utils.Date.getEnd(model.getOperateTimeEnd()));
+        }
+
+        queryWrapper.orderByDesc(YwElectricalActions::getId);
+        IPage<YwElectricalActions> result = ywElectricalActionsMapper.selectJoinPage(page, YwElectricalActions.class, queryWrapper);
+        return PageData.from(result);
+    }
+}
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
new file mode 100644
index 0000000..c873faf
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
@@ -0,0 +1,939 @@
+package com.doumee.service.business.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.doumee.core.constants.ResponseStatus;
+import com.doumee.core.device.ElectronicToolUtil;
+import com.doumee.core.device.model.ElectronicConstant;
+import com.doumee.core.device.model.ElectronicNotifyStatus;
+import com.doumee.core.device.model.request.*;
+import com.doumee.core.device.model.response.ElectronicBaseResponse;
+import com.doumee.core.device.model.response.ElectronicDataResponse;
+import com.doumee.core.device.model.response.QueryDataInfoResponse;
+import com.doumee.core.device.model.response.QueryDataV2Response;
+import com.doumee.core.exception.BusinessException;
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.core.utils.Constants;
+import com.doumee.core.utils.DateUtil;
+import com.doumee.dao.business.*;
+import com.doumee.dao.business.dto.YwElectricalEditDTO;
+import com.doumee.dao.business.dto.YwElectricalOperateDTO;
+import com.doumee.dao.business.model.*;
+import com.doumee.service.business.YwElectricalBizService;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class YwElectricalBizServiceImpl implements YwElectricalBizService {
+
+    private static final Logger log = LoggerFactory.getLogger(YwElectricalBizServiceImpl.class);
+
+    public static final int ACTION_RESET_PREPAY = 1;
+    public static final int ACTION_RESET_POSTPAY = 2;
+    public static final int ACTION_REMOTE_CLOSE = 3;
+    public static final int ACTION_TRIP = 4;
+    public static final int ACTION_CLOSE = 5;
+    public static final int ACTION_OPEN = 6;
+    public static final int ACTION_RECHARGE = 7;
+    public static final int ACTION_READ = 8;
+
+    /** ele_read 鎶勭數琛ㄦ暟鎹搷浣滅被鍨嬶紙涓� queryData functionids=253 涓嶅悓锛� */
+    private static final int ELE_READ_TYPE_METER = 3;
+    /** queryData 鐢佃〃鐘舵�佽鎯呭姛鑳� ID */
+    private static final String QUERY_DATA_FUNCTION_METER_STATUS = "253";
+
+    @Autowired
+    private YwElectricalMapper ywElectricalMapper;
+    @Autowired
+    private YwElectricalRoomMapper ywElectricalRoomMapper;
+    @Autowired
+    private YwElectricalActionsMapper ywElectricalActionsMapper;
+    @Autowired
+    private YwElectricalLogMapper ywElectricalLogMapper;
+    @Autowired
+    private YwElectricalDataMapper ywElectricalDataMapper;
+    @Autowired
+    private YwElectricalChargeMapper ywElectricalChargeMapper;
+
+    @Override
+    public YwElectricalEditDTO getDetail(Integer id) {
+        YwElectrical e = ywElectricalMapper.selectById(id);
+        if (e == null || Objects.equals(e.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY);
+        }
+        YwElectricalEditDTO dto = new YwElectricalEditDTO();
+        BeanUtils.copyProperties(e, dto);
+        List<YwElectricalRoom> rooms = ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
+                .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO)
+                .eq(YwElectricalRoom::getType, Constants.ZERO)
+                .eq(YwElectricalRoom::getObjId, id));
+        dto.setRoomIds(rooms.stream().map(YwElectricalRoom::getRoomId).filter(Objects::nonNull).collect(Collectors.toList()));
+        fillRoomNames(Collections.singletonList(dto));
+        return dto;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateDetail(YwElectricalEditDTO dto, LoginUserInfo user) {
+        if (dto == null || dto.getId() == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST);
+        }
+        ywElectricalMapper.update(null, new UpdateWrapper<YwElectrical>().lambda()
+                .eq(YwElectrical::getId, dto.getId())
+                .set(YwElectrical::getRate, dto.getRate() != null ? dto.getRate() : BigDecimal.ONE)
+                .set(YwElectrical::getElectricalParamId, dto.getElectricalParamId())
+                .set(YwElectrical::getEdirot, user.getId())
+                .set(YwElectrical::getEditDate, new Date()));
+        saveRooms(dto.getId(), dto.getRoomIds(), user);
+    }
+
+    private void saveRooms(Integer electricalId, List<Integer> roomIds, LoginUserInfo user) {
+        ywElectricalRoomMapper.update(null, new UpdateWrapper<YwElectricalRoom>().lambda()
+                .set(YwElectricalRoom::getIsdeleted, Constants.ONE)
+                .set(YwElectricalRoom::getEditDate, new Date())
+                .set(YwElectricalRoom::getEditor, user.getId())
+                .eq(YwElectricalRoom::getObjId, electricalId)
+                .eq(YwElectricalRoom::getType, Constants.ZERO));
+        if (CollectionUtils.isEmpty(roomIds)) {
+            return;
+        }
+        int sort = 0;
+        for (Integer roomId : roomIds) {
+            if (roomId == null) continue;
+            YwElectricalRoom r = new YwElectricalRoom();
+            r.setCreator(user.getId());
+            r.setCreateDate(new Date());
+            r.setEditor(user.getId());
+            r.setEditDate(new Date());
+            r.setIsdeleted(Constants.ZERO);
+            r.setType(Constants.ZERO);
+            r.setObjId(electricalId);
+            r.setRoomId(roomId);
+            r.setSortnum(++sort);
+            ywElectricalRoomMapper.insert(r);
+        }
+    }
+
+    @Override
+    public Map<String, Object> getRemoteInfo(Integer electricalId) {
+        YwElectrical e = requireElectrical(electricalId);
+        fillRoomNames(Collections.singletonList(e));
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("electrical", e);
+        YwElectricalData latest = findLatestData(e.getId(), e.getAddress());
+        map.put("latestData", latest);
+        map.put("purchaseCount", latest != null && latest.getCountnum() != null ? latest.getCountnum() : "0");
+        return map;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String operate(YwElectricalOperateDTO dto, LoginUserInfo user) {
+        YwElectrical e = requireElectrical(dto.getElectricalId());
+        String action = dto.getAction();
+        if (StringUtils.isBlank(action)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST);
+        }
+        switch (action) {
+            case "resetPrepay":
+                return doSecurityReset(e, "default", ACTION_RESET_PREPAY, user);
+            case "resetPostpay":
+                return doSecurityReset(e, "noprepay", ACTION_RESET_POSTPAY, user);
+            case "trip":
+                return doEleControl(e, 10, ACTION_TRIP, user);
+            case "close":
+                return doEleControl(e, 11, ACTION_CLOSE, user);
+            case "openAccount":
+                return doOpenAccount(e, dto, user);
+            case "recharge":
+                return doRecharge(e, dto, user);
+            case "readMeter":
+                return doReadMeter(e, user);
+            default:
+                throw new BusinessException(ResponseStatus.BAD_REQUEST);
+        }
+    }
+
+    private String doSecurityReset(YwElectrical e, String paymentMode, int actionType, LoginUserInfo user) {
+        String oprId = newOprId();
+        SecurityResetRequest req = new SecurityResetRequest();
+        req.setOpr_id(oprId);
+        req.setCid(e.getCollectorId());
+        req.setAddress(e.getAddress());
+        req.setTime_out(86400);
+        req.setMust_online("false");
+        req.setRetry_times(1);
+        SecurityResetParamRequest p = new SecurityResetParamRequest();
+        p.setPaymentmode(paymentMode);
+        p.setAccount_id(StringUtils.defaultIfBlank(e.getAccountId(), "1"));
+        req.setParams(p);
+        String reqJson = JSON.toJSONString(Collections.singletonList(req));
+        ElectronicBaseResponse resp = ElectronicToolUtil.eleSecurityReset(Collections.singletonList(req));
+        return finishAsync(e, actionType, oprId, "/Api_v2/ele_security/reset", reqJson, resp, user);
+    }
+
+    private String doEleControl(YwElectrical e, int type, int actionType, LoginUserInfo user) {
+        String oprId = newOprId();
+        EleControlApiRequest req = new EleControlApiRequest();
+        req.setOpr_id(oprId);
+        req.setCid(e.getCollectorId());
+        req.setAddress(e.getAddress());
+        req.setTime_out(86400);
+        req.setMust_online("false");
+        req.setRetry_times(1);
+        req.setType(type);
+        String reqJson = JSON.toJSONString(Collections.singletonList(req));
+        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);
+    }
+
+    private String doOpenAccount(YwElectrical e, YwElectricalOperateDTO dto, LoginUserInfo user) {
+        String oprId = newOprId();
+        OpenAccountRequest req = buildOpenAccountRequest(e, oprId, dto.getMoney(), dto.getRemark());
+        String reqJson = JSON.toJSONString(Collections.singletonList(req));
+        ElectronicBaseResponse resp = ElectronicToolUtil.openAcount(Collections.singletonList(req));
+        return finishAsync(e, ACTION_OPEN, oprId, "/Api_v2/ele_security/open_acount", reqJson, resp, user);
+    }
+
+    private String doRecharge(YwElectrical e, YwElectricalOperateDTO dto, LoginUserInfo user) {
+        if (!Objects.equals(e.getAccountStatus(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鐢佃〃鏈紑鎴凤紝璇峰厛寮�鎴�");
+        }
+        String oprId = newOprId();
+        OpenAccountRequest req = buildOpenAccountRequest(e, oprId, dto.getMoney(), dto.getRemark());
+        String reqJson = JSON.toJSONString(Collections.singletonList(req));
+        ElectronicBaseResponse resp = ElectronicToolUtil.recharger(Collections.singletonList(req));
+        String msg = finishAsync(e, ACTION_RECHARGE, oprId, "/Api_v2/ele_security/recharge", reqJson, resp, user);
+        if (isSuccess(resp)) {
+            saveChargeRecord(e, oprId, dto, user);
+        }
+        return msg;
+    }
+
+    private String doReadMeter(YwElectrical e, LoginUserInfo user) {
+        String oprId = newOprId();
+        EleReadRequest req = new EleReadRequest();
+        req.setOpr_id(oprId);
+        req.setCid(e.getCollectorId());
+        req.setAddress(e.getAddress());
+        req.setTime_out(86400);
+        req.setMust_online("false");
+        req.setType(ELE_READ_TYPE_METER);
+        req.setRetry_times(1);
+        String reqJson = JSON.toJSONString(Collections.singletonList(req));
+        ElectronicBaseResponse resp = ElectronicToolUtil.eleRead(Collections.singletonList(req));
+        String msg = finishAsync(e, ACTION_READ, oprId, "/Api_v2/ele_read", reqJson, resp, user);
+        if (!isSuccess(resp)) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), msg);
+        }
+        boolean synced = syncMeterDataForElectrical(e);
+        refreshBalanceFromData(e);
+        return synced ? "鎶勮〃鎴愬姛锛岀敤閲忎綑棰濆凡鏇存柊" : "鎶勮〃璇锋眰宸叉彁浜わ紝鏆傛棤鏂版妱琛ㄦ暟鎹紝璇风◢鍚庡埛鏂版煡鐪�";
+    }
+
+    private OpenAccountRequest buildOpenAccountRequest(YwElectrical e, String oprId, BigDecimal money, String remark) {
+        OpenAccountRequest req = new OpenAccountRequest();
+        req.setOpr_id(oprId);
+        req.setCid(e.getCollectorId());
+        req.setAddress(e.getAddress());
+        req.setTime_out(86400);
+        req.setMust_online("false");
+        req.setRetry_times(1);
+        OpenAccountParamRequest p = new OpenAccountParamRequest();
+        p.setMoney(money != null ? money.toPlainString() : "0");
+        p.setAccount_id(StringUtils.defaultIfBlank(e.getAccountId(), "1"));
+        p.setCount("1");
+        p.setRate(e.getRate() != null ? e.getRate().toPlainString() : "1");
+        req.setParams(p);
+        return req;
+    }
+
+    private String finishAsync(YwElectrical e, int actionType, String oprId, String apiName, String reqJson,
+                               ElectronicBaseResponse resp, LoginUserInfo user) {
+        saveLog(e, apiName, reqJson, resp, user);
+        YwElectricalActions act = new YwElectricalActions();
+        act.setCreator(user.getId());
+        act.setCreateDate(new Date());
+        act.setEditor(user.getId());
+        act.setEditDate(new Date());
+        act.setIsdeleted(Constants.ZERO);
+        act.setElectricalId(e.getId());
+        act.setActionType(actionType);
+        act.setOprId(oprId);
+        act.setRequestBody(reqJson);
+        act.setResponseBody(resp != null ? JSON.toJSONString(resp) : null);
+        if (isSuccess(resp)) {
+            act.setStatus(Constants.ZERO);
+            act.setResultMsg("宸叉彁浜わ紝绛夊緟骞冲彴鎵ц");
+        } else {
+            act.setStatus(Constants.TWO);
+            act.setResultMsg(resp != null ? resp.getError_msg() : "璋冪敤澶辫触");
+        }
+        ywElectricalActionsMapper.insert(act);
+        return isSuccess(resp) ? "鎻愪氦鎴愬姛锛岃绋嶅悗鍦ㄦ搷浣滆褰曟垨鍏呭�艰褰曚腑鏌ョ湅缁撴灉" : act.getResultMsg();
+    }
+
+    private void saveLog(YwElectrical e, String apiName, String reqJson, ElectronicBaseResponse resp, LoginUserInfo user) {
+        YwElectricalLog logRow = new YwElectricalLog();
+        logRow.setCreator(user.getId());
+        logRow.setCreateDate(new Date());
+        logRow.setEditor(user.getId());
+        logRow.setEditDate(new Date());
+        logRow.setIsdeleted(Constants.ZERO);
+        logRow.setDeviceType(Constants.ZERO);
+        logRow.setType(Constants.ZERO);
+        logRow.setName(apiName);
+        logRow.setUrl(apiName);
+        logRow.setRequest(reqJson);
+        logRow.setRepose(resp != null ? JSON.toJSONString(resp) : null);
+        logRow.setSuccess(isSuccess(resp) ? Constants.ZERO : Constants.ONE);
+        logRow.setObjId(String.valueOf(e.getId()));
+        ywElectricalLogMapper.insert(logRow);
+    }
+
+    private boolean isSuccess(ElectronicBaseResponse resp) {
+        return resp != null && "SUCCESS".equalsIgnoreCase(resp.getStatus());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean handleElectricalNotify(String responseContent, String timestamp, String sign) {
+        if (!ElectronicToolUtil.verifyNotifySign(responseContent, timestamp, sign)) {
+            log.warn("electricalNotify sign check failed, timestamp={}", timestamp);
+            return false;
+        }
+        JSONArray items;
+        try {
+            items = JSON.parseArray(responseContent);
+        } catch (Exception e) {
+            log.warn("electricalNotify invalid response_content", e);
+            return false;
+        }
+        if (items == null || items.isEmpty()) {
+            return true;
+        }
+        for (int i = 0; i < items.size(); i++) {
+            applyNotifyItem(items.getJSONObject(i));
+        }
+        return true;
+    }
+
+    private void applyNotifyItem(JSONObject item) {
+        if (item == null) return;
+        String oprId = item.getString("opr_id");
+        if (StringUtils.isBlank(oprId)) return;
+        YwElectricalActions act = ywElectricalActionsMapper.selectOne(new QueryWrapper<YwElectricalActions>().lambda()
+                .eq(YwElectricalActions::getOprId, oprId)
+                .eq(YwElectricalActions::getIsdeleted, Constants.ZERO)
+                .orderByDesc(YwElectricalActions::getCreateDate)
+                .last("limit 1"));
+        if (act == null) {
+            log.info("electricalNotify no action for opr_id={}", oprId);
+            return;
+        }
+        ElectronicNotifyStatus ns = ElectronicNotifyStatus.fromCode(item.getString("status"));
+        String errMsg = item.get("error_msg") != null ? String.valueOf(item.get("error_msg")) : null;
+        String resultMsg = ns.getLabel() + (StringUtils.isNotBlank(errMsg) ? "锛�" + errMsg : "");
+
+        YwElectricalActions upd = new YwElectricalActions();
+        upd.setId(act.getId());
+        upd.setEditDate(new Date());
+        upd.setResponseBody(item.toJSONString());
+        upd.setResultMsg(resultMsg);
+        if (ns.isTerminalSuccess()) {
+            upd.setStatus(Constants.ONE);
+        } else if (ns.isTerminalFail()) {
+            upd.setStatus(Constants.TWO);
+        } else if (ns.isInProgress()) {
+            upd.setStatus(Constants.ZERO);
+        }
+        ywElectricalActionsMapper.updateById(upd);
+
+        YwElectrical e = ywElectricalMapper.selectById(act.getElectricalId());
+        if (e == null) return;
+        saveNotifyLog(e, item, ns);
+        if (ns.isTerminalSuccess()) {
+            applyNotifySideEffect(e, act.getActionType());
+            if (Objects.equals(act.getActionType(), ACTION_RECHARGE)) {
+                updateChargeByNotify(oprId, Constants.ONE, resultMsg);
+            }
+        } else if (ns.isTerminalFail() && Objects.equals(act.getActionType(), ACTION_RECHARGE)) {
+            updateChargeByNotify(oprId, Constants.TWO, resultMsg);
+        }
+    }
+
+    private void saveChargeRecord(YwElectrical e, String oprId, YwElectricalOperateDTO dto, LoginUserInfo user) {
+        fillRoomNames(Collections.singletonList(e));
+        YwElectricalCharge charge = new YwElectricalCharge();
+        charge.setCreator(user.getId());
+        charge.setCreateDate(new Date());
+        charge.setEditor(user.getId());
+        charge.setEditDate(new Date());
+        charge.setIsdeleted(Constants.ZERO);
+        charge.setType(Constants.ZERO);
+        charge.setObjId(e.getId());
+        charge.setAddress(e.getAddress());
+        charge.setName(e.getName());
+        charge.setCId(e.getCollectorId());
+        charge.setMoney(dto.getMoney() != null ? dto.getMoney() : BigDecimal.ZERO);
+        charge.setRemark(dto.getRemark());
+        charge.setOprId(oprId);
+        charge.setStatus(Constants.ZERO);
+        charge.setStatusTime(new Date());
+        charge.setStatusInfo("鍏呭�间腑");
+        charge.setBanlance(e.getBalance());
+        charge.setRoomNames(e.getRoomNames());
+        if (StringUtils.isNotBlank(e.getParamId())) {
+            try {
+                charge.setParamId(Integer.parseInt(e.getParamId()));
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        ywElectricalChargeMapper.insert(charge);
+    }
+
+    private void updateChargeByNotify(String oprId, int status, String statusInfo) {
+        ywElectricalChargeMapper.update(null, new UpdateWrapper<YwElectricalCharge>().lambda()
+                .set(YwElectricalCharge::getStatus, status)
+                .set(YwElectricalCharge::getStatusTime, new Date())
+                .set(YwElectricalCharge::getStatusInfo, statusInfo)
+                .set(YwElectricalCharge::getEditDate, new Date())
+                .eq(YwElectricalCharge::getOprId, oprId)
+                .eq(YwElectricalCharge::getIsdeleted, Constants.ZERO));
+    }
+
+    private void saveNotifyLog(YwElectrical e, JSONObject item, ElectronicNotifyStatus ns) {
+        YwElectricalLog logRow = new YwElectricalLog();
+        logRow.setCreateDate(new Date());
+        logRow.setEditDate(new Date());
+        logRow.setIsdeleted(Constants.ZERO);
+        logRow.setDeviceType(Constants.ZERO);
+        logRow.setType(Constants.ONE);
+        logRow.setName("electricalNotify");
+        logRow.setUrl("/electronic/electricalNotify");
+        logRow.setRequest(null);
+        logRow.setRepose(item.toJSONString());
+        logRow.setSuccess(ns.isTerminalFail() ? Constants.ONE : Constants.ZERO);
+        logRow.setObjId(String.valueOf(e.getId()));
+        ywElectricalLogMapper.insert(logRow);
+    }
+
+    private void applyNotifySideEffect(YwElectrical e, Integer actionType) {
+        if (actionType == null) return;
+        UpdateWrapper<YwElectrical> uw = new UpdateWrapper<>();
+        uw.lambda().eq(YwElectrical::getId, e.getId()).set(YwElectrical::getEditDate, new Date());
+        switch (actionType) {
+            case ACTION_TRIP:
+                uw.lambda().set(YwElectrical::getRelayStatus, "0");
+                ywElectricalMapper.update(null, uw);
+                break;
+            case ACTION_CLOSE:
+                uw.lambda().set(YwElectrical::getRelayStatus, "1");
+                ywElectricalMapper.update(null, uw);
+                break;
+            case ACTION_OPEN:
+                uw.lambda().set(YwElectrical::getAccountStatus, Constants.ONE)
+                        .set(YwElectrical::getLastOpenDate, new Date());
+                ywElectricalMapper.update(null, uw);
+                break;
+            case ACTION_RECHARGE:
+            case ACTION_READ:
+                syncMeterDataForElectrical(e);
+                refreshBalanceFromData(e);
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void refreshBalanceFromData(YwElectrical e) {
+        YwElectricalData data = findLatestData(e.getId(), e.getAddress());
+        if (data == null) return;
+        UpdateWrapper<YwElectrical> uw = new UpdateWrapper<>();
+        uw.lambda().eq(YwElectrical::getId, e.getId());
+        if (StringUtils.isNotBlank(data.getZhygzdl())) {
+            uw.lambda().set(YwElectrical::getBalanceBattery, data.getZhygzdl());
+        }
+        if (StringUtils.isNotBlank(data.getYe())) {
+            try {
+                uw.lambda().set(YwElectrical::getBalance, new BigDecimal(data.getYe()));
+            } catch (Exception ignored) {
+            }
+        }
+        uw.lambda().set(YwElectrical::getBalanceTime, new Date());
+        uw.lambda().set(YwElectrical::getEditDate, new Date());
+        ywElectricalMapper.update(null, uw);
+    }
+
+    private YwElectricalData findLatestData(Integer electricalId, String address) {
+        QueryWrapper<YwElectricalData> q = new QueryWrapper<>();
+        q.lambda().eq(YwElectricalData::getIsdeleted, Constants.ZERO)
+                .and(w -> w.eq(YwElectricalData::getDeviceId, String.valueOf(electricalId))
+                        .or().eq(StringUtils.isNotBlank(address), YwElectricalData::getAddress, address))
+                .orderByDesc(YwElectricalData::getCreateDate).last("limit 1");
+        return ywElectricalDataMapper.selectOne(q);
+    }
+
+    @Override
+    public void syncMeterDataScheduled() {
+        try {
+            syncMeterDataInternal();
+        } catch (Exception e) {
+            log.warn("syncMeterDataScheduled failed", e);
+        }
+    }
+
+    @Override
+    public String syncMeterDataFromPlatform() {
+        MeterDataSyncStats stats = syncMeterDataInternal();
+        return "鎶勮〃鍚屾瀹屾垚锛氭柊澧炪��" + stats.addCount + "銆戞潯锛岃烦杩囬噸澶嶃��" + stats.skipCount + "銆戞潯";
+    }
+
+    private static class MeterDataSyncStats {
+        private int addCount;
+        private int skipCount;
+    }
+
+    private MeterDataSyncStats syncMeterDataInternal() {
+        MeterDataSyncStats stats = new MeterDataSyncStats();
+        String startTime = resolveSyncStartTime();
+        QueryDataRequest param = buildQueryDataRequest(startTime, DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
+        log.info("sync meter data, start_time={}, end_time={}", startTime, param.getEnd_time());
+        List<QueryDataInfoResponse> list = fetchQueryDataList(param);
+        if (CollectionUtils.isEmpty(list)) {
+            return stats;
+        }
+        for (QueryDataInfoResponse item : list) {
+            if (item == null || StringUtils.isBlank(item.getAddress())) {
+                continue;
+            }
+            YwElectrical meter = ywElectricalMapper.selectOne(new QueryWrapper<YwElectrical>().lambda()
+                    .eq(YwElectrical::getIsdeleted, Constants.ZERO)
+                    .eq(YwElectrical::getAddress, item.getAddress())
+                    .last("limit 1"));
+            if (saveDataRow(item, meter != null ? meter.getId() : null)) {
+                stats.addCount++;
+                if (meter != null) {
+                    refreshBalanceFromData(meter);
+                }
+            } else {
+                stats.skipCount++;
+            }
+        }
+        return stats;
+    }
+
+    /** 鍗曡〃鎶勮〃鍚庝粠绗笁鏂规媺鍙栨渶鏂版暟鎹叆搴擄紝杩斿洖鏄惁鏈夋柊璁板綍 */
+    private boolean syncMeterDataForElectrical(YwElectrical e) {
+        if (e == null || StringUtils.isBlank(e.getAddress())) {
+            return false;
+        }
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.HOUR_OF_DAY, -24);
+        QueryDataRequest param = buildQueryDataRequest(
+                DateUtil.formatDate(cal.getTime(), "yyyy-MM-dd HH:mm:ss"),
+                DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
+        List<QueryDataInfoResponse> list;
+        try {
+            list = fetchQueryDataList(param);
+        } catch (BusinessException ex) {
+            log.warn("sync meter data for electricalId={} failed: {}", e.getId(), ex.getMessage());
+            return false;
+        }
+        if (CollectionUtils.isEmpty(list)) {
+            return false;
+        }
+        String address = e.getAddress().trim();
+        boolean updated = false;
+        for (QueryDataInfoResponse item : list) {
+            if (item == null || StringUtils.isBlank(item.getAddress())) {
+                continue;
+            }
+            if (!address.equalsIgnoreCase(item.getAddress().trim())) {
+                continue;
+            }
+            if (saveDataRow(item, e.getId())) {
+                updated = true;
+            }
+        }
+        return updated;
+    }
+
+    private QueryDataRequest buildQueryDataRequest(String startTime, String endTime) {
+        QueryDataRequest param = new QueryDataRequest();
+        param.setType("json");
+        param.setFunctionids(QUERY_DATA_FUNCTION_METER_STATUS);
+        param.setStart_time(startTime);
+        param.setEnd_time(endTime);
+        param.setOffset(0);
+        param.setLimit(500);
+        return param;
+    }
+
+    private List<QueryDataInfoResponse> fetchQueryDataList(QueryDataRequest param) {
+        ElectronicDataResponse response = ElectronicToolUtil.queryDataRequest(param);
+        if (!ElectronicToolUtil.isDataApiSuccess(response)) {
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(),
+                    ElectronicToolUtil.dataApiErrorMessage(response, "鎶勮〃鏁版嵁鍚屾澶辫触"));
+        }
+        return parseQueryDataList(response);
+    }
+
+    private List<QueryDataInfoResponse> parseQueryDataList(ElectronicDataResponse response) {
+        if (response == null || response.getData() == null) {
+            return Collections.emptyList();
+        }
+        String json = JSON.toJSONString(response.getData());
+        if (StringUtils.isBlank(json) || "null".equals(json)) {
+            return Collections.emptyList();
+        }
+        try {
+            String trimmed = json.trim();
+            if (trimmed.startsWith("[")) {
+                return JSON.parseArray(trimmed, QueryDataInfoResponse.class);
+            }
+            if (trimmed.startsWith("{")) {
+                QueryDataInfoResponse one = JSON.parseObject(trimmed, QueryDataInfoResponse.class);
+                return one != null ? Collections.singletonList(one) : Collections.emptyList();
+            }
+        } catch (Exception e) {
+            log.warn("parse queryDataRequest failed", e);
+            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "鎶勮〃鏁版嵁瑙f瀽澶辫触");
+        }
+        return Collections.emptyList();
+    }
+
+    /** 鍙栨湰鍦板凡鍚屾鐨勬渶澶ф妱琛ㄦ椂闂翠綔涓轰笅娆� start_time锛涙棤璁板綍鏃堕粯璁よ繎24灏忔椂 */
+    private String resolveSyncStartTime() {
+        YwElectricalData latest = ywElectricalDataMapper.selectOne(new QueryWrapper<YwElectricalData>().lambda()
+                .eq(YwElectricalData::getIsdeleted, Constants.ZERO)
+                .isNotNull(YwElectricalData::getAddTime)
+                .ne(YwElectricalData::getAddTime, "")
+                .orderByDesc(YwElectricalData::getAddTime)
+                .last("limit 1"));
+        if (latest != null && StringUtils.isNotBlank(latest.getAddTime())) {
+            String addTime = latest.getAddTime().trim();
+            if (addTime.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
+                return addTime;
+            }
+            log.warn("invalid addTime in yw_electrical_data, fallback to 24h: {}", addTime);
+        }
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.HOUR_OF_DAY, -24);
+        return DateUtil.formatDate(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+    }
+
+    private boolean existsByDataId(String dataId) {
+        if (StringUtils.isBlank(dataId)) {
+            return false;
+        }
+        return ywElectricalDataMapper.selectCount(new QueryWrapper<YwElectricalData>().lambda()
+                .eq(YwElectricalData::getIsdeleted, Constants.ZERO)
+                .eq(YwElectricalData::getDataId, dataId.trim())) > 0;
+    }
+
+    /** 淇濆瓨鎶勮〃璁板綍锛沝ataId 宸插瓨鍦ㄥ垯璺宠繃锛岃繑鍥� false */
+    private boolean saveDataRow(QueryDataInfoResponse item, Integer electricalId) {
+        String dataId = item.getId();
+        if (StringUtils.isNotBlank(dataId) && existsByDataId(dataId)) {
+            return false;
+        }
+        YwElectricalData row = new YwElectricalData();
+        row.setCreateDate(new Date());
+        row.setEditDate(new Date());
+        row.setIsdeleted(Constants.ZERO);
+        row.setDeviceId(electricalId != null ? String.valueOf(electricalId) : null);
+        applyItemToRow(row, item);
+        ywElectricalDataMapper.insert(row);
+        return true;
+    }
+
+    /** 绗笁鏂� item / data_v2 瀛楁鍚嶄笌鏈湴瀹炰綋宸紓鏄犲皠 */
+    private static final Map<String, String> SYNC_FIELD_ALIASES;
+    private static final Set<String> ITEM_SKIP_KEYS;
+
+    static {
+        Map<String, String> aliases = new HashMap<>();
+        aliases.put("id", "dataId");
+        aliases.put("add_time", "addTime");
+        aliases.put("count", "countnum");
+        SYNC_FIELD_ALIASES = Collections.unmodifiableMap(aliases);
+        ITEM_SKIP_KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("data_v2", "idnum")));
+    }
+
+    /** 瑙f瀽 data 鏁扮粍涓殑 item 灞炴�ф槧灏勫埌 yw_electrical_data */
+    private void applyItemToRow(YwElectricalData row, QueryDataInfoResponse item) {
+        JSONObject itemJson = (JSONObject) JSON.toJSON(item);
+        for (String key : itemJson.keySet()) {
+            if (ITEM_SKIP_KEYS.contains(key)) {
+                continue;
+            }
+            Object val = itemJson.get(key);
+            if (val == null) {
+                continue;
+            }
+            String fieldName = SYNC_FIELD_ALIASES.getOrDefault(key, key);
+            setElectricalDataField(row, fieldName, val);
+        }
+        JSONObject dataV2 = extractDataV2Json(item, itemJson);
+        if (dataV2 != null && !dataV2.isEmpty()) {
+            applyDataV2Json(row, dataV2);
+        }
+        applyDspFallback(row, item.getDsp());
+    }
+
+    private JSONObject extractDataV2Json(QueryDataInfoResponse item, JSONObject itemJson) {
+        if (!CollectionUtils.isEmpty(item.getData_v2())) {
+            return mergeDataV2Json(item.getData_v2());
+        }
+        Object dataV2 = itemJson.get("data_v2");
+        if (dataV2 == null) {
+            return null;
+        }
+        if (dataV2 instanceof JSONObject) {
+            return (JSONObject) dataV2;
+        }
+        if (dataV2 instanceof JSONArray) {
+            JSONArray arr = (JSONArray) dataV2;
+            JSONObject merged = new JSONObject();
+            for (int i = 0; i < arr.size(); i++) {
+                Object element = arr.get(i);
+                if (element instanceof JSONObject) {
+                    mergeJsonObject(merged, (JSONObject) element);
+                } else if (element != null) {
+                    QueryDataV2Response part = arr.getObject(i, QueryDataV2Response.class);
+                    if (part != null) {
+                        mergeJsonObject(merged, (JSONObject) JSON.toJSON(part));
+                    }
+                }
+            }
+            return merged;
+        }
+        return null;
+    }
+
+    private void mergeJsonObject(JSONObject target, JSONObject source) {
+        for (String key : source.keySet()) {
+            Object val = source.get(key);
+            if (val != null) {
+                target.put(key, val);
+            }
+        }
+    }
+
+    private void applyDspFallback(YwElectricalData row, String dsp) {
+        if (StringUtils.isNotBlank(row.getZhygzdl()) || StringUtils.isBlank(dsp)) {
+            return;
+        }
+        if (dsp.contains("kWh") || dsp.contains("kwh")) {
+            String num = dsp.replaceAll("[^0-9.]", " ").trim().split("\\s+")[0];
+            if (StringUtils.isNotBlank(num)) {
+                row.setZhygzdl(num);
+            }
+        }
+    }
+
+    /** 鍚堝苟 data_v2 鍒楄〃涓悇鏉¤褰曠殑灞炴�э紙鍚庡�艰鐩栧墠鍊硷級 */
+    private JSONObject mergeDataV2Json(List<QueryDataV2Response> dataV2List) {
+        JSONObject merged = new JSONObject();
+        for (QueryDataV2Response part : dataV2List) {
+            if (part == null) {
+                continue;
+            }
+            JSONObject obj = (JSONObject) JSON.toJSON(part);
+            for (String key : obj.keySet()) {
+                Object val = obj.get(key);
+                if (val != null) {
+                    merged.put(key, val);
+                }
+            }
+        }
+        return merged;
+    }
+
+    /** 鎸夊睘鎬у悕灏� data_v2 瑙f瀽鍐欏叆 yw_electrical_data 鍚屽悕瀛楁锛堣鐩� item 灞傚悓鍚嶅瓧娈碉級 */
+    private void applyDataV2Json(YwElectricalData row, JSONObject dataV2) {
+        if (dataV2 == null || dataV2.isEmpty()) {
+            return;
+        }
+        for (String key : dataV2.keySet()) {
+            Object val = dataV2.get(key);
+            if (val == null) {
+                continue;
+            }
+            String fieldName = SYNC_FIELD_ALIASES.getOrDefault(key, key);
+            setElectricalDataField(row, fieldName, val);
+        }
+    }
+
+    private void setElectricalDataField(YwElectricalData row, String fieldName, Object val) {
+        if (val == null || StringUtils.isBlank(fieldName)) {
+            return;
+        }
+        try {
+            Field field = YwElectricalData.class.getDeclaredField(fieldName);
+            field.setAccessible(true);
+            Class<?> type = field.getType();
+            if (String.class.equals(type)) {
+                String strVal = convertFieldString(val);
+                if (StringUtils.isNotBlank(strVal)) {
+                    field.set(row, strVal);
+                }
+            } else if (Integer.class.equals(type) || int.class.equals(type)) {
+                if (val instanceof Number) {
+                    field.set(row, ((Number) val).intValue());
+                } else {
+                    String text = String.valueOf(val).trim();
+                    if (StringUtils.isNotBlank(text)) {
+                        field.set(row, Integer.parseInt(text));
+                    }
+                }
+            } else if (BigDecimal.class.equals(type)) {
+                String text = String.valueOf(val).trim();
+                if (StringUtils.isNotBlank(text)) {
+                    field.set(row, new BigDecimal(text));
+                }
+            }
+        } catch (NoSuchFieldException | IllegalAccessException | NumberFormatException ignored) {
+            // 蹇界暐绗笁鏂规墿灞曞瓧娈垫垨鏃犳硶杞崲鐨勫��
+        }
+    }
+
+    private String convertFieldString(Object val) {
+        if (val instanceof Boolean) {
+            return String.valueOf(val);
+        }
+        if (val instanceof JSONArray || val instanceof JSONObject || val instanceof Collection) {
+            return JSON.toJSONString(val);
+        }
+        return String.valueOf(val).trim();
+    }
+
+    @Override
+    public void cleanLogBeforeThreeMonths() {
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.MONTH, -3);
+        ywElectricalLogMapper.delete(new QueryWrapper<YwElectricalLog>().lambda()
+                .lt(YwElectricalLog::getCreateDate, cal.getTime()));
+    }
+
+    @Override
+    public void enrichList(List<YwElectrical> list) {
+        fillRoomNames(list);
+        if (CollectionUtils.isEmpty(list)) return;
+        for (YwElectrical row : list) {
+            if (StringUtils.isNotBlank(row.getWarnType())) {
+                row.setWarnTypeName(resolveWarnTypeNames(row.getWarnType()));
+            }
+        }
+    }
+
+    private String resolveWarnTypeNames(String warnType) {
+        String[] codes = warnType.split(",");
+        List<String> names = new ArrayList<>();
+        for (String codeStr : codes) {
+            if (StringUtils.isBlank(codeStr)) {
+                continue;
+            }
+            String trimmed = codeStr.trim();
+            try {
+                int code = Integer.parseInt(trimmed);
+                ElectronicConstant.warningDefId def = ElectronicConstant.warningDefId.getByKey(code);
+                names.add(def != null ? def.getName() : trimmed);
+            } catch (NumberFormatException ex) {
+                names.add(trimmed);
+            }
+        }
+        return names.isEmpty() ? warnType : String.join(",", names);
+    }
+
+    @Override
+    public void enrichDataList(List<YwElectricalData> list) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        List<YwElectrical> temp = new ArrayList<>();
+        for (YwElectricalData row : list) {
+            if (StringUtils.isBlank(row.getElectricalName()) && StringUtils.isNotBlank(row.getName())) {
+                row.setElectricalName(row.getName());
+            }
+            if (row.getElectricalId() == null) {
+                continue;
+            }
+            YwElectrical e = new YwElectrical();
+            e.setId(row.getElectricalId());
+            temp.add(e);
+        }
+        fillRoomNames(temp);
+        Map<Integer, String> roomMap = temp.stream()
+                .filter(e -> StringUtils.isNotBlank(e.getRoomNames()))
+                .collect(Collectors.toMap(YwElectrical::getId, YwElectrical::getRoomNames, (a, b) -> a));
+        for (YwElectricalData row : list) {
+            if (row.getElectricalId() != null) {
+                row.setRoomNames(roomMap.get(row.getElectricalId()));
+            }
+        }
+    }
+
+    private void fillRoomNames(List<? extends YwElectrical> list) {
+        if (CollectionUtils.isEmpty(list)) return;
+        List<Integer> ids = list.stream().map(YwElectrical::getId).filter(Objects::nonNull).collect(Collectors.toList());
+        if (ids.isEmpty()) return;
+        MPJLambdaWrapper<YwElectricalRoom> w = new MPJLambdaWrapper<>();
+        w.selectAll(YwElectricalRoom.class)
+                .selectAs(YwRoom::getRoomNum, YwElectricalRoom::getRoomName)
+                .selectAs(YwBuilding::getName, YwElectricalRoom::getBuildingName)
+                .selectAs(YwFloor::getName, YwElectricalRoom::getFloorName)
+                .leftJoin(YwRoom.class, YwRoom::getId, YwElectricalRoom::getRoomId)
+                .leftJoin(YwFloor.class, YwFloor::getId, YwRoom::getFloor)
+                .leftJoin(YwBuilding.class, YwBuilding::getId, YwRoom::getBuildingId)
+                .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO)
+                .eq(YwElectricalRoom::getType, Constants.ZERO)
+                .in(YwElectricalRoom::getObjId, ids);
+        List<YwElectricalRoom> rooms = ywElectricalRoomMapper.selectJoinList(YwElectricalRoom.class, w);
+        Map<Integer, List<YwElectricalRoom>> grouped = rooms.stream().collect(Collectors.groupingBy(YwElectricalRoom::getObjId));
+        for (YwElectrical row : list) {
+            List<YwElectricalRoom> rs = grouped.get(row.getId());
+            if (CollectionUtils.isEmpty(rs)) continue;
+            row.setRoomNames(rs.stream().map(this::formatRoomPath).filter(StringUtils::isNotBlank)
+                    .collect(Collectors.joining("銆�")));
+        }
+    }
+
+    private String formatRoomPath(YwElectricalRoom r) {
+        List<String> parts = new ArrayList<>();
+        if (StringUtils.isNotBlank(r.getBuildingName())) parts.add(r.getBuildingName());
+        if (StringUtils.isNotBlank(r.getFloorName())) parts.add(r.getFloorName());
+        if (StringUtils.isNotBlank(r.getRoomName())) parts.add(r.getRoomName());
+        return String.join("/", parts);
+    }
+
+    private YwElectrical requireElectrical(Integer id) {
+        YwElectrical e = ywElectricalMapper.selectById(id);
+        if (e == null || Objects.equals(e.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY);
+        }
+        return e;
+    }
+
+    private String newOprId() {
+        return UUID.randomUUID().toString().replace("-", "");
+    }
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java
index 69d376e..b05ad19 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java
@@ -1,32 +1,38 @@
 package com.doumee.service.business.impl;
 
-import com.doumee.core.model.PageData;
-import com.doumee.core.model.PageWrap;
-import com.doumee.dao.business.model.YwElectricalData;
-import com.doumee.core.utils.Utils;
-import com.doumee.dao.business.YwElectricalDataMapper;
-import com.doumee.service.business.YwElectricalDataService;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.doumee.core.model.LoginUserInfo;
+import com.doumee.core.model.PageData;
+import com.doumee.core.model.PageWrap;
+import com.doumee.core.utils.Constants;
+import com.doumee.core.utils.Utils;
+import com.doumee.dao.business.YwElectricalDataMapper;
+import com.doumee.dao.business.model.YwElectrical;
+import com.doumee.dao.business.model.YwElectricalData;
+import com.doumee.dao.business.model.YwElectricalRoom;
+import com.doumee.service.business.YwElectricalBizService;
+import com.doumee.service.business.YwElectricalDataService;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
-import com.github.yulichang.wrapper.MPJLambdaWrapper;
-import com.doumee.core.model.LoginUserInfo;
+
 import java.util.List;
 
 /**
  * 鐢佃〃鎶勮〃鏁版嵁Service瀹炵幇
- * @author doumee
- * @date 2026-05-20 14:59:07
  */
 @Service
 public class YwElectricalDataServiceImpl implements YwElectricalDataService {
 
     @Autowired
     private YwElectricalDataMapper ywElectricalDataMapper;
+    @Autowired
+    private YwElectricalBizService ywElectricalBizService;
 
     @Override
     public Integer create(YwElectricalData ywElectricalData) {
@@ -38,34 +44,37 @@
     public void deleteById(Integer id) {
         ywElectricalDataMapper.deleteById(id);
     }
-  @Override
-    public void deleteByIdInBatch(List<Integer> ids ) {
+
+    @Override
+    public void deleteByIdInBatch(List<Integer> ids) {
         if (CollectionUtils.isEmpty(ids)) {
             return;
         }
-        for(Integer id :ids){
+        for (Integer id : ids) {
             deleteById(id);
         }
     }
+
     @Override
     public void deleteById(Integer id, LoginUserInfo user) {
         ywElectricalDataMapper.deleteById(id);
     }
-  @Override
+
+    @Override
     public void deleteByIdInBatch(List<Integer> ids, LoginUserInfo user) {
         if (CollectionUtils.isEmpty(ids)) {
             return;
         }
-       for(Integer id :ids){
-            deleteById(id,user);
+        for (Integer id : ids) {
+            deleteById(id, user);
         }
     }
+
     @Override
     public void delete(YwElectricalData ywElectricalData) {
         UpdateWrapper<YwElectricalData> deleteWrapper = new UpdateWrapper<>(ywElectricalData);
         ywElectricalDataMapper.delete(deleteWrapper);
     }
-
 
     @Override
     public void updateById(YwElectricalData ywElectricalData) {
@@ -77,7 +86,7 @@
         if (CollectionUtils.isEmpty(ywElectricalDatas)) {
             return;
         }
-        for (YwElectricalData ywElectricalData: ywElectricalDatas) {
+        for (YwElectricalData ywElectricalData : ywElectricalDatas) {
             this.updateById(ywElectricalData);
         }
     }
@@ -98,79 +107,58 @@
         QueryWrapper<YwElectricalData> wrapper = new QueryWrapper<>(ywElectricalData);
         return ywElectricalDataMapper.selectList(wrapper);
     }
-  
+
     @Override
     public PageData<YwElectricalData> findPage(PageWrap<YwElectricalData> pageWrap) {
         IPage<YwElectricalData> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
+        YwElectricalData model = pageWrap.getModel() == null ? new YwElectricalData() : pageWrap.getModel();
+        Utils.MP.blankToNull(model);
+
         MPJLambdaWrapper<YwElectricalData> queryWrapper = new MPJLambdaWrapper<>();
-        Utils.MP.blankToNull(pageWrap.getModel());
-        queryWrapper.eq(pageWrap.getModel().getId() != null,YwElectricalData::getId, pageWrap.getModel().getId());
-        queryWrapper.eq(pageWrap.getModel().getCreator() != null,YwElectricalData::getCreator, pageWrap.getModel().getCreator());
-        if (pageWrap.getModel().getCreateDate() != null) {
-            queryWrapper.ge(YwElectricalData::getCreateDate, Utils.Date.getStart(pageWrap.getModel().getCreateDate()));
-            queryWrapper.le(YwElectricalData::getCreateDate, Utils.Date.getEnd(pageWrap.getModel().getCreateDate()));
+        queryWrapper.selectAll(YwElectricalData.class)
+                .selectAs(YwElectrical::getName, YwElectricalData::getElectricalName)
+                .selectAs(YwElectrical::getId, YwElectricalData::getElectricalId)
+                .leftJoin(YwElectrical.class, on -> on
+                        .eq(YwElectrical::getAddress, YwElectricalData::getAddress)
+                        .eq(YwElectrical::getIsdeleted, Constants.ZERO))
+                .eq(YwElectricalData::getIsdeleted, Constants.ZERO);
+
+        if (StringUtils.isNotBlank(model.getMeterKeyword())) {
+            String kw = model.getMeterKeyword().trim();
+            queryWrapper.and(w -> w.like(YwElectricalData::getAddress, kw)
+                    .or().like(YwElectricalData::getName, kw)
+                    .or().like(YwElectrical::getName, kw));
         }
-        queryWrapper.eq(pageWrap.getModel().getEditor() != null,YwElectricalData::getEditor, pageWrap.getModel().getEditor());
-        if (pageWrap.getModel().getEditDate() != null) {
-            queryWrapper.ge(YwElectricalData::getEditDate, Utils.Date.getStart(pageWrap.getModel().getEditDate()));
-            queryWrapper.le(YwElectricalData::getEditDate, Utils.Date.getEnd(pageWrap.getModel().getEditDate()));
+        if (model.getRoomId() != null) {
+            queryWrapper.innerJoin(YwElectricalRoom.class, on -> on
+                    .eq(YwElectricalRoom::getObjId, YwElectrical::getId)
+                    .eq(YwElectricalRoom::getType, Constants.ZERO)
+                    .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO))
+                    .eq(YwElectricalRoom::getRoomId, model.getRoomId());
         }
-        queryWrapper.eq(pageWrap.getModel().getIsdeleted() != null,YwElectricalData::getIsdeleted, pageWrap.getModel().getIsdeleted());
-        queryWrapper.eq(pageWrap.getModel().getRemark() != null,YwElectricalData::getRemark, pageWrap.getModel().getRemark());
-        queryWrapper.eq(pageWrap.getModel().getDeviceId() != null,YwElectricalData::getDeviceId, pageWrap.getModel().getDeviceId());
-        queryWrapper.eq(pageWrap.getModel().getAddTime() != null,YwElectricalData::getAddTime, pageWrap.getModel().getAddTime());
-        queryWrapper.eq(pageWrap.getModel().getJsfs() != null,YwElectricalData::getJsfs, pageWrap.getModel().getJsfs());
-        queryWrapper.eq(pageWrap.getModel().getFls() != null,YwElectricalData::getFls, pageWrap.getModel().getFls());
-        queryWrapper.eq(pageWrap.getModel().getXs() != null,YwElectricalData::getXs, pageWrap.getModel().getXs());
-        queryWrapper.eq(pageWrap.getModel().getZyje() != null,YwElectricalData::getZyje, pageWrap.getModel().getZyje());
-        queryWrapper.eq(pageWrap.getModel().getYe() != null,YwElectricalData::getYe, pageWrap.getModel().getYe());
-        queryWrapper.eq(pageWrap.getModel().getCountnum() != null,YwElectricalData::getCountnum, pageWrap.getModel().getCountnum());
-        queryWrapper.eq(pageWrap.getModel().getDqdj() != null,YwElectricalData::getDqdj, pageWrap.getModel().getDqdj());
-        queryWrapper.eq(pageWrap.getModel().getZhygzdl() != null,YwElectricalData::getZhygzdl, pageWrap.getModel().getZhygzdl());
-        queryWrapper.eq(pageWrap.getModel().getZqyl() != null,YwElectricalData::getZqyl, pageWrap.getModel().getZqyl());
-        queryWrapper.eq(pageWrap.getModel().getAxdl() != null,YwElectricalData::getAxdl, pageWrap.getModel().getAxdl());
-        queryWrapper.eq(pageWrap.getModel().getBxdl() != null,YwElectricalData::getBxdl, pageWrap.getModel().getBxdl());
-        queryWrapper.eq(pageWrap.getModel().getCxdl() != null,YwElectricalData::getCxdl, pageWrap.getModel().getCxdl());
-        queryWrapper.eq(pageWrap.getModel().getAxdy() != null,YwElectricalData::getAxdy, pageWrap.getModel().getAxdy());
-        queryWrapper.eq(pageWrap.getModel().getBxdy() != null,YwElectricalData::getBxdy, pageWrap.getModel().getBxdy());
-        queryWrapper.eq(pageWrap.getModel().getCxdy() != null,YwElectricalData::getCxdy, pageWrap.getModel().getCxdy());
-        queryWrapper.eq(pageWrap.getModel().getZyggl() != null,YwElectricalData::getZyggl, pageWrap.getModel().getZyggl());
-        queryWrapper.eq(pageWrap.getModel().getAxyggl() != null,YwElectricalData::getAxyggl, pageWrap.getModel().getAxyggl());
-        queryWrapper.eq(pageWrap.getModel().getBxyggl() != null,YwElectricalData::getBxyggl, pageWrap.getModel().getBxyggl());
-        queryWrapper.eq(pageWrap.getModel().getCxyggl() != null,YwElectricalData::getCxyggl, pageWrap.getModel().getCxyggl());
-        queryWrapper.eq(pageWrap.getModel().getZwggl() != null,YwElectricalData::getZwggl, pageWrap.getModel().getZwggl());
-        queryWrapper.eq(pageWrap.getModel().getAxwggl() != null,YwElectricalData::getAxwggl, pageWrap.getModel().getAxwggl());
-        queryWrapper.eq(pageWrap.getModel().getBxwggl() != null,YwElectricalData::getBxwggl, pageWrap.getModel().getBxwggl());
-        queryWrapper.eq(pageWrap.getModel().getCxwggl() != null,YwElectricalData::getCxwggl, pageWrap.getModel().getCxwggl());
-        queryWrapper.eq(pageWrap.getModel().getZszgl() != null,YwElectricalData::getZszgl, pageWrap.getModel().getZszgl());
-        queryWrapper.eq(pageWrap.getModel().getAxszgl() != null,YwElectricalData::getAxszgl, pageWrap.getModel().getAxszgl());
-        queryWrapper.eq(pageWrap.getModel().getBxszgl() != null,YwElectricalData::getBxszgl, pageWrap.getModel().getBxszgl());
-        queryWrapper.eq(pageWrap.getModel().getCxszgl() != null,YwElectricalData::getCxszgl, pageWrap.getModel().getCxszgl());
-        queryWrapper.eq(pageWrap.getModel().getZglys() != null,YwElectricalData::getZglys, pageWrap.getModel().getZglys());
-        queryWrapper.eq(pageWrap.getModel().getAxglys() != null,YwElectricalData::getAxglys, pageWrap.getModel().getAxglys());
-        queryWrapper.eq(pageWrap.getModel().getBxglys() != null,YwElectricalData::getBxglys, pageWrap.getModel().getBxglys());
-        queryWrapper.eq(pageWrap.getModel().getPl() != null,YwElectricalData::getPl, pageWrap.getModel().getPl());
-        queryWrapper.eq(pageWrap.getModel().getWd() != null,YwElectricalData::getWd, pageWrap.getModel().getWd());
-        queryWrapper.eq(pageWrap.getModel().getDdyy() != null,YwElectricalData::getDdyy, pageWrap.getModel().getDdyy());
-        queryWrapper.eq(pageWrap.getModel().getDbzt() != null,YwElectricalData::getDbzt, pageWrap.getModel().getDbzt());
-        queryWrapper.eq(pageWrap.getModel().getDataId() != null,YwElectricalData::getDataId, pageWrap.getModel().getDataId());
-        queryWrapper.eq(pageWrap.getModel().getConsume() != null,YwElectricalData::getConsume, pageWrap.getModel().getConsume());
-        queryWrapper.eq(pageWrap.getModel().getCid() != null,YwElectricalData::getCid, pageWrap.getModel().getCid());
-        queryWrapper.eq(pageWrap.getModel().getMid() != null,YwElectricalData::getMid, pageWrap.getModel().getMid());
-        queryWrapper.eq(pageWrap.getModel().getAddress() != null,YwElectricalData::getAddress, pageWrap.getModel().getAddress());
-        queryWrapper.eq(pageWrap.getModel().getUid() != null,YwElectricalData::getUid, pageWrap.getModel().getUid());
-        queryWrapper.eq(pageWrap.getModel().getName() != null,YwElectricalData::getName, pageWrap.getModel().getName());
-        queryWrapper.eq(pageWrap.getModel().getFid() != null,YwElectricalData::getFid, pageWrap.getModel().getFid());
-        queryWrapper.eq(pageWrap.getModel().getData() != null,YwElectricalData::getData, pageWrap.getModel().getData());
-        queryWrapper.eq(pageWrap.getModel().getDsp() != null,YwElectricalData::getDsp, pageWrap.getModel().getDsp());
-        queryWrapper.eq(pageWrap.getModel().getRadio() != null,YwElectricalData::getRadio, pageWrap.getModel().getRadio());
-         queryWrapper.orderByDesc(YwElectricalData::getId);
-         IPage<YwElectricalData> result = ywElectricalDataMapper.selectJoinPage(page, YwElectricalData.class,queryWrapper);
-         return PageData.from(result);
+        if (model.getReadTimeBegin() != null) {
+            queryWrapper.ge(YwElectricalData::getCreateDate, Utils.Date.getStart(model.getReadTimeBegin()));
+        }
+        if (model.getReadTimeEnd() != null) {
+            queryWrapper.le(YwElectricalData::getCreateDate, Utils.Date.getEnd(model.getReadTimeEnd()));
+        }
+
+        queryWrapper.orderByDesc(YwElectricalData::getCreateDate)
+                .orderByDesc(YwElectricalData::getId);
+        IPage<YwElectricalData> result = ywElectricalDataMapper.selectJoinPage(page, YwElectricalData.class, queryWrapper);
+        PageData<YwElectricalData> pageData = PageData.from(result);
+        ywElectricalBizService.enrichDataList(pageData.getRecords());
+        return pageData;
     }
+
     @Override
     public long count(YwElectricalData ywElectricalData) {
         QueryWrapper<YwElectricalData> wrapper = new QueryWrapper<>(ywElectricalData);
         return ywElectricalDataMapper.selectCount(wrapper);
     }
+
+    @Override
+    public String syncFromPlatform() {
+        return ywElectricalBizService.syncMeterDataFromPlatform();
+    }
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalServiceImpl.java
index 9bd17e6..911f9ce 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalServiceImpl.java
@@ -20,6 +20,7 @@
 import com.doumee.dao.business.model.DeviceData;
 import com.doumee.dao.business.model.YwDevice;
 import com.doumee.dao.business.model.YwElectrical;
+import com.doumee.dao.business.model.YwElectricalParam;
 import com.doumee.core.utils.Utils;
 import com.doumee.dao.business.YwElectricalMapper;
 import com.doumee.service.business.YwElectricalBizService;
@@ -130,7 +131,12 @@
         IPage<YwElectrical> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
         MPJLambdaWrapper<YwElectrical> queryWrapper = new MPJLambdaWrapper<>();
         Utils.MP.blankToNull(pageWrap.getModel());
-        queryWrapper.eq(YwElectrical::getIsdeleted, Constants.ZERO);
+        queryWrapper.selectAll(YwElectrical.class)
+                .selectAs(YwElectricalParam::getName, YwElectrical::getParamName)
+                .leftJoin(YwElectricalParam.class, on -> on
+                        .eq(YwElectricalParam::getId, YwElectrical::getElectricalParamId)
+                        .eq(YwElectricalParam::getIsdeleted, Constants.ZERO))
+                .eq(YwElectrical::getIsdeleted, Constants.ZERO);
         if (StringUtils.isNotBlank(pageWrap.getModel().getMeterKeyword())) {
             String kw = pageWrap.getModel().getMeterKeyword().trim();
             queryWrapper.and(w -> w.like(YwElectrical::getName, kw).or().like(YwElectrical::getAddress, kw));
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 d499d3b..8a06b32 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
@@ -35,9 +35,11 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -98,6 +100,7 @@
             }
             Map<String, YwElectrical> addressMap = buildElectricalAddressMap();
             Map<String, YwElectrical> didMap = buildElectricalDidMap();
+            Set<Integer> affectedElectricalIds = new LinkedHashSet<>();
             Date now = new Date();
             int addCount = 0;
             int updateCount = 0;
@@ -129,6 +132,9 @@
                 entity.setMsg(item.getMsg());
                 entity.setEditDate(now);
                 resolveElectricalId(entity, addressMap, didMap);
+                if (entity.getElectricalId() != null) {
+                    affectedElectricalIds.add(entity.getElectricalId());
+                }
                 if (isNew) {
                     ywElectricalWarningMapper.insert(entity);
                     addCount++;
@@ -137,7 +143,8 @@
                     updateCount++;
                 }
             }
-            return "鍚屾瀹屾垚锛氭柊澧炪��" + addCount + "銆戞潯锛屾洿鏂般��" + updateCount + "銆戞潯";
+            int electricalUpdateCount = updateElectricalWarnTypes(affectedElectricalIds, now);
+            return "鍚屾瀹屾垚锛氭柊澧炪��" + addCount + "銆戞潯锛屾洿鏂般��" + updateCount + "銆戞潯锛屽洖鍐欑數琛ㄩ璀︺��" + electricalUpdateCount + "銆戝彴";
         } finally {
             Constants.DEALING_ELECTRICAL_WARNING_SYNC = false;
         }
@@ -285,6 +292,37 @@
         }
     }
 
+    /** 鎸夌數琛ㄦ眹鎬诲叏閮ㄦ姤璀︾被鍨嬶紝閫楀彿鍒嗛殧鍥炲啓 warn_type */
+    private int updateElectricalWarnTypes(Set<Integer> affectedElectricalIds, Date editDate) {
+        if (CollectionUtils.isEmpty(affectedElectricalIds)) {
+            return 0;
+        }
+        for (Integer electricalId : affectedElectricalIds) {
+            refreshElectricalWarnType(electricalId, editDate);
+        }
+        return affectedElectricalIds.size();
+    }
+
+    private void refreshElectricalWarnType(Integer electricalId, Date editDate) {
+        List<YwElectricalWarning> warnings = ywElectricalWarningMapper.selectList(new QueryWrapper<YwElectricalWarning>().lambda()
+                .eq(YwElectricalWarning::getElectricalId, electricalId)
+                .eq(YwElectricalWarning::getIsdeleted, Constants.ZERO));
+        LinkedHashSet<Integer> defIds = new LinkedHashSet<>();
+        if (!CollectionUtils.isEmpty(warnings)) {
+            for (YwElectricalWarning warning : warnings) {
+                if (warning.getWarningDefId() != null) {
+                    defIds.add(warning.getWarningDefId());
+                }
+            }
+        }
+        String warnType = defIds.stream().map(String::valueOf).collect(Collectors.joining(","));
+        YwElectrical upd = new YwElectrical();
+        upd.setId(electricalId);
+        upd.setWarnType(StringUtils.isBlank(warnType) ? null : warnType);
+        upd.setEditDate(editDate);
+        ywElectricalMapper.updateById(upd);
+    }
+
     private YwElectricalWarning findExisting(String deviceAddress, Integer warningDefId, Date startTime) {
         return ywElectricalWarningMapper.selectOne(new QueryWrapper<YwElectricalWarning>().lambda()
                 .eq(YwElectricalWarning::getDeviceAddress, deviceAddress)

--
Gitblit v1.9.3