From 93de43267e1663031fe5dc2f5ae40d128a182a76 Mon Sep 17 00:00:00 2001
From: doum <doum>
Date: 星期四, 18 六月 2026 17:24:51 +0800
Subject: [PATCH] 新增智能电表、空调管理

---
 h5/pages/customer/contract/detail.vue                                                                                 |    4 
 server/db/business.yw_customer_member_openid.sql                                                                      |   22 
 h5/main.js                                                                                                            |    2 
 h5/components/customer/cu-workbench-fab.vue                                                                           |   18 
 h5/pages/customer/login.vue                                                                                           |    9 
 server/db/business.yw_conditioner_device.menu.sql                                                                     |   20 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5AuthServiceImpl.java         |  252 +++
 h5/pages/index.vue                                                                                                    |  255 +-
 h5/pages/login.vue                                                                                                    |  437 +----
 h5/pages/customer/electricity/list.vue                                                                                |  194 +
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwWxPayOrder.java                           |    2 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5BizServiceImpl.java          |   33 
 h5/styles/customer.scss                                                                                               | 1115 +++++++++++++++
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordVO.java               |    2 
 admin/src/api/business/ywconditionerdevice.js                                                                         |   23 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwConditionerServiceImpl.java            |  148 ++
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/SmsEmailServiceImpl.java                 |   30 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractServiceImpl.java               |   16 
 admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue                                             |    2 
 admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue                                            |   36 
 h5/pages/customer/electricity/recharge.vue                                                                            |   48 
 h5/pages/customer/index.vue                                                                                           |  100 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java                     |    4 
 h5/api/customer.js                                                                                                    |    1 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerWxPayServiceImpl.java          |   18 
 h5/pages/customer/bill/list.vue                                                                                       |    4 
 server/system_service/src/main/java/com/doumee/config/jwt/JwtTokenUtil.java                                           |   22 
 h5/pages/customer/contract/list.vue                                                                                   |    3 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwConditionerService.java                     |    7 
 h5/pages/customer/recharge/record.vue                                                                                 |  236 ++
 admin/src/views/business/ywconditionerdevice.vue                                                                      |  132 +
 h5/pages/roleSelect.vue                                                                                               |   42 
 admin/src/views/business/components/YwConditionerDeviceEdit.vue                                                       |  192 ++
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerDeviceAutoBindService.java          |   19 
 h5/pages/customer/pay/result.vue                                                                                      |    3 
 server/system_service/src/main/java/com/doumee/core/model/LoginUserInfo.java                                          |    9 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwConditionerCloudController.java                    |   27 
 h5/pages/customer/conditioner/recharge.vue                                                                            |   39 
 h5/pages/customer/bill/pay.vue                                                                                        |    3 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java    |  179 +
 server/db/business.yw_electrical_charge.member.sql                                                                    |   32 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerH5Controller.java                            |   17 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwConditionerEditDTO.java                     |   14 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceH5VO.java                    |    1 
 h5/pages/customer/bill/detail.vue                                                                                     |    4 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5BizService.java                   |    2 
 admin/src/views/contract/contractList.vue                                                                             |   10 
 admin/src/views/business/components/AccountRechargePanel.vue                                                          |   36 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5AuthService.java                  |    5 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerDeviceAutoBindServiceImpl.java |  281 +++
 h5/utils/utils.js                                                                                                     |    8 
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwConditioner.java                          |   12 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java          |   31 
 53 files changed, 3,336 insertions(+), 825 deletions(-)

diff --git a/admin/src/api/business/ywconditionerdevice.js b/admin/src/api/business/ywconditionerdevice.js
new file mode 100644
index 0000000..155253d
--- /dev/null
+++ b/admin/src/api/business/ywconditionerdevice.js
@@ -0,0 +1,23 @@
+import request from '../../utils/request'
+
+const base = '/visitsAdmin/cloudService/business/ywConditioner'
+
+export function fetchDeviceManagePage (data) {
+  return request.post(base + '/deviceManagePage', data, { trim: true })
+}
+
+export function getManageDetail (id) {
+  return request.get(base + '/manageDetail/' + id)
+}
+
+export function saveManageDetail (data) {
+  return request.post(base + '/saveManageDetail', data)
+}
+
+export function syncDevicesAndStatus (data) {
+  return request.post(base + '/syncDevicesAndStatus', data || {})
+}
+
+export function gatewayOptions () {
+  return request.get(base + '/gatewayOptions')
+}
diff --git a/admin/src/views/business/components/AccountRechargePanel.vue b/admin/src/views/business/components/AccountRechargePanel.vue
index 262f11d..566ac65 100644
--- a/admin/src/views/business/components/AccountRechargePanel.vue
+++ b/admin/src/views/business/components/AccountRechargePanel.vue
@@ -11,7 +11,15 @@
     </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-input-number
+          v-model="form.money"
+          class="money-input-number"
+          :min="0"
+          :precision="4"
+          :step="10"
+          controls-position="right"
+          placeholder="璇疯緭鍏ラ噾棰�"
+        />
       </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"/>
@@ -79,4 +87,30 @@
   width: auto;
   white-space: nowrap;
 }
+.money-input-number {
+  width: 220px !important;
+}
+.money-input-number ::v-deep .el-input {
+  width: 100% !important;
+}
+.money-input-number ::v-deep .el-input__inner {
+  height: 32px;
+  line-height: 32px;
+  text-align: left;
+  padding-left: 12px;
+  padding-right: 42px;
+}
+.money-input-number ::v-deep .el-input-number__increase,
+.money-input-number ::v-deep .el-input-number__decrease {
+  width: 28px;
+  background: #f5f7fa;
+  border-left: 1px solid #dcdfe6;
+}
+.money-input-number ::v-deep .el-input-number__increase {
+  border-bottom: 1px solid #dcdfe6;
+  line-height: 15px;
+}
+.money-input-number ::v-deep .el-input-number__decrease {
+  line-height: 15px;
+}
 </style>
diff --git a/admin/src/views/business/components/YwConditionerDeviceEdit.vue b/admin/src/views/business/components/YwConditionerDeviceEdit.vue
new file mode 100644
index 0000000..a886ec2
--- /dev/null
+++ b/admin/src/views/business/components/YwConditionerDeviceEdit.vue
@@ -0,0 +1,192 @@
+<template>
+  <GlobalWindow title="缂栬緫绌鸿皟鍐呮満" :visible.sync="visible" width="720px" :confirm-working="isWorking" @confirm="confirm">
+    <el-form ref="form" :model="form" label-width="120px" class="conditioner-edit-form">
+      <el-form-item label="璁惧鍚嶇О">
+        <span>{{ form.name || '-' }}</span>
+      </el-form-item>
+      <el-form-item label="璁惧缂栧彿">
+        <span>{{ form.code || '-' }}</span>
+      </el-form-item>
+      <el-form-item label="缃戝叧MAC">
+        <span>{{ form.wgMac || '-' }}</span>
+        <span :class="isOnline ? 'green' : 'red'" style="margin-left: 12px">{{ form.online || '绂荤嚎' }}</span>
+      </el-form-item>
+      <el-form-item label="璁惧绫诲瀷">
+        <span>{{ form.devTypeName || form.pid || '-' }}</span>
+      </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-item label="澶囨敞">
+        <el-input
+          v-model="form.remark"
+          type="textarea"
+          :rows="4"
+          maxlength="500"
+          show-word-limit
+          placeholder="璇疯緭鍏ュ娉�"
+          style="width: 400px"
+        />
+      </el-form-item>
+    </el-form>
+  </GlobalWindow>
+</template>
+
+<script>
+import GlobalWindow from '@/components/common/GlobalWindow'
+import { tree } from '@/api/project/ywProject'
+import { getManageDetail, saveManageDetail } from '@/api/business/ywconditionerdevice'
+
+export default {
+  name: 'YwConditionerDeviceEdit',
+  components: { GlobalWindow },
+  data () {
+    return {
+      visible: false,
+      isWorking: false,
+      form: {},
+      houseList: [],
+      checkedKeys: [],
+      selectedRooms: []
+    }
+  },
+  computed: {
+    isOnline () {
+      const o = this.form && this.form.online
+      return o === '鍦ㄧ嚎' || o === 1 || o === '1'
+    }
+  },
+  methods: {
+    open (row) {
+      this.visible = true
+      this.checkedKeys = []
+      this.selectedRooms = []
+      this.loadHouseTree()
+      getManageDetail(row.id).then(data => {
+        this.form = { ...data }
+        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))
+    },
+    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 treeRef = this.$refs.roomTree
+      if (!treeRef) {
+        this.selectedRooms = []
+        return
+      }
+      const nodes = treeRef.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 treeRef = this.$refs.roomTree
+      if (!treeRef || !room) return
+      treeRef.setChecked(room.idd, false, true)
+      this.syncSelectedRoomsFromTree()
+    },
+    onRoomCheck () {
+      this.syncSelectedRoomsFromTree()
+    },
+    confirm () {
+      this.isWorking = true
+      saveManageDetail({
+        id: this.form.id,
+        roomIds: this.form.roomIds || [],
+        remark: this.form.remark || ''
+      })
+        .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; }
+.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/YwCustomerConditionerRechargePanel.vue b/admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue
index dc8fe75..1e55e2e 100644
--- a/admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue
+++ b/admin/src/views/business/components/YwCustomerConditionerRechargePanel.vue
@@ -8,7 +8,15 @@
     </div>
     <el-form label-width="120px" size="small">
       <el-form-item label="鍏呭�奸噾棰�">
-        <el-input-number v-model="form.money" :min="0" :precision="2" style="width: 200px"/>
+        <el-input-number
+          v-model="form.money"
+          class="money-input-number"
+          :min="0"
+          :precision="2"
+          :step="10"
+          controls-position="right"
+          placeholder="璇疯緭鍏ラ噾棰�"
+        />
       </el-form-item>
       <el-form-item label="鍏呭�煎娉�">
         <el-input v-model="form.remark" maxlength="300" style="width: 400px"/>
@@ -103,4 +111,30 @@
 .info-block p { margin: 4px 0; line-height: 26px; }
 .footer-btns { text-align: right; margin-top: 16px; }
 .footer-btns .el-button { margin-left: 8px; }
+.money-input-number {
+  width: 220px !important;
+}
+.money-input-number ::v-deep .el-input {
+  width: 100% !important;
+}
+.money-input-number ::v-deep .el-input__inner {
+  height: 32px;
+  line-height: 32px;
+  text-align: left;
+  padding-left: 12px;
+  padding-right: 42px;
+}
+.money-input-number ::v-deep .el-input-number__increase,
+.money-input-number ::v-deep .el-input-number__decrease {
+  width: 28px;
+  background: #f5f7fa;
+  border-left: 1px solid #dcdfe6;
+}
+.money-input-number ::v-deep .el-input-number__increase {
+  border-bottom: 1px solid #dcdfe6;
+  line-height: 15px;
+}
+.money-input-number ::v-deep .el-input-number__decrease {
+  line-height: 15px;
+}
 </style>
diff --git a/admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue b/admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue
index eabda81..7367b5c 100644
--- a/admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue
+++ b/admin/src/views/business/components/YwCustomerElectricalRechargePanel.vue
@@ -23,7 +23,7 @@
       <el-button type="warning" plain :loading="isOperating" v-permissions="['business:ywcustomerrecharge:recharge']" @click="resetAccount('resetPostpay')">娓呴浂(鍚庝粯璐�)</el-button>
     </div>
     -->
-    <div v-if="!electricalList.length && !loading" class="empty-tip">璇ュ晢鎴峰皻鏈叧鑱旂數琛紝璇峰厛鍦ㄥ叧鑱旇澶囦腑娣诲姞</div>
+    <div v-if="!electricalList.length && !loading" class="empty-tip">璇ュ晢鎴锋湁鏁堝悎鍚屾埧婧愪笅鏆傛棤鍏宠仈鐢佃〃</div>
   </div>
 </template>
 
diff --git a/admin/src/views/business/ywconditionerdevice.vue b/admin/src/views/business/ywconditionerdevice.vue
new file mode 100644
index 0000000..70d3c16
--- /dev/null
+++ b/admin/src/views/business/ywconditionerdevice.vue
@@ -0,0 +1,132 @@
+<template>
+  <TableLayout :permissions="['business:ywconditioner:query']">
+    <template v-slot:table-wrap>
+      <ul class="toolbar">
+        <li>
+          <el-button
+            type="primary"
+            :loading="isSyncing"
+            v-permissions="['business:ywconditioner:sync']"
+            @click="handleSync"
+          >鍚屾绌鸿皟鍐呮満</el-button>
+        </li>
+      </ul>
+      <el-table v-loading="isWorking.search" :data="tableData.list" stripe>
+        <el-table-column label="璁惧绫诲瀷" min-width="100" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ formatDeviceType(row) }}</template>
+        </el-table-column>
+        <el-table-column label="璁惧ID" min-width="140" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.code || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="name" label="璁惧鍚嶇О" min-width="140" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.name || row.code || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="wgMac" label="缃戝叧MAC" min-width="140" align="center" show-overflow-tooltip />
+        <el-table-column prop="roomNames" label="缁戝畾鎴块棿" min-width="160" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.roomNames || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="remark" label="澶囨敞" min-width="160" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.remark || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="璁惧鐘舵��" min-width="90" align="center">
+          <template slot-scope="{ row }">
+            <span :class="isOnline(row) ? 'green' : 'red'">{{ row.online || '绂荤嚎' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="鏇存柊鏃堕棿" min-width="150" align="center" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ formatUpdateTime(row) }}</template>
+        </el-table-column>
+        <el-table-column label="鎿嶄綔" align="center" min-width="90" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button type="text" @click="openEdit(row)" v-permissions="['business:ywconditioner:update']">缂栬緫</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination @size-change="handleSizeChange" @current-change="handlePageChange" :pagination="tableData.pagination" />
+    </template>
+    <YwConditionerDeviceEdit 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 * as deviceApi from '@/api/business/ywconditionerdevice'
+import YwConditionerDeviceEdit from './components/YwConditionerDeviceEdit'
+
+export default {
+  name: 'YwConditionerDevice',
+  extends: BaseTable,
+  components: { TableLayout, Pagination, YwConditionerDeviceEdit },
+  data () {
+    return {
+      isSyncing: false
+    }
+  },
+  created () {
+    this.module = '绌鸿皟璁惧绠$悊'
+    this.configData['field.id'] = 'id'
+    this.configData['field.main'] = 'name'
+    this.tableData.pagination.pageSize = 10
+    this.search()
+  },
+  methods: {
+    handlePageChange (pageIndex) {
+      this.tableData.pagination.pageIndex = pageIndex || this.tableData.pagination.pageIndex
+      this.isWorking.search = true
+      deviceApi.fetchDeviceManagePage({
+        page: this.tableData.pagination.pageIndex,
+        capacity: this.tableData.pagination.pageSize,
+        model: {},
+        sorts: this.tableData.sorts
+      })
+        .then(data => {
+          this.tableData.list = data.records || []
+          this.tableData.pagination.total = data.total || 0
+        })
+        .catch(() => {})
+        .finally(() => { this.isWorking.search = false })
+    },
+    formatDeviceType (row) {
+      if (row.devTypeName) return row.devTypeName
+      const pid = (row.pid || '').toLowerCase()
+      if (pid === 'dlj') return '澶氳仈鏈�'
+      if (pid === 'sj') return '姘存満'
+      return row.pid || '-'
+    },
+    isOnline (row) {
+      const o = row && row.online
+      return o === '鍦ㄧ嚎' || o === 1 || o === '1'
+    },
+    formatUpdateTime (row) {
+      const val = row.lastSyncDate || row.editDate
+      if (!val) return '-'
+      const text = String(val)
+      return text.length >= 16 ? text.slice(0, 16) : text
+    },
+    handleSync () {
+      this.$dialog.actionConfirm('纭浠庢櫤绮剧伒骞冲彴鍚屾绌鸿皟鍐呮満璁惧淇℃伅鍚楋紵', '鍚屾绌鸿皟鍐呮満')
+        .then(() => {
+          this.isSyncing = true
+          deviceApi.syncDevicesAndStatus()
+            .then(res => {
+              this.$tip.apiSuccess(res || '鍚屾鎴愬姛')
+              this.search()
+            })
+            .catch(e => this.$tip.apiFailed(e))
+            .finally(() => { this.isSyncing = false })
+        })
+        .catch(() => {})
+    },
+    openEdit (row) {
+      this.$refs.editWindow.open(row)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.green { color: #67c23a; }
+.red { color: #f56c6c; }
+</style>
diff --git a/admin/src/views/contract/contractList.vue b/admin/src/views/contract/contractList.vue
index baeffdf..c5c2e9a 100644
--- a/admin/src/views/contract/contractList.vue
+++ b/admin/src/views/contract/contractList.vue
@@ -122,11 +122,15 @@
     },
     getList (page) {
       const { pagination, filters } = this
+      const model = { ...filters }
+      if (model.selDate && model.selDate.length === 2) {
+        model.queryStartTime = model.selDate[0]
+        model.queryEndTime = model.selDate[1]
+      }
+      delete model.selDate
       this.loading = true
       fetchList({
-        model: {
-          ...filters
-        },
+        model,
         sorts: [{ direction: 'DESC', property: 'param1' }],
         capacity: pagination.pageSize,
         page: page || pagination.page
diff --git a/h5/api/customer.js b/h5/api/customer.js
index 7ca8f0d..ccb628b 100644
--- a/h5/api/customer.js
+++ b/h5/api/customer.js
@@ -9,6 +9,7 @@
   data: { phone: data.phone }
 })
 export const customerGetUserInfo = () => http({ url: `${prefix}/getUserInfo`, method: 'get' })
+export const customerLogout = () => http({ url: `${prefix}/logout`, method: 'post' })
 export const customerWxAuthorize = (data) => http({ url: 'visitsAdmin/cloudService/web/visitor/ywWxAuthorize', method: 'get', data: { ...data, userType: 1 } })
 export const customerBanners = () => http({ url: `${prefix}/banners`, method: 'get' })
 export const customerHome = () => http({ url: `${prefix}/home`, method: 'get' })
diff --git a/h5/components/customer/cu-workbench-fab.vue b/h5/components/customer/cu-workbench-fab.vue
new file mode 100644
index 0000000..87f8eb0
--- /dev/null
+++ b/h5/components/customer/cu-workbench-fab.vue
@@ -0,0 +1,18 @@
+<template>
+  <view class="cu-workbench-fab" @click="goWorkbench">
+    <u-icon name="home-fill" color="#5c6370" size="22" />
+  </view>
+</template>
+
+<script>
+const WORKBENCH_URL = '/pages/customer/index'
+
+export default {
+  name: 'CuWorkbenchFab',
+  methods: {
+    goWorkbench () {
+      uni.reLaunch({ url: WORKBENCH_URL })
+    }
+  }
+}
+</script>
diff --git a/h5/main.js b/h5/main.js
index 3250c70..bc3fb14 100644
--- a/h5/main.js
+++ b/h5/main.js
@@ -1,12 +1,14 @@
 import App from './App'
 import store from './store/index.js'
 import uView from "uview-ui"
+import CuWorkbenchFab from '@/components/customer/cu-workbench-fab.vue'
 // #ifndef VUE3
 import Vue from 'vue'
 import './uni.promisify.adaptor'
 
 Vue.config.productionTip = false
 Vue.use(uView)
+Vue.component('cu-workbench-fab', CuWorkbenchFab)
 Vue.prototype.$store = store
 Vue.prototype.showToast = (str) => {
 	setTimeout(() => {
diff --git a/h5/pages/customer/bill/detail.vue b/h5/pages/customer/bill/detail.vue
index 3638804..0b3dc49 100644
--- a/h5/pages/customer/bill/detail.vue
+++ b/h5/pages/customer/bill/detail.vue
@@ -1,6 +1,6 @@
 <template>
 
-  <view :class="['cu-page cu-bill-detail', needPayAmount > 0 ? 'cu-page--with-footer' : '']" v-if="bill">
+  <view :class="['cu-page cu-bill-detail cu-page--with-fab', needPayAmount > 0 ? 'cu-page--with-footer' : '']" v-if="bill">
 
     <view :class="['cu-bill-detail-hero', heroClass]">
 
@@ -208,6 +208,8 @@
 
     </view>
 
+    <cu-workbench-fab />
+
   </view>
 
 </template>
diff --git a/h5/pages/customer/bill/list.vue b/h5/pages/customer/bill/list.vue
index ea842a2..2b93418 100644
--- a/h5/pages/customer/bill/list.vue
+++ b/h5/pages/customer/bill/list.vue
@@ -1,6 +1,6 @@
 <template>
 
-  <view class="cu-page cu-bill-page">
+  <view class="cu-page cu-page--with-fab cu-bill-page">
 
     <view class="cu-bill-page__header">
 
@@ -198,6 +198,8 @@
 
     </view>
 
+    <cu-workbench-fab />
+
   </view>
 
 </template>
diff --git a/h5/pages/customer/bill/pay.vue b/h5/pages/customer/bill/pay.vue
index 9910c28..9535376 100644
--- a/h5/pages/customer/bill/pay.vue
+++ b/h5/pages/customer/bill/pay.vue
@@ -1,5 +1,5 @@
 <template>
-  <view class="cu-page cu-page--with-footer" v-if="billId">
+  <view class="cu-page cu-page--with-footer cu-page--with-fab" v-if="billId">
     <view class="cu-detail-hero cu-detail-hero--warm">
       <view class="cu-detail-hero__label">鏈缂磋垂</view>
       <view class="cu-detail-hero__amount">
@@ -22,6 +22,7 @@
     <view class="cu-page-footer">
       <view class="cu-btn cu-btn--primary" @click="submit">纭缂磋垂{{ amount ? ' 楼' + amount : '' }}</view>
     </view>
+    <cu-workbench-fab />
   </view>
 </template>
 
diff --git a/h5/pages/customer/conditioner/recharge.vue b/h5/pages/customer/conditioner/recharge.vue
index 9c06412..9b3bea9 100644
--- a/h5/pages/customer/conditioner/recharge.vue
+++ b/h5/pages/customer/conditioner/recharge.vue
@@ -1,14 +1,10 @@
 <template>
-  <view class="cu-page cu-page--with-footer">
-    <view class="cu-device-summary" v-if="device">
-      <view class="cu-row cu-row--between">
-        <text class="cu-name">{{ device.deviceName }}</text>
-        <text class="cu-status cu-status--ok">{{ device.statusText }}</text>
-      </view>
-      <view class="cu-line">鎴块棿锛歿{ device.roomInfo }}</view>
+  <view class="cu-page cu-page--with-footer cu-page--with-fab">
+    <view class="cu-device-summary">
+      <view class="cu-device-summary__title">鍟嗘埛绌鸿皟缁熶竴璐︽埛</view>
       <view class="cu-device-summary__balance">
-        <view class="cu-device-summary__balance-label">褰撳墠璐︽埛浣欓</view>
-        <view class="cu-device-summary__balance-value">{{ device.balance }}</view>
+        <view class="cu-device-summary__balance-label">褰撳墠绌鸿皟鐢佃垂浣欓</view>
+        <view :class="['cu-device-summary__balance-value', balanceTone ? 'cu-device-summary__balance-value--' + balanceTone : '']">{{ balanceText }}</view>
       </view>
     </view>
 
@@ -35,26 +31,41 @@
     <view class="cu-page-footer">
       <view class="cu-btn cu-btn--primary" @click="submit">纭鍏呭�納{ amount ? ' 楼' + amount : '' }}</view>
     </view>
+    <cu-workbench-fab />
   </view>
 </template>
 
 <script>
-import { customerDeviceDetail, customerPayCreate } from '@/api'
+import { customerHome, customerPayCreate } from '@/api'
 import { invokeWxPay } from '@/utils/wxpay.js'
+import { getBalanceTone } from '@/utils/utils.js'
+
 export default {
   data () {
     return {
-      deviceId: null,
-      device: null,
+      gsBalance: null,
       amount: '',
       remark: '',
       quickAmounts: [50, 100, 200, 500]
     }
   },
-  onLoad (q) { this.deviceId = q.id; this.load() },
+  computed: {
+    balanceText () {
+      if (this.gsBalance === null || this.gsBalance === undefined || this.gsBalance === '') return '-'
+      const n = Number(this.gsBalance)
+      return Number.isNaN(n) ? this.gsBalance : n.toFixed(2)
+    },
+    balanceTone () {
+      return getBalanceTone(this.gsBalance)
+    }
+  },
+  onShow () { this.load() },
   methods: {
     load () {
-      customerDeviceDetail({ deviceType: 1, deviceId: this.deviceId }).then(res => { this.device = res.data })
+      customerHome().then(res => {
+        const gs = res.data && res.data.gsConfig
+        this.gsBalance = gs ? gs.leftMoney : null
+      })
     },
     submit () {
       if (!this.amount) return uni.showToast({ title: '璇疯緭鍏ラ噾棰�', icon: 'none' })
diff --git a/h5/pages/customer/contract/detail.vue b/h5/pages/customer/contract/detail.vue
index caabd9c..b3934ac 100644
--- a/h5/pages/customer/contract/detail.vue
+++ b/h5/pages/customer/contract/detail.vue
@@ -1,6 +1,6 @@
 <template>
 
-  <view class="cu-page cu-page--with-footer" v-if="contract">
+  <view class="cu-page cu-page--with-footer cu-page--with-fab" v-if="contract">
 
     <view :class="['cu-detail-hero', contract.status === 1 ? 'cu-detail-hero--green' : '']">
 
@@ -606,6 +606,8 @@
 
     </template>
 
+    <cu-workbench-fab />
+
   </view>
 
 </template>
diff --git a/h5/pages/customer/contract/list.vue b/h5/pages/customer/contract/list.vue
index ac70d09..db65d61 100644
--- a/h5/pages/customer/contract/list.vue
+++ b/h5/pages/customer/contract/list.vue
@@ -1,5 +1,5 @@
 <template>
-  <view class="cu-page">
+  <view class="cu-page cu-page--with-fab">
     <scroll-view scroll-x class="cu-tabs cu-tabs--scroll">
       <view
         v-for="(t, i) in tabs"
@@ -55,6 +55,7 @@
       </view>
       <u-empty v-if="!list.length" text="鏆傛棤鍚堝悓" margin-top="80" />
     </view>
+    <cu-workbench-fab />
   </view>
 </template>
 
diff --git a/h5/pages/customer/electricity/list.vue b/h5/pages/customer/electricity/list.vue
index 97e7891..c6e5e9b 100644
--- a/h5/pages/customer/electricity/list.vue
+++ b/h5/pages/customer/electricity/list.vue
@@ -1,95 +1,173 @@
 <template>
-  <view class="cu-page">
-    <view class="cu-filters">
-      <picker :range="typeOptions" range-key="label" @change="onTypeChange">
-        <view class="cu-filter">{{ typeLabel }} 鈻�</view>
-      </picker>
-      <picker :range="statusOptions" range-key="label" @change="onStatusChange">
-        <view class="cu-filter">{{ statusLabel }} 鈻�</view>
-      </picker>
-    </view>
+  <view class="cu-page cu-page--with-fab cu-device-page">
+    <view class="cu-device-page__header">
+      <view class="cu-device-type-tabs">
+        <view
+          :class="['cu-device-type-card', 'cu-device-type-card--electric', tabIdx === 0 ? 'cu-device-type-card--active' : '']"
+          @click="switchTab(0)"
+        >
+          <view class="cu-device-type-card__icon-wrap">
+            <text class="cu-device-type-card__icon">鈿�</text>
+          </view>
+          <view class="cu-device-type-card__text">
+            <text class="cu-device-type-card__label">鐢佃〃</text>
+            <text class="cu-device-type-card__hint">鏅鸿兘鐢佃〃</text>
+          </view>
+        </view>
+        <view
+          :class="['cu-device-type-card', 'cu-device-type-card--conditioner', tabIdx === 1 ? 'cu-device-type-card--active' : '']"
+          @click="switchTab(1)"
+        >
+          <view class="cu-device-type-card__icon-wrap">
+            <text class="cu-device-type-card__icon">鉂勶笍</text>
+          </view>
+          <view class="cu-device-type-card__text">
+            <text class="cu-device-type-card__label">绌鸿皟</text>
+            <text class="cu-device-type-card__hint">绌鸿皟璁惧</text>
+          </view>
+        </view>
+      </view>
 
-    <view class="cu-list-header">
-      <text class="cu-list-header__count">鍏� {{ list.length }} 鍙拌澶�</text>
+      <view v-if="tabIdx === 1" class="cu-ac-balance-card">
+        <view class="cu-ac-balance-card__main">
+          <text class="cu-ac-balance-card__label">绌鸿皟鐢佃垂浣欓</text>
+          <text :class="['cu-ac-balance-card__value', acBalanceTone ? 'cu-ac-balance-card__value--' + acBalanceTone : '']">{{ acBalanceText }}</text>
+        </view>
+        <view class="cu-ac-balance-card__btn" @click="goAcRecharge">鍘诲厖鍊�</view>
+      </view>
+
+      <view class="cu-list-header cu-list-header--inline">
+        <text class="cu-list-header__count">鍏� {{ list.length }} 鍙皗{ tabIdx === 0 ? '鐢佃〃' : '绌鸿皟' }}</text>
+      </view>
     </view>
 
     <view class="cu-list-wrap">
-      <view v-for="item in list" :key="item.deviceType + '-' + item.deviceId" class="cu-list-card">
-        <view class="cu-list-card__head">
-          <view :class="['cu-list-card__icon', item.deviceType === 0 ? 'cu-list-card__icon--electric' : 'cu-list-card__icon--conditioner']">
-            {{ item.deviceType === 0 ? '鈿�' : '鉂勶笍' }}
-          </view>
-          <view class="cu-list-card__main">
-            <view class="cu-list-card__title-row">
-              <text class="cu-list-card__title">{{ item.deviceName }}</text>
-              <text :class="['cu-status', item.statusCode === 1 ? 'cu-status--ok' : 'cu-status--bad']">{{ item.statusText }}</text>
+      <template v-if="tabIdx === 0">
+        <view v-for="item in list" :key="item.deviceType + '-' + item.deviceId" class="cu-list-card">
+          <view class="cu-list-card__head">
+            <view class="cu-list-card__icon cu-list-card__icon--electric">鈿�</view>
+            <view class="cu-list-card__main">
+              <view class="cu-list-card__title-row">
+                <view class="cu-list-card__title-wrap">
+                  <text class="cu-list-card__title">{{ item.deviceName }}</text>
+                </view>
+                <text :class="['cu-status', item.statusCode === 1 ? 'cu-status--ok' : 'cu-status--bad']">{{ item.statusText }}</text>
+              </view>
+              <text class="cu-list-card__sub">{{ item.roomInfo || '鏆傛棤鎴块棿淇℃伅' }}</text>
             </view>
-            <text class="cu-list-card__sub">{{ item.roomInfo || '鏆傛棤鎴块棿淇℃伅' }}</text>
+          </view>
+
+          <view v-if="item.alarmTags && item.alarmTags.length" class="cu-list-card__tags">
+            <text v-for="tag in item.alarmTags" :key="tag" class="cu-tag">{{ tag }}</text>
+          </view>
+
+          <view class="cu-info-grid">
+            <view class="cu-info-cell">
+              <text class="cu-info-cell__label">鐢佃〃鍦板潃</text>
+              <text class="cu-info-cell__value">{{ item.meterAddress || '-' }}</text>
+            </view>
+            <view class="cu-info-cell">
+              <text class="cu-info-cell__label">璐︽埛浣欓</text>
+              <text :class="['cu-info-cell__value', balanceToneClass(item.balance)]">{{ formatBalance(item.balance) }}</text>
+            </view>
+          </view>
+
+          <view class="cu-list-card__foot">
+            <text class="cu-time">鏇存柊 {{ formatTime(item.updateTime) }}</text>
+            <text class="cu-list-card__arrow" @click="goElectricRecharge(item)">鍘诲厖鍊� 鈫�</text>
           </view>
         </view>
+      </template>
 
-        <view v-if="item.alarmTags && item.alarmTags.length" class="cu-list-card__tags">
-          <text v-for="tag in item.alarmTags" :key="tag" class="cu-tag">{{ tag }}</text>
-        </view>
-
-        <view class="cu-info-grid">
-          <view v-if="item.deviceType === 0" class="cu-info-cell">
-            <text class="cu-info-cell__label">鐢佃〃鎴峰彿</text>
-            <text class="cu-info-cell__value">{{ item.meterAccountNo || '-' }}</text>
+      <template v-else>
+        <view v-for="item in list" :key="item.deviceType + '-' + item.deviceId" class="cu-list-card cu-list-card--readonly">
+          <view class="cu-list-card__head">
+            <view class="cu-list-card__icon cu-list-card__icon--conditioner">鉂勶笍</view>
+            <view class="cu-list-card__main">
+              <view class="cu-list-card__title-row">
+                <view class="cu-list-card__title-wrap">
+                  <text class="cu-list-card__title">{{ item.deviceName }}</text>
+                </view>
+                <text :class="['cu-status', item.statusCode === 1 ? 'cu-status--ok' : 'cu-status--bad']">{{ item.statusText }}</text>
+              </view>
+              <text class="cu-list-card__sub">{{ item.roomInfo || '鏆傛棤鎴块棿淇℃伅' }}</text>
+            </view>
           </view>
-          <view :class="['cu-info-cell', item.deviceType !== 0 ? 'cu-info-cell--full' : '']">
-            <text class="cu-info-cell__label">璐︽埛浣欓</text>
-            <text :class="['cu-info-cell__value', item.balanceLow ? 'cu-info-cell__value--danger' : '']">{{ item.balance }}</text>
+
+          <view class="cu-list-card__meta-row">
+            <text class="cu-time">鏇存柊 {{ formatTime(item.updateTime) }}</text>
           </view>
         </view>
+      </template>
 
-        <view class="cu-list-card__foot">
-          <text class="cu-time">鏇存柊 {{ formatTime(item.updateTime) }}</text>
-          <text class="cu-list-card__arrow" @click="goRecharge(item)">鍘诲厖鍊� 鈫�</text>
-        </view>
-      </view>
-      <u-empty v-if="!list.length" text="鏆傛棤璁惧" margin-top="80" />
+      <u-empty v-if="!list.length" :text="tabIdx === 0 ? '鏆傛棤鐢佃〃' : '鏆傛棤绌鸿皟璁惧'" margin-top="80" />
     </view>
+    <cu-workbench-fab />
   </view>
 </template>
 
 <script>
-import { customerDevicePage } from '@/api'
+import { customerDevicePage, customerHome } from '@/api'
+import { getBalanceTone } from '@/utils/utils.js'
+
 export default {
   data () {
     return {
       list: [],
-      typeOptions: [{ label: '鍏ㄩ儴绫诲瀷', value: null }, { label: '鐢佃〃', value: 0 }, { label: '绌鸿皟', value: 1 }],
-      statusOptions: [{ label: '鍏ㄩ儴鐘舵��', value: null }, { label: '姝e父', value: 1 }, { label: '寮傚父', value: 2 }],
-      typeIdx: 0,
-      statusIdx: 0,
+      tabIdx: 0,
+      gsBalance: null,
       page: 1
     }
   },
   computed: {
-    typeLabel () { return this.typeOptions[this.typeIdx].label },
-    statusLabel () { return this.statusOptions[this.statusIdx].label }
+    acBalanceText () {
+      if (this.gsBalance === null || this.gsBalance === undefined || this.gsBalance === '') return '-'
+      return this.formatBalance(this.gsBalance)
+    },
+    acBalanceTone () {
+      return getBalanceTone(this.gsBalance)
+    }
   },
-  onShow () { this.load() },
+  onShow () {
+    this.load()
+    if (this.tabIdx === 1) this.loadGsBalance()
+  },
   methods: {
     load () {
       customerDevicePage({
         page: this.page,
-        capacity: 20,
-        model: {
-          deviceType: this.typeOptions[this.typeIdx].value,
-          statusFilter: this.statusOptions[this.statusIdx].value
-        }
+        capacity: 200,
+        model: { deviceType: this.tabIdx === 0 ? 0 : 1 }
       }).then(res => { this.list = (res.data && res.data.records) || [] })
     },
-    onTypeChange (e) { this.typeIdx = Number(e.detail.value); this.load() },
-    onStatusChange (e) { this.statusIdx = Number(e.detail.value); this.load() },
+    loadGsBalance () {
+      customerHome().then(res => {
+        const gs = res.data && res.data.gsConfig
+        this.gsBalance = gs ? gs.leftMoney : null
+      })
+    },
+    switchTab (i) {
+      if (this.tabIdx === i) return
+      this.tabIdx = i
+      this.list = []
+      this.load()
+      if (i === 1) this.loadGsBalance()
+    },
+    formatBalance (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      const n = Number(val)
+      return Number.isNaN(n) ? val : n.toFixed(2)
+    },
+    balanceToneClass (val) {
+      const tone = getBalanceTone(val)
+      return tone ? `cu-info-cell__value--${tone}` : ''
+    },
     formatTime (t) { return t ? String(t).replace('T', ' ').substring(0, 19) : '-' },
-    goRecharge (item) {
-      const url = item.deviceType === 0
-        ? `/pages/customer/electricity/recharge?id=${item.deviceId}`
-        : `/pages/customer/conditioner/recharge?id=${item.deviceId}`
-      uni.navigateTo({ url })
+    goElectricRecharge (item) {
+      uni.navigateTo({ url: `/pages/customer/electricity/recharge?id=${item.deviceId}` })
+    },
+    goAcRecharge () {
+      uni.navigateTo({ url: '/pages/customer/conditioner/recharge' })
     }
   }
 }
diff --git a/h5/pages/customer/electricity/recharge.vue b/h5/pages/customer/electricity/recharge.vue
index 94bacbc..b8789cd 100644
--- a/h5/pages/customer/electricity/recharge.vue
+++ b/h5/pages/customer/electricity/recharge.vue
@@ -1,15 +1,31 @@
 <template>
-  <view class="cu-page cu-page--with-footer">
-    <view class="cu-device-summary" v-if="device">
-      <view class="cu-row cu-row--between">
-        <text class="cu-name">{{ device.deviceName }}</text>
-        <text class="cu-status cu-status--ok">{{ device.statusText }}</text>
+  <view class="cu-page cu-page--with-footer cu-page--with-fab">
+    <view v-if="device" class="cu-recharge-hero cu-recharge-hero--electric">
+      <view class="cu-recharge-hero__gradient">
+        <view class="cu-recharge-hero__balance-block">
+          <text class="cu-recharge-hero__balance-label">褰撳墠璐︽埛浣欓锛堝厓锛�</text>
+          <text :class="['cu-recharge-hero__balance-value', balanceTone ? 'cu-recharge-hero__balance-value--' + balanceTone : '']">{{ balanceText }}</text>
+        </view>
       </view>
-      <view class="cu-line">鎴块棿锛歿{ device.roomInfo }}</view>
-      <view class="cu-line">鎴峰彿锛歿{ device.meterAccountNo }}</view>
-      <view class="cu-device-summary__balance">
-        <view class="cu-device-summary__balance-label">褰撳墠璐︽埛浣欓</view>
-        <view class="cu-device-summary__balance-value">{{ device.balance }}</view>
+      <view class="cu-recharge-hero__panel">
+        <view class="cu-recharge-hero__device">
+          <view class="cu-recharge-hero__icon cu-recharge-hero__icon--electric">鈿�</view>
+          <view class="cu-recharge-hero__device-main">
+            <view class="cu-recharge-hero__title-row">
+              <view class="cu-recharge-hero__title-wrap">
+                <text class="cu-recharge-hero__title">{{ device.deviceName || '鐢佃〃' }}</text>
+              </view>
+              <text :class="['cu-recharge-hero__status', device.statusCode === 1 ? 'cu-recharge-hero__status--ok' : 'cu-recharge-hero__status--bad']">{{ device.statusText || '鏈煡' }}</text>
+            </view>
+            <text class="cu-recharge-hero__sub">{{ device.roomInfo || '鏆傛棤鎴块棿淇℃伅' }}</text>
+          </view>
+        </view>
+        <view class="cu-recharge-hero__meta">
+          <view class="cu-recharge-hero__meta-item">
+            <text class="cu-recharge-hero__meta-label">鐢佃〃鍦板潃</text>
+            <text class="cu-recharge-hero__meta-value">{{ device.meterAddress || '-' }}</text>
+          </view>
+        </view>
       </view>
     </view>
 
@@ -36,12 +52,14 @@
     <view class="cu-page-footer">
       <view class="cu-btn cu-btn--primary" @click="submit">纭鍏呭�納{ amount ? ' 楼' + amount : '' }}</view>
     </view>
+    <cu-workbench-fab />
   </view>
 </template>
 
 <script>
 import { customerDeviceDetail, customerPayCreate } from '@/api'
 import { invokeWxPay } from '@/utils/wxpay.js'
+import { getBalanceTone } from '@/utils/utils.js'
 export default {
   data () {
     return {
@@ -53,6 +71,16 @@
     }
   },
   onLoad (q) { this.deviceId = q.id; this.load() },
+  computed: {
+    balanceText () {
+      if (!this.device || this.device.balance === null || this.device.balance === undefined || this.device.balance === '') return '-'
+      const n = Number(this.device.balance)
+      return Number.isNaN(n) ? this.device.balance : n.toFixed(2)
+    },
+    balanceTone () {
+      return getBalanceTone(this.device && this.device.balance)
+    }
+  },
   methods: {
     load () {
       customerDeviceDetail({ deviceType: 0, deviceId: this.deviceId }).then(res => { this.device = res.data })
diff --git a/h5/pages/customer/index.vue b/h5/pages/customer/index.vue
index 6948e8a..125a7c5 100644
--- a/h5/pages/customer/index.vue
+++ b/h5/pages/customer/index.vue
@@ -1,11 +1,19 @@
 <template>
   <view class="cu-page cu-page--home">
     <view class="cu-hero">
-      <view class="cu-hero__greet">
-        <view class="cu-avatar">{{ customerInitial }}</view>
-        <view>
-          <view class="cu-hero__hi">{{ greeting }}</view>
-          <view class="cu-hero__name">{{ home.customerName || '鍟嗘埛鐢ㄦ埛' }}</view>
+      <view class="cu-profile-bar">
+        <view class="cu-profile-bar__info">
+          <view class="cu-avatar">{{ customerInitial }}</view>
+          <view>
+            <view class="cu-hero__hi">{{ greeting }}</view>
+            <view class="cu-hero__name">{{ displayName }}</view>
+          </view>
+        </view>
+        <view class="cu-profile-actions">
+          <view class="cu-profile-action cu-profile-action--pill" @click="logout">
+            <u-icon name="minus-circle-fill" color="#ffffff" size="18" />
+            <text class="cu-profile-action__text">閫�鍑�</text>
+          </view>
         </view>
       </view>
     </view>
@@ -15,43 +23,61 @@
         <u-swiper :list="banners" keyName="imageUrl" height="160" radius="12" indicator indicatorMode="dot" />
       </view>
 
-      <view class="cu-section-title">涓撳睘鏈嶅姟</view>
-      <view class="cu-service-grid">
-        <view class="cu-service-item cu-service-item--electric" @click="go('/pages/customer/electricity/list')">
-          <view class="cu-service-item__icon">鈿�</view>
-          <text class="cu-service-item__label">浜ょ數璐�</text>
-          <text class="cu-service-item__desc">鐢佃〃 / 绌鸿皟鍏呭��</text>
+      <view class="cu-service-panel">
+        <view class="cu-section-head">
+          <view class="cu-section-head__bar" />
+          <text class="cu-section-head__title">涓撳睘鏈嶅姟</text>
         </view>
-        <view class="cu-service-item cu-service-item--contract" @click="go('/pages/customer/contract/list')">
-          <view class="cu-service-item__icon">
-            <u-icon name="file-text-fill" color="#40a9ff" size="24" />
+        <view class="cu-service-grid">
+          <view class="cu-service-card cu-service-card--electric" @click="go('/pages/customer/electricity/list')">
+            <view class="cu-service-card__top">
+              <view class="cu-service-card__icon">
+                <u-icon name="coupon-fill" color="#fa8c16" size="26" />
+              </view>
+              <text class="cu-service-card__arrow">鈥�</text>
+            </view>
+            <text class="cu-service-card__label">浜ょ數璐�</text>
+            <text class="cu-service-card__desc">鐢佃〃 / 绌鸿皟鍏呭��</text>
           </view>
-          <text class="cu-service-item__label">鏌ュ悎鍚�</text>
-          <text class="cu-service-item__desc">绉熻祦鍚堝悓鏌ヨ</text>
+          <view class="cu-service-card cu-service-card--contract" @click="go('/pages/customer/contract/list')">
+            <view class="cu-service-card__top">
+              <view class="cu-service-card__icon">
+                <u-icon name="file-text-fill" color="#40a9ff" size="26" />
+              </view>
+              <text class="cu-service-card__arrow">鈥�</text>
+            </view>
+            <text class="cu-service-card__label">鏌ュ悎鍚�</text>
+            <text class="cu-service-card__desc">绉熻祦鍚堝悓鏌ヨ</text>
+          </view>
+          <view class="cu-service-card cu-service-card--bill" @click="go('/pages/customer/bill/list')">
+            <view class="cu-service-card__top">
+              <view class="cu-service-card__icon">
+                <u-icon name="red-packet-fill" color="#597ef7" size="26" />
+              </view>
+              <text class="cu-service-card__arrow">鈥�</text>
+            </view>
+            <text class="cu-service-card__label">鏌ヨ处鍗�</text>
+            <text class="cu-service-card__desc">鍦ㄧ嚎缂磋垂</text>
+          </view>
+          <view class="cu-service-card cu-service-card--record" @click="go('/pages/customer/recharge/record')">
+            <view class="cu-service-card__top">
+              <view class="cu-service-card__icon">
+                <u-icon name="list" color="#9254de" size="26" />
+              </view>
+              <text class="cu-service-card__arrow">鈥�</text>
+            </view>
+            <text class="cu-service-card__label">鍏呭�艰褰�</text>
+            <text class="cu-service-card__desc">鍘嗗彶鍏呭�兼槑缁�</text>
+          </view>
         </view>
-        <view class="cu-service-item cu-service-item--bill" @click="go('/pages/customer/bill/list')">
-          <view class="cu-service-item__icon">馃挸</view>
-          <text class="cu-service-item__label">鏌ヨ处鍗�</text>
-          <text class="cu-service-item__desc">鍦ㄧ嚎缂磋垂</text>
-        </view>
-        <view class="cu-service-item cu-service-item--record" @click="go('/pages/customer/recharge/record')">
-          <view class="cu-service-item__icon">馃搵</view>
-          <text class="cu-service-item__label">鍏呭�艰褰�</text>
-          <text class="cu-service-item__desc">鍘嗗彶鍏呭�兼槑缁�</text>
-        </view>
-      </view>
-
-      <view class="cu-footer-bar">
-        <view class="cu-footer-btn cu-footer-btn--primary" @click="onSwitchRole">鍒囨崲瑙掕壊</view>
-        <view class="cu-footer-btn" @click="logout">閫�鍑虹櫥褰�</view>
       </view>
     </view>
   </view>
 </template>
 
 <script>
-import { customerBanners, customerHome } from '@/api'
-import { switchRole, goRoleSelect } from '@/utils/roleSwitch.js'
+import { customerBanners, customerHome, customerLogout } from '@/api'
+import { switchRole } from '@/utils/roleSwitch.js'
 export default {
   data () {
     return { banners: [], home: {} }
@@ -63,8 +89,11 @@
       if (h < 18) return '涓嬪崍濂�'
       return '鏅氫笂濂�'
     },
+    displayName () {
+      return this.home.displayName || this.home.customerName || '鍟嗘埛鐢ㄦ埛'
+    },
     customerInitial () {
-      const name = (this.home.customerName || '鍟�').trim()
+      const name = (this.home.memberName || this.home.customerName || '鍟�').trim()
       return name.charAt(0)
     }
   },
@@ -76,8 +105,7 @@
   },
   methods: {
     go (url) { uni.navigateTo({ url }) },
-    onSwitchRole () { switchRole() },
-    logout () { goRoleSelect() }
+    logout () { switchRole(customerLogout) }
   }
 }
 </script>
diff --git a/h5/pages/customer/login.vue b/h5/pages/customer/login.vue
index 492b5b7..f17c286 100644
--- a/h5/pages/customer/login.vue
+++ b/h5/pages/customer/login.vue
@@ -1,5 +1,11 @@
 <template>
   <view class="cu-login">
+    <view class="cu-auth-topbar">
+      <view class="cu-auth-topbar__btn" @click="goRoleSelect">
+        <u-icon name="reload" color="#2080f7" size="22" />
+      </view>
+    </view>
+
     <view class="cu-login__brand">
       <view class="cu-login__title">鍟嗘埛鐧诲綍</view>
       <view class="cu-login__sub">闃滃畞鏂囦綋涓績 路 鍟嗘埛鏈嶅姟骞冲彴</view>
@@ -52,6 +58,9 @@
   },
   methods: {
     ...mapMutations(['setToken', 'setUserInfo', 'setOpenId', 'setUserType']),
+    goRoleSelect () {
+      uni.redirectTo({ url: '/pages/roleSelect?switch=1' })
+    },
     onLogin () {
       if (!this.form.phone || !this.form.code) {
         return uni.showToast({ title: '璇峰~鍐欐墜鏈哄彿鍜岄獙璇佺爜', icon: 'none' })
diff --git a/h5/pages/customer/pay/result.vue b/h5/pages/customer/pay/result.vue
index fe628e4..a514108 100644
--- a/h5/pages/customer/pay/result.vue
+++ b/h5/pages/customer/pay/result.vue
@@ -1,5 +1,5 @@
 <template>
-  <view class="cu-result">
+  <view class="cu-result cu-page--with-fab">
     <view :class="['cu-result__icon', success ? 'cu-result__icon--ok' : 'cu-result__icon--fail']">
       {{ success ? '鉁�' : '鉁�' }}
     </view>
@@ -9,6 +9,7 @@
     </view>
     <view class="cu-btn cu-btn--primary" @click="goRecord">{{ type === 'bill' ? '鏌ョ湅璐﹀崟鏄庣粏' : '鏌ョ湅鍏呭�艰褰�' }}</view>
     <view class="cu-btn" @click="goHome">杩斿洖涓婚〉</view>
+    <cu-workbench-fab />
   </view>
 </template>
 
diff --git a/h5/pages/customer/recharge/record.vue b/h5/pages/customer/recharge/record.vue
index 7b4d931..0e13d36 100644
--- a/h5/pages/customer/recharge/record.vue
+++ b/h5/pages/customer/recharge/record.vue
@@ -1,83 +1,235 @@
 <template>
-  <view class="cu-page">
-    <view class="cu-filters">
-      <picker :range="statusOptions" range-key="label" @change="onStatusChange">
-        <view class="cu-filter">{{ statusLabel }} 鈻�</view>
-      </picker>
-      <picker mode="date" fields="month" @change="onMonthChange">
-        <view class="cu-filter">{{ month || '鍏呭�兼湀浠�' }} 鈻�</view>
-      </picker>
-    </view>
+  <view class="cu-page cu-page--with-fab cu-recharge-record-page">
+    <view class="cu-recharge-record-page__header">
+      <view class="cu-recharge-filter-panel">
+        <view class="cu-recharge-filter-panel__section">
+          <text class="cu-recharge-filter-panel__label">鍏呭�肩姸鎬�</text>
+          <scroll-view scroll-x class="cu-recharge-status-tabs" :show-scrollbar="false">
+            <view
+              v-for="(opt, i) in statusOptions"
+              :key="opt.value == null ? 'all' : opt.value"
+              :class="['cu-recharge-status-tab', statusIdx === i ? 'cu-recharge-status-tab--active' : '', opt.tabClass]"
+              @click="switchStatus(i)"
+            >{{ opt.label }}</view>
+          </scroll-view>
+        </view>
 
-    <view class="cu-list-header">
-      <text class="cu-list-header__count">鍏� {{ list.length }} 鏉¤褰�</text>
+        <view class="cu-recharge-filter-panel__divider" />
+
+        <view class="cu-recharge-filter-panel__section">
+          <view class="cu-recharge-filter-panel__row">
+            <text class="cu-recharge-filter-panel__label">鏃堕棿绛涢��</text>
+            <view class="cu-recharge-date-mode">
+              <view
+                :class="['cu-recharge-date-mode__item', dateMode === 'month' ? 'cu-recharge-date-mode__item--active' : '']"
+                @click="switchDateMode('month')"
+              >鎸夋湀</view>
+              <view
+                :class="['cu-recharge-date-mode__item', dateMode === 'range' ? 'cu-recharge-date-mode__item--active' : '']"
+                @click="switchDateMode('range')"
+              >鎸夋椂闂存</view>
+            </view>
+          </view>
+
+          <view class="cu-recharge-filter-panel__dates">
+            <template v-if="dateMode === 'month'">
+              <picker mode="date" fields="month" :value="monthPickerValue" @change="onMonthChange">
+                <view :class="['cu-recharge-date-field', month ? 'cu-recharge-date-field--active' : '']">
+                  <text class="cu-recharge-date-field__text">{{ monthLabel }}</text>
+                  <text class="cu-recharge-date-field__arrow">鈻�</text>
+                </view>
+              </picker>
+              <text v-if="month" class="cu-recharge-date-clear" @click="clearMonth">閲嶇疆</text>
+            </template>
+
+            <template v-else>
+              <view class="cu-recharge-date-range">
+                <picker mode="date" fields="day" :value="dateStart || today" @change="onDateStartChange">
+                  <view :class="['cu-recharge-date-field', dateStart ? 'cu-recharge-date-field--active' : '']">
+                    <text class="cu-recharge-date-field__text">{{ dateStart || '寮�濮嬫棩鏈�' }}</text>
+                    <text class="cu-recharge-date-field__arrow">鈻�</text>
+                  </view>
+                </picker>
+                <text class="cu-recharge-date-sep">鑷�</text>
+                <picker mode="date" fields="day" :value="dateEnd || today" @change="onDateEndChange">
+                  <view :class="['cu-recharge-date-field', dateEnd ? 'cu-recharge-date-field--active' : '']">
+                    <text class="cu-recharge-date-field__text">{{ dateEnd || '缁撴潫鏃ユ湡' }}</text>
+                    <text class="cu-recharge-date-field__arrow">鈻�</text>
+                  </view>
+                </picker>
+              </view>
+              <text v-if="dateStart || dateEnd" class="cu-recharge-date-clear" @click="clearRange">閲嶇疆</text>
+            </template>
+          </view>
+        </view>
+      </view>
+
+      <view class="cu-recharge-summary">
+        <text class="cu-recharge-summary__count">{{ list.length }}</text>
+        <text class="cu-recharge-summary__label">鏉″厖鍊艰褰�</text>
+      </view>
     </view>
 
     <view class="cu-list-wrap">
-      <view v-for="item in list" :key="item.id" class="cu-list-card">
-        <view class="cu-list-card__head">
-          <view class="cu-list-card__icon cu-list-card__icon--record">馃搵</view>
-          <view class="cu-list-card__main">
-            <view class="cu-list-card__title-row">
-              <text class="cu-list-card__title">{{ item.deviceInfo || item.name || '鍏呭�艰褰�' }}</text>
-              <text :class="['cu-status', statusClass(item.status)]">{{ item.statusText }}</text>
+      <view v-for="item in list" :key="item.id" class="cu-recharge-card">
+        <view :class="['cu-recharge-card__accent', accentClass(item)]" />
+        <view class="cu-recharge-card__body">
+          <view class="cu-recharge-card__top">
+            <view :class="['cu-recharge-card__type', typeTagClass(item.type)]">
+              <text class="cu-recharge-card__type-icon">{{ item.type === 1 ? '鉂�' : '鈿�' }}</text>
+              <text class="cu-recharge-card__type-text">{{ typeText(item.type) }}</text>
             </view>
-            <text class="cu-list-card__sub" v-if="item.address">鎴峰彿 {{ item.address }}</text>
+            <text :class="['cu-status', statusClass(item.status)]">{{ item.statusText }}</text>
           </view>
-        </view>
 
-        <view class="cu-info-grid">
-          <view class="cu-info-cell">
-            <text class="cu-info-cell__label">鍏呭�奸噾棰�</text>
-            <text class="cu-info-cell__value cu-info-cell__value--primary">楼{{ item.money }}</text>
+          <text class="cu-recharge-card__title">{{ item.deviceInfo || item.name || '鍏呭�艰褰�' }}</text>
+          <text v-if="item.rechargeUserName" class="cu-recharge-card__operator">鍏呭�间汉 {{ item.rechargeUserName }}</text>
+
+          <view class="cu-recharge-card__amount-box">
+            <view class="cu-recharge-card__amount-main">
+              <text class="cu-recharge-card__amount-label">鍏呭�奸噾棰�</text>
+              <text class="cu-recharge-card__amount-value">楼{{ formatMoney(item.money) }}</text>
+            </view>
+            <view class="cu-recharge-card__amount-side">
+              <text class="cu-recharge-card__amount-label">鍏呭悗浣欓</text>
+              <text :class="['cu-recharge-card__amount-side-value', balanceToneClass(item.balanceAfter)]">{{ formatMoney(item.balanceAfter) }}</text>
+            </view>
           </view>
-          <view class="cu-info-cell">
-            <text class="cu-info-cell__label">鍏呭悗浣欓</text>
-            <text class="cu-info-cell__value">{{ item.balanceAfter }}</text>
-          </view>
-          <view class="cu-info-cell cu-info-cell--full">
-            <text class="cu-info-cell__label">鍏呭�兼椂闂�</text>
-            <text class="cu-info-cell__value">{{ item.createDate }}</text>
+
+          <view class="cu-recharge-card__foot">
+            <text class="cu-recharge-card__time">{{ formatTime(item.createDate) }}</text>
           </view>
         </view>
       </view>
       <u-empty v-if="!list.length" text="鏆傛棤璁板綍" margin-top="80" />
     </view>
+    <cu-workbench-fab />
   </view>
 </template>
 
 <script>
 import { customerRechargeRecordPage } from '@/api'
+import { getBalanceTone } from '@/utils/utils.js'
+
+const STATUS_OPTIONS = [
+  { label: '鍏ㄩ儴', value: null, tabClass: '' },
+  { label: '鎴愬姛', value: 1, tabClass: 'cu-recharge-status-tab--ok' },
+  { label: '澶辫触', value: 2, tabClass: 'cu-recharge-status-tab--bad' },
+  { label: '鍏呭�间腑', value: 0, tabClass: 'cu-recharge-status-tab--pending' }
+]
+
 export default {
   data () {
+    const now = new Date()
+    const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
     return {
       list: [],
+      dateMode: 'month',
       month: '',
+      dateStart: '',
+      dateEnd: '',
+      today,
       statusIdx: 0,
-      statusOptions: [
-        { label: '鍏ㄩ儴鐘舵��', value: null },
-        { label: '鍏呭�兼垚鍔�', value: 1 },
-        { label: '鍏呭�煎け璐�', value: 2 },
-        { label: '鍏呭�间腑', value: 0 }
-      ]
+      statusOptions: STATUS_OPTIONS
     }
   },
-  computed: { statusLabel () { return this.statusOptions[this.statusIdx].label } },
-  onShow () { this.load() },
+  computed: {
+    monthPickerValue () {
+      return this.month || this.today.substring(0, 7)
+    },
+    monthLabel () {
+      if (!this.month) return '閫夋嫨鏈堜唤'
+      const parts = this.month.split('-')
+      if (parts.length >= 2) return `${parts[0]}骞�${parts[1]}鏈坄
+      return this.month
+    }
+  },
+  onShow () {
+    this.load()
+  },
   methods: {
     load () {
+      const model = { status: this.statusOptions[this.statusIdx].value }
+      if (this.dateMode === 'month') {
+        if (this.month) model.month = this.month
+      } else {
+        if (this.dateStart) model.createTimeBegin = `${this.dateStart} 00:00:00`
+        if (this.dateEnd) model.createTimeEnd = `${this.dateEnd} 23:59:59`
+      }
       customerRechargeRecordPage({
         page: 1,
         capacity: 50,
-        model: { status: this.statusOptions[this.statusIdx].value, month: this.month || null }
+        model
       }).then(res => { this.list = (res.data && res.data.records) || [] })
     },
-    onStatusChange (e) { this.statusIdx = Number(e.detail.value); this.load() },
-    onMonthChange (e) { this.month = e.detail.value; this.load() },
+    switchStatus (i) {
+      if (this.statusIdx === i) return
+      this.statusIdx = i
+      this.load()
+    },
+    switchDateMode (mode) {
+      if (this.dateMode === mode) return
+      this.dateMode = mode
+      if (mode === 'month') {
+        this.dateStart = ''
+        this.dateEnd = ''
+      } else {
+        this.month = ''
+      }
+      this.load()
+    },
+    onMonthChange (e) {
+      this.month = e.detail.value
+      this.load()
+    },
+    onDateStartChange (e) {
+      this.dateStart = e.detail.value
+      if (this.dateEnd && this.dateStart > this.dateEnd) {
+        this.dateEnd = this.dateStart
+      }
+      this.load()
+    },
+    onDateEndChange (e) {
+      this.dateEnd = e.detail.value
+      if (this.dateStart && this.dateEnd < this.dateStart) {
+        this.dateStart = this.dateEnd
+      }
+      this.load()
+    },
+    clearMonth () {
+      this.month = ''
+      this.load()
+    },
+    clearRange () {
+      this.dateStart = ''
+      this.dateEnd = ''
+      this.load()
+    },
+    typeText (type) {
+      return type === 1 ? '绌鸿皟鍏呭��' : '鐢佃〃鍏呭��'
+    },
+    typeTagClass (type) {
+      return type === 1 ? 'cu-recharge-card__type--conditioner' : 'cu-recharge-card__type--electric'
+    },
+    accentClass (item) {
+      return item.type === 1 ? 'cu-recharge-card__accent--conditioner' : 'cu-recharge-card__accent--electric'
+    },
     statusClass (s) {
       if (s === 1) return 'cu-status--ok'
       if (s === 2) return 'cu-status--bad'
       return 'cu-status--warn'
+    },
+    formatMoney (val) {
+      if (val === null || val === undefined || val === '') return '-'
+      const n = Number(val)
+      return Number.isNaN(n) ? val : n.toFixed(2)
+    },
+    formatTime (t) {
+      return t ? String(t).replace('T', ' ').substring(0, 19) : '-'
+    },
+    balanceToneClass (val) {
+      const tone = getBalanceTone(val)
+      return tone ? `cu-recharge-card__amount-side-value--${tone}` : ''
     }
   }
 }
diff --git a/h5/pages/index.vue b/h5/pages/index.vue
index ba479e2..915a056 100644
--- a/h5/pages/index.vue
+++ b/h5/pages/index.vue
@@ -1,39 +1,51 @@
 <template>
-	<view class="main_app">
-		<view class="hone_name title">{{ userInfo.realname }}锛屾杩庣櫥褰曪綖</view>
-		<view class="home_con">
-			<image class="bg" src="@/static/home/home_bg.jpg" mode=""></image>
-			<view class="h1">闃滃畞鏂囦綋涓績</view>
-			<view class="h2">娆㈣繋浣�</view>
-		</view>
-		<view class="title">涓氬姟鍔炵悊</view>
-		<view class="list">
-			<view v-for="item in list1" class="item" @click="itemClick(item)">
-				<image :src="item.img"></image>
-				<view class="name">{{item.name}}</view>
+	<view class="cu-page ops-home">
+		<view class="cu-hero">
+			<view class="cu-profile-bar">
+				<view class="cu-profile-bar__info">
+					<view class="cu-avatar">{{ userInitial }}</view>
+					<view>
+						<view class="cu-hero__hi">{{ greeting }}</view>
+						<view class="cu-hero__name">{{ userInfo.realname || '杩愮淮浜哄憳' }}</view>
+					</view>
+				</view>
+				<view class="cu-profile-actions">
+					<view class="cu-profile-action cu-profile-action--pill" @click="loginOut">
+						<u-icon name="minus-circle-fill" color="#ffffff" size="18" />
+						<text class="cu-profile-action__text">閫�鍑�</text>
+					</view>
+				</view>
 			</view>
 		</view>
-		<view class="title">涓氬姟鏌ヨ</view>
-		<view class="list">
-			<view v-for="item in list2" class="item" @click="itemClick(item)">
-				<image :src="item.img"></image>
-				<view class="name">{{item.name}}</view>
-				<view v-if="item.name == '寰呭姙涓績' && taskNum" class="superscript">{{taskNum}}</view>
+
+		<view class="ops-home__body">
+			<view class="home_con">
+				<image class="bg" src="@/static/home/home_bg.jpg" mode=""></image>
+				<view class="h1">闃滃畞鏂囦綋涓績</view>
+				<view class="h2">娆㈣繋浣�</view>
 			</view>
-		</view>
-		<view class="footer-actions">
-			<view class="switch-role" @click="switchRole">鍒囨崲瑙掕壊</view>
-			<view class="loginout" @click="loginOut">閫�鍑虹櫥褰�</view>
+			<view class="title">涓氬姟鍔炵悊</view>
+			<view class="list">
+				<view v-for="item in list1" :key="item.name" class="item" @click="itemClick(item)">
+					<image :src="item.img"></image>
+					<view class="name">{{ item.name }}</view>
+				</view>
+			</view>
+			<view class="title">涓氬姟鏌ヨ</view>
+			<view class="list list--last">
+				<view v-for="item in list2" :key="item.name" class="item" @click="itemClick(item)">
+					<image :src="item.img"></image>
+					<view class="name">{{ item.name }}</view>
+					<view v-if="item.name == '寰呭姙涓績' && taskNum" class="superscript">{{ taskNum }}</view>
+				</view>
+			</view>
 		</view>
 	</view>
 </template>
 
 <script>
-	import {
-		logoutPost,
-		myNoticesH5
-	} from '@/api'
-	import { switchRole as doSwitchRole, goRoleSelect } from '@/utils/roleSwitch.js'
+	import { logoutPost, myNoticesH5 } from '@/api'
+	import { goRoleSelect } from '@/utils/roleSwitch.js'
 	export default {
 		data() {
 			return {
@@ -85,8 +97,21 @@
 				taskNum: 0
 			}
 		},
+		computed: {
+			greeting () {
+				const h = new Date().getHours()
+				if (h < 12) return '鏃╀笂濂�'
+				if (h < 18) return '涓嬪崍濂�'
+				return '鏅氫笂濂�'
+			},
+			userInitial () {
+				const name = (this.userInfo.realname || '杩�').trim()
+				return name.charAt(0)
+			}
+		},
 		onShow() {
-			myNoticesH5({ page: 1, capacity: 1,model: {status: 0}}).then(res => {
+			this.userInfo = uni.getStorageSync('userInfo') || {}
+			myNoticesH5({ page: 1, capacity: 1, model: { status: 0 } }).then(res => {
 				this.taskNum = res.data.total
 			})
 		},
@@ -96,116 +121,98 @@
 					url: item.url
 				})
 			},
-			switchRole () {
-				doSwitchRole(logoutPost)
-			},
 			loginOut() {
 				logoutPost().catch(() => {}).finally(() => goRoleSelect())
 			},
-
 		}
 	}
 </script>
 
 <style lang="scss" scoped>
-	.main_app {
-		padding: 0 30rpx;
+	.ops-home__body {
+		margin-top: -28rpx;
+		padding: 0 30rpx 48rpx;
+	}
 
-		.hone_name {
+	.home_con {
+		width: 100%;
+		height: 270rpx;
+		border-radius: 16rpx;
+		margin-bottom: 40rpx;
+		padding: 36rpx 40rpx;
+		position: relative;
+		color: #fff;
+		box-sizing: border-box;
+		overflow: hidden;
+		box-shadow: 0 8rpx 32rpx rgba(15, 35, 95, 0.08);
 
-			height: 90rpx;
-			display: flex;
-			align-items: center;
-		}
-
-		.home_con {
-			width: 690rpx;
-			height: 270rpx;
-			border-radius: 8rpx;
-			margin-bottom: 40rpx;
-			padding: 36rpx 40rpx;
-			position: relative;
-			color: #fff;
-
-			.h1 {
-				font-weight: bold;
-				font-size: 44rpx;
-				margin-bottom: 14rpx;
-			}
-		}
-
-		.title {
-			font-weight: 500;
-			font-size: 34rpx;
-		}
-
-		.list {
-			margin-top: 30rpx;
-			margin-bottom: 80rpx;
-			display: flex;
-
-			.item {
-				display: flex;
-				flex-direction: column;
-				align-items: center;
-				width: 25%;
-				position: relative;
-				image {
-					width: 88rpx;
-					height: 88rpx;
-					margin-bottom: 20rpx;
-				}
-
-				.name {
-					font-size: 26rpx;
-				}
-				.superscript{
-					height: 40rpx;
-					width: 40rpx;
-					position: absolute;
-					top: -16rpx;
-					right: 24rpx;
-					background-color: red;
-					color: #fff;
-					font-size: 24rpx;
-					display: flex;
-					align-items: center;
-					justify-content: center;
-					border-radius: 50%;
-				}
-			}
-		}
-
-		.footer-actions {
-			position: fixed;
-			bottom: 88rpx;
+		.bg {
+			position: absolute;
 			left: 0;
-			right: 0;
-			display: flex;
-			justify-content: center;
-			align-items: center;
-			gap: 24rpx;
+			top: 0;
+			width: 100%;
+			height: 100%;
+			z-index: 0;
 		}
 
-		.switch-role,
-		.loginout {
-			height: 60rpx;
-			padding: 0 32rpx;
-			border-radius: 30rpx;
-			font-size: 26rpx;
-			display: flex;
-			justify-content: center;
-			align-items: center;
+		.h1,
+		.h2 {
+			position: relative;
+			z-index: 1;
 		}
 
-		.switch-role {
-			border: 1rpx solid $primaryColor;
-			color: $primaryColor;
-		}
-
-		.loginout {
-			border: 1rpx solid #ccc;
-			color: #666;
+		.h1 {
+			font-weight: bold;
+			font-size: 44rpx;
+			margin-bottom: 14rpx;
 		}
 	}
-</style>
\ No newline at end of file
+
+	.title {
+		font-weight: 500;
+		font-size: 34rpx;
+	}
+
+	.list {
+		margin-top: 30rpx;
+		margin-bottom: 40rpx;
+		display: flex;
+
+		&--last {
+			margin-bottom: 48rpx;
+		}
+
+		.item {
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+			width: 25%;
+			position: relative;
+
+			image {
+				width: 88rpx;
+				height: 88rpx;
+				margin-bottom: 20rpx;
+			}
+
+			.name {
+				font-size: 26rpx;
+			}
+
+			.superscript {
+				height: 40rpx;
+				width: 40rpx;
+				position: absolute;
+				top: -16rpx;
+				right: 24rpx;
+				background-color: red;
+				color: #fff;
+				font-size: 24rpx;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				border-radius: 50%;
+			}
+		}
+	}
+</style>
diff --git a/h5/pages/login.vue b/h5/pages/login.vue
index a7ce852..899344a 100644
--- a/h5/pages/login.vue
+++ b/h5/pages/login.vue
@@ -1,313 +1,138 @@
 <template>
-	<view class="login">
-		<view class="login_title">娆㈣繋鐧诲綍</view>
-		<view class="login_title login_title2">闃滃畞鏂囦綋涓績</view>
-		<view v-if="devMockTip" class="dev-tip">{{ devMockTip }}</view>
-		<view class="login_list">
-			<view class="login_list_item">
-				<image src="@/static/login_ic_phone@2x.png" mode="widthFix" />
-				<input v-model="form.phone" maxlength="18" placeholder="璇疯緭鍏ユ墜鏈哄彿" />
-			</view>
-			<!--     <view class="login_list_item">
-        <image src="@/static/login_ic_password@2x.png" mode="widthFix" />
-        <input v-model="form.password" type="password" placeholder="瀵嗙爜" />
-      </view> -->
-			<view class="login_list_item">
-				<image src="@/static/login_ic_password@2x.png" mode="widthFix"></image>
-				<input v-model="form.code" placeholder="璇疯緭鍏ラ獙璇佺爜" type="text" />
-				<view v-if="downTime == 0" class="btn" @click="sendSms">鑾峰彇楠岃瘉鐮�</view>
-				<view v-else class="btn gray">{{ downTime }}</view>
-			</view>
-		</view>
-		<view class="login_btn">
-			<view class="login_btn_n" @click="onLogin">鐧诲綍</view>
-		</view>
-	</view>
+  <view class="cu-login">
+    <view class="cu-auth-topbar">
+      <view class="cu-auth-topbar__btn" @click="goRoleSelect">
+        <u-icon name="reload" color="#2080f7" size="22" />
+      </view>
+    </view>
+
+    <view class="cu-login__brand">
+      <view class="cu-login__title">娆㈣繋鐧诲綍</view>
+      <view class="cu-login__sub">闃滃畞鏂囦綋涓績 路 杩愮淮鏈嶅姟骞冲彴</view>
+    </view>
+
+    <view v-if="devMockTip" class="cu-login__tip">{{ devMockTip }}</view>
+
+    <view class="cu-input-wrap">
+      <input v-model="form.phone" maxlength="18" placeholder="璇疯緭鍏ユ墜鏈哄彿" />
+    </view>
+    <view class="cu-input-wrap">
+      <input v-model="form.code" placeholder="璇疯緭鍏ラ獙璇佺爜" />
+      <view v-if="downTime == 0" class="cu-sms-btn" @click="sendSms">鑾峰彇楠岃瘉鐮�</view>
+      <view v-else class="cu-sms-btn cu-sms-btn--disabled">{{ downTime }}s</view>
+    </view>
+
+    <view class="cu-btn cu-btn--primary" @click="onLogin">鐧诲綍</view>
+  </view>
 </template>
 
 <script>
-	import {
-		loginPost,
-		getUserInfo,
-		sendSMsPost,
-		ywWxAuthorize,
+import {
+  loginPost,
+  getUserInfo,
+  sendSMsPost,
+  ywWxAuthorize,
+  getRecordByUserPoint
+} from '@/api'
+import { devWechatMock } from '@/utils/config.js'
+import { runWechatOAuthFlow } from '@/utils/wechatAuth.js'
+import { requestLoginSmsCode } from '@/utils/loginSms.js'
+import { mapMutations } from 'vuex'
 
-		getRecordByUserPoint
-	} from '@/api'
-	import { devWechatMock } from '@/utils/config.js'
-	import { runWechatOAuthFlow } from '@/utils/wechatAuth.js'
-	import { requestLoginSmsCode } from '@/utils/loginSms.js'
-	import {
-		mapState,
-		mapMutations
-	} from 'vuex'
-	export default {
-		name: 'login',
+export default {
+  name: 'login',
+  data () {
+    return {
+      form: { phone: '', code: '' },
+      ywinfo: {},
+      downTime: 0,
+      code: '',
+      devMockTip: devWechatMock.enabled ? `寮�鍙戞ā寮忥細妯℃嫙 openid ${devWechatMock.openId}` : ''
+    }
+  },
+  onLoad (option) {
+    uni.setStorageSync('userType', 0)
+    const ywinfo = uni.getStorageSync('ywinfo') || {}
+    if (ywinfo.ywid && (ywinfo.type || ywinfo.type == 0)) {
+      this.ywinfo = ywinfo
+      uni.setStorageSync('ywinfo', {})
+    }
+    if (option.ywid || option.ywid == 0) {
+      uni.setStorageSync('ywinfo', {
+        type: option.type,
+        ywid: option.ywid
+      })
+    }
+  },
+  onShow () {
+    const that = this
+    runWechatOAuthFlow({
+      authorizeApi: ywWxAuthorize,
+      fallbackCode: this.code,
+      onSuccess: (res) => {
+        if (res.data.openid) {
+          that.$store.commit('setOpenId', res.data.openid)
+        }
+        if (res.data.token && res.data.token != '') {
+          that.$store.commit('setToken', res.data.token)
+          getUserInfo().then(ress => {
+            that.$store.commit('setUserInfo', ress.data)
+          })
+          const ywinfo = this.ywinfo
+          if (ywinfo.ywid && (ywinfo.type || ywinfo.type == 0)) {
+            getRecordByUserPoint({ pointCode: ywinfo.ywid }).then(res => {
+              if (res.data && res.data.id) {
+                uni.redirectTo({ url: '/pages/polling/point?id=' + res.data.id })
+              } else {
+                uni.redirectTo({ url: '/pages/polling/empty?message=' + res.message })
+              }
+            })
+          } else {
+            setTimeout(() => {
+              uni.redirectTo({ url: '/pages/index' })
+            }, 300)
+          }
+        }
+      }
+    })
+  },
+  methods: {
+    ...mapMutations(['setToken', 'setUserInfo']),
+    goRoleSelect () {
+      uni.redirectTo({ url: '/pages/roleSelect?switch=1' })
+    },
+    onLogin () {
+      const { form } = this
+      if (!form.phone) return uni.showToast({ title: '鎵嬫満鍙蜂笉鑳戒负绌�', icon: 'none' })
+      if (!form.code) return uni.showToast({ title: '楠岃瘉鐮佷笉鑳戒负绌�', icon: 'none' })
 
-		data() {
-			return {
-				form: {
-					phone: '',
-					code: ''
-				},
-				ywinfo: {},
-				downTime: 0,
-				code: '',
-				devMockTip: devWechatMock.enabled ? `寮�鍙戞ā寮忥細妯℃嫙 openid ${devWechatMock.openId}` : ''
-			}
-		},
-		onLoad(option) {
-			console.log('onLoad');
-			// https://zhcg.fnwtzx.com/#/pages/login?type=0&ywid=ywid
-			const ywinfo = uni.getStorageSync('ywinfo') || {}
-			if (ywinfo.ywid && (ywinfo.type || ywinfo.type == 0)) {
-				this.ywinfo = ywinfo
-				uni.setStorageSync('ywinfo', {})
-			}
-			if (option.ywid || option.ywid == 0) {
-				uni.setStorageSync('ywinfo', {
-					type: option.type,
-					ywid: option.ywid
-				})
-			}
-		},
-		onShow() {
-			const that = this
-			runWechatOAuthFlow({
-				authorizeApi: ywWxAuthorize,
-				fallbackCode: this.code,
-				onSuccess: (res) => {
-					if (res.data.openid) {
-						that.$store.commit('setOpenId', res.data.openid)
-					}
-					if (res.data.token && res.data.token != '') {
-						that.$store.commit('setToken', res.data.token)
-						getUserInfo().then(ress => {
-							that.$store.commit('setUserInfo', ress.data)
-						})
-						const ywinfo = this.ywinfo
-						if (ywinfo.ywid && (ywinfo.type || ywinfo.type == 0)) {
-							getRecordByUserPoint({
-								pointCode: ywinfo.ywid
-							}).then(res => {
-								if (res.data && res.data.id) {
-									uni.redirectTo({
-										url: "/pages/polling/point?id=" + res.data.id
-									})
-								} else {
-									uni.redirectTo({
-										url: "/pages/polling/empty?message=" + res.message
-									})
-								}
-							})
-						} else {
-							setTimeout(() => {
-								uni.redirectTo({
-									url: "/pages/index"
-								})
-							}, 300)
-						}
-					}
-				}
-			})
-		},
-		methods: {
-			...mapMutations(["setToken", "setUserInfo"]),
-			onLogin() {
-				const {
-					form,
-					ProtocolFlag
-				} = this
-				if (!form.phone) return uni.showToast({
-					title: '鎵嬫満鍙蜂笉鑳戒负绌�',
-					icon: 'none'
-				})
-				if (!form.code) return uni.showToast({
-					title: '楠岃瘉鐮佷笉鑳戒负绌�',
-					icon: 'none'
-				})
-
-				loginPost({
-					...form,
-					openid: this.$store.state.openId
-				}).then(res => {
-					if (res.code === 200) {
-						this.setToken(res.data)
-						this.showToast('鐧诲綍鎴愬姛')
-						getUserInfo().then(ress => {
-							this.setUserInfo(ress.data)
-							const ywinfo = this.ywinfo
-							if (ywinfo.ywid && (ywinfo.type || ywinfo.type == 0)) {
-								// getRecordByUserPoint({pointCode: ywinfo.ywid}).then(res => {
-								getRecordByUserPoint({
-									pointCode: ywinfo.ywid
-								}).then(res => {
-									if (res.data && res.data.id) {
-										uni.redirectTo({
-											url: "/pages/polling/point?id=" + res.data.id
-										})
-									} else {
-										uni.redirectTo({
-											url: "/pages/polling/empty?message=" + res.message
-										})
-									}
-								})
-								// })
-							} else {
-								uni.redirectTo({
-									url: "/pages/index"
-								})
-							}
-						})
-					}
-				})
-
-
-
-			},
-			sendSms() {
-				requestLoginSmsCode(this, this.form.phone, sendSMsPost, { phone: this.form.phone, userType: 0 })
-			},
-		}
-	}
+      loginPost({
+        ...form,
+        openid: this.$store.state.openId
+      }).then(res => {
+        if (res.code === 200) {
+          this.setToken(res.data)
+          getUserInfo().then(ress => {
+            this.setUserInfo(ress.data)
+            const ywinfo = this.ywinfo
+            if (ywinfo.ywid && (ywinfo.type || ywinfo.type == 0)) {
+              getRecordByUserPoint({ pointCode: ywinfo.ywid }).then(res => {
+                if (res.data && res.data.id) {
+                  uni.redirectTo({ url: '/pages/polling/point?id=' + res.data.id })
+                } else {
+                  uni.redirectTo({ url: '/pages/polling/empty?message=' + res.message })
+                }
+              })
+            } else {
+              uni.redirectTo({ url: '/pages/index' })
+            }
+          })
+        }
+      })
+    },
+    sendSms () {
+      requestLoginSmsCode(this, this.form.phone, sendSMsPost, { phone: this.form.phone, userType: 0 })
+    }
+  }
+}
 </script>
-
-<style lang="scss" scoped>
-	.login {
-		width: 100%;
-		height: 100vh;
-		display: flex;
-		padding-top: 130rpx;
-		box-sizing: border-box;
-		align-items: center;
-		flex-direction: column;
-		background: linear-gradient(180deg, #C5DDFF 0%, #FFFFFF 100%);
-
-		.login_title {
-			font-weight: 500;
-			font-size: 52rpx;
-			color: #222222;
-			margin-top: 180rpx;
-			width: 100%;
-			padding-left: 60rpx;
-		}
-
-		.login_title2 {
-			margin-top: 10rpx;
-			margin-bottom: 40rpx;
-		}
-
-		.dev-tip {
-			width: 100%;
-			padding: 0 60rpx;
-			box-sizing: border-box;
-			font-size: 24rpx;
-			color: #e6a23c;
-			margin-bottom: 40rpx;
-			line-height: 1.5;
-		}
-
-		.login_list {
-			width: 100%;
-			padding: 0 60rpx;
-			box-sizing: border-box;
-
-			.login_list_item {
-				width: 100%;
-				border-radius: 50rpx;
-				height: 98rpx;
-				padding: 0 40rpx;
-				box-sizing: border-box;
-				background: #ffffff;
-				margin-bottom: 40rpx;
-				display: flex;
-				align-items: center;
-				justify-content: space-between;
-
-				&:last-child {
-					margin-bottom: 0 !important;
-				}
-
-				image {
-					flex-shrink: 0;
-					width: 40rpx;
-					height: 40rpx;
-				}
-
-				.btn {
-					width: 145rpx;
-					color: $primaryColor;
-					text-align: center;
-				}
-
-				.gray {
-					color: #999999;
-				}
-
-				input {
-					flex: 1;
-					height: 100%;
-					color: #666666;
-					margin-left: 24rpx;
-					border: none;
-				}
-			}
-		}
-
-		.login_btn {
-			width: 100%;
-			padding: 0 60rpx;
-			box-sizing: border-box;
-			margin-top: 60rpx;
-
-			.for_psd {
-				color: $uni-color-primary;
-				width: 140rpx;
-				text-align: center;
-				margin: 40rpx auto;
-			}
-
-			.login_btn_n {
-				width: 100%;
-				height: 98rpx;
-				background: $uni-color-primary;
-				box-shadow: 0rpx 12rpx 24rpx 0rpx rgba(39, 155, 170, 0.2);
-				display: flex;
-				align-items: center;
-				justify-content: center;
-				color: #ffffff;
-				border-radius: 50rpx;
-				font-weight: 500;
-				font-size: 32rpx;
-			}
-		}
-
-		.deal_wrap {
-			position: absolute;
-			width: 100%;
-			left: 0;
-			text-align: center;
-			bottom: 88rpx;
-			display: flex;
-			justify-content: center;
-			align-items: center;
-
-			.deal {
-				color: $uni-color-primary;
-			}
-
-			.checked {
-				width: 48rpx;
-				margin-right: 12rpx;
-			}
-		}
-	}
-
-	.modal {
-		width: 690rpx;
-		min-height: 920rpx;
-		max-height: 720px;
-		border-radius: 24rpx;
-		padding: 32rpx;
-	}
-</style>
\ No newline at end of file
diff --git a/h5/pages/roleSelect.vue b/h5/pages/roleSelect.vue
index 68df7e0..503c40a 100644
--- a/h5/pages/roleSelect.vue
+++ b/h5/pages/roleSelect.vue
@@ -1,14 +1,28 @@
 <template>
-  <view class="page">
-    <view class="title">璇烽�夋嫨鐧诲綍韬唤</view>
-    <view class="sub-title">鍒囨崲瑙掕壊鍚庨渶浣跨敤瀵瑰簲韬唤閲嶆柊鐧诲綍</view>
-    <view class="card" @click="goOps">
-      <view class="name">杩愮淮浜哄憳</view>
-      <view class="desc">宸ュ崟銆佸贰妫�銆佽澶囪繍缁�</view>
+  <view class="cu-auth-page">
+    <view class="cu-auth-page__title">閫夋嫨韬唤</view>
+    <view class="cu-auth-page__sub">鍒囨崲瑙掕壊鍚庨渶浣跨敤瀵瑰簲韬唤閲嶆柊鐧诲綍</view>
+
+    <view class="cu-role-card cu-role-card--ops" @click="goOps">
+      <view class="cu-role-card__icon">
+        <u-icon name="setting-fill" color="#40a9ff" size="28" />
+      </view>
+      <view class="cu-role-card__main">
+        <view class="cu-role-card__name">杩愮淮浜哄憳</view>
+        <view class="cu-role-card__desc">宸ュ崟銆佸贰妫�銆佽澶囪繍缁�</view>
+      </view>
+      <text class="cu-role-card__arrow">鈥�</text>
     </view>
-    <view class="card merchant" @click="goMerchant">
-      <view class="name">鍟嗘埛</view>
-      <view class="desc">浜ょ數璐广�佹煡鍚堝悓銆佹煡璐﹀崟</view>
+
+    <view class="cu-role-card cu-role-card--merchant" @click="goMerchant">
+      <view class="cu-role-card__icon">
+        <u-icon name="home-fill" color="#fa8c16" size="28" />
+      </view>
+      <view class="cu-role-card__main">
+        <view class="cu-role-card__name">鍟嗘埛</view>
+        <view class="cu-role-card__desc">浜ょ數璐广�佹煡鍚堝悓銆佹煡璐﹀崟</view>
+      </view>
+      <text class="cu-role-card__arrow">鈥�</text>
     </view>
   </view>
 </template>
@@ -37,13 +51,3 @@
   }
 }
 </script>
-
-<style lang="scss" scoped>
-.page { min-height: 100vh; padding: 120rpx 48rpx; background: linear-gradient(180deg, #e8f0ff 0%, #fff 100%); }
-.title { font-size: 44rpx; font-weight: 600; margin-bottom: 16rpx; color: #222; }
-.sub-title { font-size: 26rpx; color: #999; margin-bottom: 48rpx; }
-.card { background: #fff; border-radius: 24rpx; padding: 40rpx; margin-bottom: 32rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,.06); }
-.card.merchant { border: 2rpx solid #3c7cff; }
-.name { font-size: 36rpx; font-weight: 600; color: #222; }
-.desc { margin-top: 12rpx; font-size: 26rpx; color: #888; }
-</style>
diff --git a/h5/styles/customer.scss b/h5/styles/customer.scss
index f6723d5..8b7223e 100644
--- a/h5/styles/customer.scss
+++ b/h5/styles/customer.scss
@@ -41,6 +41,87 @@
   align-items: center;
 }
 
+.cu-profile-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16rpx;
+}
+
+.cu-profile-bar__info {
+  display: flex;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-profile-actions {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  flex-shrink: 0;
+}
+
+.cu-profile-action {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.2);
+  border: 2rpx solid rgba(255, 255, 255, 0.35);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: opacity 0.2s ease;
+}
+
+.cu-profile-action--pill {
+  width: auto;
+  height: 56rpx;
+  padding: 0 20rpx;
+  border-radius: 999rpx;
+  gap: 6rpx;
+  flex-direction: row;
+  background: rgba(255, 255, 255, 0.18);
+  border: 2rpx solid rgba(255, 255, 255, 0.4);
+}
+
+.cu-profile-action__text {
+  font-size: 24rpx;
+  color: #fff;
+  line-height: 1;
+  font-weight: 500;
+}
+
+.cu-profile-action:active {
+  opacity: 0.75;
+}
+
+/* 鍟嗘埛瀛愰〉鍥為椤� */
+.cu-workbench-fab {
+  position: fixed;
+  top: 50%;
+  right: 24rpx;
+  z-index: 200;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 84rpx;
+  height: 84rpx;
+  padding: 0;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.96);
+  border: 1rpx solid rgba(15, 35, 95, 0.08);
+  box-shadow: 0 6rpx 24rpx rgba(15, 35, 95, 0.1);
+  transform: translateY(-50%);
+  transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
+}
+
+.cu-workbench-fab:active {
+  transform: translateY(-50%) scale(0.96);
+  opacity: 0.94;
+  box-shadow: 0 4rpx 16rpx rgba(15, 35, 95, 0.08);
+}
+
 .cu-avatar {
   width: 88rpx;
   height: 88rpx;
@@ -89,12 +170,147 @@
   padding-left: 8rpx;
 }
 
+.cu-service-panel {
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  padding: 28rpx 20rpx 20rpx;
+  box-shadow: $cu-shadow;
+  border: 1rpx solid rgba(15, 35, 95, 0.04);
+}
+
+.cu-section-head {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-bottom: 24rpx;
+  padding: 0 8rpx;
+}
+
+.cu-section-head__bar {
+  width: 6rpx;
+  height: 28rpx;
+  border-radius: 4rpx;
+  background: linear-gradient(180deg, #2080f7 0%, #4a9bff 100%);
+  flex-shrink: 0;
+}
+
+.cu-section-head__title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: $cu-text;
+  line-height: 1.2;
+}
+
 .cu-service-grid {
   display: flex;
   flex-wrap: wrap;
-  gap: 20rpx;
+  gap: 16rpx;
 }
 
+.cu-service-card {
+  width: calc(50% - 8rpx);
+  background: linear-gradient(180deg, #fafbfd 0%, #fff 100%);
+  border-radius: 20rpx;
+  padding: 22rpx 20rpx 24rpx;
+  border: 1rpx solid rgba(15, 35, 95, 0.06);
+  box-sizing: border-box;
+  position: relative;
+  overflow: hidden;
+  transition: transform 0.2s ease, opacity 0.2s ease;
+}
+
+.cu-service-card::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 4rpx;
+}
+
+.cu-service-card:active {
+  transform: scale(0.98);
+  opacity: 0.92;
+}
+
+.cu-service-card__top {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 14rpx;
+}
+
+.cu-service-card__icon {
+  width: 76rpx;
+  height: 76rpx;
+  border-radius: 22rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.cu-service-card__arrow {
+  font-size: 36rpx;
+  color: rgba(154, 163, 178, 0.7);
+  line-height: 1;
+  font-weight: 300;
+  margin-top: 4rpx;
+}
+
+.cu-service-card__label {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+  line-height: 1.3;
+}
+
+.cu-service-card__desc {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-top: 8rpx;
+  line-height: 1.4;
+}
+
+.cu-service-card--electric::before {
+  background: linear-gradient(90deg, #fa8c16 0%, #ffc069 100%);
+}
+
+.cu-service-card--electric .cu-service-card__icon {
+  background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%);
+  box-shadow: 0 6rpx 16rpx rgba(250, 140, 22, 0.15);
+}
+
+.cu-service-card--contract::before {
+  background: linear-gradient(90deg, #40a9ff 0%, #69c0ff 100%);
+}
+
+.cu-service-card--contract .cu-service-card__icon {
+  background: linear-gradient(135deg, #e6f4ff 0%, #bae7ff 100%);
+  box-shadow: 0 6rpx 16rpx rgba(64, 169, 255, 0.18);
+}
+
+.cu-service-card--bill::before {
+  background: linear-gradient(90deg, #597ef7 0%, #85a5ff 100%);
+}
+
+.cu-service-card--bill .cu-service-card__icon {
+  background: linear-gradient(135deg, #eef2ff 0%, #d6e4ff 100%);
+  box-shadow: 0 6rpx 16rpx rgba(89, 126, 247, 0.15);
+}
+
+.cu-service-card--record::before {
+  background: linear-gradient(90deg, #9254de 0%, #b37feb 100%);
+}
+
+.cu-service-card--record .cu-service-card__icon {
+  background: linear-gradient(135deg, #f9f0ff 0%, #efdbff 100%);
+  box-shadow: 0 6rpx 16rpx rgba(146, 84, 222, 0.15);
+}
+
+/* 鍏煎鏃х被鍚� */
 .cu-service-item {
   width: calc(50% - 10rpx);
   background: $cu-card-bg;
@@ -137,25 +353,112 @@
 .cu-service-item--bill .cu-service-item__icon { background: #eef2ff; }
 .cu-service-item--record .cu-service-item__icon { background: #fce8f3; }
 
-.cu-footer-bar {
-  margin-top: 64rpx;
-  display: flex;
-  justify-content: center;
-  gap: 24rpx;
+/* 璁よ瘉 / 瑙掕壊閫夋嫨 */
+.cu-auth-page {
+  min-height: 100vh;
+  padding: 120rpx 48rpx 48rpx;
+  background: linear-gradient(180deg, #dce9ff 0%, #f4f6fb 45%, #fff 100%);
+  box-sizing: border-box;
 }
 
-.cu-footer-btn {
-  font-size: 28rpx;
-  padding: 18rpx 48rpx;
-  border-radius: 999rpx;
-  background: #fff;
-  color: $cu-text-secondary;
+.cu-auth-page__title {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: $cu-text;
+  margin-bottom: 12rpx;
+}
+
+.cu-auth-page__sub {
+  font-size: 26rpx;
+  color: $cu-text-muted;
+  margin-bottom: 48rpx;
+  line-height: 1.5;
+}
+
+.cu-role-card {
+  display: flex;
+  align-items: center;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  padding: 32rpx 28rpx;
+  margin-bottom: 24rpx;
   box-shadow: $cu-shadow;
 }
 
-.cu-footer-btn--primary {
-  color: $cu-primary;
-  border: 1rpx solid rgba(32, 128, 247, 0.35);
+.cu-role-card:active {
+  opacity: 0.92;
+}
+
+.cu-role-card__icon {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 22rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 24rpx;
+  flex-shrink: 0;
+}
+
+.cu-role-card--ops .cu-role-card__icon {
+  background: linear-gradient(135deg, #e6f4ff 0%, #bae7ff 100%);
+  box-shadow: 0 6rpx 16rpx rgba(105, 192, 255, 0.28);
+}
+
+.cu-role-card--merchant .cu-role-card__icon {
+  background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%);
+  box-shadow: 0 6rpx 16rpx rgba(250, 173, 20, 0.22);
+}
+
+.cu-role-card__main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-role-card__name {
+  font-size: 34rpx;
+  font-weight: 600;
+  color: $cu-text;
+  margin-bottom: 8rpx;
+}
+
+.cu-role-card__desc {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  line-height: 1.4;
+}
+
+.cu-role-card__arrow {
+  font-size: 32rpx;
+  color: $cu-text-muted;
+  flex-shrink: 0;
+  margin-left: 12rpx;
+}
+
+.cu-auth-topbar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 10;
+  display: flex;
+  justify-content: flex-end;
+  padding: 24rpx 32rpx;
+}
+
+.cu-auth-topbar__btn {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.9);
+  box-shadow: $cu-shadow;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.cu-auth-topbar__btn:active {
+  opacity: 0.8;
 }
 
 /* 鍗$墖 */
@@ -229,10 +532,13 @@
 
 /* 鐘舵�� */
 .cu-status {
+  flex-shrink: 0;
+  white-space: nowrap;
   font-size: 24rpx;
   padding: 4rpx 16rpx;
   border-radius: 999rpx;
   font-weight: 500;
+  line-height: 1.4;
 }
 
 .cu-status--ok {
@@ -402,7 +708,9 @@
 .cu-login {
   min-height: 100vh;
   padding: 120rpx 48rpx 48rpx;
+  padding-top: 160rpx;
   background: linear-gradient(180deg, #dce9ff 0%, #f4f6fb 45%, #fff 100%);
+  box-sizing: border-box;
 }
 
 .cu-login__brand {
@@ -509,6 +817,200 @@
   padding: 8rpx 24rpx 16rpx;
 }
 
+.cu-list-header--inline {
+  padding: 0 8rpx 12rpx;
+}
+
+.cu-device-page .cu-list-wrap {
+  padding-top: 0;
+}
+
+.cu-device-page__header {
+  padding-bottom: 8rpx;
+}
+
+.cu-device-type-tabs {
+  display: flex;
+  gap: 20rpx;
+  margin: 24rpx 24rpx 20rpx;
+}
+
+.cu-device-type-card {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+  padding: 22rpx 20rpx;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+  border: 2rpx solid transparent;
+  transition: all 0.22s ease;
+  box-sizing: border-box;
+}
+
+.cu-device-type-card:active {
+  transform: scale(0.97);
+}
+
+.cu-device-type-card__icon-wrap {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  transition: background 0.22s ease;
+}
+
+.cu-device-type-card__icon {
+  font-size: 34rpx;
+  line-height: 1;
+}
+
+.cu-device-type-card__text {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-device-type-card__label {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+  line-height: 1.3;
+  transition: color 0.22s ease;
+}
+
+.cu-device-type-card__hint {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-top: 4rpx;
+  line-height: 1.3;
+  transition: color 0.22s ease;
+}
+
+.cu-device-type-card--electric .cu-device-type-card__icon-wrap {
+  background: linear-gradient(135deg, #fff7e6, #ffe8b3);
+}
+
+.cu-device-type-card--conditioner .cu-device-type-card__icon-wrap {
+  background: linear-gradient(135deg, #e8f7ef, #c8f0dc);
+}
+
+.cu-device-type-card--electric:not(.cu-device-type-card--active) {
+  border-color: rgba(250, 140, 22, 0.18);
+}
+
+.cu-device-type-card--conditioner:not(.cu-device-type-card--active) {
+  border-color: rgba(25, 190, 107, 0.18);
+}
+
+.cu-device-type-card--electric.cu-device-type-card--active {
+  background: linear-gradient(135deg, #ffb347 0%, #fa8c16 100%);
+  border-color: transparent;
+  box-shadow: 0 10rpx 28rpx rgba(250, 140, 22, 0.32);
+}
+
+.cu-device-type-card--electric.cu-device-type-card--active .cu-device-type-card__icon-wrap {
+  background: rgba(255, 255, 255, 0.28);
+}
+
+.cu-device-type-card--electric.cu-device-type-card--active .cu-device-type-card__label,
+.cu-device-type-card--electric.cu-device-type-card--active .cu-device-type-card__hint {
+  color: #fff;
+}
+
+.cu-device-type-card--electric.cu-device-type-card--active .cu-device-type-card__hint {
+  opacity: 0.88;
+}
+
+.cu-device-type-card--conditioner.cu-device-type-card--active {
+  background: linear-gradient(135deg, #5cdbd3 0%, #13c2c2 100%);
+  border-color: transparent;
+  box-shadow: 0 10rpx 28rpx rgba(19, 194, 194, 0.32);
+}
+
+.cu-device-type-card--conditioner.cu-device-type-card--active .cu-device-type-card__icon-wrap {
+  background: rgba(255, 255, 255, 0.28);
+}
+
+.cu-device-type-card--conditioner.cu-device-type-card--active .cu-device-type-card__label,
+.cu-device-type-card--conditioner.cu-device-type-card--active .cu-device-type-card__hint {
+  color: #fff;
+}
+
+.cu-device-type-card--conditioner.cu-device-type-card--active .cu-device-type-card__hint {
+  opacity: 0.88;
+}
+
+.cu-ac-balance-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20rpx;
+  margin: 0 24rpx 16rpx;
+  padding: 28rpx 24rpx;
+  background: linear-gradient(135deg, #f0f7ff 0%, #fff 100%);
+  border-radius: $cu-radius;
+  border: 1rpx solid rgba(32, 128, 247, 0.12);
+  box-shadow: $cu-shadow;
+}
+
+.cu-ac-balance-card__main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-ac-balance-card__label {
+  display: block;
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+}
+
+.cu-ac-balance-card__value {
+  display: block;
+  font-size: 44rpx;
+  font-weight: 700;
+  color: $cu-primary;
+  line-height: 1.2;
+}
+
+.cu-ac-balance-card__value--danger {
+  color: $cu-danger;
+}
+
+.cu-ac-balance-card__value--success {
+  color: $cu-success;
+}
+
+.cu-ac-balance-card__btn {
+  flex-shrink: 0;
+  padding: 16rpx 32rpx;
+  background: linear-gradient(135deg, #2080f7 0%, #4a9bff 100%);
+  border-radius: 999rpx;
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #fff;
+  box-shadow: 0 6rpx 18rpx rgba(32, 128, 247, 0.28);
+}
+
+.cu-ac-balance-card__btn:active {
+  opacity: 0.9;
+}
+
+.cu-list-card--readonly .cu-list-card__meta-row {
+  padding: 16rpx 24rpx 24rpx;
+  border-top: 1rpx solid #f0f2f6;
+}
+
+.cu-list-card__meta-row {
+  padding: 0 24rpx 24rpx;
+}
+
 .cu-list-header__count {
   font-size: 24rpx;
   color: $cu-text-muted;
@@ -570,13 +1072,21 @@
   justify-content: space-between;
   gap: 12rpx;
   margin-bottom: 8rpx;
+  min-width: 0;
+}
+
+.cu-list-card__title-wrap {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
 }
 
 .cu-list-card__title {
+  display: block;
   font-size: 30rpx;
   font-weight: 600;
   color: $cu-text;
-  flex: 1;
+  width: 100%;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -645,6 +1155,10 @@
 
 .cu-info-cell__value--danger {
   color: $cu-danger;
+}
+
+.cu-info-cell__value--success {
+  color: $cu-success;
 }
 
 .cu-info-cell__value--primary {
@@ -1067,6 +1581,163 @@
 }
 
 /* 鍏呭��/缂磋垂椤� */
+.cu-recharge-hero {
+  margin: 24rpx 24rpx 0;
+  border-radius: $cu-radius;
+  overflow: hidden;
+  box-shadow: $cu-shadow;
+}
+
+.cu-recharge-hero__gradient {
+  padding: 40rpx 32rpx 56rpx;
+  background: linear-gradient(145deg, #2080f7 0%, #4a9bff 52%, #6eb3ff 100%);
+}
+
+.cu-recharge-hero--electric .cu-recharge-hero__gradient {
+  background: linear-gradient(145deg, #1a6fe0 0%, #2080f7 45%, #ffb84d 165%);
+}
+
+.cu-recharge-hero__balance-block {
+  text-align: center;
+}
+
+.cu-recharge-hero__balance-label {
+  display: block;
+  font-size: 24rpx;
+  color: rgba(255, 255, 255, 0.88);
+  margin-bottom: 12rpx;
+  letter-spacing: 1rpx;
+}
+
+.cu-recharge-hero__balance-value {
+  display: block;
+  font-size: 64rpx;
+  font-weight: 700;
+  color: #fff;
+  line-height: 1.15;
+  font-variant-numeric: tabular-nums;
+}
+
+.cu-recharge-hero__balance-value--danger {
+  color: #ffb4b0;
+  text-shadow: 0 2rpx 8rpx rgba(250, 53, 52, 0.35);
+}
+
+.cu-recharge-hero__balance-value--success {
+  color: #b8f5d0;
+  text-shadow: 0 2rpx 8rpx rgba(25, 190, 107, 0.35);
+}
+
+.cu-recharge-hero__panel {
+  margin-top: -28rpx;
+  padding: 28rpx 24rpx 24rpx;
+  background: $cu-card-bg;
+  border-radius: $cu-radius $cu-radius 0 0;
+}
+
+.cu-recharge-hero__device {
+  display: flex;
+  align-items: flex-start;
+  gap: 20rpx;
+  padding-bottom: 20rpx;
+  border-bottom: 1rpx solid #f0f2f6;
+}
+
+.cu-recharge-hero__icon {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 20rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 38rpx;
+  flex-shrink: 0;
+}
+
+.cu-recharge-hero__icon--electric {
+  background: linear-gradient(135deg, #fff7e6, #ffe8b3);
+}
+
+.cu-recharge-hero__device-main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-recharge-hero__title-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12rpx;
+  margin-bottom: 8rpx;
+  min-width: 0;
+}
+
+.cu-recharge-hero__title-wrap {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+}
+
+.cu-recharge-hero__title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 100%;
+}
+
+.cu-recharge-hero__status {
+  flex-shrink: 0;
+  font-size: 22rpx;
+  padding: 4rpx 14rpx;
+  border-radius: 999rpx;
+}
+
+.cu-recharge-hero__status--ok {
+  color: $cu-success;
+  background: rgba(25, 190, 107, 0.12);
+}
+
+.cu-recharge-hero__status--bad {
+  color: $cu-danger;
+  background: rgba(250, 53, 52, 0.1);
+}
+
+.cu-recharge-hero__sub {
+  display: block;
+  font-size: 24rpx;
+  color: $cu-text-secondary;
+  line-height: 1.5;
+}
+
+.cu-recharge-hero__meta {
+  padding-top: 20rpx;
+}
+
+.cu-recharge-hero__meta-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16rpx;
+}
+
+.cu-recharge-hero__meta-label {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  flex-shrink: 0;
+}
+
+.cu-recharge-hero__meta-value {
+  font-size: 26rpx;
+  color: $cu-text;
+  font-weight: 500;
+  text-align: right;
+  word-break: break-all;
+}
+
 .cu-device-summary {
   margin: 24rpx 24rpx 0;
   padding: 28rpx;
@@ -1075,12 +1746,19 @@
   box-shadow: $cu-shadow;
 }
 
+.cu-device-summary__title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+  margin-bottom: 4rpx;
+}
+
 .cu-device-summary__balance {
   margin-top: 20rpx;
   padding: 24rpx;
-  background: linear-gradient(135deg, #fff5f5, #fff);
+  background: linear-gradient(135deg, #f0f7ff 0%, #fff 100%);
   border-radius: 16rpx;
-  border: 1rpx solid rgba(250, 53, 52, 0.12);
+  border: 1rpx solid rgba(32, 128, 247, 0.12);
   text-align: center;
 }
 
@@ -1093,7 +1771,15 @@
 .cu-device-summary__balance-value {
   font-size: 48rpx;
   font-weight: 700;
+  color: $cu-primary;
+}
+
+.cu-device-summary__balance-value--danger {
   color: $cu-danger;
+}
+
+.cu-device-summary__balance-value--success {
+  color: $cu-success;
 }
 
 .cu-pay-amount-box {
@@ -1277,6 +1963,399 @@
   transform: translateY(2rpx);
 }
 
+.cu-bill-cost-picker--status-ok {
+  border-color: #52c41a;
+  background: linear-gradient(135deg, #f0faf4 0%, #fff 100%);
+}
+
+.cu-bill-cost-picker--status-ok .cu-bill-cost-picker__label {
+  color: $cu-text;
+  font-weight: 600;
+}
+
+.cu-bill-cost-picker--status-bad {
+  border-color: $cu-danger;
+  background: linear-gradient(135deg, #fff1f0 0%, #fff 100%);
+}
+
+.cu-bill-cost-picker--status-bad .cu-bill-cost-picker__label {
+  color: $cu-danger;
+  font-weight: 600;
+}
+
+.cu-bill-cost-picker--status-pending {
+  border-color: $cu-warning;
+  background: linear-gradient(135deg, #fff7e6 0%, #fff 100%);
+}
+
+.cu-bill-cost-picker--status-pending .cu-bill-cost-picker__label {
+  color: $cu-warning;
+  font-weight: 600;
+}
+
+.cu-recharge-record-page__header {
+  padding: 24rpx 24rpx 8rpx;
+}
+
+.cu-recharge-filter-panel {
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+  padding: 24rpx;
+  margin-bottom: 16rpx;
+}
+
+.cu-recharge-filter-panel__section + .cu-recharge-filter-panel__section {
+  margin-top: 0;
+}
+
+.cu-recharge-filter-panel__label {
+  display: block;
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  margin-bottom: 16rpx;
+  line-height: 1.2;
+}
+
+.cu-recharge-filter-panel__row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16rpx;
+  margin-bottom: 16rpx;
+}
+
+.cu-recharge-filter-panel__row .cu-recharge-filter-panel__label {
+  margin-bottom: 0;
+  flex-shrink: 0;
+}
+
+.cu-recharge-filter-panel__divider {
+  height: 1rpx;
+  background: #f0f2f6;
+  margin: 24rpx 0;
+}
+
+.cu-recharge-filter-panel__dates {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+
+.cu-recharge-status-tabs {
+  white-space: nowrap;
+  width: 100%;
+}
+
+.cu-recharge-status-tab {
+  display: inline-block;
+  padding: 14rpx 28rpx;
+  margin-right: 12rpx;
+  font-size: 26rpx;
+  color: $cu-text-secondary;
+  background: #f4f6fb;
+  border-radius: 999rpx;
+  line-height: 1.2;
+  border: 2rpx solid transparent;
+  transition: all 0.2s ease;
+}
+
+.cu-recharge-status-tab:last-child {
+  margin-right: 0;
+}
+
+.cu-recharge-status-tab--active {
+  color: $cu-primary;
+  background: linear-gradient(135deg, #e8f2ff 0%, #f0f7ff 100%);
+  border-color: rgba(32, 128, 247, 0.25);
+  font-weight: 600;
+}
+
+.cu-recharge-status-tab--active.cu-recharge-status-tab--ok {
+  color: $cu-success;
+  background: linear-gradient(135deg, #f0faf4 0%, #fff 100%);
+  border-color: rgba(25, 190, 107, 0.25);
+}
+
+.cu-recharge-status-tab--active.cu-recharge-status-tab--bad {
+  color: $cu-danger;
+  background: linear-gradient(135deg, #fff1f0 0%, #fff 100%);
+  border-color: rgba(250, 53, 52, 0.25);
+}
+
+.cu-recharge-status-tab--active.cu-recharge-status-tab--pending {
+  color: $cu-warning;
+  background: linear-gradient(135deg, #fff7e6 0%, #fff 100%);
+  border-color: rgba(255, 153, 0, 0.25);
+}
+
+.cu-recharge-date-mode {
+  display: flex;
+  padding: 4rpx;
+  background: #eef1f6;
+  border-radius: 999rpx;
+  flex-shrink: 0;
+}
+
+.cu-recharge-date-mode__item {
+  padding: 10rpx 22rpx;
+  font-size: 22rpx;
+  color: $cu-text-secondary;
+  border-radius: 999rpx;
+  line-height: 1.2;
+}
+
+.cu-recharge-date-mode__item--active {
+  background: #fff;
+  color: $cu-primary;
+  font-weight: 600;
+  box-shadow: 0 4rpx 12rpx rgba(15, 35, 95, 0.08);
+}
+
+.cu-recharge-date-range {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-recharge-date-field {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8rpx;
+  padding: 16rpx 20rpx;
+  background: #f8fafc;
+  border-radius: 16rpx;
+  border: 2rpx solid #eef1f6;
+}
+
+.cu-recharge-date-field--active {
+  background: #f0f7ff;
+  border-color: rgba(32, 128, 247, 0.2);
+}
+
+.cu-recharge-date-field__text {
+  flex: 1;
+  min-width: 0;
+  font-size: 26rpx;
+  color: $cu-text-secondary;
+  line-height: 1.3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.cu-recharge-date-field--active .cu-recharge-date-field__text {
+  color: $cu-primary;
+  font-weight: 600;
+}
+
+.cu-recharge-date-field__arrow {
+  flex-shrink: 0;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  line-height: 1;
+}
+
+.cu-recharge-date-sep {
+  flex-shrink: 0;
+  font-size: 24rpx;
+  color: $cu-text-muted;
+}
+
+.cu-recharge-date-clear {
+  flex-shrink: 0;
+  font-size: 24rpx;
+  color: $cu-primary;
+  padding: 8rpx 0;
+}
+
+.cu-recharge-summary {
+  display: flex;
+  align-items: baseline;
+  gap: 8rpx;
+  padding: 0 8rpx 16rpx;
+}
+
+.cu-recharge-summary__count {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: $cu-text;
+  line-height: 1;
+}
+
+.cu-recharge-summary__label {
+  font-size: 26rpx;
+  color: $cu-text-muted;
+}
+
+.cu-recharge-record-page .cu-list-wrap {
+  padding-top: 0;
+}
+
+.cu-recharge-card {
+  display: flex;
+  background: $cu-card-bg;
+  border-radius: $cu-radius;
+  box-shadow: $cu-shadow;
+  overflow: hidden;
+  margin-bottom: 20rpx;
+}
+
+.cu-recharge-card__accent {
+  width: 8rpx;
+  flex-shrink: 0;
+}
+
+.cu-recharge-card__accent--electric {
+  background: linear-gradient(180deg, #ffc069 0%, #fa8c16 100%);
+}
+
+.cu-recharge-card__accent--conditioner {
+  background: linear-gradient(180deg, #5cdbd3 0%, #13c2c2 100%);
+}
+
+.cu-recharge-card__body {
+  flex: 1;
+  min-width: 0;
+  padding: 24rpx 24rpx 20rpx;
+}
+
+.cu-recharge-card__top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16rpx;
+  margin-bottom: 16rpx;
+}
+
+.cu-recharge-card__type {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  padding: 8rpx 16rpx;
+  border-radius: 999rpx;
+  flex-shrink: 0;
+}
+
+.cu-recharge-card__type--electric {
+  background: #fff7e6;
+}
+
+.cu-recharge-card__type--conditioner {
+  background: #e6fffb;
+}
+
+.cu-recharge-card__type-icon {
+  font-size: 22rpx;
+  line-height: 1;
+}
+
+.cu-recharge-card__type-text {
+  font-size: 22rpx;
+  font-weight: 600;
+  line-height: 1.2;
+}
+
+.cu-recharge-card__type--electric .cu-recharge-card__type-text {
+  color: #d46b08;
+}
+
+.cu-recharge-card__type--conditioner .cu-recharge-card__type-text {
+  color: #08979c;
+}
+
+.cu-recharge-card__title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 600;
+  color: $cu-text;
+  line-height: 1.4;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.cu-recharge-card__operator {
+  display: block;
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  margin-top: 8rpx;
+  line-height: 1.4;
+}
+
+.cu-recharge-card__amount-box {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  gap: 24rpx;
+  margin-top: 20rpx;
+  padding: 20rpx;
+  background: #f8fafc;
+  border-radius: 16rpx;
+}
+
+.cu-recharge-card__amount-main {
+  flex: 1;
+  min-width: 0;
+}
+
+.cu-recharge-card__amount-side {
+  flex-shrink: 0;
+  text-align: right;
+}
+
+.cu-recharge-card__amount-label {
+  display: block;
+  font-size: 22rpx;
+  color: $cu-text-muted;
+  margin-bottom: 8rpx;
+  line-height: 1.2;
+}
+
+.cu-recharge-card__amount-value {
+  display: block;
+  font-size: 36rpx;
+  font-weight: 700;
+  color: $cu-primary;
+  line-height: 1.2;
+  font-variant-numeric: tabular-nums;
+}
+
+.cu-recharge-card__amount-side-value {
+  display: block;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $cu-text;
+  line-height: 1.2;
+  font-variant-numeric: tabular-nums;
+}
+
+.cu-recharge-card__amount-side-value--success {
+  color: $cu-success;
+}
+
+.cu-recharge-card__amount-side-value--danger {
+  color: $cu-danger;
+}
+
+.cu-recharge-card__foot {
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #f0f2f6;
+}
+
+.cu-recharge-card__time {
+  font-size: 24rpx;
+  color: $cu-text-muted;
+  line-height: 1.4;
+}
+
 .cu-bill-tabs {
   margin-bottom: 16rpx;
 }
diff --git a/h5/utils/utils.js b/h5/utils/utils.js
index 08378e8..b0423f4 100644
--- a/h5/utils/utils.js
+++ b/h5/utils/utils.js
@@ -161,3 +161,11 @@
         return false;
     }
 }
+
+/** 璐︽埛浣欓鑹茶皟锛�>0 缁胯壊锛�<=0 绾㈣壊锛屾棤鏁堝�兼棤鑹茶皟 */
+export function getBalanceTone (val) {
+  if (val === null || val === undefined || val === '') return ''
+  const n = Number(val)
+  if (Number.isNaN(n)) return ''
+  return n > 0 ? 'success' : 'danger'
+}
diff --git a/server/db/business.yw_conditioner_device.menu.sql b/server/db/business.yw_conditioner_device.menu.sql
new file mode 100644
index 0000000..e9c90fe
--- /dev/null
+++ b/server/db/business.yw_conditioner_device.menu.sql
@@ -0,0 +1,20 @@
+-- 绌鸿皟澶氳仈鏈猴細鏂板瀛愯彍鍗曘�岀┖璋冭澶囩鐞嗐��+ 瓒呯骇绠$悊鍛樻巿鏉冿紙鍙噸澶嶆墽琛岋級
+
+INSERT INTO `SYSTEM_MENU` (`PARENT_ID`, `NAME`, `PATH`, `REMARK`, `ICON`, `DISABLED`, `SORT`, `FIXED`, `CREATE_TIME`, `UPDATE_TIME`, `CREATE_USER`, `UPDATE_USER`, `DELETED`, `PARAMS`)
+SELECT p.`ID`, '绌鸿皟璁惧绠$悊', '/business/ywconditionerdevice', '绌鸿皟鍐呮満鍒楄〃涓庢埧婧愬叧鑱旂淮鎶�', NULL, 0,
+       IFNULL((SELECT MAX(sm.`SORT`) FROM `SYSTEM_MENU` sm WHERE sm.`PARENT_ID` = p.`ID` AND sm.`DELETED` = 0), 0) + 1,
+       0, CURRENT_TIMESTAMP, NULL, 1, NULL, 0, NULL
+FROM `SYSTEM_MENU` p
+WHERE p.`DELETED` = 0 AND p.`NAME` = '绌鸿皟澶氳仈鏈�' AND (p.`PATH` IS NULL OR p.`PATH` = '')
+  AND NOT EXISTS (SELECT 1 FROM `SYSTEM_MENU` x WHERE x.`DELETED` = 0 AND x.`PATH` = '/business/ywconditionerdevice')
+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/ywconditionerdevice' AND menu.`DELETED` = 0
+WHERE r.`DELETED` = 0 AND (r.`CODE` = 'admin' OR r.`NAME` IN ('瓒呯骇绠$悊鍛�', '绠$悊鍛�'))
+  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
+  );
diff --git a/server/db/business.yw_customer_member_openid.sql b/server/db/business.yw_customer_member_openid.sql
new file mode 100644
index 0000000..dd57603
--- /dev/null
+++ b/server/db/business.yw_customer_member_openid.sql
@@ -0,0 +1,22 @@
+-- 鍟嗘埛 H5 浜哄憳璐﹀彿 openid锛坢ember.type=3锛夛紝鏀寔鍚屼竴瀹㈡埛澶氫汉鍛樼嫭绔嬬櫥褰�
+SET @db = DATABASE();
+
+SET @sql = IF(
+  (SELECT COUNT(*) FROM information_schema.statistics
+   WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'member' AND INDEX_NAME = 'idx_member_openid') = 0,
+  'ALTER TABLE `member` ADD INDEX `idx_member_openid` (`openid`)',
+  'SELECT 1'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 灏嗗巻鍙� yw_customer.openid 杩佺Щ鍒伴粯璁よ仈绯讳汉 member.openid
+UPDATE `member` m
+INNER JOIN `yw_customer` c ON c.member_id = m.id AND c.isdeleted = 0
+SET m.openid = c.openid, m.edit_date = NOW()
+WHERE m.isdeleted = 0
+  AND m.type = 3
+  AND c.openid IS NOT NULL
+  AND c.openid != ''
+  AND (m.openid IS NULL OR m.openid = '');
diff --git a/server/db/business.yw_electrical_charge.member.sql b/server/db/business.yw_electrical_charge.member.sql
new file mode 100644
index 0000000..98e3723
--- /dev/null
+++ b/server/db/business.yw_electrical_charge.member.sql
@@ -0,0 +1,32 @@
+-- 鍏呭�艰褰曪細鍏宠仈鎿嶄綔浜哄憳锛坢ember锛夊強灞曠ず濮撳悕
+SET @db = DATABASE();
+
+SET @sql = IF(
+  (SELECT COUNT(*) FROM information_schema.columns
+   WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'yw_electrical_charge' AND COLUMN_NAME = 'member_id') = 0,
+  'ALTER TABLE `yw_electrical_charge` ADD COLUMN `member_id` int DEFAULT NULL COMMENT ''鍏呭�兼搷浣滀汉鍛�(member.id)'' AFTER `customer_id`',
+  'SELECT 1'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @sql = IF(
+  (SELECT COUNT(*) FROM information_schema.columns
+   WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'yw_electrical_charge' AND COLUMN_NAME = 'recharge_user_name') = 0,
+  'ALTER TABLE `yw_electrical_charge` ADD COLUMN `recharge_user_name` varchar(64) DEFAULT NULL COMMENT ''鍏呭�间汉濮撳悕'' AFTER `member_id`',
+  'SELECT 1'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @sql = IF(
+  (SELECT COUNT(*) FROM information_schema.columns
+   WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'yw_wx_pay_order' AND COLUMN_NAME = 'member_id') = 0,
+  'ALTER TABLE `yw_wx_pay_order` ADD COLUMN `member_id` int DEFAULT NULL COMMENT ''涓嬪崟鎿嶄綔浜哄憳(member.id)'' AFTER `customer_id`',
+  'SELECT 1'
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/server/system_service/src/main/java/com/doumee/config/jwt/JwtTokenUtil.java b/server/system_service/src/main/java/com/doumee/config/jwt/JwtTokenUtil.java
index b5a1acc..e277949 100644
--- a/server/system_service/src/main/java/com/doumee/config/jwt/JwtTokenUtil.java
+++ b/server/system_service/src/main/java/com/doumee/config/jwt/JwtTokenUtil.java
@@ -132,21 +132,29 @@
         try {
             //鐧诲嚭娴峰悍绯荤粺鏁版嵁
             LoginUserInfo loginUserInfo = this.getUserInfoByToken(token);
-            String url = systemDictDataBiz.queryByCode(Constants.HK_PARAM,Constants.HK_HTTPS).getCode() +
-                    systemDictDataBiz.queryByCode(Constants.HK_PARAM,Constants.HK_HOST).getCode() +
-                    systemDictDataBiz.queryByCode(Constants.HK_PARAM,Constants.LOGIN_OUT_URL).getCode();
-            if(StringUtils.isNotBlank(loginUserInfo.getHkMenuToken())){
+            if (loginUserInfo != null && StringUtils.isNotBlank(loginUserInfo.getHkMenuToken())) {
+                String url = systemDictDataBiz.queryByCode(Constants.HK_PARAM,Constants.HK_HTTPS).getCode() +
+                        systemDictDataBiz.queryByCode(Constants.HK_PARAM,Constants.HK_HOST).getCode() +
+                        systemDictDataBiz.queryByCode(Constants.HK_PARAM,Constants.LOGIN_OUT_URL).getCode();
                 log.info("璋冭捣娴峰悍閫�鍑虹櫥褰�=======================>"+url+"?token="+loginUserInfo.getHkMenuToken());
-//                this.hkLoginOut(url+"?token="+loginUserInfo.getHkMenuToken());
                 HttpsUtil.get(url+"?token="+loginUserInfo.getHkMenuToken(),true);
             }
-            redisTemplate.delete(Constants.REDIS_TOKEN_KEY+token);//鍒犻櫎鑰佺殑token
-            systemLoginService.cleanOpenid(loginUserInfo.getId());
+            invalidateToken(token);
+            if (loginUserInfo != null && !Constants.equalsInteger(loginUserInfo.getH5UserType(), LoginUserInfo.H5_USER_CUSTOMER)) {
+                systemLoginService.cleanOpenid(loginUserInfo.getId());
+            }
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 
+    /** 浠呭け鏁� token锛屼笉娓呯悊 openid锛堝晢鎴� H5 閫�鍑虹敱涓氬姟灞傚崟鐙В缁� member锛� */
+    public void invalidateToken(String token) {
+        if (StringUtils.isNotBlank(token)) {
+            redisTemplate.delete(Constants.REDIS_TOKEN_KEY + token);
+        }
+    }
+
 
     public void hkLoginOut(String url){
         try {
diff --git a/server/system_service/src/main/java/com/doumee/core/model/LoginUserInfo.java b/server/system_service/src/main/java/com/doumee/core/model/LoginUserInfo.java
index fc97fe1..8bf289f 100644
--- a/server/system_service/src/main/java/com/doumee/core/model/LoginUserInfo.java
+++ b/server/system_service/src/main/java/com/doumee/core/model/LoginUserInfo.java
@@ -59,6 +59,15 @@
     @ApiModelProperty("鍟嗘埛ID锛坔5UserType=1鏃舵湁鍊硷級")
     private Integer customerId;
 
+    @ApiModelProperty("鍟嗘埛鍚嶇О锛坔5UserType=1鏃舵湁鍊硷級")
+    private String customerName;
+
+    @ApiModelProperty("鍟嗘埛浜哄憳濮撳悕锛坔5UserType=1鏃舵湁鍊硷級")
+    private String memberName;
+
+    @ApiModelProperty("灞曠ず鍚嶇О锛氬鎴峰悕绉�-浜哄憳鍚嶇О")
+    private String displayName;
+
     public static final int H5_USER_OPS = 0;
     public static final int H5_USER_CUSTOMER = 1;
     public static final int SOURCE_H5_CUSTOMER = 10;
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwConditionerCloudController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwConditionerCloudController.java
index 9f79a7a..35e33c4 100644
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwConditionerCloudController.java
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwConditionerCloudController.java
@@ -7,6 +7,7 @@
 import com.doumee.core.model.PageData;
 import com.doumee.core.model.PageWrap;
 import com.doumee.core.utils.Constants;
+import com.doumee.dao.business.dto.YwConditionerEditDTO;
 import com.doumee.dao.business.dto.YwConditionerLockDTO;
 import com.doumee.dao.business.dto.YwConditionerOperateDTO;
 import com.doumee.dao.business.model.YwConditioner;
@@ -158,4 +159,30 @@
     public ApiResponse<List<Map<String, Object>>> gatewayOptions(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
         return ApiResponse.success(ywConditionerService.gatewayOptions());
     }
+
+    @ApiOperation("绌鸿皟璁惧绠$悊鍒嗛〉")
+    @PostMapping("/deviceManagePage")
+    @CloudRequiredPermission("business:ywconditioner:query")
+    public ApiResponse<PageData<YwConditioner>> deviceManagePage(@RequestBody PageWrap<YwConditioner> pageWrap,
+                                                                 @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywConditionerService.findDeviceManagePage(pageWrap));
+    }
+
+    @ApiOperation("绌鸿皟璁惧绠$悊璇︽儏")
+    @GetMapping("/manageDetail/{id}")
+    @CloudRequiredPermission("business:ywconditioner:update")
+    public ApiResponse<YwConditionerEditDTO> manageDetail(@PathVariable Integer id,
+                                                            @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        return ApiResponse.success(ywConditionerService.getManageDetail(id));
+    }
+
+    @PreventRepeat
+    @ApiOperation("淇濆瓨绌鸿皟鎴挎簮鍏宠仈")
+    @PostMapping("/saveManageDetail")
+    @CloudRequiredPermission("business:ywconditioner:update")
+    public ApiResponse<Void> saveManageDetail(@RequestBody YwConditionerEditDTO dto,
+                                              @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        ywConditionerService.updateManageDetail(dto, this.getLoginUser(token));
+        return ApiResponse.success(null);
+    }
 }
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerH5Controller.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerH5Controller.java
index 0cf420e..6de51ca 100644
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerH5Controller.java
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/web/YwCustomerH5Controller.java
@@ -95,7 +95,20 @@
         if (user == null || !Constants.equalsInteger(user.getH5UserType(), LoginUserInfo.H5_USER_CUSTOMER)) {
             return ApiResponse.failed("鐧诲綍宸插け鏁�");
         }
-        return ApiResponse.success(ywCustomerH5AuthService.buildLoginUserInfo(user.getCustomerId()));
+        return ApiResponse.success(ywCustomerH5AuthService.buildLoginUserInfo(user.getCustomerId(), user.getMemberId()));
+    }
+
+    @ApiOperation("鍟嗘埛閫�鍑虹櫥褰�")
+    @PostMapping("/logout")
+    public ApiResponse<String> logout(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        try {
+            LoginUserInfo user = requireCustomerUser(token);
+            ywCustomerH5AuthService.logout(user, token);
+            return ApiResponse.success("閫�鍑烘垚鍔�");
+        } catch (Exception e) {
+            log.error("customer logout failed", e);
+            return ApiResponse.failed(ResponseStatus.SERVER_ERROR.getCode(), "閫�鍑哄け璐�");
+        }
     }
 
     @ApiOperation("宸ヤ綔鍙拌疆鎾浘")
@@ -109,7 +122,7 @@
     @GetMapping("/home")
     public ApiResponse<Map<String, Object>> home(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
         LoginUserInfo user = requireCustomerUser(token);
-        return ApiResponse.success(ywCustomerH5BizService.home(user.getCustomerId()));
+        return ApiResponse.success(ywCustomerH5BizService.home(user.getCustomerId(), user.getMemberId()));
     }
 
     @ApiOperation("浜ょ數璐硅澶囧垪琛�")
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwConditionerEditDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwConditionerEditDTO.java
new file mode 100644
index 0000000..f739b59
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwConditionerEditDTO.java
@@ -0,0 +1,14 @@
+package com.doumee.dao.business.dto;
+
+import com.doumee.dao.business.model.YwConditioner;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class YwConditionerEditDTO extends YwConditioner {
+
+    private List<Integer> roomIds;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordVO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordVO.java
index 8ff517e..8eec451 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordVO.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwCustomerRechargeRecordVO.java
@@ -58,4 +58,6 @@
     private Integer objId;
     private String address;
     private String name;
+    private Integer memberId;
+    private String rechargeUserName;
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceH5VO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceH5VO.java
index 9c3e768..378d07c 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceH5VO.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/h5/CustomerDeviceH5VO.java
@@ -17,6 +17,7 @@
     private String roomInfo;
     private List<String> roomList;
     private String meterAccountNo;
+    private String meterAddress;
     private BigDecimal totalUsage;
     private BigDecimal balance;
     private Boolean balanceLow;
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwConditioner.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwConditioner.java
index da9272c..4e5e117 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwConditioner.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwConditioner.java
@@ -139,4 +139,16 @@
     @TableField(exist = false)
     @ApiModelProperty("鍟嗘埛鍏宠仈-鐢佃垂鍗犳瘮%")
     private Integer devRatio;
+
+    @TableField(exist = false)
+    @ApiModelProperty("缁戝畾鎴块棿灞曠ず鍚嶏紙澶氭埧婧愶級")
+    private String roomNames;
+
+    @TableField(exist = false)
+    @ApiModelProperty("璁惧绠$悊鍒楄〃-鍚嶇О/缂栧彿鍏抽敭璇�")
+    private String manageKeyword;
+
+    @TableField(exist = false)
+    @ApiModelProperty("璁惧绠$悊鍒楄〃-鍦ㄧ嚎绛涢�夛細鍦ㄧ嚎/绂荤嚎")
+    private String manageOnlineFilter;
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java
index 7970889..631a406 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwElectricalCharge.java
@@ -65,6 +65,10 @@
     @ApiModelProperty("瀹㈡埛涓婚敭锛堝叧鑱攜w_customer)")
     @ExcelColumn(name="瀹㈡埛涓婚敭锛堝叧鑱攜w_customer)",index=14 ,width=10)
     private Integer customerId;
+    @ApiModelProperty("鍏呭�兼搷浣滀汉鍛�(member.id)")
+    private Integer memberId;
+    @ApiModelProperty("鍏呭�间汉濮撳悕")
+    private String rechargeUserName;
     @ApiModelProperty("寰俊鏀粯璁㈠崟鍙�")
     private String wxOrderNo;
     @ApiModelProperty("鍏ヨ处鏃ユ湡")
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwWxPayOrder.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwWxPayOrder.java
index 21687ad..b2cc867 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwWxPayOrder.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/model/YwWxPayOrder.java
@@ -29,6 +29,8 @@
     private String orderNo;
     @ApiModelProperty("浠樻鍟嗘埛")
     private Integer customerId;
+    @ApiModelProperty("涓嬪崟鎿嶄綔浜哄憳(member.id)")
+    private Integer memberId;
     @ApiModelProperty("0鐢佃〃 1绌鸿皟 2璐﹀崟")
     private Integer orderType;
     @ApiModelProperty("涓氬姟寮曠敤ID")
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwConditionerService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwConditionerService.java
index 682e7e0..27ba7ff 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwConditionerService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwConditionerService.java
@@ -3,6 +3,7 @@
 import com.doumee.core.model.LoginUserInfo;
 import com.doumee.core.model.PageData;
 import com.doumee.core.model.PageWrap;
+import com.doumee.dao.business.dto.YwConditionerEditDTO;
 import com.doumee.dao.business.dto.YwConditionerLockDTO;
 import com.doumee.dao.business.dto.YwConditionerOperateDTO;
 import com.doumee.dao.business.model.YwConditioner;
@@ -37,4 +38,10 @@
     PageData<YwConditionerActions> historyPage(PageWrap<YwConditionerActions> pageWrap);
 
     List<Map<String, Object>> gatewayOptions();
+
+    PageData<YwConditioner> findDeviceManagePage(PageWrap<YwConditioner> pageWrap);
+
+    YwConditionerEditDTO getManageDetail(Integer id);
+
+    void updateManageDetail(YwConditionerEditDTO dto, LoginUserInfo user);
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerDeviceAutoBindService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerDeviceAutoBindService.java
index 9b2dbfa..a1c34f1 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerDeviceAutoBindService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerDeviceAutoBindService.java
@@ -2,6 +2,9 @@
 
 import com.doumee.core.model.LoginUserInfo;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * 鏍规嵁鍟嗘埛绉熻祦鍚堝悓鑷姩鍏宠仈鐢佃〃/绌鸿皟璁惧
  */
@@ -15,4 +18,20 @@
 
     /** 鍚堝悓閫�绉�/鍒版湡鏃惰В闄よ嚜鍔ㄥ叧鑱� */
     void unbindByContractId(Integer contractId, LoginUserInfo user);
+
+    /** 鍒锋柊鍟嗘埛璁惧锛氭竻鐞嗗け鏁堝悎鍚岀粦瀹氬苟鎸夋湁鏁堝悎鍚岄噸鏂板叧鑱� */
+    void refreshCustomerDevices(Integer customerId, LoginUserInfo user);
+
+    /** 鏈夋晥鍚堝悓绉熻祦鎴挎簮 ID */
+    List<Integer> listActiveContractRoomIds(Integer customerId);
+
+    /** 鏈夋晥鍚堝悓鍏宠仈鐨勭數琛� ID */
+    List<Integer> listElectricalIdsByActiveContracts(Integer customerId);
+
+    /** 鏈夋晥鍚堝悓鍏宠仈鐨勭┖璋冨唴鏈� ID */
+    List<Integer> listConditionerIdsByActiveContracts(Integer customerId);
+
+    Map<Integer, List<Integer>> batchListElectricalIdsByActiveContracts(List<Integer> customerIds);
+
+    Map<Integer, List<Integer>> batchListConditionerIdsByActiveContracts(List<Integer> customerIds);
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5AuthService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5AuthService.java
index ca0cae6..962b68f 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5AuthService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5AuthService.java
@@ -11,8 +11,11 @@
 
     String loginByOpenId(String openId);
 
-    LoginUserInfo buildLoginUserInfo(Integer customerId);
+    LoginUserInfo buildLoginUserInfo(Integer customerId, Integer memberId);
 
     /** 鍙戦獙璇佺爜鍓嶆牎楠岋細鍟嗘埛鎵嬫満鍙烽』瀵瑰簲鏈夋晥 yw_customer锛堝惈鑱旂郴浜� member.phone锛� */
     void assertActiveCustomerByPhone(String phone);
+
+    /** 閫�鍑虹櫥褰曪細瑙g粦 member openid 骞跺け鏁� token */
+    void logout(LoginUserInfo user, String token);
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5BizService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5BizService.java
index 30760de..0576e25 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5BizService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwCustomerH5BizService.java
@@ -16,7 +16,7 @@
 
     List<YwH5Banner> listBanners();
 
-    Map<String, Object> home(Integer customerId);
+    Map<String, Object> home(Integer customerId, Integer memberId);
 
     PageData<CustomerDeviceH5VO> devicePage(PageWrap<CustomerDeviceQueryDTO> pageWrap, Integer customerId);
 
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/SmsEmailServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/SmsEmailServiceImpl.java
index 4129a20..05bea91 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/SmsEmailServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/SmsEmailServiceImpl.java
@@ -176,13 +176,6 @@
     }
 
     private YwCustomer findMerchantByPhone(String phone) {
-        YwCustomer byCustomerPhone = ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
-                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                .eq(YwCustomer::getPhone, phone)
-                .last(" limit 1 "));
-        if (byCustomerPhone != null) {
-            return byCustomerPhone;
-        }
         Member member = memberMapper.selectOne(new QueryWrapper<Member>().lambda()
                 .eq(Member::getIsdeleted, Constants.ZERO)
                 .eq(Member::getType, Constants.memberType.customer)
@@ -190,19 +183,22 @@
                 .isNotNull(Member::getCustomerId)
                 .orderByDesc(Member::getId)
                 .last(" limit 1 "));
-        if (member == null || member.getCustomerId() == null) {
-            return null;
-        }
-        YwCustomer customer = ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
-                .eq(YwCustomer::getId, member.getCustomerId())
-                .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                .last(" limit 1 "));
-        if (customer != null) {
-            return customer;
+        if (member != null && member.getCustomerId() != null) {
+            YwCustomer customer = ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
+                    .eq(YwCustomer::getId, member.getCustomerId())
+                    .eq(YwCustomer::getIsdeleted, Constants.ZERO)
+                    .last(" limit 1 "));
+            if (customer != null) {
+                return customer;
+            }
+            return ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
+                    .eq(YwCustomer::getIsdeleted, Constants.ZERO)
+                    .eq(YwCustomer::getMemberId, member.getId())
+                    .last(" limit 1 "));
         }
         return ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
                 .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                .eq(YwCustomer::getMemberId, member.getId())
+                .eq(YwCustomer::getPhone, phone)
                 .last(" limit 1 "));
     }
 
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwConditionerServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwConditionerServiceImpl.java
index 8c74504..ed66477 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwConditionerServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwConditionerServiceImpl.java
@@ -8,13 +8,19 @@
 import com.doumee.core.utils.Constants;
 import com.doumee.core.utils.DateUtil;
 import com.doumee.core.utils.Utils;
+import com.doumee.dao.business.dto.YwConditionerEditDTO;
 import com.doumee.dao.business.dto.YwConditionerLockDTO;
 import com.doumee.dao.business.dto.YwConditionerOperateDTO;
 import com.doumee.dao.business.YwConditionerGatewayMapper;
 import com.doumee.dao.business.YwConditionerMapper;
+import com.doumee.dao.business.YwElectricalRoomMapper;
 import com.doumee.dao.business.model.YwConditioner;
 import com.doumee.dao.business.model.YwConditionerActions;
 import com.doumee.dao.business.model.YwConditionerGateway;
+import com.doumee.dao.business.model.YwElectricalRoom;
+import com.doumee.dao.business.model.YwBuilding;
+import com.doumee.dao.business.model.YwFloor;
+import com.doumee.dao.business.model.YwRoom;
 import com.doumee.service.business.ConditionerBizService;
 import com.doumee.service.business.YwConditionerActionsService;
 import com.doumee.service.business.YwConditionerService;
@@ -25,9 +31,13 @@
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -45,6 +55,8 @@
 
     @Autowired
     private YwConditionerMapper ywConditionerMapper;
+    @Autowired
+    private YwElectricalRoomMapper ywElectricalRoomMapper;
     @Autowired
     private YwConditionerGatewayMapper gatewayMapper;
     @Autowired
@@ -252,4 +264,140 @@
     public List<Map<String, Object>> gatewayOptions() {
         return conditionerBizService.gatewayOptions();
     }
+
+    @Override
+    public PageData<YwConditioner> findDeviceManagePage(PageWrap<YwConditioner> pageWrap) {
+        IPage<YwConditioner> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
+        Utils.MP.blankToNull(pageWrap.getModel());
+        YwConditioner model = pageWrap.getModel() == null ? new YwConditioner() : pageWrap.getModel();
+        MPJLambdaWrapper<YwConditioner> queryWrapper = new MPJLambdaWrapper<>();
+        queryWrapper.selectAll(YwConditioner.class)
+                .eq(YwConditioner::getIsdeleted, Constants.ZERO)
+                .and(StringUtils.isNotBlank(model.getManageKeyword()), w -> w
+                        .like(YwConditioner::getName, model.getManageKeyword())
+                        .or().like(YwConditioner::getCode, model.getManageKeyword())
+                        .or().like(YwConditioner::getRoomName, model.getManageKeyword())
+                        .or().like(YwConditioner::getWgMac, model.getManageKeyword()))
+                .eq(StringUtils.isNotBlank(model.getManageOnlineFilter()), YwConditioner::getOnline, model.getManageOnlineFilter())
+                .eq(StringUtils.isNotBlank(model.getWgMacFilter()), YwConditioner::getWgMac, model.getWgMacFilter())
+                .orderByDesc(YwConditioner::getCreateDate)
+                .orderByDesc(YwConditioner::getId);
+        PageData<YwConditioner> pageData = PageData.from(
+                ywConditionerMapper.selectJoinPage(page, YwConditioner.class, queryWrapper));
+        fillRoomNames(pageData.getRecords());
+        fillGatewayBzFromGateway(pageData.getRecords());
+        return pageData;
+    }
+
+    @Override
+    public YwConditionerEditDTO getManageDetail(Integer id) {
+        YwConditioner conditioner = findById(id);
+        if (conditioner == null || Objects.equals(conditioner.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY);
+        }
+        YwConditionerEditDTO dto = new YwConditionerEditDTO();
+        BeanUtils.copyProperties(conditioner, dto);
+        List<YwElectricalRoom> rooms = ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
+                .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO)
+                .eq(YwElectricalRoom::getType, Constants.ONE)
+                .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 updateManageDetail(YwConditionerEditDTO dto, LoginUserInfo user) {
+        if (dto == null || dto.getId() == null) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST);
+        }
+        YwConditioner exist = findById(dto.getId());
+        if (exist == null || Objects.equals(exist.getIsdeleted(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.DATA_EMPTY);
+        }
+        saveRooms(dto.getId(), dto.getRoomIds(), user);
+        Integer primaryRoomId = CollectionUtils.isEmpty(dto.getRoomIds()) ? null : dto.getRoomIds().get(0);
+        ywConditionerMapper.update(null, new UpdateWrapper<YwConditioner>().lambda()
+                .eq(YwConditioner::getId, dto.getId())
+                .set(YwConditioner::getRoomId, primaryRoomId)
+                .set(YwConditioner::getRemark, dto.getRemark())
+                .set(YwConditioner::getEditor, user.getId())
+                .set(YwConditioner::getEditDate, new Date()));
+    }
+
+    private void saveRooms(Integer conditionerId, 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, conditionerId)
+                .eq(YwElectricalRoom::getType, Constants.ONE));
+        if (CollectionUtils.isEmpty(roomIds)) {
+            return;
+        }
+        int sort = 0;
+        for (Integer roomId : roomIds) {
+            if (roomId == null) {
+                continue;
+            }
+            YwElectricalRoom rel = new YwElectricalRoom();
+            rel.setCreator(user.getId());
+            rel.setCreateDate(new Date());
+            rel.setEditor(user.getId());
+            rel.setEditDate(new Date());
+            rel.setIsdeleted(Constants.ZERO);
+            rel.setType(Constants.ONE);
+            rel.setObjId(conditionerId);
+            rel.setRoomId(roomId);
+            rel.setSortnum(++sort);
+            ywElectricalRoomMapper.insert(rel);
+        }
+    }
+
+    private void fillRoomNames(List<? extends YwConditioner> list) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        List<Integer> ids = list.stream().map(YwConditioner::getId).filter(Objects::nonNull).collect(Collectors.toList());
+        if (ids.isEmpty()) {
+            return;
+        }
+        MPJLambdaWrapper<YwElectricalRoom> wrapper = new MPJLambdaWrapper<>();
+        wrapper.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.ONE)
+                .in(YwElectricalRoom::getObjId, ids);
+        List<YwElectricalRoom> rooms = ywElectricalRoomMapper.selectJoinList(YwElectricalRoom.class, wrapper);
+        Map<Integer, List<YwElectricalRoom>> grouped = rooms.stream()
+                .collect(Collectors.groupingBy(YwElectricalRoom::getObjId));
+        for (YwConditioner 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 room) {
+        List<String> parts = new ArrayList<>();
+        if (StringUtils.isNotBlank(room.getBuildingName())) {
+            parts.add(room.getBuildingName());
+        }
+        if (StringUtils.isNotBlank(room.getFloorName())) {
+            parts.add(room.getFloorName());
+        }
+        if (StringUtils.isNotBlank(room.getRoomName())) {
+            parts.add(room.getRoomName());
+        }
+        return String.join("/", parts);
+    }
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractServiceImpl.java
index 20689e4..b1d2cb3 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwContractServiceImpl.java
@@ -383,6 +383,9 @@
         update.setBtRemark(getbackRentRemarkByParam(param));
         ywContractMapper.updateById(update);
         dealLogBiz(param,Constants.YwLogType.CONTRACT_BACK, param.getLoginUserInfo().getRealname(),getbackRentLogByParam(param));
+        if (model.getRenterId() != null) {
+            ywCustomerDeviceAutoBindService.refreshCustomerDevices(model.getRenterId(), param.getLoginUserInfo());
+        }
         //濡傛灉閫�绉熸棩鏈熷皬浜庡綋鍓嶆棩鏈� 鍒欑洿鎺ラ噴鏀炬埧婧愪俊鎭� 鏈璧�
         if(Utils.Date.getEnd(param.getBtDate()).getTime()<System.currentTimeMillis()){
             List<YwContractRoom> contractRoomList = ywContractRoomMapper.selectList(new QueryWrapper<YwContractRoom>().lambda()
@@ -1645,7 +1648,7 @@
             queryWrapper.eq(YwContract::getType, pageWrap.getModel().getType());
         }
         if (pageWrap.getModel().getCode() != null) {
-            queryWrapper.eq(YwContract::getCode, pageWrap.getModel().getCode());
+            queryWrapper.like(YwContract::getCode, pageWrap.getModel().getCode());
         }
         if (pageWrap.getModel().getUserId() != null) {
             queryWrapper.eq(YwContract::getUserId, pageWrap.getModel().getUserId());
@@ -1716,6 +1719,9 @@
         if (pageWrap.getModel().getCompanyName() != null) {
             queryWrapper.like(Company::getName, pageWrap.getModel().getCompanyName());
         }
+        if (pageWrap.getModel().getRenterName() != null) {
+            queryWrapper.like(YwCustomer::getName, pageWrap.getModel().getRenterName());
+        }
         if (pageWrap.getModel().getRoomId() != null) {
             queryWrapper.apply(" t.id in ( select ycr.CONTRACT_ID from yw_contract_room ycr where ycr.type = 0 and  ycr.ROOM_ID = "+pageWrap.getModel().getRoomId()+" )  ");
         }
@@ -1785,6 +1791,14 @@
                     .in(YwContract::getId,ywContractList.stream().map(i->i.getId()).collect(Collectors.toList()))
             );
 
+            LoginUserInfo timerUser = new LoginUserInfo();
+            timerUser.setId(1);
+            timerUser.setRealname("timer");
+            for (YwContract c : ywContractList) {
+                if (c.getRenterId() != null) {
+                    ywCustomerDeviceAutoBindService.refreshCustomerDevices(c.getRenterId(), timerUser);
+                }
+            }
 
             List<YwContractRoom> contractRoomList = ywContractRoomMapper.selectList(new QueryWrapper<YwContractRoom>().lambda().in(YwContractRoom::getContractId,
                     ywContractList.stream().map(i->i.getId()).collect(Collectors.toList())));
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerDeviceAutoBindServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerDeviceAutoBindServiceImpl.java
index 07a3d71..3fdaadc 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerDeviceAutoBindServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerDeviceAutoBindServiceImpl.java
@@ -2,10 +2,10 @@
 
 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.exception.BusinessException;
 import com.doumee.core.model.LoginUserInfo;
 import com.doumee.core.utils.Constants;
+import com.doumee.core.utils.Utils;
 import com.doumee.dao.business.*;
 import com.doumee.dao.business.dto.YwCustomerGsConfigDTO;
 import com.doumee.dao.business.model.*;
@@ -13,6 +13,7 @@
 import com.doumee.service.business.YwCustomerRechargeBizService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
@@ -40,6 +41,7 @@
     @Autowired
     private YwConditionerMapper ywConditionerMapper;
     @Autowired
+    @Lazy
     private YwCustomerRechargeBizService ywCustomerRechargeBizService;
 
     @Override
@@ -49,38 +51,52 @@
             return;
         }
         YwContract contract = ywContractMapper.selectById(contractId);
-        if (contract == null || Objects.equals(contract.getIsdeleted(), Constants.ONE)) {
+        if (contract == null || Objects.equals(contract.getIsdeleted(), Constants.ONE) || contract.getRenterId() == null) {
             return;
         }
-        if (contract.getRenterId() == null) {
-            return;
-        }
-        if (!isActiveContract(contract)) {
-            return;
-        }
-        List<Integer> roomIds = listContractRoomIds(contractId);
-        if (roomIds.isEmpty()) {
-            return;
-        }
-        bindElectricals(contract, roomIds, user);
-        bindConditioners(contract, roomIds, user);
+        refreshCustomerDevices(contract.getRenterId(), user);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void syncByCustomerId(Integer customerId, LoginUserInfo user) {
+        refreshCustomerDevices(customerId, user);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void refreshCustomerDevices(Integer customerId, LoginUserInfo user) {
         if (customerId == null) {
             return;
         }
+        LoginUserInfo opUser = user != null ? user : systemUser();
         List<YwContract> contracts = ywContractMapper.selectList(new QueryWrapper<YwContract>().lambda()
                 .eq(YwContract::getRenterId, customerId)
-                .eq(YwContract::getIsdeleted, Constants.ZERO)
-                .in(YwContract::getStatus, Arrays.asList(Constants.ZERO, Constants.ONE, Constants.THREE)));
-        for (YwContract c : contracts) {
-            if (isActiveContract(c)) {
-                syncByContractId(c.getId(), user);
+                .eq(YwContract::getIsdeleted, Constants.ZERO));
+        List<YwContract> activeContracts = contracts.stream().filter(this::isActiveContract).collect(Collectors.toList());
+        Set<Integer> activeContractIds = activeContracts.stream().map(YwContract::getId).collect(Collectors.toSet());
+
+        for (YwContract contract : contracts) {
+            if (!activeContractIds.contains(contract.getId())) {
+                unbindByContractId(contract.getId(), opUser);
             }
         }
+
+        Set<Integer> roomIds = new LinkedHashSet<>();
+        for (YwContract contract : activeContracts) {
+            roomIds.addAll(listContractRoomIds(contract.getId()));
+        }
+        if (roomIds.isEmpty()) {
+            clearContractElectricalBindings(customerId, opUser);
+            softDeleteAllConditionerRels(customerId, opUser);
+            return;
+        }
+
+        List<Integer> roomIdList = new ArrayList<>(roomIds);
+        for (YwContract contract : activeContracts) {
+            bindElectricals(contract, listContractRoomIds(contract.getId()), opUser);
+        }
+        bindConditionersMerged(customerId, roomIdList, opUser);
     }
 
     @Override
@@ -100,14 +116,153 @@
                 .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO));
     }
 
+    @Override
+    public List<Integer> listActiveContractRoomIds(Integer customerId) {
+        if (customerId == null) {
+            return Collections.emptyList();
+        }
+        List<YwContract> active = listActiveContracts(customerId);
+        Set<Integer> roomIds = new LinkedHashSet<>();
+        for (YwContract contract : active) {
+            roomIds.addAll(listContractRoomIds(contract.getId()));
+        }
+        return new ArrayList<>(roomIds);
+    }
+
+    @Override
+    public List<Integer> listElectricalIdsByActiveContracts(Integer customerId) {
+        List<Integer> roomIds = listActiveContractRoomIds(customerId);
+        if (roomIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<>(resolveElectricalIdsFromRooms(roomIds));
+    }
+
+    @Override
+    public List<Integer> listConditionerIdsByActiveContracts(Integer customerId) {
+        List<Integer> roomIds = listActiveContractRoomIds(customerId);
+        if (roomIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return new ArrayList<>(resolveConditionerIdsFromRooms(roomIds));
+    }
+
+    @Override
+    public Map<Integer, List<Integer>> batchListElectricalIdsByActiveContracts(List<Integer> customerIds) {
+        return batchResolveDeviceIds(customerIds, Constants.ZERO);
+    }
+
+    @Override
+    public Map<Integer, List<Integer>> batchListConditionerIdsByActiveContracts(List<Integer> customerIds) {
+        return batchResolveDeviceIds(customerIds, Constants.ONE);
+    }
+
+    private Map<Integer, List<Integer>> batchResolveDeviceIds(List<Integer> customerIds, int deviceType) {
+        if (CollectionUtils.isEmpty(customerIds)) {
+            return Collections.emptyMap();
+        }
+        Map<Integer, Set<Integer>> roomsByCustomer = batchActiveContractRoomIds(customerIds);
+        Set<Integer> allRoomIds = roomsByCustomer.values().stream()
+                .flatMap(Set::stream).collect(Collectors.toCollection(LinkedHashSet::new));
+        Map<Integer, Set<Integer>> devicesByRoom = mapDevicesByRoom(allRoomIds, deviceType);
+
+        Map<Integer, List<Integer>> result = new LinkedHashMap<>();
+        for (Integer customerId : customerIds) {
+            Set<Integer> roomIds = roomsByCustomer.getOrDefault(customerId, Collections.emptySet());
+            Set<Integer> deviceIds = new LinkedHashSet<>();
+            for (Integer roomId : roomIds) {
+                deviceIds.addAll(devicesByRoom.getOrDefault(roomId, Collections.emptySet()));
+            }
+            result.put(customerId, new ArrayList<>(deviceIds));
+        }
+        return result;
+    }
+
+    private Map<Integer, Set<Integer>> batchActiveContractRoomIds(List<Integer> customerIds) {
+        List<YwContract> contracts = ywContractMapper.selectList(new QueryWrapper<YwContract>().lambda()
+                .in(YwContract::getRenterId, customerIds)
+                .eq(YwContract::getIsdeleted, Constants.ZERO));
+        Map<Integer, List<YwContract>> activeByCustomer = contracts.stream()
+                .filter(this::isActiveContract)
+                .collect(Collectors.groupingBy(YwContract::getRenterId));
+
+        List<Integer> contractIds = activeByCustomer.values().stream()
+                .flatMap(List::stream).map(YwContract::getId).distinct().collect(Collectors.toList());
+        Map<Integer, List<Integer>> roomsByContract = loadContractRoomMap(contractIds);
+
+        Map<Integer, Set<Integer>> roomsByCustomer = new LinkedHashMap<>();
+        for (Integer customerId : customerIds) {
+            Set<Integer> roomIds = new LinkedHashSet<>();
+            for (YwContract contract : activeByCustomer.getOrDefault(customerId, Collections.emptyList())) {
+                roomIds.addAll(roomsByContract.getOrDefault(contract.getId(), Collections.emptyList()));
+            }
+            roomsByCustomer.put(customerId, roomIds);
+        }
+        return roomsByCustomer;
+    }
+
+    private Map<Integer, List<Integer>> loadContractRoomMap(List<Integer> contractIds) {
+        if (CollectionUtils.isEmpty(contractIds)) {
+            return Collections.emptyMap();
+        }
+        return ywContractRoomMapper.selectList(new QueryWrapper<YwContractRoom>().lambda()
+                        .in(YwContractRoom::getContractId, contractIds)
+                        .eq(YwContractRoom::getType, Constants.ZERO)
+                        .eq(YwContractRoom::getIsdeleted, Constants.ZERO))
+                .stream()
+                .collect(Collectors.groupingBy(YwContractRoom::getContractId,
+                        Collectors.mapping(YwContractRoom::getRoomId,
+                                Collectors.collectingAndThen(Collectors.toList(),
+                                        list -> list.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList())))));
+    }
+
+    private Map<Integer, Set<Integer>> mapDevicesByRoom(Set<Integer> roomIds, int deviceType) {
+        if (CollectionUtils.isEmpty(roomIds)) {
+            return Collections.emptyMap();
+        }
+        List<Integer> roomIdList = new ArrayList<>(roomIds);
+        Map<Integer, Set<Integer>> result = new HashMap<>();
+        List<YwElectricalRoom> relRooms = ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
+                .in(YwElectricalRoom::getRoomId, roomIdList)
+                .eq(YwElectricalRoom::getType, deviceType)
+                .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO));
+        for (YwElectricalRoom rel : relRooms) {
+            if (rel.getRoomId() == null || rel.getObjId() == null) {
+                continue;
+            }
+            result.computeIfAbsent(rel.getRoomId(), k -> new LinkedHashSet<>()).add(rel.getObjId());
+        }
+        if (deviceType == Constants.ONE) {
+            List<YwConditioner> byRoom = ywConditionerMapper.selectList(new QueryWrapper<YwConditioner>().lambda()
+                    .in(YwConditioner::getRoomId, roomIdList)
+                    .eq(YwConditioner::getIsdeleted, Constants.ZERO));
+            for (YwConditioner conditioner : byRoom) {
+                if (conditioner.getRoomId() == null || conditioner.getId() == null) {
+                    continue;
+                }
+                result.computeIfAbsent(conditioner.getRoomId(), k -> new LinkedHashSet<>()).add(conditioner.getId());
+            }
+        }
+        return result;
+    }
+
+    private List<YwContract> listActiveContracts(Integer customerId) {
+        return ywContractMapper.selectList(new QueryWrapper<YwContract>().lambda()
+                        .eq(YwContract::getRenterId, customerId)
+                        .eq(YwContract::getIsdeleted, Constants.ZERO))
+                .stream().filter(this::isActiveContract).collect(Collectors.toList());
+    }
+
     private boolean isActiveContract(YwContract contract) {
-        if (contract.getStartDate() == null || contract.getEndDate() == null) {
+        if (contract == null || contract.getStartDate() == null || contract.getEndDate() == null) {
+            return false;
+        }
+        if (Objects.equals(contract.getStatus(), Constants.FOUR)) {
             return false;
         }
         long now = System.currentTimeMillis();
         return contract.getStartDate().getTime() <= now
-                && contract.getEndDate().getTime() >= now
-                && !Objects.equals(contract.getStatus(), Constants.FOUR);
+                && Utils.Date.getEnd(contract.getEndDate()).getTime() >= now;
     }
 
     private List<Integer> listContractRoomIds(Integer contractId) {
@@ -119,13 +274,39 @@
                 .collect(Collectors.toList());
     }
 
-    private void bindElectricals(YwContract contract, List<Integer> roomIds, LoginUserInfo user) {
-        List<YwElectricalRoom> relRooms = ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
+    private Set<Integer> resolveElectricalIdsFromRooms(List<Integer> roomIds) {
+        if (CollectionUtils.isEmpty(roomIds)) {
+            return Collections.emptySet();
+        }
+        return ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
+                        .in(YwElectricalRoom::getRoomId, roomIds)
+                        .eq(YwElectricalRoom::getType, Constants.ZERO)
+                        .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO))
+                .stream().map(YwElectricalRoom::getObjId).filter(Objects::nonNull)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    private Set<Integer> resolveConditionerIdsFromRooms(List<Integer> roomIds) {
+        if (CollectionUtils.isEmpty(roomIds)) {
+            return Collections.emptySet();
+        }
+        Set<Integer> conditionerIds = new LinkedHashSet<>();
+        List<YwElectricalRoom> acRooms = ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
                 .in(YwElectricalRoom::getRoomId, roomIds)
-                .eq(YwElectricalRoom::getType, Constants.ZERO)
+                .eq(YwElectricalRoom::getType, Constants.ONE)
                 .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO));
-        Set<Integer> electricalIds = relRooms.stream().map(YwElectricalRoom::getObjId)
-                .filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
+        acRooms.stream().map(YwElectricalRoom::getObjId).filter(Objects::nonNull).forEach(conditionerIds::add);
+        if (conditionerIds.isEmpty()) {
+            ywConditionerMapper.selectList(new QueryWrapper<YwConditioner>().lambda()
+                            .in(YwConditioner::getRoomId, roomIds)
+                            .eq(YwConditioner::getIsdeleted, Constants.ZERO))
+                    .stream().map(YwConditioner::getId).filter(Objects::nonNull).forEach(conditionerIds::add);
+        }
+        return conditionerIds;
+    }
+
+    private void bindElectricals(YwContract contract, List<Integer> roomIds, LoginUserInfo user) {
+        Set<Integer> electricalIds = resolveElectricalIdsFromRooms(roomIds);
         if (electricalIds.isEmpty()) {
             return;
         }
@@ -142,7 +323,8 @@
                     .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
                     .last("limit 1"));
             if (exist != null) {
-                if (exist.getContractId() == null) {
+                if (!Objects.equals(exist.getContractId(), contract.getId())
+                        || !Objects.equals(exist.getBindSource(), BIND_SOURCE_CONTRACT)) {
                     exist.setContractId(contract.getId());
                     exist.setBindSource(BIND_SOURCE_CONTRACT);
                     exist.setEditDate(now);
@@ -165,24 +347,14 @@
         }
     }
 
-    private void bindConditioners(YwContract contract, List<Integer> roomIds, LoginUserInfo user) {
-        Set<Integer> conditionerIds = new LinkedHashSet<>();
-        List<YwElectricalRoom> acRooms = ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
-                .in(YwElectricalRoom::getRoomId, roomIds)
-                .eq(YwElectricalRoom::getType, Constants.ONE)
-                .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO));
-        acRooms.stream().map(YwElectricalRoom::getObjId).filter(Objects::nonNull).forEach(conditionerIds::add);
+    private void bindConditionersMerged(Integer customerId, List<Integer> roomIds, LoginUserInfo user) {
+        Set<Integer> conditionerIds = resolveConditionerIdsFromRooms(roomIds);
         if (conditionerIds.isEmpty()) {
-            List<YwConditioner> byRoom = ywConditionerMapper.selectList(new QueryWrapper<YwConditioner>().lambda()
-                    .in(YwConditioner::getRoomId, roomIds)
-                    .eq(YwConditioner::getIsdeleted, Constants.ZERO));
-            byRoom.stream().map(YwConditioner::getId).forEach(conditionerIds::add);
-        }
-        if (conditionerIds.isEmpty()) {
+            softDeleteAllConditionerRels(customerId, user);
             return;
         }
         YwCustomerGsConfigDTO dto = new YwCustomerGsConfigDTO();
-        dto.setCustomerId(contract.getRenterId());
+        dto.setCustomerId(customerId);
         dto.setIsPwr(Constants.ONE);
         dto.setIsRestStop(Constants.ZERO);
         dto.setGsBz("鍚堝悓鑷姩鍏宠仈");
@@ -196,12 +368,33 @@
         }
         dto.setConditioners(items);
         try {
-            ywCustomerRechargeBizService.saveCustomerGsConfig(dto, user != null ? user : systemUser());
+            ywCustomerRechargeBizService.saveCustomerGsConfig(dto, user);
         } catch (BusinessException e) {
-            log.warn("auto bind conditioner GS failed contractId={}: {}", contract.getId(), e.getMessage());
+            log.warn("auto bind conditioner GS failed customerId={}: {}", customerId, e.getMessage());
         }
     }
 
+    private void clearContractElectricalBindings(Integer customerId, LoginUserInfo user) {
+        Date now = new Date();
+        ywCustomerElectricalMapper.update(null, new UpdateWrapper<YwCustomerElectrical>().lambda()
+                .set(YwCustomerElectrical::getIsdeleted, Constants.ONE)
+                .set(YwCustomerElectrical::getEditDate, now)
+                .set(YwCustomerElectrical::getEditor, user != null ? user.getId() : null)
+                .eq(YwCustomerElectrical::getCustomerId, customerId)
+                .eq(YwCustomerElectrical::getBindSource, BIND_SOURCE_CONTRACT)
+                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO));
+    }
+
+    private void softDeleteAllConditionerRels(Integer customerId, LoginUserInfo user) {
+        Date now = new Date();
+        ywCustomerConditionerMapper.update(null, new UpdateWrapper<YwCustomerConditioner>().lambda()
+                .set(YwCustomerConditioner::getIsdeleted, Constants.ONE)
+                .set(YwCustomerConditioner::getEditDate, now)
+                .set(YwCustomerConditioner::getEditor, user != null ? user.getId() : null)
+                .eq(YwCustomerConditioner::getCustomerId, customerId)
+                .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO));
+    }
+
     private Set<Integer> listBoundElectricalIdsExcept(Integer customerId) {
         List<YwCustomerElectrical> list = ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
                 .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5AuthServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5AuthServiceImpl.java
index 59d02f7..3d0b5fd 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5AuthServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5AuthServiceImpl.java
@@ -12,6 +12,7 @@
 import com.doumee.dao.business.model.Member;
 import com.doumee.dao.business.model.YwCustomer;
 import com.doumee.dao.system.dto.LoginPhoneDTO;
+import com.doumee.service.business.SmsEmailService;
 import com.doumee.service.business.YwCustomerH5AuthService;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,22 +31,37 @@
     private MemberMapper memberMapper;
     @Autowired
     private JwtTokenUtil jwtTokenUtil;
+    @Autowired
+    private SmsEmailService smsEmailService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public String loginByPhone(LoginPhoneDTO dto) {
-        YwCustomer customer = findActiveByPhone(dto.getPhone());
-        bindOpenId(customer, dto.getOpenid());
-        touchLogin(customer);
-        return jwtTokenUtil.generateToken(toLoginUserInfo(customer));
+        smsEmailService.validateCode(dto.getCode(), dto.getPhone());
+        CustomerMemberContext ctx = resolveByPhone(dto.getPhone());
+        if (ctx == null || ctx.customer == null) {
+            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT.getCode(), "鍟嗘埛涓嶅瓨鍦ㄦ垨鏈敞鍐�");
+        }
+        assertCustomerEnabled(ctx.customer);
+        if (ctx.member != null) {
+            assertMemberEnabled(ctx.member);
+            bindMemberOpenId(ctx.member, dto.getOpenid());
+            touchMemberLogin(ctx.member);
+        }
+        touchLogin(ctx.customer);
+        return jwtTokenUtil.generateToken(toLoginUserInfo(ctx.customer, ctx.member));
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public String loginByCustomerId(Integer customerId) {
         YwCustomer customer = requireActiveCustomer(customerId);
+        Member member = resolveDefaultMember(customer);
         touchLogin(customer);
-        return jwtTokenUtil.generateToken(toLoginUserInfo(customer));
+        if (member != null) {
+            touchMemberLogin(member);
+        }
+        return jwtTokenUtil.generateToken(toLoginUserInfo(customer, member));
     }
 
     @Override
@@ -54,58 +70,97 @@
         if (StringUtils.isBlank(openId)) {
             return null;
         }
+        String trimmed = openId.trim();
+        Member member = findActiveCustomerMemberByOpenId(trimmed);
+        if (member != null) {
+            YwCustomer customer = loadCustomerByMember(member);
+            if (customer != null) {
+                assertCustomerEnabled(customer);
+                assertMemberEnabled(member);
+                touchMemberLogin(member);
+                touchLogin(customer);
+                return jwtTokenUtil.generateToken(toLoginUserInfo(customer, member));
+            }
+        }
         YwCustomer customer = ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
                 .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                .eq(YwCustomer::getOpenid, openId.trim())
+                .eq(YwCustomer::getOpenid, trimmed)
                 .last(" limit 1 "));
         if (customer == null) {
             return null;
         }
         assertCustomerEnabled(customer);
+        Member defaultMember = resolveDefaultMember(customer);
+        if (defaultMember != null && StringUtils.isBlank(defaultMember.getOpenid())) {
+            bindMemberOpenId(defaultMember, trimmed);
+            touchMemberLogin(defaultMember);
+        }
         touchLogin(customer);
-        return jwtTokenUtil.generateToken(toLoginUserInfo(customer));
+        return jwtTokenUtil.generateToken(toLoginUserInfo(customer, defaultMember));
     }
 
     @Override
-    public LoginUserInfo buildLoginUserInfo(Integer customerId) {
-        return toLoginUserInfo(requireActiveCustomer(customerId));
+    public LoginUserInfo buildLoginUserInfo(Integer customerId, Integer memberId) {
+        YwCustomer customer = requireActiveCustomer(customerId);
+        Member member = resolveMemberForCustomer(customer, memberId);
+        return toLoginUserInfo(customer, member);
     }
 
     @Override
     public void assertActiveCustomerByPhone(String phone) {
-        findActiveByPhone(phone);
+        CustomerMemberContext ctx = resolveByPhone(phone);
+        if (ctx == null || ctx.customer == null) {
+            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT.getCode(), "鍟嗘埛涓嶅瓨鍦ㄦ垨鏈敞鍐�");
+        }
+        assertCustomerEnabled(ctx.customer);
+        if (ctx.member != null) {
+            assertMemberEnabled(ctx.member);
+        }
     }
 
-    private YwCustomer findActiveByPhone(String phone) {
+    private CustomerMemberContext resolveByPhone(String phone) {
         if (StringUtils.isBlank(phone)) {
             throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鎵嬫満鍙蜂笉鑳戒负绌�");
         }
-        YwCustomer customer = findCustomerByPhone(phone.trim());
-        if (customer == null) {
-            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT.getCode(), "鍟嗘埛涓嶅瓨鍦ㄦ垨鏈敞鍐�");
+        String trimmed = phone.trim();
+        Member member = findActiveCustomerMemberByPhone(trimmed);
+        if (member != null) {
+            YwCustomer customer = loadCustomerByMember(member);
+            if (customer != null) {
+                return new CustomerMemberContext(customer, member);
+            }
         }
-        assertCustomerEnabled(customer);
-        return customer;
-    }
-
-    /**
-     * 鍟嗘埛鎵嬫満鍙凤細浼樺厛 yw_customer.phone锛屽惁鍒欏尮閰嶈仈绯讳汉 member.phone
-     */
-    private YwCustomer findCustomerByPhone(String phone) {
         YwCustomer byCustomerPhone = ywCustomerMapper.selectOne(new QueryWrapper<YwCustomer>().lambda()
                 .eq(YwCustomer::getIsdeleted, Constants.ZERO)
-                .eq(YwCustomer::getPhone, phone)
+                .eq(YwCustomer::getPhone, trimmed)
                 .last(" limit 1 "));
         if (byCustomerPhone != null) {
-            return byCustomerPhone;
+            return new CustomerMemberContext(byCustomerPhone, resolveDefaultMember(byCustomerPhone));
         }
-        Member member = memberMapper.selectOne(new QueryWrapper<Member>().lambda()
+        return null;
+    }
+
+    private Member findActiveCustomerMemberByPhone(String phone) {
+        return memberMapper.selectOne(new QueryWrapper<Member>().lambda()
                 .eq(Member::getIsdeleted, Constants.ZERO)
                 .eq(Member::getType, Constants.memberType.customer)
                 .eq(Member::getPhone, phone)
                 .isNotNull(Member::getCustomerId)
                 .orderByDesc(Member::getId)
                 .last(" limit 1 "));
+    }
+
+    private Member findActiveCustomerMemberByOpenId(String openId) {
+        return memberMapper.selectOne(new QueryWrapper<Member>().lambda()
+                .eq(Member::getIsdeleted, Constants.ZERO)
+                .eq(Member::getType, Constants.memberType.customer)
+                .eq(Member::getOpenid, openId)
+                .isNotNull(Member::getCustomerId)
+                .orderByDesc(Member::getId)
+                .last(" limit 1 "));
+    }
+
+    private YwCustomer loadCustomerByMember(Member member) {
         if (member == null || member.getCustomerId() == null) {
             return null;
         }
@@ -120,6 +175,48 @@
                 .eq(YwCustomer::getIsdeleted, Constants.ZERO)
                 .eq(YwCustomer::getMemberId, member.getId())
                 .last(" limit 1 "));
+    }
+
+    private Member resolveDefaultMember(YwCustomer customer) {
+        if (customer.getMemberId() != null) {
+            Member member = memberMapper.selectById(customer.getMemberId());
+            if (isActiveCustomerMember(member)) {
+                return member;
+            }
+        }
+        return memberMapper.selectOne(new QueryWrapper<Member>().lambda()
+                .eq(Member::getIsdeleted, Constants.ZERO)
+                .eq(Member::getType, Constants.memberType.customer)
+                .eq(Member::getCustomerId, customer.getId())
+                .orderByAsc(Member::getId)
+                .last(" limit 1 "));
+    }
+
+    private Member resolveMemberForCustomer(YwCustomer customer, Integer memberId) {
+        if (memberId != null) {
+            Member member = memberMapper.selectById(memberId);
+            if (member != null && isMemberBelongsToCustomer(member, customer)) {
+                assertMemberEnabled(member);
+                return member;
+            }
+        }
+        return resolveDefaultMember(customer);
+    }
+
+    private boolean isMemberBelongsToCustomer(Member member, YwCustomer customer) {
+        if (!isActiveCustomerMember(member)) {
+            return false;
+        }
+        if (member.getCustomerId() != null && Constants.equalsInteger(member.getCustomerId(), customer.getId())) {
+            return true;
+        }
+        return customer.getMemberId() != null && Constants.equalsInteger(customer.getMemberId(), member.getId());
+    }
+
+    private boolean isActiveCustomerMember(Member member) {
+        return member != null
+                && Constants.equalsInteger(member.getIsdeleted(), Constants.ZERO)
+                && Constants.equalsInteger(member.getType(), Constants.memberType.customer);
     }
 
     private YwCustomer requireActiveCustomer(Integer customerId) {
@@ -140,15 +237,32 @@
         }
     }
 
-    private void bindOpenId(YwCustomer customer, String openid) {
-        if (StringUtils.isBlank(openid)) {
+    private void assertMemberEnabled(Member member) {
+        if (member.getStatus() != null && Constants.equalsInteger(member.getStatus(), Constants.ONE)) {
+            throw new BusinessException(ResponseStatus.NO_ALLOW_LOGIN.getCode(), "浜哄憳璐﹀彿宸茬鐢�");
+        }
+    }
+
+    private void bindMemberOpenId(Member member, String openid) {
+        if (member == null || StringUtils.isBlank(openid)) {
             return;
         }
-        ywCustomerMapper.update(null, new UpdateWrapper<YwCustomer>().lambda()
-                .set(YwCustomer::getOpenid, null)
-                .eq(YwCustomer::getOpenid, openid.trim())
-                .ne(YwCustomer::getId, customer.getId()));
-        customer.setOpenid(openid.trim());
+        String trimmed = openid.trim();
+        memberMapper.update(null, new UpdateWrapper<Member>().lambda()
+                .set(Member::getOpenid, null)
+                .eq(Member::getOpenid, trimmed)
+                .ne(Member::getId, member.getId()));
+        member.setOpenid(trimmed);
+        member.setEditDate(new Date());
+        memberMapper.updateById(member);
+    }
+
+    private void touchMemberLogin(Member member) {
+        if (member == null) {
+            return;
+        }
+        member.setEditDate(new Date());
+        memberMapper.updateById(member);
     }
 
     private void touchLogin(YwCustomer customer) {
@@ -159,18 +273,43 @@
         ywCustomerMapper.updateById(customer);
     }
 
-    private LoginUserInfo toLoginUserInfo(YwCustomer customer) {
+    private LoginUserInfo toLoginUserInfo(YwCustomer customer, Member member) {
         LoginUserInfo loginUserInfo = new LoginUserInfo();
         loginUserInfo.setCustomerId(customer.getId());
         loginUserInfo.setId(customer.getId());
         loginUserInfo.setH5UserType(LoginUserInfo.H5_USER_CUSTOMER);
-        loginUserInfo.setRealname(customer.getName());
-        loginUserInfo.setMobile(resolveLoginMobile(customer));
-        loginUserInfo.setUsername("customer_" + customer.getId());
+        loginUserInfo.setCustomerName(customer.getName());
+        if (member != null) {
+            loginUserInfo.setMemberId(member.getId());
+            loginUserInfo.setMemberName(member.getName());
+            loginUserInfo.setMobile(member.getPhone());
+        } else {
+            loginUserInfo.setMobile(resolveLoginMobile(customer));
+        }
+        String displayName = buildDisplayName(customer.getName(), member != null ? member.getName() : null);
+        loginUserInfo.setDisplayName(displayName);
+        loginUserInfo.setRealname(displayName);
+        loginUserInfo.setUsername("customer_" + customer.getId()
+                + "_member_" + (member != null ? member.getId() : 0));
         loginUserInfo.setSource(LoginUserInfo.SOURCE_H5_CUSTOMER);
         loginUserInfo.setRoles(Collections.singletonList("h5_customer"));
         loginUserInfo.setPermissions(Collections.emptyList());
         return loginUserInfo;
+    }
+
+    private String buildDisplayName(String customerName, String memberName) {
+        String customer = StringUtils.trimToEmpty(customerName);
+        String member = StringUtils.trimToEmpty(memberName);
+        if (StringUtils.isBlank(customer) && StringUtils.isBlank(member)) {
+            return "";
+        }
+        if (StringUtils.isBlank(member)) {
+            return customer;
+        }
+        if (StringUtils.isBlank(customer)) {
+            return member;
+        }
+        return customer + "-" + member;
     }
 
     private String resolveLoginMobile(YwCustomer customer) {
@@ -185,4 +324,45 @@
         }
         return customer.getPhone();
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void logout(LoginUserInfo user, String token) {
+        if (user == null) {
+            jwtTokenUtil.invalidateToken(token);
+            return;
+        }
+        String memberOpenId = null;
+        if (user.getMemberId() != null) {
+            Member member = memberMapper.selectById(user.getMemberId());
+            if (member != null) {
+                memberOpenId = member.getOpenid();
+                if (StringUtils.isNotBlank(memberOpenId)) {
+                    memberMapper.update(null, new UpdateWrapper<Member>().lambda()
+                            .set(Member::getOpenid, null)
+                            .eq(Member::getId, member.getId()));
+                }
+            }
+        }
+        if (user.getCustomerId() != null) {
+            YwCustomer customer = ywCustomerMapper.selectById(user.getCustomerId());
+            if (customer != null && StringUtils.isNotBlank(customer.getOpenid())
+                    && (memberOpenId == null || StringUtils.equals(customer.getOpenid(), memberOpenId))) {
+                ywCustomerMapper.update(null, new UpdateWrapper<YwCustomer>().lambda()
+                        .set(YwCustomer::getOpenid, null)
+                        .eq(YwCustomer::getId, customer.getId()));
+            }
+        }
+        jwtTokenUtil.invalidateToken(token);
+    }
+
+    private static class CustomerMemberContext {
+        private final YwCustomer customer;
+        private final Member member;
+
+        private CustomerMemberContext(YwCustomer customer, Member member) {
+            this.customer = customer;
+            this.member = member;
+        }
+    }
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5BizServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5BizServiceImpl.java
index 5e55186..d9494ab 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5BizServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerH5BizServiceImpl.java
@@ -29,6 +29,8 @@
 import com.doumee.dao.business.model.*;
 import com.doumee.dao.system.MultifileMapper;
 import com.doumee.dao.system.model.Multifile;
+import com.doumee.service.business.YwCustomerDeviceAutoBindService;
+import com.doumee.service.business.YwCustomerH5AuthService;
 import com.doumee.service.business.YwCustomerH5BizService;
 import com.doumee.service.business.YwCustomerRechargeBizService;
 import com.doumee.service.business.YwElectricalBizService;
@@ -57,6 +59,8 @@
     @Autowired
     private YwCustomerRechargeBizService ywCustomerRechargeBizService;
     @Autowired
+    private YwCustomerDeviceAutoBindService ywCustomerDeviceAutoBindService;
+    @Autowired
     private YwCustomerElectricalMapper ywCustomerElectricalMapper;
     @Autowired
     private YwElectricalMapper ywElectricalMapper;
@@ -78,6 +82,8 @@
     private MultifileMapper multifileMapper;
     @Autowired
     private SystemDictDataBiz systemDictDataBiz;
+    @Autowired
+    private YwCustomerH5AuthService ywCustomerH5AuthService;
 
     @Override
     public List<YwH5Banner> listBanners() {
@@ -85,11 +91,14 @@
     }
 
     @Override
-    public Map<String, Object> home(Integer customerId) {
+    public Map<String, Object> home(Integer customerId, Integer memberId) {
         YwCustomer customer = requireCustomer(customerId);
+        LoginUserInfo loginUser = ywCustomerH5AuthService.buildLoginUserInfo(customerId, memberId);
         YwCustomerRechargeDetailVO detail = ywCustomerRechargeBizService.getDetail(customerId);
         Map<String, Object> map = new LinkedHashMap<>();
-        map.put("customerName", customer.getName());
+        map.put("customerName", loginUser.getCustomerName() != null ? loginUser.getCustomerName() : customer.getName());
+        map.put("memberName", loginUser.getMemberName());
+        map.put("displayName", loginUser.getDisplayName());
         map.put("electricalCount", detail.getElectricalList() != null ? detail.getElectricalList().size() : 0);
         map.put("conditionerCount", detail.getConditionerList() != null ? detail.getConditionerList().size() : 0);
         map.put("gsConfig", detail.getGsConfig());
@@ -100,6 +109,7 @@
     @Override
     public PageData<CustomerDeviceH5VO> devicePage(PageWrap<CustomerDeviceQueryDTO> pageWrap, Integer customerId) {
         requireCustomer(customerId);
+        ywCustomerDeviceAutoBindService.refreshCustomerDevices(customerId, systemUser());
         CustomerDeviceQueryDTO q = pageWrap.getModel() != null ? pageWrap.getModel() : new CustomerDeviceQueryDTO();
         List<CustomerDeviceH5VO> all = new ArrayList<>();
         if (q.getDeviceType() == null || q.getDeviceType() == 0) {
@@ -254,10 +264,8 @@
         if (Objects.equals(customer.getFirstRechargeDone(), Constants.ONE)) {
             return;
         }
-        List<Integer> electricalIds = ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
-                        .eq(YwCustomerElectrical::getCustomerId, customerId)
-                        .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO))
-                .stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toList());
+        ywCustomerDeviceAutoBindService.refreshCustomerDevices(customerId, user != null ? user : systemUser());
+        List<Integer> electricalIds = ywCustomerDeviceAutoBindService.listElectricalIdsByActiveContracts(customerId);
         for (Integer eid : electricalIds) {
             YwCustomerRechargeElectricalDTO dto = new YwCustomerRechargeElectricalDTO();
             dto.setCustomerId(customerId);
@@ -280,10 +288,7 @@
     }
 
     private List<CustomerDeviceH5VO> buildElectricalDevices(Integer customerId) {
-        List<Integer> ids = ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
-                        .eq(YwCustomerElectrical::getCustomerId, customerId)
-                        .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO))
-                .stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toList());
+        List<Integer> ids = ywCustomerDeviceAutoBindService.listElectricalIdsByActiveContracts(customerId);
         if (ids.isEmpty()) {
             return Collections.emptyList();
         }
@@ -297,6 +302,7 @@
             vo.setDeviceId(e.getId());
             vo.setDeviceName(e.getName());
             vo.setMeterAccountNo(e.getParamId());
+            vo.setMeterAddress(e.getAddress());
             vo.setRoomInfo(e.getRoomNames());
             vo.setBalance(e.getBalance());
             vo.setBalanceLow(e.getBalance() != null && e.getBalance().compareTo(new BigDecimal("50")) < 0);
@@ -345,6 +351,13 @@
         return true;
     }
 
+    private LoginUserInfo systemUser() {
+        LoginUserInfo user = new LoginUserInfo();
+        user.setId(1);
+        user.setRealname("system");
+        return user;
+    }
+
     private YwCustomer requireCustomer(Integer customerId) {
         YwCustomer c = ywCustomerMapper.selectById(customerId);
         if (c == null || Objects.equals(c.getIsdeleted(), Constants.ONE)) {
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java
index c1136e4..2e5321f 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerRechargeBizServiceImpl.java
@@ -24,6 +24,7 @@
 import com.doumee.dao.business.dto.*;
 import com.doumee.dao.business.model.*;
 import com.doumee.service.business.ConditionerBizService;
+import com.doumee.service.business.YwCustomerDeviceAutoBindService;
 import com.doumee.service.business.YwCustomerRechargeBizService;
 import com.doumee.service.business.YwElectricalBizService;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
@@ -57,6 +58,10 @@
     @Autowired
     private YwElectricalMapper ywElectricalMapper;
     @Autowired
+    private YwElectricalRoomMapper ywElectricalRoomMapper;
+    @Autowired
+    private YwRoomMapper ywRoomMapper;
+    @Autowired
     private YwConditionerMapper ywConditionerMapper;
     @Autowired
     private YwElectricalChargeMapper ywElectricalChargeMapper;
@@ -66,6 +71,10 @@
     private YwElectricalBizService ywElectricalBizService;
     @Autowired
     private ConditionerBizService conditionerBizService;
+    @Autowired
+    private YwCustomerDeviceAutoBindService ywCustomerDeviceAutoBindService;
+    @Autowired
+    private MemberMapper memberMapper;
 
     @Override
     public PageData<YwCustomerRechargeMerchantVO> findMerchantPage(PageWrap<YwCustomerRechargeQueryDTO> pageWrap) {
@@ -156,20 +165,17 @@
 
         Map<Integer, YwCustomerGs> gsMap = loadGsMap(customerIds);
 
-        List<YwCustomerElectrical> relE = loadCustomerElectricalRels(customerIds);
-        Map<Integer, List<Integer>> customerElectricalIds = relE.stream()
-                .collect(Collectors.groupingBy(YwCustomerElectrical::getCustomerId,
-                        Collectors.mapping(YwCustomerElectrical::getElectricalId, Collectors.toList())));
+        Map<Integer, List<Integer>> customerElectricalIds = ywCustomerDeviceAutoBindService
+                .batchListElectricalIdsByActiveContracts(customerIds);
+        Map<Integer, List<Integer>> customerConditionerIds = ywCustomerDeviceAutoBindService
+                .batchListConditionerIdsByActiveContracts(customerIds);
 
-        List<YwCustomerConditioner> relC = loadCustomerConditionerRels(customerIds);
-        Map<Integer, List<Integer>> customerConditionerIds = relC.stream()
-                .collect(Collectors.groupingBy(YwCustomerConditioner::getCustomerId,
-                        Collectors.mapping(YwCustomerConditioner::getConditionerId, Collectors.toList())));
-
-        Set<Integer> allElectricalIds = relE.stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toSet());
+        Set<Integer> allElectricalIds = customerElectricalIds.values().stream()
+                .flatMap(List::stream).collect(Collectors.toSet());
         Map<Integer, YwElectrical> electricalMap = loadElectricalMap(allElectricalIds);
 
-        Set<Integer> allConditionerIds = relC.stream().map(YwCustomerConditioner::getConditionerId).collect(Collectors.toSet());
+        Set<Integer> allConditionerIds = customerConditionerIds.values().stream()
+                .flatMap(List::stream).collect(Collectors.toSet());
         Map<Integer, YwConditioner> conditionerMap = loadConditionerMap(allConditionerIds);
 
         List<YwCustomerRechargeMerchantVO> list = new ArrayList<>();
@@ -260,36 +266,6 @@
         }
     }
 
-    private List<YwCustomerElectrical> loadCustomerElectricalRels(List<Integer> customerIds) {
-        if (CollectionUtils.isEmpty(customerIds)) {
-            return Collections.emptyList();
-        }
-        try {
-            return ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
-                    .select(YwCustomerElectrical::getCustomerId, YwCustomerElectrical::getElectricalId)
-                    .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO)
-                    .in(YwCustomerElectrical::getCustomerId, customerIds));
-        } catch (Exception e) {
-            log.warn("load yw_customer_electrical failed, skip electrical stats: {}", e.getMessage());
-            return Collections.emptyList();
-        }
-    }
-
-    private List<YwCustomerConditioner> loadCustomerConditionerRels(List<Integer> customerIds) {
-        if (CollectionUtils.isEmpty(customerIds)) {
-            return Collections.emptyList();
-        }
-        try {
-            return ywCustomerConditionerMapper.selectList(new QueryWrapper<YwCustomerConditioner>().lambda()
-                    .select(YwCustomerConditioner::getCustomerId, YwCustomerConditioner::getConditionerId)
-                    .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO)
-                    .in(YwCustomerConditioner::getCustomerId, customerIds));
-        } catch (Exception e) {
-            log.warn("load yw_customer_conditioner failed, skip conditioner stats: {}", e.getMessage());
-            return Collections.emptyList();
-        }
-    }
-
     private Map<Integer, YwElectrical> loadElectricalMap(Set<Integer> electricalIds) {
         if (CollectionUtils.isEmpty(electricalIds)) {
             return Collections.emptyMap();
@@ -326,6 +302,7 @@
     @Override
     public YwCustomerRechargeDetailVO getDetail(Integer customerId) {
         YwCustomer customer = requireCustomer(customerId);
+        ywCustomerDeviceAutoBindService.refreshCustomerDevices(customerId, systemUser());
         YwCustomerRechargeDetailVO vo = new YwCustomerRechargeDetailVO();
         vo.setCustomerId(customer.getId());
         vo.setCustomerName(customer.getName());
@@ -339,6 +316,7 @@
     @Override
     public PageData<YwElectrical> listCustomerElectrical(PageWrap<YwElectrical> pageWrap, Integer customerId) {
         requireCustomer(customerId);
+        ywCustomerDeviceAutoBindService.refreshCustomerDevices(customerId, systemUser());
         List<Integer> ids = listBoundElectricalIds(customerId);
         if (ids.isEmpty()) {
             return emptyPage(pageWrap);
@@ -412,6 +390,7 @@
     @Override
     public PageData<YwConditioner> listCustomerConditioner(PageWrap<YwConditioner> pageWrap, Integer customerId) {
         requireCustomer(customerId);
+        ywCustomerDeviceAutoBindService.refreshCustomerDevices(customerId, systemUser());
         List<YwConditioner> list = loadCustomerConditionerList(customerId);
         if (CollectionUtils.isEmpty(list)) {
             return emptyPage(pageWrap);
@@ -898,7 +877,38 @@
         charge.setStatusTime(new Date());
         charge.setStatusInfo("鍏呭�间腑");
         charge.setDeviceInfo("GS-" + gs.getPlatformGsId() + " " + customer.getName());
+        applyRechargeOperator(charge, user);
         return charge;
+    }
+
+    private void applyRechargeOperator(YwElectricalCharge charge, LoginUserInfo user) {
+        if (charge == null || user == null) {
+            return;
+        }
+        if (user.getMemberId() != null) {
+            charge.setMemberId(user.getMemberId());
+        }
+        String operatorName = resolveRechargeUserName(user);
+        if (StringUtils.isNotBlank(operatorName)) {
+            charge.setRechargeUserName(operatorName);
+        }
+    }
+
+    private String resolveRechargeUserName(LoginUserInfo user) {
+        if (user == null) {
+            return null;
+        }
+        if (StringUtils.isNotBlank(user.getMemberName())) {
+            return user.getMemberName();
+        }
+        if (Constants.equalsInteger(user.getH5UserType(), LoginUserInfo.H5_USER_CUSTOMER)
+                && StringUtils.isNotBlank(user.getDisplayName())) {
+            int idx = user.getDisplayName().lastIndexOf('-');
+            if (idx >= 0 && idx < user.getDisplayName().length() - 1) {
+                return user.getDisplayName().substring(idx + 1).trim();
+            }
+        }
+        return StringUtils.trimToNull(user.getRealname());
     }
 
     private void fillRecordText(YwCustomerRechargeRecordVO vo) {
@@ -907,7 +917,13 @@
                 vo.setDeviceInfo(StringUtils.defaultString(vo.getAddress()) + " " + StringUtils.defaultString(vo.getName()));
             }
         }
-        vo.setTypeText(Objects.equals(vo.getType(), Constants.ONE) ? "绌鸿皟" : "鐢佃〃");
+        vo.setTypeText(Objects.equals(vo.getType(), Constants.ONE) ? "绌鸿皟鍏呭��" : "鐢佃〃鍏呭��");
+        if (StringUtils.isBlank(vo.getRechargeUserName()) && vo.getMemberId() != null) {
+            Member member = memberMapper.selectById(vo.getMemberId());
+            if (member != null && StringUtils.isNotBlank(member.getName())) {
+                vo.setRechargeUserName(member.getName());
+            }
+        }
         if (Objects.equals(vo.getStatus(), Constants.ZERO)) {
             vo.setStatusText("鍏呭�间腑");
         } else if (Objects.equals(vo.getStatus(), Constants.ONE)) {
@@ -1013,20 +1029,14 @@
     }
 
     private void assertElectricalBound(Integer customerId, Integer electricalId) {
-        Long cnt = ywCustomerElectricalMapper.selectCount(new QueryWrapper<YwCustomerElectrical>().lambda()
-                .eq(YwCustomerElectrical::getCustomerId, customerId)
-                .eq(YwCustomerElectrical::getElectricalId, electricalId)
-                .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO));
-        if (cnt == null || cnt == 0) {
-            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鐢佃〃鏈叧鑱旇鍟嗘埛");
+        List<Integer> boundIds = ywCustomerDeviceAutoBindService.listElectricalIdsByActiveContracts(customerId);
+        if (!boundIds.contains(electricalId)) {
+            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "鐢佃〃鏈叧鑱旇鍟嗘埛鏈夋晥鍚堝悓鎴挎簮");
         }
     }
 
     private List<Integer> listBoundElectricalIds(Integer customerId) {
-        return ywCustomerElectricalMapper.selectList(new QueryWrapper<YwCustomerElectrical>().lambda()
-                        .eq(YwCustomerElectrical::getCustomerId, customerId)
-                        .eq(YwCustomerElectrical::getIsdeleted, Constants.ZERO))
-                .stream().map(YwCustomerElectrical::getElectricalId).collect(Collectors.toList());
+        return ywCustomerDeviceAutoBindService.listElectricalIdsByActiveContracts(customerId);
     }
 
     private Set<Integer> listAllBoundElectricalIds() {
@@ -1054,22 +1064,73 @@
     }
 
     private List<YwConditioner> loadCustomerConditionerList(Integer customerId) {
-        List<YwCustomerConditioner> rels = ywCustomerConditionerMapper.selectList(new QueryWrapper<YwCustomerConditioner>().lambda()
-                .eq(YwCustomerConditioner::getCustomerId, customerId)
-                .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO));
-        if (rels.isEmpty()) {
+        List<Integer> conditionerIds = ywCustomerDeviceAutoBindService.listConditionerIdsByActiveContracts(customerId);
+        if (conditionerIds.isEmpty()) {
             return Collections.emptyList();
         }
-        Map<Integer, Integer> ratioMap = rels.stream()
+        Map<Integer, Integer> ratioMap = ywCustomerConditionerMapper.selectList(new QueryWrapper<YwCustomerConditioner>().lambda()
+                        .eq(YwCustomerConditioner::getCustomerId, customerId)
+                        .eq(YwCustomerConditioner::getIsdeleted, Constants.ZERO))
+                .stream()
                 .collect(Collectors.toMap(YwCustomerConditioner::getConditionerId, YwCustomerConditioner::getDevRatio, (a, b) -> a));
-        List<YwConditioner> list = ywConditionerMapper.selectBatchIds(ratioMap.keySet());
+        List<YwConditioner> list = ywConditionerMapper.selectBatchIds(conditionerIds);
         list = list.stream().filter(c -> !Objects.equals(c.getIsdeleted(), Constants.ONE)).collect(Collectors.toList());
+        enrichConditionerRoomNames(list, customerId);
         for (YwConditioner c : list) {
-            c.setDevRatio(ratioMap.get(c.getId()));
+            c.setDevRatio(ratioMap.getOrDefault(c.getId(), 100));
         }
         return list;
     }
 
+    private void enrichConditionerRoomNames(List<YwConditioner> list, Integer customerId) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        List<Integer> contractRoomIds = ywCustomerDeviceAutoBindService.listActiveContractRoomIds(customerId);
+        Map<Integer, Integer> conditionerRoomMap = new HashMap<>();
+        if (!CollectionUtils.isEmpty(contractRoomIds)) {
+            ywElectricalRoomMapper.selectList(new QueryWrapper<YwElectricalRoom>().lambda()
+                            .in(YwElectricalRoom::getRoomId, contractRoomIds)
+                            .eq(YwElectricalRoom::getType, Constants.ONE)
+                            .eq(YwElectricalRoom::getIsdeleted, Constants.ZERO))
+                    .forEach(rel -> {
+                        if (rel.getObjId() != null && rel.getRoomId() != null) {
+                            conditionerRoomMap.putIfAbsent(rel.getObjId(), rel.getRoomId());
+                        }
+                    });
+        }
+        Set<Integer> roomIds = new HashSet<>(contractRoomIds);
+        for (YwConditioner conditioner : list) {
+            Integer roomId = conditionerRoomMap.getOrDefault(conditioner.getId(), conditioner.getRoomId());
+            if (roomId != null) {
+                roomIds.add(roomId);
+            }
+        }
+        if (roomIds.isEmpty()) {
+            return;
+        }
+        Map<Integer, YwRoom> roomMap = ywRoomMapper.selectBatchIds(roomIds).stream()
+                .filter(r -> !Objects.equals(r.getIsdeleted(), Constants.ONE))
+                .collect(Collectors.toMap(YwRoom::getId, r -> r, (a, b) -> a));
+        for (YwConditioner conditioner : list) {
+            if (StringUtils.isNotBlank(conditioner.getRoomName())) {
+                continue;
+            }
+            Integer roomId = conditionerRoomMap.getOrDefault(conditioner.getId(), conditioner.getRoomId());
+            YwRoom room = roomId != null ? roomMap.get(roomId) : null;
+            if (room != null) {
+                conditioner.setRoomName(StringUtils.defaultString(room.getRoomNum()));
+            }
+        }
+    }
+
+    private LoginUserInfo systemUser() {
+        LoginUserInfo user = new LoginUserInfo();
+        user.setId(1);
+        user.setRealname("system");
+        return user;
+    }
+
     private PageData<YwElectrical> pageElectricalByIds(PageWrap<YwElectrical> pageWrap, List<Integer> ids) {
         IPage<YwElectrical> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
         YwElectrical model = pageWrap.getModel();
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerWxPayServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerWxPayServiceImpl.java
index 35e1b09..5b94c6c 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerWxPayServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwCustomerWxPayServiceImpl.java
@@ -15,6 +15,7 @@
 import com.doumee.dao.business.model.*;
 import com.doumee.service.business.YwContractRevenueService;
 import com.doumee.service.business.YwCustomerH5BizService;
+import com.doumee.service.business.YwCustomerH5AuthService;
 import com.doumee.service.business.YwCustomerRechargeBizService;
 import com.doumee.service.business.YwCustomerWxPayService;
 import lombok.extern.slf4j.Slf4j;
@@ -48,6 +49,8 @@
     private YwContractRevenueService ywContractRevenueService;
     @Autowired
     private YwCustomerH5BizService ywCustomerH5BizService;
+    @Autowired
+    private YwCustomerH5AuthService ywCustomerH5AuthService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -74,6 +77,7 @@
         order.setIsdeleted(Constants.ZERO);
         order.setOrderNo(orderNo);
         order.setCustomerId(user.getCustomerId());
+        order.setMemberId(user.getMemberId());
         order.setOrderType(orderType);
         order.setBizRefId(bizRefId);
         order.setAmount(dto.getAmount());
@@ -120,7 +124,7 @@
         if (Objects.equals(order.getStatus(), YwWxPayOrder.STATUS_SUCCESS)) {
             return successXml();
         }
-        LoginUserInfo user = buildCustomerUser(order.getCustomerId());
+        LoginUserInfo user = buildCustomerUser(order);
         try {
             Integer bizRecordId = fulfillBiz(order, user);
             order.setStatus(YwWxPayOrder.STATUS_SUCCESS);
@@ -238,13 +242,11 @@
         throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "璁㈠崟绫诲瀷鏃犳晥");
     }
 
-    private LoginUserInfo buildCustomerUser(Integer customerId) {
-        LoginUserInfo u = new LoginUserInfo();
-        u.setId(customerId);
-        u.setCustomerId(customerId);
-        u.setH5UserType(LoginUserInfo.H5_USER_CUSTOMER);
-        u.setRealname("鍟嗘埛H5");
-        return u;
+    private LoginUserInfo buildCustomerUser(YwWxPayOrder order) {
+        if (order == null || order.getCustomerId() == null) {
+            return null;
+        }
+        return ywCustomerH5AuthService.buildLoginUserInfo(order.getCustomerId(), order.getMemberId());
     }
 
     private String successXml() {
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
index 2a76435..e39073e 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
@@ -625,6 +625,7 @@
         charge.setStatusInfo("鍏呭�间腑");
         charge.setBanlance(e.getBalance());
         charge.setRoomNames(e.getRoomNames());
+        applyRechargeOperator(charge, user);
         if (dto.getCustomerId() != null) {
             charge.setCustomerId(dto.getCustomerId());
         }
@@ -1401,4 +1402,34 @@
     private String newOprId() {
         return UUID.randomUUID().toString().replace("-", "");
     }
+
+    private void applyRechargeOperator(YwElectricalCharge charge, LoginUserInfo user) {
+        if (charge == null || user == null) {
+            return;
+        }
+        if (user.getMemberId() != null) {
+            charge.setMemberId(user.getMemberId());
+        }
+        String operatorName = resolveRechargeUserName(user);
+        if (StringUtils.isNotBlank(operatorName)) {
+            charge.setRechargeUserName(operatorName);
+        }
+    }
+
+    private String resolveRechargeUserName(LoginUserInfo user) {
+        if (user == null) {
+            return null;
+        }
+        if (StringUtils.isNotBlank(user.getMemberName())) {
+            return user.getMemberName();
+        }
+        if (Constants.equalsInteger(user.getH5UserType(), LoginUserInfo.H5_USER_CUSTOMER)
+                && StringUtils.isNotBlank(user.getDisplayName())) {
+            int idx = user.getDisplayName().lastIndexOf('-');
+            if (idx >= 0 && idx < user.getDisplayName().length() - 1) {
+                return user.getDisplayName().substring(idx + 1).trim();
+            }
+        }
+        return StringUtils.trimToNull(user.getRealname());
+    }
 }

--
Gitblit v1.9.3